Плаваюча (рухома) крапка. Частина 9: RDBMS і SQL: земля невизначеності

Частини 1-2 з каталогом всіх частин.

«Ми кажемо RDBMS, маємо на увазі SQL; кажемо SQL, маємо на увазі RDBMS» (за радянським класиком). Тут я не розділятиму їх, хоча SQL використовується і для інших типів баз даних, і бувають інші інтерфейси до RDBMS.

Із RDBMS все погано — це в двох словах. Спочатку я хотів в цій частині перебрати більше рушіїв, включаючи MS SQL, Oracle DB, Firebird... Але після того, як набрався жахів, вирішив, що прикладів двох найбільш доступних і всіх ефектів в них вистачить, щоб були гідні приклади, як ідуть справи на практиці і на що дивитись, що перевіряти і що обходити у реальному використанні. Набір цих перевірок можна вже самостійно застосувати до конкретного рушія. Сподіваюсь, після цього у читачів не все волосся буде дибки і можна буде виробити щось корисне навіть через такі перешкоди. 😉

(Хтось може спитати, навіщо взагалі хотіти від такого рушія чогось крім точного зберігання даних 1:1, навіть в текстовій формі? Але той, хто багато працює з БД, знає, що в великих базах дуже багато чого перенавантажено на збережені процедури (stored procedures, хранимые процедуры), які працюють саме всередині рушія і яким потрібна коректність операцій, контроль неочікуваних ефектів... від їх якости залежить все. Та навіть вирази в усяких SELECT вже мають бути виконані згідно з правилами. Вибачаюсь перед тими, для кого це капітанські ази.)

Спочатку, що є в стандарті?

Стандарт

Тут і далі перевіряю по версії 2013 року, для якої вдалось знайти текст драфта Foundation. (І навіть це може бути занадто пізньою версією, бо відповідність стандарту втілюється в реальні рушії дуже повільно.)

Принципово розділені дві ґрупи типів представлень нецілих чисел (або, фактично, просто два типи, але з різними іменуваннями): «точні» і «неточні» представлення чисел. Точні — десяткові (див. частину 7), неточні (approximate) — двійкові (див. частину 3). Це відповідає тому, що ми казали про типові домени, хоча реально для двійкових типів це образливо: чим це вони неточні? Спишемо це на зверхність тих, для кого все, що не фінанси, то якась нісенітниця.

«Точний» тип указується як NUMERIC(n,m) або DECIMAL(n,m), де n — кількість десяткових цифр всього (precision), m — після крапки (scale). Якщо параметри не задані, є якийсь дефолт. «Неточний» тип указується як FLOAT(n), де n — кількість двійкових цифр всього, або REAL чи DOUBLE PRECISION, для яких є якийсь дефолт.

Здається, це гарно? Проте, перейдемо в реальний світ, де в усьому є обмеження. В стандарті нема жодної ґарантії на мінімальні значення для цих параметрів, все віддано на відкуп реалізаціям. Формально ніщо не заважає зробити максимум для NUMERIC на одну цифру, REAL з одного біта мантиси, і DOUBLE PRECISION з двох. 😱 Те ж саме для діапазону порядків. Для неточних типів, рекомендовані відповідності типам мов — як float і double для C — натякають на бажані точність і діапазон порядків, але це лише натяк.

Тобто, будь-які параметри тут можна брати тільки з конкретних реалізацій.

(Хм, стандарт дозволяє також, щоб у «точного» типа precision було при основі 2, при тому, что scale все одно вираховується як степінь 10. Я не думаю, що хоч одна реалізація в реальному світі таке зробить. Нащо таке дозволяти?)

Справжня десяткова рухома крапка не дозволена (є якісь драфти щодо наступного стандарту, тобто, побачимо, у кращому випадку, десь в середині 2030-х).

Не краще з лоґікою операцій. Наприклад (Foundation глава 4.4.2):

Whenever an exact or approximate numeric value is assigned to an exact numeric value site, an approximation of its value that preserves leading significant digits after rounding or truncating is represented in the declared type of the target. The value is converted to have the precision and scale of the target. The choice of whether to truncate or round is implementation-defined.

Керування принципами присвоєння нема, функцій нема. Чи укоротить, чи округлить? Округлення визначено як half-що-завгодно-реалізації (глава «Characterics of numbers»), а округляти чи просто укоротити — всюди тотальне «implementation-defined». Видно, що тема не пророблена аж ніяк.

Infinity і NaN в стандарті не дозволяються, на них не посилаються, але означення того, що може зберігатись в таких числових типах, просто не включає в себе такі спеціальні значення. Хто їх дозволяє, той виходить тут за межі стандарту.

Підсумовуючи висновки щодо стандарту: залагатись можна тільки на конкретні реалізації. Тому спробуємо подивитись на них.

MySQL

Почнемо з найбільш доступного в середньому — MySQL, почнемо з нього. Дивлюсь по версії 8.0.44, бо така в Ubuntu 24.04.

FLOAT(m) веде себе згідно стандарту з поправкою на те, що m ⩽ 24 дає зберігання як IEEE754 binary32, а 25 ⩽ m ⩽ 53 — відповідно як binary64. Якщо вказати такий тип в CREATE TABLE, він автоматично буде замінений відповідно на float або double (що видно по відповіді команди SHOW CREATE TABLE або DESCRIBE). Слова зі стандарта SQL, REAL і DOUBLE PRECISION, обидва переходять в DOUBLE. Це не відповідає стандарту, який явно каже: «DOUBLE PRECISION specifies the data type approximate numeric, with implementation-defined precision that is greater than the implementation-defined precision of REAL.» Чому REAL не реалізується як FLOAT? Може, це вже леґасі...

В MySQL нема прямого отримання типу значення, доводиться іти кривими шляхами. Один з них — записати в проміжну таблицю (краще тимчасову, temporary) і дивитись на тип значення. Але навіть це не показує всіх деталей. Тому викликаємо консольного клієнта з допоміжними опціями: --column-type-info -v. Як це виглядає:

(Тут і далі, з виводу зрізаю рамки і видаляю «k рядків за v секунд» і інші несуттєві нам подробиці)

mysql> SELECT 0.1;
--------------
SELECT 0.1
--------------

Field   1:  `0.1`
Catalog:    `def`
Database:   ``
Table:      ``
Org_table:  ``
Type:       NEWDECIMAL
Collation:  binary (63)
Length:     4
Max_length: 3
Decimals:   1
Flags:      NOT_NULL BINARY NUM

+-----+
| 0.1 |
+-----+

Проте, якщо записати в проміжну змінну, вже буде зовсім інший тип (змінились розміри):

mysql> SET @a = 0.1;
--------------
SET @a = 0.1
--------------

mysql> SELECT @a;
--------------
SELECT @a
--------------

Field   1:  `@a`
Catalog:    `def`
Database:   ``
Table:      ``
Org_table:  ``
Type:       NEWDECIMAL
Collation:  binary (63)
Length:     67
Max_length: 3
Decimals:   30
Flags:      BINARY NUM

+------+
|  0.1 |
+------+

Також дивимось через властивості таблиці:

mysql> SET @a = 0.3;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TEMPORARY TABLE t1 SELECT @a;
Query OK, 1 row affected (0.01 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> SHOW CREATE TABLE t1;

| t1    | CREATE TEMPORARY TABLE `t1` (
  `@a` decimal(65,30) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |

До типізації, десяткові обчислення можуть бути довгими, але після вони скорочуються до максимуму — 65 цифр всього і 30 після крапки:

mysql> select 3.141592653589793238462643383279502884197 + 2.7182818284590452353602874713526624977572 as v;

Field   1:  `v`
Catalog:    `def`
Database:   ``
Table:      ``
Org_table:  ``
Type:       NEWDECIMAL
Collation:  binary (63)
Length:     44
Max_length: 42
Decimals:   40
Flags:      NOT_NULL BINARY NUM

+--------------------------------------------+
| 5.8598744820488384738229308546321653819542 |
+--------------------------------------------+

(примітимо, 40 цифр після крапки. документація каже, що більше 30 не дозволяється...)

mysql> create temporary table t3 select 3.141592653589793238462643383279502884197 + 2.7182818284590452353602874713526624977572 as v;

mysql> select * from t3;
+----------------------------------+
| 5.859874482048838473822930854632 |
+----------------------------------+

(вже 30 цифр)

mysql> describe t3;
+-------+----------------+------+-----+---------+-------+
| Field | Type           | Null | Key | Default | Extra |
+-------+----------------+------+-----+---------+-------+
| v     | decimal(65,30) | YES  |     | NULL    | NULL  |
+-------+----------------+------+-----+---------+-------+

Реально, DECIMAL(65,30) — саме такий тип буде, якщо записати зі змінної в базу — це максимум, що дозволяє MySQL, ха-ха. (Length: 67 — це з додачею крапки і можливого мінусу в текстовій формі. Ця довжина показується для текстового представлення. Ось така тут лоґіка.) Але чому при додаванні виконало на 40 цифр?

Взагалі, в документації сказано, що точність (після крапки) результату операцій з DECIMAL — максимум для додавання і віднімання, сумма для множення і ділення. Для множення воно вже скорочує до 30 цифр після крапки... всі 80 не хоче. 40 — можна. 80 — скорочується до 30. Хто так пише?

Відповідно, дивимось на ефекти. В літералах: 0.1 * 3 показує 0.3; 0.1 / 3 (виконав: SELECT 0.1 / 3) показує 0.03333. Чому? Є такий сесійний параметр div_precision_increment. Точність (кількість цифр після крапки) ділення дорівнює максимуму точностей параметрів, плюс цей div_precision_increment, за замовчуванням рівний 4. Підняв до 10, бачу: 0.03333333333.

Думаєте, хоча б тут все просто? Ні...

mysql> SET @a = 0.1;

mysql> SELECT @a / 3;
--------------
SELECT @a / 3
--------------

Field   1:  `@a / 3`
Catalog:    `def`
Database:   ``
Table:      ``
Org_table:  ``
Type:       NEWDECIMAL
Collation:  binary (63)
Length:     67
Max_length: 32
Decimals:   30
Flags:      BINARY NUM

+----------------------------------+
| 0.033333333000000000000000000000 |
+----------------------------------+

Пробачте, раз @a має тип DECIMAL(65,30), я очікував всі цифри.

Додаємо другий параметр:

mysql> SET @b = 3.0;
--------------
SET @b = 3.0
--------------

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @a / @b;
--------------
SELECT @a / @b
--------------

Field   1:  `@a / @b`
Catalog:    `def`
Database:   ``
Table:      ``
Org_table:  ``
Type:       NEWDECIMAL
Collation:  binary (63)
Length:     67
Max_length: 32
Decimals:   30
Flags:      BINARY NUM

+----------------------------------+
| 0.033333333333333333000000000000 |
+----------------------------------+

17 цифр натякають на внутрішній DOUBLE. При div_precision_increment = 16 результат той же. При 17, отримуємо 0.033333333333333333333333333000. «Вдруг разъедется машина — едет вправо половина. Что такое? Почему? Ничего я не пойму!» ([Самійло Маршак])

І тільки через явний CAST можна отримати нормальну точність (навіть не підвищуючи div_precision_increment):

mysql> SELECT CAST(0.1 AS DECIMAL(65,30)) / CAST(3 AS DECIMAL(65,30));
+---------------------------------------------------------+
|                        0.033333333333333333333333333333 |
+---------------------------------------------------------+

або те ж саме з нашими @a і @b.

Якось неврівноважено, мʼяко кажучи... Якщо порівняти видачу SELECT на @a і CAST(@a AS DECIMAL(65,30)), різниця в max_length: 3 і 32 відповідно. Розмова з LLM підказує, що CAST можна було б застосувати тільки для одного з операндів ділення, цього було б достатньо; перевіряю — так воно і є.

З іншого боку, є двійкова арифметика. Подивимось на неї і на взаємодію з десятковою.

Є оператор CAST(вираз AS тип) як універсальний засіб перетворення. При цьому немає завдання режиму округлення.

Сам по собі результат CAST(0.3 as FLOAT) має тип FLOAT. Але якщо його записати чи в змінну, чи в таблицю показаним чище методом, отримуємо вже DOUBLE.

Спробуємо конверсії:

mysql> SELECT 0.1 * 3;
+---------+
|     0.3 |
+---------+
1 row in set (0.00 sec)

mysql> SELECT CAST(0.1 AS FLOAT) * 3;
+------------------------+
|    0.30000000447034836 |
+------------------------+

mysql> SELECT CAST(0.1 AS DOUBLE) * 3;
+-------------------------+
|     0.30000000000000004 |
+-------------------------+

Другий результат показує, що операція множення конвертує вхідний арґумент з FLOAT в DOUBLE і уже тоді множить (можете самі перевірити). Третій результат повністю відповідає тому, що ми вже знаємо про double і константи з нього. Типи результату у обоих — DOUBLE.

У першому, до того ж, представлення в таблиці «балакуче» — число в DECIMAL доповнюється до повної довжини дробної частини нулями:

mysql> SELECT * FROM t1;
+----------------------------------+
| @a                               |
+----------------------------------+
| 0.300000000000000000000000000000 |
+----------------------------------+

Через Python клієнта (як pymysql) можна подавати і двійковий формат (пітонівський float), і десятковий (пітонівський Decimal). (Можна і в вигляді текстового рядка, спочатку буде переведено в двійковий DOUBLE і вже з нього прийняте в роботу.) Сервер проконвертує представлення... з округленням, звісно. Округлення не реґулюється, і буде half-even при конвертації до двійкових форматів, і half-away — до десяткових.

При SELECT і інших операціях отримання данних з бази, типи FLOAT і DOUBLE переходять у пітонівський float, а всі DECIMAL — у Decimal. Це зручно, якщо бути готовим (RTFM ніхто не відміняв;)) Про питонівський Decimal розповідалось в частині 7, його можливості (крім, може, швидкости) нас задовільняють на 101%.

А що при переповненні? Представлення Infinity чи NaN просто неможливе. (NULL може виступати замість NaN, якщо треба... але це теж милиця.) Документація каже: «MySQL uses four bytes for single-precision values and eight bytes for double-precision values.» Навіть не вказано про IEEE754, мабуть, з ідеєю, що той, хто буде читати, автоматично підставить їх і помилиться:) Ну, стандарт SQL не дозволяє Infinity і NaN як значення для «approximate numeric type». Тут вони не захотіли розширити. Субнормальні, проте, є — 1e-45 зберігається у float-стовпчику як 1.4013e-45.

А ось на переповнення при конверсії значень діє STRICT_TRANS_TABLES. За замовчуванням для спроби нижче буде помилка виконання і SELECT не виконається взагалі. Якщо його виключити і тим вимкнути, можемо отримати усічення:

mysql> select @@sql_mode;
| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |

(...видаляємо з опцій — не показую, просто редаґування тексту. інтерфейс, чесно кажучи, кривий...)

mysql> select cast(1.0e300 as decimal(60,30));
+---------------------------------------------------------------+
| cast(1.0e300 as decimal(60,30))                               |
+---------------------------------------------------------------+
| 999999999999999999999999999999.999999999999999999999999999999 |
+---------------------------------------------------------------+

Є явна функція ROUND. Випробуймо її:

mysql> select round(1.665, 2);
+-----------------+
|            1.67 |
+-----------------+

mysql> select round(-1.665, 2);
+------------------+
|            -1.67 |
+------------------+

Ну, нормальний half-away.

Інші проби з таким же результатом не показую, дивіться попередні частини. Видно, що це half-away, що стандартно очікувано для реалізацій десяткової арифметики. Але документація тут крива:

For exact-value numbers, ROUND() uses the «round half up» rule: A value with a fractional part of .5 or greater is rounded up to the next integer if positive or down to the next integer if negative. (In other words, it is rounded away from zero.)

Half-up це не half-away, в термінолоґію вони не дуже вміють.

(А тепер бережіть своє волосся...)

І ще є проблеми виводу значень, дуже неочікувані (все ще актуальне на 8.0.44):

mysql> create temporary table simpletable (v float);

mysql> INSERT INTO simpletable VALUES (1223482), (1223484);

mysql> SELECT * FROM simpletable;

+---------+
| 1223480 |
| 1223480 |
+---------+

Що????

mysql> SELECT v+0 from simpletable;

+---------+
| 1223482 |
| 1223484 |
+---------+

Ага. У першому SELECT воно показує 6 цифр, бо, типа, більше і немає у FLOAT типа. У другому v+0 виконується в double (конверсія першого доданка, додавання і повернення результату), друкується вже з точністю для double. Як було описано, головним чином в частині 5, для точного представлення в десятковому вигляді будь-якого значення з двійкового 32-бітного float має бути використано FLT_DECIMAL_DIG цифр, що для IEEE754 дорівнює 9. Девʼять, а не шість, що б там ні було в замовчуваннях усяких printf.

В баґрепорті ще згадується, що така ж проблема не тільки в консольному клієнті, а ще і в Java-клієнті (хм, а чому клієнт отримує float через десяткову конверсію?) 😢

І мене найбільше напрягає, що це визнано «not a bug». На останнє у мене просто нема цензурних слів, як і на поведінку технічної підтримки, кваліфікація якої явно замала.

Не впевнений, що цей список дивувань не буде розширений...

В сумі, я б зробив висновок: довіряти цій реалізації можна дуже помірно і обережно. Тестувати всі крайні випадки в обовʼязковому порядку.

PostgreSQL

Мучимо версію 16.11.

Є типи: двійкові: real = float4; double precision = float = float8; десяткові: numeric без обмежень; numeric(precision,scale) з обмеженнями. Float4 і float8 відповідають платформі, вважаємо, IEEE754.

Для отримання типу таки є явна функція:

netch_test=> SELECT pg_typeof(0.1);
 numeric

І працює такий же CAST як в MySQL:

netch_test=> select pg_typeof(cast(0.1 as float));
 double precision

ну не дивно, бо він такий є в стандарті щонайменше з версії 1992.

Хоча є і інший синтаксис:

netch_test=> select pg_typeof(3.14159265358979326::float);
 double precision

При створенні таблиці, real(n) при n ⩽ 24 переходить в real, більше — double precision (в нутрощах PostgreSQL прийнятий запис типу рядковими буквами). Це вже відповідає вказаній вище вимозі стандарту. Float без уточнення переходить в double precision.

Сесійні змінні є, але з іншим синтаксисом і тільки в тексті, тому вони нам не підходять для випробувань:

netch_test=> set a = 1;
ERROR:  unrecognized configuration parameter "a"
netch_test=> set my.a = 1;
SET
netch_test=> select current_setting('my.a');
 1
netch_test=> select pg_typeof(current_setting('my.a'));
 text

Документація каже, що з двійковими типами операції виконуються, як зроблено в хост-системі (тобто вважаємо, що крім IEEE754 варіантів не буде), а ось десяткові не пояснені, тому їх треба окремо дослідити.

Випробуємо такі ж операції, як з MySQL:

netch_test=> select 3.141592653589793238462643383279502884197 + 2.7182818284590452353602874713526624977572 as v;
 5.8598744820488384738229308546321653819542

netch_test=> select 3.141592653589793238462643383279502884197::numeric(100,50) + 2.7182818284590452353602874713526624977572::numeric(100,50) as v;
 5.85987448204883847382293085463216538195420000000000

Здається, тут арифметика чесна. Перевіримо неявне округлення:

netch_test=> select 1.6665::numeric(4,3);
   1.667

netch_test=> select 1.6655::numeric(4,3);
   1.666

netch_test=> select -1.6665::numeric(4,3);
   -1.667

При такій конверсії бачимо чесне half-away. Те ж саме при розміщенні в таблиці.

На додаванні, відніманні, множенні точність результату — максимум з двох точностей арґументів.

На множенні — сумма точностей, як і має бути в ідеалі...

netch_test=> select 3.141592653589793238462643383279502884197 * 2.7182818284590452353602874713526624977572 as v;
 8.5397342226735670654635508695465744950342801113001891285113341016658554438229684

Збігається 1:1 з тим, що каже Python Decimal. Не став перевіряти далі сумми точностей 240 (120+120), мало де воно може бути дійно корисне, але документація каже про 16383 цифри без явного обмеження і 1000 при обмеженні... смакує, але дивно.

А що на діленні? Памʼятаємо, що там потенційно необмежений результат, і усяке урізання буде в чомусь волюнтаристським.

netch_test=> select 0.1 / 3;
 0.03333333333333333333

Це вже цікавіше: хтось продовжив точність. Але до якого значення?

netch_test=> select 0.1::numeric(29,19) / 3;
 0.03333333333333333333 <-- 20 цифр
netch_test=> select 0.1::numeric(30,20) / 3;
 0.03333333333333333333
netch_test=> select 0.1::numeric(31,21) / 3;
 0.033333333333333333333 <-- вже 21 цифра

Те ж саме, якщо «подовжувати» дільник. Проте є знову якесь відхилення:

netch_test=> select 10::numeric(4,2) / 3;
 3.3333333333333333 <-- тільки 17 цифр

1/3 і 0.1/3 мають 20 цифр в такому режимі. 10/3 — тільки 17. Число 17 ми знаємо.

netch_test=> select 10.0::numeric / 3;
 3.3333333333333333
netch_test=> select 10.0::numeric(40,16) / 3;
 3.3333333333333333
netch_test=> select 10.0::numeric(40,17) / 3;
 3.33333333333333333

Цей ефект вже не вкладається в те, що ми бачили... явне 0.1/3 дає 20 цифр. Було б double — було б 17. Справжній метод можна вирахувати з сирців (src/backend/utils/adt/numeric.c): це максимум зі значень: scale (кількість цифр після крапки) першого арґументу, другого, і деякої попередньої оцінки потрібної довжини, вирахованої на базі значень діленого і дільника, округленої догори до кратного 4 (бо внутрішнє представлення numeric іде по 4 цифри в int-лімбі); тому і 17 округлено до 20.

І знову, округлення неконтрольовано захардкоджено в half-away. Як показано в попередніх частинах, це може у деяких випадках давати проблему подвійного округлення. Шкода, що немає явного керування.

Є більше операцій з numeric, включаючи степені і корені. Не вдаватимусь тут вже в деталі, бо сама практична доцільність таких операцій під сумнівом.

Є явна round(), яка визначена в двох варіантах: до цілого — для всіх типів; для вказаної кількости десяткових цифр — тільки для numeric. Виконує, безальтернативно, half-away.

А що з двійковим представленням? Як уже сказано, воно покладено на реалізацію у платформі; зараз вважаємо, що IEEE754. Її принципи ми вже знаємо. Вибору режиму округлення немає, обробки помилок (навіть на рівні читання флаґів помилок) немає, але хоча б мінімальна база є. Двійкова передача робиться у відповідних клієнтах. Текстовий імпорт-експорт — єдине, що залишилось розглянути на принциповому рівні.

Сучасні версії на експорті дають найкоротше еквівалентне десяткове представлення (shortest precise decimal representation), якщо їм це не заборонено. В сучасних версіях воно намагається вивести необхідний мінімум, але до ґарантованої кількости десяткових цифр у типа (6 і 15 відповідно) плюс extra_float_digits (параметр клієнтского зʼєднання, за замовчуванням рівний 3). Це і дає вивід до повного необхідного розміру для конверсії в обидва боки без втрати точности.

Infinity і NaN можуть бути представлені, на відміну від MySQL, в будь-якому з нецілих чисельних типів; в SQL їх треба представляти в апострофах, включаючи варіант '-Infinity'.

Є спеціальна обробка для NaN:

In most implementations of the «not-a-number» concept, NaN is not considered equal to any other numeric value (including NaN). In order to allow numeric values to be sorted and used in tree-based indexes, PostgreSQL treats NaN values as equal, and greater than all non-NaN values.

що відповідає політиці «NULLS LAST» в сортуванні в SQL. Як для рушія БД — дуже розумно. Ця ж політика використовується у формуванні ключа для індексів, і всі NaN, незалежно від знака і пейлоада, зводяться в єдине значення, яке трансформується в байтовий рядок «більший», ніж звичайне число (див. в частині 6 про ґенерацію таких рядків).

Висновок про PostgreSQL: це вже якось схоже на якісну реалізацію (на відміну від MySQL), майже все зроблено рівно і аккуратно, неочікуваного мало. Ідеал не досягнутий (для операцій за межами + - * / хотілось б, як описано в частині 7, контролювати точність більш акуратно, мати можливість задавати явний режим округлення), але вже це можна контролювати.

Висновки

Інші рушії, ніж описані два, я подробно не розбираю. Проте результати в двох словах: суттєво більш адекватного, з загальної точки зору, не знайшов; у всіх ці аспекти зроблені відкрито «на відчепись» людьми, які можуть бути супер-експертами в інших питаннях... але не в цих.

Маленький приклад на дивні ефекти: Before SQL Server 2016 (13.x), conversion of float values to decimal or numeric is restricted to values of precision 17 digits only. Any float value less than 5E-18 (when set using either the scientific notation of 5E-18 or the decimal notation of 0.000000000000000005) rounds down to 0. This restriction doesn’t appear in SQL Server 2016 (13.x) and later versions. (тут)
Це тільки один з реально десятків дивних моментів.

(Якщо комусь важливо розглянути специфіку конкретного рушія з неописаних чи ще більш тонкі аспекти, ласкаво прошу в коментарі.)

Тяжка розбіжність між рушіями окремо і між ними і стандартом, яка має місце майже у всьому (і числові типи тут це кілька відсотків) — це окрема проблема, яка суттєво псує життя всій галузі. Локальні особливості конкретних рушіїв переважають. Привʼязка до конкретного рушія може бути настільки серйозною, що перехід на інший стає катастрофою (навіть все видалити і написати з нуля). Але це для іншої статті. Нам важливе те, що ми розуміємо, на що і як дивитись і як хоча б перевіряти забезпечення потрібних нам характеристик. І хай щастить тому, хто з цим далі працюватиме...

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter