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

d

Спецификация типов данных для работы с датами в 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 и альтернативы

Операции сложения/вычитания дней:

Разница дат в днях: стандарт 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 без риска арифметического переполнения. Для сравнения дат:

Сравнение реализаций в различных СУБД: класс точности и производительность

При разработке Delphi-приложений, где компонент TFDQuery шлёт сериализованные TDateTime, необходимо явно задавать FetchOptions.Mode := fmExactRecNum, чтобы избежать преобразования дат в строку через драйвер СУБД. Целевое качество (0 ошибок при миграции) достигается, только если код SQL использует явные литералы (DATE '2026-01-15') и избегает функций форматирования на уровне базы.

Добавлено: 27.04.2026