Раздел 35 · SQLite с нуля

Встроенные функции, даты и CAST

junior~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В прошлом уроке мы сгруппировали заказы по дням через date(ordered_at). Это была встроенная функция: SQLite взял строку 2026-10-01 08:15:00 и вернул 2026-10-01. Таких функций в SQLite десятки. Сегодня разбираем самые нужные: для строк, для чисел, для дат и для превращения одного типа в другой.

Функция как кухонный прибор

Представь кухню «Пенки». Кофемолка получает зёрна и отдаёт молотый кофе. Вспениватель получает молоко и отдаёт пену. Каждый прибор делает одно преобразование: на входе одно, на выходе другое.

Скалярная функция устроена так же: получает значения из одной строки и возвращает одно значение. В отличие от агрегатных функций, она не сворачивает строки: если в таблице девять напитков, функция сработает девять раз.

Строки

Начнём с длины и кусочков строки:

sqlite> SELECT name, length(name) AS chars, substr(name, 1, 3) AS short FROM drinks WHERE id <= 3;
╭───────────┬───────┬───────╮
│   name    │ chars │ short │
╞═══════════╪═══════╪═══════╡
│ Эспрессо  │     8 │ Эсп   │
│ Американо │     9 │ Аме   │
│ Капучино  │     8 │ Кап   │
╰───────────┴───────┴───────╯
  • length(x) считает символы, а не байты: в «Эспрессо» восемь букв, хотя в UTF-8 они занимают шестнадцать байтов;
  • substr(x, начало, длина) вырезает кусок, символы нумеруются с единицы.

Замена, обрезка пробелов и поиск:

sqlite> SELECT replace('Чай с чабрецом', 'чабрецом', 'мятой') AS new_name, trim('  Раф  ') AS t, instr('Матча-латте', '-') AS dash;
╭─────────────┬─────┬──────╮
│  new_name   │  t  │ dash │
╞═════════════╪═════╪══════╡
│ Чай с мятой │ Раф │    6 │
╰─────────────┴─────┴──────╯

replace заменяет все вхождения подстроки, trim срезает пробелы по краям, instr возвращает позицию подстроки или 0, если её нет.

Теперь ловушка. Регистр меняют upper и lower:

sqlite> SELECT upper('latte') AS en, upper('латте') AS ru, lower('ЛАТТЕ') AS ru_lower;
╭───────┬───────┬──────────╮
│  en   │  ru   │ ru_lower │
╞═══════╪═══════╪══════════╡
│ LATTE │ латте │ ЛАТТЕ    │
╰───────┴───────┴──────────╯

Латиница стала заглавной, а кириллица не изменилась. Встроенные upper и lower в SQLite знают только английский алфавит: таблицы регистров всех языков мира заняли бы больше места, чем вся библиотека. Тот же предел у LIKE из урока про WHERE: без учёта регистра он сравнивает только латиницу. Если нужен регистр кириллицы, его меняют в коде приложения или подключают расширение ICU.

Числа

sqlite> SELECT round(177.456, 1) AS r1, round(2.5) AS r2, abs(-40) AS a, printf('%.2f ₽', 180) AS money;
╭───────┬─────┬────┬──────────╮
│  r1   │ r2  │ a  │  money   │
╞═══════╪═════╪════╪══════════╡
│ 177.5 │ 3.0 │ 40 │ 180.00 ₽ │
╰───────┴─────┴────┴──────────╯

round(x, n) округляет до n знаков после точки и всегда возвращает дробное число, отсюда 3.0. abs снимает знак. printf собирает строку по шаблону: %.2f значит «число с двумя знаками после точки». У printf есть второе имя format.

Даты это текст

В SQLite нет отдельного типа для даты: классов хранения всего пять, и даты среди них нет, вспомни урок про типы. Даты хранят обычным текстом в формате ISO 8601: 2026-10-01 08:15:00, от года к секундам. У этого формата полезное свойство: строки сортируются по алфавиту ровно в хронологическом порядке. Поэтому ORDER BY ordered_at и max(ordered_at) работают правильно, хотя для SQLite это просто текст.

Функции дат умеют читать такие строки:

sqlite> SELECT ordered_at, date(ordered_at) AS day, time(ordered_at) AS t FROM orders WHERE id <= 2;
╭─────────────────────┬────────────┬──────────╮
│     ordered_at      │    day     │    t     │
╞═════════════════════╪════════════╪══════════╡
│ 2026-10-01 08:15:00 │ 2026-10-01 │ 08:15:00 │
│ 2026-10-01 08:40:00 │ 2026-10-01 │ 08:40:00 │
╰─────────────────────┴────────────┴──────────╯

date() оставляет дату, time() время, datetime() и то и другое. Вместо строки можно передать слово 'now': date('now') вернёт сегодняшнюю дату. Её значение меняется каждый день, поэтому в примерах урока мы пишем даты явно, и результат у тебя совпадёт с нашим.

Модификаторы

Главная сила функций дат это модификаторы: строки после даты, которые сдвигают её или округляют:

sqlite> SELECT date('2026-10-01', '+1 day') AS tomorrow, date('2026-10-17', 'start of month') AS first, date('2026-10-01', 'weekday 0') AS sunday, date('2026-10-31', '+1 month') AS tricky;
╭────────────┬────────────┬────────────┬────────────╮
│  tomorrow  │   first    │   sunday   │   tricky   │
╞════════════╪════════════╪════════════╪════════════╡
│ 2026-10-02 │ 2026-10-01 │ 2026-10-04 │ 2026-12-01 │
╰────────────┴────────────┴────────────┴────────────╯
  • '+1 day', '-2 hours', '+3 months' сдвигают дату;
  • 'start of month' и 'start of year' округляют к началу месяца или года;
  • 'weekday 0' двигает вперёд до ближайшего воскресенья (0 это воскресенье, 1 понедельник и так до 6).

Посмотри на последний столбец. 31 октября плюс месяц дало не 30 ноября, а 1 декабря. SQLite прибавил единицу к номеру месяца, получил несуществующее 31 ноября и перенёс лишний день на декабрь. Для дат в конце месяца проверяй результат.

strftime: дата по шаблону

strftime раскладывает дату по шаблону. Каждый кусок шаблона начинается с %:

sqlite> SELECT strftime('%H', ordered_at) AS hour, strftime('%w', ordered_at) AS weekday, strftime('%d.%m.%Y', ordered_at) AS ru FROM orders WHERE id IN (1, 9);
╭──────┬─────────┬────────────╮
│ hour │ weekday │     ru     │
╞══════╪═════════╪════════════╡
│ '08' │ '4'     │ 01.10.2026 │
│ '19' │ '0'     │ 04.10.2026 │
╰──────┴─────────┴────────────╯

Обрати внимание на кавычки вокруг '08' и '4'. strftime всегда возвращает текст, и sqlite3 показывает строку, похожую на число, в кавычках, чтобы ты её не перепутал с числом. Заказ 9 сделан в воскресенье, отсюда '0'.

Разница между датами

Чтобы посчитать, сколько дней прошло, даты переводят в числа функцией julianday и вычитают:

sqlite> SELECT name, round(julianday('2026-10-24') - julianday(hired_on)) AS days_in_team FROM staff;
╭───────┬──────────────╮
│ name  │ days_in_team │
╞═══════╪══════════════╡
│ Ольга │       2063.0 │
│ Тимур │       1592.0 │
│ Аня   │       1149.0 │
│ Гоша  │        987.0 │
│ Вера  │ NULL         │
╰───────┴──────────────╯

У Веры дата найма не заполнена, и NULL прошёл через все функции до конца, как в уроке про NULL.

CAST и деление

Последняя тема про типы в выражениях. Сначала сюрприз:

sqlite> SELECT 7 / 2 AS int_div, 7 / 2.0 AS real_div, CAST(7 AS REAL) / 2 AS cast_div;
╭─────────┬──────────┬──────────╮
│ int_div │ real_div │ cast_div │
╞═════════╪══════════╪══════════╡
│       3 │      3.5 │      3.5 │
╰─────────┴──────────┴──────────╯

Когда оба числа целые, SQLite делит нацело и отбрасывает остаток. Чтобы получить 3.5, хотя бы одно число должно быть дробным. Сделать его дробным можно двумя способами: написать 2.0 или привести тип через CAST.

CAST переводит значение в другой тип. Посмотри, как он обращается с текстом:

sqlite> SELECT CAST('42' AS INTEGER) + 1 AS a, CAST('300 мл' AS INTEGER) AS b, CAST('abc' AS INTEGER) AS c, CAST(3.99 AS INTEGER) AS d, CAST(120 AS TEXT) AS e;
╭────┬─────┬───┬───┬───────╮
│ a  │  b  │ c │ d │   e   │
╞════╪═════╪═══╪═══╪═══════╡
│ 43 │ 300 │ 0 │ 3 │ '120' │
╰────┴─────┴───┴───┴───────╯

Строка '42' стала числом. Из '300 мл' SQLite взял цифры в начале и выбросил остальное. Из 'abc' получился 0, без ошибки. Дробное 3.99 обрезано до 3, а не округлено. Ошибок CAST в SQLite не бывает, поэтому проверяй данные заранее: typeof(x) скажет, что лежит в столбце.

Попробуй сам

CASE в первом запросе выбирает подпись по дню недели, подробно о нём в следующем уроке. Поменяй шаблон strftime на '%Y-%m' и сгруппируй заказы по месяцам. Почему во втором запросе upper не изменил ни одного названия?

Проверь себя

Задание

Дальше

Мы уже подсмотрели CASE: он выбирает значение по условию прямо в запросе. В следующем уроке разбираем CASE подробно, а потом знакомимся с оконными функциями, которые считают агрегаты, не сворачивая строки.

домашка

Домашка