Learning Platform
Глоссарий Troubleshooting
Урок 05.01 · 22 мин
Средний
type-systemprimitivesdecimal

Примитивные типы: целые, decimal, float, строки, blob

Тип колонки в DuckDB — это не аннотация для документации.

Числовые типы в PostgreSQL: INT, BIGINT, NUMERIC и float Это решение о том, сколько байт занимает каждое значение, как оно раскладывается в памяти, какие операции над ним возможны без потери точности и насколько хорошо колонка сожмётся на диске. DuckDB — колоночная аналитическая СУБД, и в ней колонка одного типа лежит сплошным массивом фиксированного формата. Выбор типа определяет геометрию этого массива.

В этом уроке разберём примитивные типы — те, что хранят одно скалярное значение: целые числа, точные десятичные DECIMAL, числа с плавающей точкой, строки и бинарные BLOB. Главная мысль урока — типы не взаимозаменяемы. DECIMAL и DOUBLE оба «дробные числа», но ведут себя принципиально по-разному, и путаница между ними — источник реальных финансовых багов.


Целые типы: ширина определяет диапазон

DuckDB предлагает целочисленные типы пяти размеров. Каждый — это фиксированное число байт, и оно жёстко задаёт диапазон представимых значений:

ТипРазмерДиапазон (знаковый)
TINYINT1 байтпримерно от -128 до 127
SMALLINT2 байтапримерно +/- 32 тысячи
INTEGER4 байтапримерно +/- 2.1 миллиарда
BIGINT8 байтпримерно +/- 9.2 квинтиллиона
HUGEINT16 байтпримерно +/- 1.7 в 38-й степени

У каждого есть беззнаковый вариант (UTINYINT, UINTEGER и т. д.): он отдаёт бит знака под величину и удваивает верхнюю границу, но запрещает отрицательные значения.

Ширина типа — это компромисс. Узкий тип занимает меньше места и быстрее обрабатывается (в один вектор фиксированного размера влезает столько же значений, но каждое легче). Широкий тип защищает от переполнения. Practическое правило: берите самый узкий тип, который гарантированно вместит все возможные значения колонки с запасом на рост. Для счётчика товаров на складе INTEGER избыточен — хватит SMALLINT или INTEGER; для глобального счётчика событий нужен BIGINT.

WARNING

Переполнение целого типа в DuckDB — это ошибка времени выполнения, а не молчаливое «заворачивание» значения. Если результат арифметики не влезает в тип, запрос упадёт с сообщением об overflow. Это безопаснее тихого переполнения (как в C), но означает, что слишком узкий тип для растущего счётчика однажды уронит пайплайн. Закладывайте запас.

HUGEINT (128-битное целое) нужен редко — для промежуточных результатов, где BIGINT может переполниться, например при перемножении больших величин или в основе DECIMAL высокой точности.


DECIMAL против DOUBLE: точность против скорости

Здесь — самая важная развилка урока. DECIMAL и DOUBLE оба представляют дробные числа, но устроены принципиально по-разному, и от выбора зависит корректность расчётов.

DOUBLE (он же FLOAT8) и REAL (FLOAT4) — это числа с плавающей точкой стандарта IEEE 754, 8 и 4 байта. Они хранят число как мантиссу и двоичную экспоненту. Ключевое следствие: десятичные дроби, не представимые точно в двоичной системе, хранятся приближённо. Число 0.1 в двоичном представлении — бесконечная периодическая дробь, и DOUBLE округляет её. Накопленная ошибка проявляется в классическом эффекте:

-- Плавающая точка: 0.1 + 0.2 не равно ровно 0.3
SELECT 0.1::DOUBLE + 0.2::DOUBLE AS sum_double;
-- результат: 0.30000000000000004

SELECT (0.1::DOUBLE + 0.2::DOUBLE) = 0.3::DOUBLE AS is_equal;
-- результат: false

DECIMAL устроен иначе. DECIMAL(precision, scale) хранит число как целое, масштабированное на 10^scale. DECIMAL(10, 2) — это 10 значащих цифр всего, 2 после запятой; внутри значение 123.45 хранится как целое 12345. Никакого двоичного приближения — десятичные дроби представляются точно:

-- DECIMAL: арифметика точная
SELECT 0.1::DECIMAL(4,2) + 0.2::DECIMAL(4,2) AS sum_decimal;
-- результат: 0.30  (ровно)
DECIMAL и DOUBLE хранят дробь по-разному
DECIMAL(10,2)Хранит число как масштабированное целое: 123.45 лежит как целое 12345. Десятичные дроби представляются точно, без приближения.
точная арифметика
Деньги, счетаФинансовые расчёты требуют, чтобы 0.1 + 0.2 давало ровно 0.30. DECIMAL это гарантирует.
DOUBLE (IEEE 754)Хранит число как мантиссу и двоичную экспоненту. Десятичные дроби, не представимые в двоичной системе, округляются.
накопленная ошибка
Метрики, наукаДля измерений, координат, ML-фичей крошечная ошибка несущественна, зато DOUBLE быстрее и компактнее.

Когда что применять — однозначное правило:

Используйте DECIMALИспользуйте DOUBLE / REAL
Деньги, цены, суммы счетовНаучные измерения, координаты
Любые величины, где «копейки» обязаны сходитьсяML-фичи, статистика, доли
Бухгалтерия, налоги, финансовая отчётностьПромежуточные расчёты, где скорость важнее точности

Цена точности DECIMAL — скорость и размер: операции над масштабированными целыми сложнее, чем над аппаратным DOUBLE (который процессор считает «бесплатно»), а высокая precision занимает больше байт. Но для денег это не обсуждается: финансовый отчёт, где сумма не сходится на копейку из-за DOUBLE, — это баг, который стоит дороже любой экономии на скорости.

DANGER

Никогда не храните деньги в DOUBLE или REAL. Накопленная ошибка плавающей точки приводит к тому, что суммы не сходятся, сравнения на равенство дают неожиданный результат, а итоги отчёта расходятся с источником. Для денежных колонок всегда DECIMAL с явными precision и scale (обычно scale=2 для валют с копейками).


Строки: VARCHAR без ограничения длины

Текст в DuckDB — это тип VARCHAR (синонимы TEXT, STRING). Важная особенность: указание длины не влияет на хранение. VARCHAR(10) и VARCHAR хранятся одинаково — DuckDB не дополняет строку до фиксированной длины и не обрезает её. Длина в скобках — это в лучшем случае декларативное ограничение, но не способ хранения. Строки всегда переменной длины.

На уровне физического представления у строк DuckDB две формы. Короткие строки (примерно до 12 байт) хранятся inline — прямо внутри структуры вектора, без отдельной аллокации. Длинные строки хранятся как указатель на внешний буфер плюс короткий префикс. Префикс позволяет быстро сравнивать строки: если префиксы различаются, полное сравнение не нужно. Эта деталь — почему сравнения и группировки по строкам в DuckDB быстрые: движок часто обходится первыми байтами.

-- VARCHAR(5) не обрежет и не дополнит — длина в скобках декоративна
SELECT 'analytics'::VARCHAR(5) AS s, length('analytics'::VARCHAR(5)) AS len;
-- s = 'analytics', len = 9

Строки в DuckDB хранятся в кодировке UTF-8. Функция length() возвращает число символов (code points), а не байт — для подсчёта байт есть отдельная функция. Это важно для не-ASCII текста: кириллический символ в UTF-8 занимает 2 байта, и length посчитает его как один символ, а размер в байтах будет больше.

Две физические формы VARCHAR
Короткая строка (примерно до 12 байт)Хранится inline — прямо внутри структуры вектора, без отдельной аллокации памяти.
vs
Длинная строкаХранится как указатель на внешний буфер плюс короткий префикс. Префикс позволяет сравнивать строки без полного чтения.

BLOB: сырые байты

BLOB (Binary Large Object) хранит произвольную последовательность байт без какой-либо интерпретации. Если VARCHAR — это текст в кодировке UTF-8, который движок понимает как символы, то BLOB — это просто байты: изображение, сериализованный объект, хеш, зашифрованные данные.

-- BLOB-литерал: \x задаёт байты в hex
SELECT '\xDE\xAD\xBE\xEF'::BLOB AS raw_bytes;

-- Превратить строку в её байтовое представление
SELECT encode('привет') AS utf8_bytes;
-- результат: BLOB с UTF-8 байтами строки

Принципиальная разница между VARCHAR и BLOB — в семантике, а не в ёмкости. Над VARCHAR работают строковые функции (upper, length по символам, LIKE), потому что движок знает: это текст. Над BLOB строковые функции по символам не имеют смысла — там нет символов, только байты. BLOB нужен ровно тогда, когда данные бинарны по своей природе и любая попытка трактовать их как текст была бы ошибкой.

NOTE

DuckDB — аналитическая СУБД, и хранить в ней крупные бинарные объекты (мегабайтные файлы) обычно неудачная идея: это раздувает файл базы и не даёт аналитической пользы. BLOB уместен для небольших бинарных значений — хешей, коротких сериализованных структур, ключей. Большие файлы лучше держать в object storage, а в DuckDB — только путь к ним.


Как тип влияет на сжатие

Ещё одна причина выбирать тип осознанно — сжатие. DuckDB сжимает колонки персистентной базы, и применимые схемы сжатия зависят от типа (детально это разбирается в модуле про компрессию). Узкий целый тип и так компактен; DECIMAL на основе целого сжимается хорошо схемами для целых; DOUBLE сжимается специальными float-схемами, которые в среднем менее эффективны, чем схемы для целых. Это ещё один аргумент не хранить в DOUBLE то, что по смыслу целое или десятичное: вы теряете не только точность, но и плотность хранения.

Общий принцип урока: тип — это контракт о представлении. Целые — выбирайте минимальную достаточную ширину с запасом. Деньги — только DECIMAL. Измерения и наука — DOUBLE. Текст — VARCHAR без оглядки на длину в скобках. Сырые байты — BLOB. Каждый выбор влияет на корректность, скорость и размер одновременно.


Попробуй сам

Запустите DuckDB CLI и проверьте поведение типов:

-- 1. Точность
SELECT 0.1::DOUBLE + 0.2::DOUBLE AS d, 0.1::DECIMAL(4,2) + 0.2::DECIMAL(4,2) AS dec;

Задания:

  1. Выполните запрос выше. Объясните, почему d не равно ровно 0.3, а dec равно.
  2. Создайте таблицу с колонкой price DECIMAL(10,2) и колонкой price_bad DOUBLE. Вставьте в обе значение 0.1 десять раз через GROUP BY/sum и сравните суммы.
  3. Узнайте диапазон TINYINT: попробуйте вставить 200::TINYINT и посмотрите на ошибку overflow.
  4. Возьмите кириллическую строку и сравните length('строка') с числом байт в ней (length(encode('строка'))). Объясните разницу.
  5. Создайте BLOB из hex-литерала '\xCA\xFE'::BLOB и попробуйте применить к нему upper(). Объясните, почему это не имеет смысла.

Проверка знанийKnowledge check
В чём принципиальная разница между DECIMAL и DOUBLE на уровне хранения, и почему деньги нужно хранить в DECIMAL?
ОтветAnswer
DOUBLE и REAL — это числа с плавающей точкой стандарта IEEE 754: они хранят значение как мантиссу и двоичную экспоненту. Десятичные дроби, не представимые точно в двоичной системе (например, 0.1 — это бесконечная периодическая двоичная дробь), хранятся приближённо, и ошибки накапливаются: 0.1 + 0.2 в DOUBLE даёт 0.30000000000000004, а не ровно 0.3. DECIMAL(precision, scale) устроен иначе — он хранит число как целое, масштабированное на 10^scale: 123.45 в DECIMAL(10,2) лежит как целое 12345. Никакого двоичного приближения нет, десятичные дроби представляются точно, и арифметика над ними точная. Деньги нужно хранить именно в DECIMAL, потому что финансовые расчёты требуют, чтобы копейки сходились до последней: сумма счетов, налоги, итоги отчёта обязаны совпадать с источником. Если хранить деньги в DOUBLE, накопленная ошибка приводит к расхождению сумм и к неожиданным результатам сравнений на равенство — это реальный финансовый баг. Цена точности DECIMAL — арифметика над масштабированными целыми медленнее аппаратного DOUBLE, но для денег это не обсуждается. DOUBLE же уместен для измерений, координат и ML-фичей, где крошечная погрешность несущественна, а скорость важна.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Почему денежные суммы нужно хранить в DECIMAL, а не в DOUBLE?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6