Работа с датами в SQL

Спецификация типов данных для работы с датами в SQL
Фундаментальным элементом работы с датами является тип DATE, который в строгом соответствии с ISO 9075 и ISO 8601 хранит три поля: год (YYYY), месяц (MM) и день (DD). Внутреннее представление — 4 байта (в PostgreSQL, MySQL) или 3 байта (SQL Server до 2008 года). В отличие от текстовых строк, DATE гарантирует валидацию диапазона: 0001-01-01...9999-12-31 (SQL Server) или 4713 BC...5874897 AD (PostgreSQL). Критическое отличие: при использовании строковых литералов ('2026-03-15') СУБД выполняет неявное приведение, что теряет производительность на 12-18% по тестам индексов. Стандарт предписывает использование литералов через ключевое слово DATE '2026-03-15' — такой код проходит компиляцию быстрее и исключает зависимость от настроек локали.
Тип TIMESTAMP (8 байт) расширяет DATE фиксацией времени: часы, минуты, секунды (до 6 знаков долей секунды в PostgreSQL и Oracle). В MySQL DATETIME использует 5 байт при точности до секунд, 8 байт при микросекундах. Технический нюанс: TIMESTAMP в MySQL и SQL Server имеет встроенную привязку к временной зоне сервера — при записи значение конвертируется из зоны сессии в UTC, при чтении обратно. Это экономит 2 байта на хранение по сравнению с DATETIME, но порождает баги при переносе базы между часовыми поясами. Для Delphi-приложений, работающих через ODBC/FireDAC, критично: если поле объявлено как TIMESTAMP WITHOUT TIME ZONE (PostgreSQL), драйвер передаёт сырые байты без коррекции зоны.
Материалы и внутреннее представление: индексы и производительность
Даты в SQL занимают фиксированное место в b-tree индексе. Для DATE размер ключа — 4 байта, для TIMESTAMP(0) — 6-8 байт. Это влияет на глубину дерева: для таблицы в 100 млн записей индекс по TIMESTAMP будет на 25-30% глубже, чем по INTEGER с эпохой Unix. Однако современные СУБД (Oracle 23c, PostgreSQL 17) используют page-level compression: для дат внутри одного месяца PostgreSQL применяет ADPCM-подобное сжатие разницы, уменьшая хранение повторяющихся годов и месяцев до 1 байта.
Качество работы с датами измеряется через pg_stat_user_tables (PostgreSQL) или sys.dm_db_index_usage_stats (SQL Server). Замеры показывают: использование функции DATE_TRUNC() в JOIN с условием DATE_TRUNC('month', d) = '2026-01-01' делает индексный scan в 4.2 раза медленнее, чем селективное сравнение диапазонов WHERE d >= '2026-01-01' AND d < '2026-02-01'. Для Delphi-приложений, где дата передаётся как TDateTime, критично параметризовать диапазоны именно через два параметра (начало и конец интервалa), а не через TRUNC.
Арифметика над датами: стандарты SQL и альтернативы
Операции сложения/вычитания дней:
- ANSI SQL:2003 —
INTERVAL '1' DAY. Материал: интервальный литерал хранится как строка фиксированной длины 12 байт, но требует парсинга на этапе компиляции. - PostgreSQL —
date + integer(1 день = 1), самое быстрое исполнение: библиотечные C-функции работают без контекста интервала. - SQL Server —
DATEADD(day, 1, d). Недостаток: вызов скалярной функции на каждую строку; при 500k строках CPU Time на 40% выше по сравнению с PostgreSQL. - Oracle —
d + 1напрямую, поскольку внутреннее представление — число с плавающей точкой (11 байт, день = 1). Это быстрее INTERVAL на 15%, но нарушает переносимость кода.
Разница дат в днях: стандарт ISO SQL предписывает EXTRACT(DAY FROM (d2 - d1)), но в IBM Db2 результат d2 - d1 даёт число дней, а в MySQL — число секунд. Для универсации в Delphi-коде используют DATEDIFF('d', d1, d2) (SQL Server) или EXTRACT(EPOCH FROM (d2 - d1)) / 86400 (PostgreSQL) — второй вариант точнее, так как учитывает високосные секунды.
Форматы и функции преобразования: спецификации ISO vs зависимость от локали
Функция TO_CHAR(d, 'YYYY-MM-DD') (PostgreSQL, Oracle) гарантирует вывод только по маске; FORMAT(d, 'yyyyMMdd', 'en-US') (SQL Server) зависит от DLL глобализации .NET. Технически FORMAT в SQL Server под капотом вызывает .NET String.Format, что потребляет 2.3 мс на вызов против 0.004 мс для CONVERT(varchar, d, 23) (стиль 23 — ISO 8601). При построении отчёта на 100k записей разница составляет 38 секунд. Для Delphi-приложений, передающих даты в SQL без форматирования, оптимальна константа #2026-03-15# (синтаксис BDE/ADO) — она компилируется сразу в TDateTime без преобразований, но только для Access и SQL Server.
Качество работы с NULL и граничными значениями
Тип DATE в строгом SQL (PostgreSQL, Oracle) не принимает 0000-00-00 — MySQL допускает, что нарушает ISO 9075 и порождает битые данные при миграции. Проверка коррекции: ISDATE() (SQL Server) возвращает 1 для високосных дат, 0 для '2026-02-30'. PostgreSQL не имеет встроенной ISDATE, но валидация идёт на уровне приведения SELECT '2026-02-30'::date — бросает ERROR: date/time field value out of range. Это выше качество стандарта, так как он исключает невалидные данные на этапе вставки.
При обработке в Delphi важно знать: если поле DATE допускает NULL, ADO возвращает Null как Variant(NULL), но FireDAC возвращает Null как TField.IsNull = True без риска арифметического переполнения. Для сравнения дат:
WHERE d IS NULL— исключает строки, не использует индекс (full scan), если в столбце больше 5% NULL-значений. План запроса покажет Index Scan вместо Seek.WHERE d = CAST('' AS DATE)— ошибается: в SQL Server пустая строка становится '1900-01-01', в PostgreSQL — ошибка. Правильным решением являетсяWHERE d IS NULL OR d = :ref_date.
Сравнение реализаций в различных СУБД: класс точности и производительность
- PostgreSQL 17 — полная поддержка ISO 8601,
infinityкак специальное значение. Хранение: 4 байта DATE, 8 байт TIMESTAMP. Качество: все даты строго валидируются, поэтому миграция из MySQL требует скриптаUPDATE t SET d = NULL WHERE d = '0000-00-00'. - SQL Server 2022 — тип
DATETIME2(6 байт, точность 100 нс) вместо старогоDATETIME(8 байт, точность 3.33 мс). Новый стандарт занимает меньше места и не зависит от 1753-01-01 (минимальная дата). Delphi при подключении через OLEDB используетDBTYPE_DBTIME2, что требует драйвера версии 2012+. Использование старогоDATETIMEна новой таблице — грубая ошибка: потеря точности на доли секунды. - MySQL 8.0 — тип
DATETIMEстал by default 5 байт+1 байт на фракции, но позволяет0000-00-00. РежимSTRICT_TRANS_TABLESдолжен быть установлен для соответствия ISO. Разница с PostgreSQL:INTERVALв MySQL работает только сDATE_ADD, не с арифметикой. - Oracle 23c — тип
DATEсодержит и время (минуты и секунды обязательно), что принципиально отличается от других СУБД. Для чистых дат необходимо использоватьTRUNC(date_col)или типDATEс маской'YYYY-MM-DD'. При извлечении в Delphi через ODAC это даёт 7 байт на значение, а не 4 — фактор для больших объёмов.
При разработке Delphi-приложений, где компонент TFDQuery шлёт сериализованные TDateTime, необходимо явно задавать FetchOptions.Mode := fmExactRecNum, чтобы избежать преобразования дат в строку через драйвер СУБД. Целевое качество (0 ошибок при миграции) достигается, только если код SQL использует явные литералы (DATE '2026-01-15') и избегает функций форматирования на уровне базы.
Добавлено: 27.04.2026
