Примитивные типы: целые, decimal, float, строки, blob
Тип колонки в DuckDB — это не аннотация для документации.
Числовые типы в PostgreSQL: INT, BIGINT, NUMERIC и float Это решение о том, сколько байт занимает каждое значение, как оно раскладывается в памяти, какие операции над ним возможны без потери точности и насколько хорошо колонка сожмётся на диске. DuckDB — колоночная аналитическая СУБД, и в ней колонка одного типа лежит сплошным массивом фиксированного формата. Выбор типа определяет геометрию этого массива.
В этом уроке разберём примитивные типы — те, что хранят одно скалярное значение: целые числа, точные десятичные DECIMAL, числа с плавающей точкой, строки и бинарные BLOB. Главная мысль урока — типы не взаимозаменяемы. DECIMAL и DOUBLE оба «дробные числа», но ведут себя принципиально по-разному, и путаница между ними — источник реальных финансовых багов.
Целые типы: ширина определяет диапазон
DuckDB предлагает целочисленные типы пяти размеров. Каждый — это фиксированное число байт, и оно жёстко задаёт диапазон представимых значений:
| Тип | Размер | Диапазон (знаковый) |
|---|---|---|
TINYINT | 1 байт | примерно от -128 до 127 |
SMALLINT | 2 байта | примерно +/- 32 тысячи |
INTEGER | 4 байта | примерно +/- 2.1 миллиарда |
BIGINT | 8 байт | примерно +/- 9.2 квинтиллиона |
HUGEINT | 16 байт | примерно +/- 1.7 в 38-й степени |
У каждого есть беззнаковый вариант (UTINYINT, UINTEGER и т. д.): он отдаёт бит знака под величину и удваивает верхнюю границу, но запрещает отрицательные значения.
Ширина типа — это компромисс. Узкий тип занимает меньше места и быстрее обрабатывается (в один вектор фиксированного размера влезает столько же значений, но каждое легче). Широкий тип защищает от переполнения. Practическое правило: берите самый узкий тип, который гарантированно вместит все возможные значения колонки с запасом на рост. Для счётчика товаров на складе INTEGER избыточен — хватит SMALLINT или INTEGER; для глобального счётчика событий нужен BIGINT.
Переполнение целого типа в 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 / REAL |
|---|---|
| Деньги, цены, суммы счетов | Научные измерения, координаты |
| Любые величины, где «копейки» обязаны сходиться | ML-фичи, статистика, доли |
| Бухгалтерия, налоги, финансовая отчётность | Промежуточные расчёты, где скорость важнее точности |
Цена точности DECIMAL — скорость и размер: операции над масштабированными целыми сложнее, чем над аппаратным DOUBLE (который процессор считает «бесплатно»), а высокая precision занимает больше байт. Но для денег это не обсуждается: финансовый отчёт, где сумма не сходится на копейку из-за DOUBLE, — это баг, который стоит дороже любой экономии на скорости.
Никогда не храните деньги в 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 посчитает его как один символ, а размер в байтах будет больше.
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 нужен ровно тогда, когда данные бинарны по своей природе и любая попытка трактовать их как текст была бы ошибкой.
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;
Задания:
- Выполните запрос выше. Объясните, почему
dне равно ровно0.3, аdecравно. - Создайте таблицу с колонкой
price DECIMAL(10,2)и колонкойprice_bad DOUBLE. Вставьте в обе значение0.1десять раз черезGROUP BY/sumи сравните суммы. - Узнайте диапазон
TINYINT: попробуйте вставить200::TINYINTи посмотрите на ошибку overflow. - Возьмите кириллическую строку и сравните
length('строка')с числом байт в ней (length(encode('строка'))). Объясните разницу. - Создайте
BLOBиз hex-литерала'\xCA\xFE'::BLOBи попробуйте применить к немуupper(). Объясните, почему это не имеет смысла.