В прошлом уроке мы сгруппировали заказы по дням через 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 подробно, а потом знакомимся с оконными функциями, которые считают агрегаты, не сворачивая строки.
домашка