Типы данных, которые вы должны и не должны использовать для финансовой информации в PostgreSQL, и риски, специфичные для JavaScript и Hasura.

Что бы вы ни строили, велика вероятность, что вы захотите монетизировать проект в будущем.

А с монетизацией возникает множество проблем, связанных с хранением финансовых данных и манипулированием ими.

Ниже я рассмотрю типы данных, которые вы должны и не должны использовать для получения денежной информации в Postgres. Затем я рассмотрю дополнительные соображения при использовании этих типов с JavaScript и Hasura.

Денежные типы в PostgreSQL

⚠ Реальная / двойная точность (с плавающей запятой)

Используется для хранения приблизительных значений переменной точности.

Многие начинающие разработчики неосознанно выбирают для хранения финансовых транзакций тип операций с плавающей запятой.

В конце концов, Float является типом по умолчанию для чисел в распространенных языках, включая JavaScript, и ввод const price = 4,99 для элемента за 4,99 доллара имеет интуитивный смысл.

Проблема в том, что арифметика с плавающей запятой не точна на 100%.

Просто попробуйте запустить приведенный ниже код JavaScript в качестве примера:

Или аналогичный скрипт в Postgres:

Такая неточность может привести к значительным финансовым потерям в масштабе. Следовательно, денежные данные никогда не следует хранить как Float, и обычно следует избегать этого типа.

Числовой / десятичный

Используется для хранения точных значений с практически неограниченной точностью, определяемой пользователем (до 131072 цифр до десятичной точки; до 16383 цифр после десятичной точки).

Чтобы обойти проблему точности с плавающей запятой, Postgres предоставляет тип Numeric и его псевдоним Decimal. Эти типы могут хранить числа с очень большим количеством цифр и при этом выполнять точные вычисления.

Давайте повторно запустим наш цикл SQL, описанный ранее, с типом Numeric вместо Float:

Если вы имеете дело только с долларами США и суммами меньше миллиарда долларов, Numeric (12,2) должен дать вам достаточную точность.

Если вы конвертируете между несколькими валютами или имеете дело с товарами, оцениваемыми в дробных центах (например, газом), используйте вместо этого более крупное числовое значение (18, 8).

Деньги

Используется для хранения денежной суммы с фиксированной дробной точностью.

В отличие от MySQL, Postgres также предлагает специальный тип Money, который хранит денежные суммы с фиксированной дробной точностью.

Входные данные могут принимать различные формы, включая целые числа, числа с плавающей запятой и даже строки, такие как «20 долларов США». Вывод также автоматически форматируется как денежное значение:

И дробная точность, и символ валюты определяются настройкой LC_MONETARY базы данных или среды.

Это означает, что вы не можете одновременно работать с долларами и ¥, поэтому использование типа «Деньги» не рекомендуется, если вы имеете дело с более чем одной валютой.

Обратите внимание, что деление денежного значения на целое число выполняется с усечением дробной части до нуля.

Чтобы получить округленный результат, перед делением приведите значение «Деньги» к «Числовому», а затем обратно к «Деньги».

Если для вашего варианта использования важна высокая производительность, операции со значениями типа Money выполняются медленнее, чем со значениями Numeric или Integer.

Int / целое число

Используется для хранения значений без десятичной дроби, от -2147483648 до +2147483647 .

Чтобы избежать проблем с точностью с плавающей запятой, общий способ работы с денежными значениями, особенно в JavaScript, состоит в использовании положительных целочисленных значений, представляющих, сколько взимать в наименьшей денежной единице.

Деление целых чисел и умножение на десятичные дроби могут по-прежнему приводить к неточным значениям, но сравнения, а также базовое сложение, вычитание и умножение целых чисел будут точными.

Если вы когда-либо работали с Stripe, вы уже знакомы с этим шаблоном. Stripe API возвращает 1 доллар США как 100 (центов) и 100 иен как 100 (поскольку иена является валютой с нулевым десятичным числом).

Обратите внимание, что максимальное безопасное целое число в Postgres составляет 2 147 483 647.

Это означает, что вы можете использовать целые числа только для хранения примерно 2 миллиардов центов или 20 миллионов долларов.

Если вам нужно обрабатывать доли цента, например, для конвертации валюты, цен на бензин, показов рекламы или платы за API, вам нужно вместо этого использовать целые микродоллары (0,000001 доллара США).

Это оставляет нам максимальное сохраненное значение всего 2147 долларов.

Если для вашего варианта использования важна высокая производительность, операции с целыми числами выполняются быстрее, чем с деньгами, числами и строками.

BigInt

Используется для хранения значений без десятичной дроби, от -9223372036854775808 до +9223372036854775807

Если максимальное значение целого числа составляет чуть более 20 миллионов долларов, как Stripe может обрабатывать миллиарды каждый день?

Введите BigInt, тип, который может хранить значения до квинтиллионов центов или квадриллионов долларов!

Даже если вы решите хранить ценности в виде микродолларов, BigInt предоставит вам более триллиона для работы.

Обратите внимание, что до недавнего времени JavaScript не поддерживал BigInt, а новый тип, представленный в ECMA-262, по-прежнему требует полифилла для работы в браузере Safari (включая iOS).

Также обратите внимание, что при делении BigInt дробные результаты усекаются, а не округляются:

Наконец, если вы превышаете только максимальные значения Integer в сгруппированных данных, помните, что вы всегда можете преобразовать отдельные значения в BigInt перед вызовом своей функции агрегирования:

Если для вашего варианта использования важна высокая производительность, обратите внимание, что операции с BigInt медленнее, чем с Integer, но быстрее, чем с Money и Numeric.

Работа с деньгами в Хасуре

Прочитав вышеизложенное или бесчисленные аргументы в StackOverflow и других местах, вы, возможно, уже выбрали тот или иной тип хранения ваших финансовых данных.

Возможно, дело в деньгах за форматирование и простоту использования или в Numeric за интуитивную точность.

Но подождите, если вы в конечном итоге собираетесь использовать данные с Hasura и JavaScript, есть еще несколько вопросов, которые следует учитывать!

Риски использования числового типа с JS

Числовой - это, пожалуй, самый популярный тип денежных значений в сообществе Postgres.

Он достаточно интуитивно понятен, достаточно эффективен и не имеет проблем с округлением.

Однако это означает, что вы фактически получите float в JavaScript после запроса данных. Вы должны помнить, что умножайте значение на 100 (или больше, если имеете дело с дробными центами) перед любыми операциями, а затем делите на 100 перед сохранением конечного результата.

То же самое относится и к типу «Деньги».

Риски использования целочисленного типа с JS

Целые числа, наиболее популярный вариант хранения значений валюты в сообществе JavaScript, довольно прост в использовании, но есть одна серьезная проблема, о которой следует помнить.

В то время как MAX_SAFE_INTEGER в JavaScript составляет 9 007 199 254 740 991, максимальное целочисленное значение в Postgres составляет всего 2 147 483 647 (то же самое, что и максимальный безопасный целочисленный размер для побитовых операций в JS).

Если вы имеете дело с очень большими или очень маленькими суммами в валюте, вы сталкиваетесь с риском превышения максимального значения в Postgres без каких-либо ошибок в вашем JS-коде.

Чтобы избежать этой проблемы, рассмотрите возможность использования собственной константы целочисленного ограничения вместо того, чтобы полагаться на Number.MAX_SAFE_INTEGER.

Преобразование и запрос значений BigInt с помощью Hasura

BigInt, пожалуй, самое надежное и производительное решение, если вам нужно иметь дело с очень большими (или очень маленькими) значениями в вашей системе.

Однако важно помнить, что JSON не поддерживает этот тип, и большие целые числа будут ограничены до MAX_SAFE_INTEGER при возврате в запросе!

Чтобы решить эту проблему, включите преобразование числовых типов Postgres в строку с помощью флага --stringify-numeric-types или переменной среды HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES = true.

Если этот параметр включен, Hasura будет вместо этого представлять BigInt (а также числовые, десятичные и т. Д.) Как строки и принимать тот же формат при мутациях:

Затем в Node.js вам нужно не забыть преобразовать String в безопасное значение с помощью функции ES6 BigInt (), а если вам нужно выполнять операции на клиенте Safari, используйте полифилл BigInteger. .

И последнее, но не менее важное: вам следует избегать использования пользовательского интерфейса консоли Hasura для вставки или обновления любых таких больших значений, так как в конце концов они также будут ограничены тегом JavaScript MAX_SAFE_INTEGER.

Понравился этот пост? Следуй за мной в 🐦Twitter @seifip