Документация PostgreSQL по numeric содержит два плохо согласующихся утверждения:
«especially recommended for storing monetary amounts and other quantities where exactness is required» — и сразу же: «calculations on numeric values are very slow compared to the integer types, or to the floating-point types». То есть рекомендуют для хранения денежных величин и тут же признают, что это весьма дорого.
Для меня, как разработчика СУБД это сигнал к действию. Если операции с типом заметно медленнее bigint, возникает соблазн: а нельзя ли хранить денежные величины целым числом копеек и округлять по стандартному правилу? Это бы прилично сэкономило вычислительные ресурсы наших серверов баз данных, разве нет? А что, если вообще использовать double precision?
Но прежде чем оптимизировать тип или менять его на целое, стоит понять, что от него на самом деле требуют: закон, форматы обмена данными, прикладные платформы. Правда ли, что точный десятичный тип — стандарт для финансовых приложений, пусть даже и де-факто? Или это инженерный фольклор, который можно спокойно обойти?
Не будем полагаться на обзорную литературу и копнём первоисточники. Эта задача не была простой, однако с AI-агентами стало сильно легче. Так что засучим рукава и приступим. Если текст покажется чрезмерно сухим или скучным — то он такой и есть. Поэтому здесь имеется оглавление, чтобы была возможность быстро найти то, что нужно.
Содержание
1. Что говорит стандарт SQL
В ISO SQL нет типа MONEY. Нет не только типа — в стандарте языка SQL вообще отсутствует понятие валюты. Тип money в PostgreSQL и money/smallmoney в SQL Server — вендорские расширения, а не реализация стандарта.
Зато имеется три категории числовых типов:
-
Exact numeric types:
NUMERIC,DECIMAL,SMALLINT,INTEGER,BIGINT. -
Approximate numeric types:
FLOAT,REAL,DOUBLE PRECISION. -
The decimal floating-point type:
DECFLOAT.
У типов Numeric и Decimal есть небольшая семантическая разница.
Подраздел 6.1 «data type», Syntax Rules 28 и 29:
«NUMERIC specifies the data type exact numeric, with the decimal precision and scale specified by the precision and scale. 29) DECIMAL specifies the data type exact numeric, with the decimal scale specified by the scale and the implementation-defined (ID063) decimal precision equal to or greater than the value of the specified precision.»
Для понимания различия: NUMERIC(15,2) — это жёсткое ограничение ровно на 15 цифр, DECIMAL(15,2) — на «не меньше 15». По этому определению оба типа можно скомбинировать в один, чем и пользуется numeric в PostgreSQL.
Стандарт не запрещает реализацию точного десятичного типа поверх двоичного целого. Фиксированная ширина на базе int64/int128 — это предусмотренный вариант. Это даёт возможность существовать вариантам реализации в Arrow, SQL Server, DuckDB и пр.
Подраздел 4.5.2 «Characteristics of numbers»:
«An exact numeric type has a precision P and a scale S. P is a positive integer that determines the number of significant digits in a particular radix R, where R is either 2 or 10. S is a non-negative integer. Every value of an exact numeric type of scale S is of the form n × 10⁻ˢ, where n is an integer such that −Rᴾ ≤ n < Rᴾ.»
Итого: точный десятичный тип DECIMAL/NUMERIC входит в обязательное ядро SQL (фича E011-03), а DECFLOAT — опциональная (T076), наравне с BIGINT.
Также для анализа требований к типу numeric важно, что стандарт SQL ничего не говорит про минимальную и максимальную точность, но накладывает ограничения на арифметику: масштаб сложения, правила сложения, вычитания и умножения заданы жёстко. Масштаб деления отдан на откуп реализации.
2. Чего требуют закон и регуляторы
Регламент Совета ЕС № 1103/97 о введении евро хорошо проработан и достаточно однозначно требует шесть значащих десятичных цифр без округления и усечения, и фиксирует детерминированное поведение при округлении за пределами точности. Тип float под такие требования не подходит.
Статья 4 этого регламента задаёт саму механику пересчёта: курс берётся с шестью значащими цифрами, округлять или усекать его запрещено, а пересчёт из одной национальной валюты в другую идёт только через евро. И финальная оговорка: любой другой метод расчёта допустим лишь при условии, что он даёт тот же результат.
Цитата
«The conversion rates shall be adopted with six significant figures.»
«The conversion rates shall not be rounded or truncated when making conversions.»
«Inverse rates derived from the conversion rates shall not be used.»
«Monetary amounts to be converted from one national currency unit into another shall first be converted into a monetary amount expressed in the euro unit, which amount may be rounded to not less than three decimals… No alternative method of calculation may be used unless it produces the same results.»
Статья 5 добавляет то, чего в стандартах не найти вообще: правило поведения ровно на половине. Суммы к уплате округляются до ближайшего цента, а если пересчёт дал ровно половину — вверх.
Цитата
«Monetary amounts to be paid or accounted for when a rounding takes place after a conversion into the euro unit pursuant to Article 4 shall be rounded up or down to the nearest cent. … If the application of the conversion rate gives a result which is exactly half-way, the sum shall be rounded up.»
Британский налоговый регулятор формулирует то же самое, но в долях пенни. HMRC требует считать НДС с точностью до трёх знаков и округлять по тому же правилу: меньше половины пенни — вниз, половина и больше — вверх (HMRC VAT Trader Records, VATREC12030).
Цитата
«If the VAT on any transaction comes to less than 0.5 of one penny, it should be rounded down. If the VAT comes to 0.5 of one penny or more, it should be rounded up.»
Таким образом, у регуляторов не удаётся найти прямого предписания «используйте точный десятичный тип». Но из их совокупности складывается функционально эквивалентное требование, и оно состоит из четырёх частей:
-
Хранение обязано быть точным до минимальной денежной единицы. Двоичный
floatне представляет 0,01 точно, значит система наdouble precisionнарушает это по построению, независимо от того, насколько аккуратно написан прикладной код. -
Округление — нормируемая операция в единственной точке, а не побочный эффект арифметики. Регламент ЕС формулирует это предельно жёстко: курсы «shall not be rounded or truncated», а округление до цента происходит ровно один раз, после пересчёта.
-
Поведение ровно на половине задано явно. И ЕС, и HMRC отдельно оговаривают случай
0,5: округлять вверх. Тип, у которого правило ничьей зависит от реализации, такую норму сам по себе не обеспечивает. -
Разрядность зависит от валюты, а не от типа: три знака у ЕС на промежуточном пересчёте, доли пенни у HMRC. Значит масштаб должен быть параметром модели, а не константой, зашитой в схему.
Закон не называет тип, но описывает поведение. В PostgreSQL этому набору требований удовлетворяет numeric: точное десятичное хранение, отсутствие неявного округления, явно управляемый масштаб.
И тут же вылезает то, чего ни один регулятор не предусмотрел. Правило округления, которое закон задаёт однозначно, в СУБД не зафиксировано — оно различается и между системами, и между типами внутри одной системы. Bill Schneider разбирает расхождение Spark и SQL Server на одних и тех же десятичных данных: одна система округляет, другая усекает. IBM в Db2 пошла дальше всех и просто вынесла правило в настройку — семь режимов на выбор, значение по умолчанию задаётся при установке. То есть требование «округляй вверх на ровно половине» тип данных вам не гарантирует ни в одной СУБД: округление придётся делать явно и в одном месте, как того и требует пункт 2.
3. Форматы обмена финансовыми данными
Закон говорит о поведении, но не о представлении. А вот форматы обмена вынуждены определяться: им нужно договариваться о конкретной кодировке, иначе две системы просто не поймут друг друга.
ISO 20022 — это то, на чём работают SWIFT, SEPA и национальные платёжные системы. Схемы сообщений лежат в официальном каталоге, и денежная величина в них определена так (camt.053):
<xs:simpleType name="ActiveCurrencyAndAmount_SimpleType"> <xs:restriction base="xs:decimal"> <xs:totalDigits value="18"/> <xs:fractionDigits value="5"/> </xs:restriction></xs:simpleType><xs:complexType name="ActiveCurrencyAndAmount"> <xs:extension base="ActiveCurrencyAndAmount_SimpleType"> <xs:attribute name="Ccy" type="ActiveCurrencyCode" use="required"/> </xs:extension></xs:complexType>
То есть структурно это NUMERIC(18,5) плюс обязательный код валюты, приделанный к самому значению: сообщение без Ccy не проходит валидацию. Прямого запрета на плавающую точку в тексте нет, но суть понятна: в официальном JSON-биндинге суммы передаются строками, а значит фиксированный размер предпочтителен.
ISO 20022 JSON Schema draft, 10.06.2025
«Number type are represented as strings because there was a preference for validating the total digits and fraction digits in the schema.»
FIX (биржевая торговля) устроен любопытно: тип там называется float, но никакой двоичной плавающей точки за этим названием нет. В текстовом FIX 4.4 float — это последовательность цифр с необязательной точкой и знаком, то есть десятичное число символами, и спецификация требует вместить не меньше пятнадцати значащих цифр.
FIX 4.4 dictionary, Onix
«Sequence of digits with optional decimal point and sign character… All float fields must accommodate up to fifteen significant digits.»
В FIXML привязка явная:
float, Qty, Price, Amt, Percentage → xs:decimal.
А в бинарной кодировке SBE — двоичном формате, на котором биржи гоняют потоки котировок, — явно указано использовать десятичные кодировки для цен и всего денежного, а двоичная плавающая точка — только для тех числовых полей, которые не являются ценами или денежными суммами.
FIX SBE v1.0 RC4, Field Encoding
«Decimal encodings should be used for prices and related monetary data types like PriceOffset and Amt.»
«Binary floating point encodings are compatible with IEEE Standard for Floating-Point Arithmetic (IEEE 754-2008). They should be used for floating point numeric fields that do not represent prices or monetary amounts.»
4. Платежные системы: масштаб как атрибут валюты
Отдельно стоит разобрать платежи, потому что там сложилось решение, которого нет ни в стандарте, ни в законе. Платёжная индустрия деньги дробным числом не передаёт вообще. А причина здесь скорее всего в возрасте системы, стандартах и тех возможностях ИТ, которые определили эти стандарты в 60–70-х гг.
ISO 8583, протокол карточных сетей, задаёт элемент данных DE 4 «Amount, transaction» форматом n 12 — двенадцать цифр, и всё. В чисто числовом поле фиксированной длины десятичному разделителю просто негде разместиться, поэтому масштаб берётся снаружи — из поля «Currency code». Сумма на проводе — целое, а сколько у него знаков после запятой, определяет валюта. Количество знаков после запятой у валют разное: у JPY, KRW и VND — ноль, у USD, EUR и GBP — два, а у BHD, KWD, OMR, TND и JOD — три.
Таким образом в платежах масштаб — это атрибут валюты, а не свойство числа. Он не хранится вместе со значением и не передаётся вместе с ним; он лежит в отдельном поле, а число остаётся целым.
Платёжные HTTP-API сохраняют карточное наследие, а те, кто пришёл позже и не из карточного мира, часто выбирают способ представления десятичной строкой:
|
Система / формат |
Величина |
Формат сообщения |
|---|---|---|
|
ISO 8583 |
целое |
binary |
|
целое |
binary |
|
|
целое |
JSON |
|
|
целое |
JSON |
|
|
целое |
JSON |
|
|
целое, копейки |
JSON |
|
|
целое |
protobuf (binary), JSON в REST |
|
|
десятичное |
JSON |
|
|
десятичное |
JSON (GraphQL) |
|
|
десятичное |
JSON |
|
|
десятичное |
JSON |
|
|
Wise (legacy v1) |
десятичное |
JSON |
|
десятичное ( |
JSON |
|
|
десятичное ( |
XML |
|
|
десятичное, текст |
текст (tag=value) |
Из этой таблицы вылезает третий слой требований, о котором обычно не думают. Спорят про хранение — представима ли копейка. Иногда доходят до вычислений — детерминированность, точка округления, правило на ровно половине. Но есть ещё сериализация: значение должно пережить переход через границу, где его разбирает парсер, который вы не писали и не контролируете.
Требование на этом слое устроено иначе, чем на первых двух. Здесь нужна не точность внутри вашей системы, а согласие: две независимые реализации на разных языках обязаны сойтись на одном и том же значении, до последней цифры. И этому удовлетворяет ровно одна конструкция — точное целое плюс масштаб, заданный вне значения. Не потому, что целые «точнее», а потому что это единственный числовой тип, о котором договорились все языки и все парсеры без исключения. Масштаб при этом едет отдельной дорогой: в схеме потока (Arrow, Parquet, FIX SBE), в соседнем поле (units + nanos у Google) или в коде валюты (ISO 8583, Stripe, Adyen).
Так что платёжная практика точный десятичный тип не отвергает — она добавляет к нему третье, независимое требование, которого нет ни в законе, ни в стандарте SQL.
5. Чего требуют TPC-бенчмарки
Бенчмарки — источник особого рода: они не описывают, как надо, а фиксируют то, на чём вендоры согласились соревноваться. И тут обнаруживается расслоение. Бенчмарк TPC-C требует точных вычислений и следования стандарту SQL.
Standard Specification Rev. 5.11, клауза 1.3.1
«Numeric fields that contain monetary values (W_YTD, D_YTD, C_CREDIT_LIM, C_BALANCE, C_YTD_PAYMENT, H_AMOUNT, OL_AMOUNT, I_PRICE) must use data types that are defined by the DBMS as being an exact numeric data type or that satisfy the ANSI SQL Standard definition of being an exact numeric representation.»
То же в TPC-E, только строже: там денежные типы объявлены с конкретной разрядностью — баланс как SENUM(12,2), агрегаты как SENUM(15,2), — и реализация обязана обеспечить точное представление заявленных знаков после запятой.
v1.14.0, клауза 2.2.1
«ENUM and SENUM… must be implemented using a Native Data Type which provides exact representation of at least n Digits of precision after the decimal place.»
«BALANCE_T is defined as SENUM(12,2)… FIN_AGG_T is defined as SENUM(15,2)…»
Бенчмарки, ориентированные на аналитику, — TPC-H и TPC-DS — снижают требования к точности. Для Integer требование точности сформулировано жёстко, а для Decimal нет: в агрегатах допуск составляет 1% (AVG и отношения) и «within $100» (SUM). Точное соответствие требуется только в COUNT.
TPC-H v3.0.1, клауза 1.3.1
«Decimal means that the column must be able to represent values in the range −9,999,999,999.99 to +9,999,999,999.99 in increments of 0.01; the values can be either represented exactly or interpreted to be in this range»
Интересный вывод: транзакционные бенчмарки требуют точного типа дословно, аналитические — явно разрешают приближение. Такое послабление потенциально позволяет включать различные оптимизации для запроса, объявленного как «аналитический».
6. Что говорят вендоры, авторитеты и практики
Дальше стоит посмотреть, что об этом пишут те, кто должен знать: разработчики самих СУБД и авторы, на которых в таких спорах принято ссылаться.
С вендорами картина однородная. Документация PostgreSQL, SQL Server, MySQL и материалы IBM сходятся в одном: там, где нужен точный ответ, двоичную плавающую точку использовать не следует, и финансовые расчёты упоминаются как пример прямым текстом. Но обратите внимание, чего в этих текстах нет. Ни один вендор не говорит «используйте numeric» — все формулируют требование от противного, оставляя выбор между точным десятичным типом и целым числом минимальных единиц открытым.
Ровно то же самое у авторитетов, и это, пожалуй, главный сюрприз для меня: их обычно цитируют в поддержку decimal, а на самом деле они говорят другое.
Джошуа Блох в «Effective Java» (3-е издание, Addison-Wesley; item 60 «Avoid float and double if exact answers are required») действительно запрещает float и double там, где нужен точный ответ. Но дальше он ставит BigDecimal и целые типы на равных: BigDecimal — если хочется, чтобы за десятичной точкой следила система, и вы готовы платить за это неудобством и производительностью; целое — если производительность критична. И даёт конкретную границу: до девяти знаков хватит int, до восемнадцати — long.
Цитата
«In summary, don’t use float or double for any calculations that require an exact answer. Use BigDecimal if you want the system to keep track of the decimal point and you don’t mind the inconvenience and cost of not using a primitive type… If performance is of the essence… use int or long. If the quantities don’t exceed nine decimal digits, you can use int; if they don’t exceed eighteen digits, you can use long.»
Мартин Фаулер в Patterns of Enterprise Application Architecture выделяет денежную величину в отдельный паттерн Money — и суть его не в выборе типа, а в том, что сумма и валюта должны ездить вместе, а округление до минимальной единицы быть явной операцией. Про представление Фаулер категоричен только в одном: не двоичная плавающая точка. Между целым и десятичным он не выбирает. Книгу можно купить.
А вот в постгресовом сообществе позиции расходятся, и обе стоит привести. Ханс-Юрген Шёниг из Cybertec высказывается однозначно: деньги требуют особых правил округления, поэтому финансовые данные надо держать в numeric.
Цитата
«In the case of money, different rounding rules are needed, which is why numeric is the data type you have to use to handle financial data.»
Элизабет Кристенсен из Crunchy Data даёт рекомендацию с развилкой, и она ближе к тому, что мы видели у Блоха: целое — если вас устраивают целые копейки и дробные не нужны; numeric — если нужны доли копейки и вообще много знаков. И отдельным пунктом: валюту хранить рядом, но отдельным полем.
Цитата
«Use int or bigint if you can work with whole numbers of cents and you don’t need fractional cents.» «Use decimal / numeric for storing money in fractional cents and even out to many many decimal points.» «Store currency separately from the actual monetary values…»
Самый развёрнутый аргумент против целых копеек — у Otar Chekurishvili, и он не про точность, а про место, где живёт ошибка. Храня 1999 вместо 19.99, вы не решаете задачу, а выносите её из базы во все остальные слои приложения: каждый, кто прочитает эту колонку, обязан знать про неявный масштаб.
Цитата
«When you store 1999 instead of 19.99, you don’t actually solve a problem. You move it out of the database and into every other layer of the app.»
Показательно и то, как поступила спецификация Java для денег (JSR 354): она намеренно не фиксирует представление, поскольку требования к нему слишком разные в разных сценариях.
Цитата
«JSR 354 explicitly supports different types of monetary amounts to be implemented and used. Reason behind is that the requirements to an implementation heavily vary for different usage scenarios.»
И это не абстрактная осторожность: референсная реализация везёт сразу два класса — Money поверх BigDecimal и FastMoney поверх long. В javadoc второго прямо написано, что он в 10–15 раз быстрее, и платой за это становится ограниченная точность.
7. Что происходит в ERP
Остаётся посмотреть на прикладные системы, которые с деньгами работают каждый день, — и заодно проверить, совпадает ли объявленный тип с тем, что реально происходит с числом.
-
SAP —
packed decimalплюс обязательное поле валюты ABAP docs, currency field. -
Oracle E-Business Suite / Fusion —
NUMBERвообще без точности и масштаба. GL_JE_LINES. -
Odoo — самый интересный случай, к нему вернёмся отдельно. Колонка в базе имеет тип
numeric(odoo/fields.py 17.0), а значение в ORM — Pythonfloat; отсюда и целый модуль хелперов odoo/tools/float_utils.py. -
1С. Тип «Число» в платформе — ИТС, «Разрядность результатов выражений и агрегатных функций». Максимальная разрядность 38 знаков, нотация вида
Число(17,4), правила вывода разрядности результата для сложения, умножения и деления.
Про Odoo стоит сказать отдельно, потому что это неожиданный результат. Получается production-ERP, у которой колонки объявлены как numeric, а вся арифметика ведётся в двоичном double — со всеми вытекающими, из-за которых и пришлось написать float_utils.py с функциями сравнения и округления. Вывод из этого шире, чем один вендор: тип колонки в схеме сам по себе не означает, что вычисления идут в десятичной арифметике. Схема гарантирует только хранение. Если приложение вытащило значение в double, посчитало и записало обратно, точность потерялась в прикладном слое, а база при этом выглядит безупречно.
И общий вывод по разделу, который важен для ответа на вопрос статьи. В платежах, как мы видели, масштаб — атрибут валюты: он один и приходит извне. В учётных системах всё иначе. У 1С разрядность задаётся на каждый реквизит отдельно, нотацией Число(N,M): суммы обычно с двумя знаками, количества с тремя, коэффициенты и курсы — с большим числом. У SAP денежное поле обязано идти в паре с полем валюты, но масштаб при этом определяется видом величины, а не только валютой. Иными словами, здесь масштаб принадлежит предметной области, а не денежной единице, и в одной базе их одновременно несколько. Именно поэтому целое число минимальных единиц ERP не спасает: одной константы масштаба на всю схему не хватит, а SQL задаёт масштаб типом колонки, а не значением.
8. Куда движется точный десятичный тип
Теперь про тенденцию. Точный десятичный тип никуда не уходит — наоборот, расширяется.
-
Международные платежи с конца 2025 года живут на ISO 20022 — и разрядность денежной величины там
totalDigits="18", то есть меньше, чем даётint64. Формат, через который идут межбанковские расчёты всего мира, в произвольной точности не нуждается. -
C23 внёс
Decimal32/64/128в стандарт языка C (cppreference, C23) — правда, опционально, через макрос_STDC_IEC_60559_DFP__. GCC поддерживает его частично, Clang и MSVC — пока нет. (RFC в LLVM всё ещё открыт). -
DECFLOATиз SQL:2016 реализован не только в Db2, но и в Firebird 4.0 (README: floating_point_types). -
Точный десятичный тип есть во множестве СУБД и во всех колоночных форматах.
А вот в аналитических движках картина разнородная, и она хорошо показывает, где точность считают обязательной, а где договорной. Apache Druid точного числового типа не имеет вовсе и отклонил предложение его добавить. ClickHouse спокойно живёт с Float64 и лишь рекомендует Decimal там, где нужна точность. А Elasticsearch и Power BI выбрали третий путь — точную арифметику на фиксированном масштабе поверх целого. То есть аналитика сошлась не на «точном типе», а на «точном сложении на фиксированном масштабе», и это ровно то послабление, которое разрешают TPC-H и TPC-DS.
Потолок в 38 цифр
А вот что не актуально, так это произвольная точность переменной длины. Потолок в 38 цифр (int128) выглядит де-факто универсальной константой:
|
Система |
Потолок |
Представление |
|---|---|---|
|
38 |
адаптивная ширина по фактическому диапазону |
|
|
38 |
int64 до 19 цифр, int128 до 38 |
|
|
38 |
5/9/13/17 байт |
|
|
38 |
long fast-path ≤18 цифр, BigDecimal дальше |
|
|
38 |
INT16/32/64/128 |
|
|
38 |
«precision must be 38 or less» |
|
|
76 |
int32/64/128/256 |
|
|
38 / ~76.8 |
int128 с разным scale |
|
|
35 |
int128, 16 байт, precision и scale в типе |
Чем платят за фиксированную ширину
Каждое техническое решение подразумевает какие-то компромиссы. Не обходится без этого и с СУБД, которые ограничивают точный десятичный тип. Redshift, например, прямо отговаривает пользователей брать максимальную точность «на всякий случай»: 128-битные значения занимают вдвое больше места, чем 64-битные, и замедляют выполнение запросов (документация):
Цитата
«Do not arbitrarily assign maximum precision to DECIMAL columns unless you are certain that your application requires that precision. 128-bit values use twice as much disk space as 64-bit values and can slow down query execution time.»
Apache Arrow фиксирует ровно четыре ширины и прямо описывает представление: точное десятичное значение хранится как целое в дополнительном коде — 32, 64, 128 или 256 бит, а масштаб живёт в схеме потока (Schema.fbs):
Цитата
«Exact decimal value represented as an integer value in two’s complement. Currently 32-bit (4-byte), 64-bit (8-byte), 128-bit (16-byte) and 256-bit (32-byte) integers are used.» / «The accepted widths are 32, 64, 128 and 256.»
Причём эволюция идёт в сторону сужения: Arrow 18.0.0 (октябрь 2024) добавил Decimal32 и Decimal64, а не более широкие типы. Легко можно понять, что суть здесь в повышении производительности: 32-битные и 64-битные операции сильно легче 128-битных в текущих аппаратных системах.
Самый показательный источник — CedarDB, коммерческий наследник Umbra, движок, спроектированный в 2020-х. Он явно и письменно противопоставляет себя PostgreSQL: там, где PostgreSQL даёт точность до 131072 цифр, CedarDB ограничивает её 38 — и прямым текстом объясняет, что сделано это ради производительности. Заодно рекомендует не выходить за 18 цифр, потому что операции над 16-байтовыми значениями дороги, и запрещает NaN и бесконечности, которые PostgreSQL разрешает (документация по numeric):
Цитаты
«PostgreSQL offers a maximum precision of 131072 and scale of 16383, where CedarDB restricts precision and scale to a maximum of 38, for performance reasons.»
«Operations on 16 Byte types are expensive to compute. We recommend using a precision of 18 or less when possible for your application.»
«PostgreSQL allows NaN, +Infinity, and -Infinity as special numeric values» — «CedarDB forbids entering these values as numeric data types.»
Команда, которая целенаправленно делает быстрый PostgreSQL-совместимый движок, отказалась ровно от того, что делает numeric медленным: от произвольной точности, от varlena и от специальных значений.
Понятно, что у всего есть своя цена. ClickHouse, например, честно документирует, что у широких фиксированных типов проверки переполнения нет вовсе. То есть у Decimal32/Decimal64 переполнение целой части даёт исключение, а у Decimal128/Decimal256 — молча неверный результат.
Поскольку у типа фиксированной ширины бюджет разрядов конечен, то разработке приходится балансировать. В данном случае, приходится искать баланс между шириной диапазона, проверками переполнения и специальными значениями. В таблице ниже компромиссы, которые удалось идентифицировать:
|
Движок |
Чем заплатил |
|---|---|
|
ClickHouse |
проверками переполнения — у |
|
CedarDB |
специальными значениями — |
|
YDB |
диапазоном — три десятичных разряда из 38 отданы под служебные значения |
|
PostgreSQL |
ничем |
Показательно и то, что YDB спроектирован независимо и в те же 2010–2020-е, что CedarDB и DuckDB, — и пришёл к той же конструкции: целое фиксированной ширины, масштаб в типе, потолок в районе 35–38. Произвольную точность переменной длины не выбрал никто.
9. Выводы
Собственно, что даёт нам это небольшое исследование?
Во-первых, область применения real и double precision для величин, над которыми выполняется арифметика, стремится к нулю.
А вот между целым и точным десятичным выбор действительно есть. Различает их то, чем задан масштаб и как выполняется округление. Если масштаб приходит извне и одинаков для всех строк — из кода валюты, из константы протокола, а правила округления неважны — целое работает идеально. Если же масштаб задан предметной областью, разный у разных полей и настраивается в работающей системе — целое не подходит в принципе, потому что одной константы масштаба на всю базу не хватит. Otar Chekurishvili в тексте Storing money as integer cents is often over-engineering очень точно подметил: целое не решает проблему, а перекладывает её на приложение.
Третий вывод оказался достаточно неожиданным. Обычно дискутируют вопросы хранения — как представлять копейки. Иногда доходят до вычислений — детерминированность, точка округления, правило на ровно половине. Но есть и третий аспект — сериализация: значение должно пережить переход через границу, где его разбирает парсер, который вы не писали и не контролируете.
Производительность точного десятичного типа при этом явно стоит на повестке у разработчиков движков, и общий ответ выглядит одинаково — ограничить ширину. Типовым потолком стали 38 цифр. Однако, это не всегда влазит в int128 и приходится искать компромисс: кто-то ослабляет проверки переполнения, кто-то выкидывает специальные значения вроде NAN или Infinity, кто-то часть диапазона. Никто не выбрал произвольную точность переменной длины — кроме PostgreSQL.
И главный постгресовый инсайт для меня состоит в том, что системе встроенных типов PostgreSQL вероятно не хватает numeric ограниченной ширины и точности. Именно эту конструкцию выбрали Arrow, SQL Server, DuckDB, ClickHouse, YDB и Power BI, и именно её у нас нет. Да, история знает как минимум три заброшенные попытки сделать «быстрый» decimal расширением. Но, судя по тому, куда двигаются все остальные, игнорировать этот запрос будет всё труднее.
А вы что думаете? Поделитесь своим мнением в комментариях!
THE END,
13 августа 2026 г., Мадрид, Испания.
ссылка на оригинал статьи https://habr.com/ru/articles/1070300/