SQL HowTo: наперегонки со временем
В PostgreSQL несложно написать запрос, который уйдет в глубокую рекурсию или просто будет выполняться гораздо дольше, чем нам хотелось бы. Как от этого защититься?
А чтобы еще и полезную работу сделать? Например, набрать следующий сегмент данных при постраничной навигации со сложным условием фильтрации.
statement_timeout
Очевидное решение — использовать те средства, которые нам дает для этого сама база: установить устраивающее нас значение параметра statement_timeout:
Задаёт максимальную длительность выполнения оператора, при превышении которой оператор прерывается.
Ну-ка, ну-ка… Возьмем тестовый запрос, который ждет 20 раз по 100ms и попробуем остановить его через секунду:
BEGIN;
SET LOCAL statement_timeout = '1s';
SELECT
i
, pg_sleep(0.1)
FROM
generate_series(1, 20) i
-- ERROR: canceling statement due to statement timeout
ROLLBACK;
Таки да, «в бесконечность» наш запрос не ушел, но и полезного мы ничего не получили — то есть попросту нагрузили базу бесполезной работой.
Неужели нет способа заставить запрос успеть вернуть хоть что-то за отмеренное ему время?
clock_timestamp
Оказывается, есть, если использовать одну из не очень известных функций работы с датой/временем — clock_timestamp:
Функция | Результат |
| момент начала текущей транзакции |
| момент начала текущего запроса |
| момент вычисления |
| момент вычисления (строка) |
Проверим:
BEGIN;
SELECT
transaction_timestamp() -- 2022-07-13 18:00:27.352057+03
, statement_timestamp() -- 2022-07-13 18:00:29.782563+03
, clock_timestamp() -- 2022-07-13 18:00:29.805303+03
, timeofday(); -- Wed Jul 13 18:00:29.805304 2022 MSK
ROLLBACK;
Обратим внимание, что даже у вызванных последовательно clock_timestamp/timeofday
возникла разница на 1 микросекунду — то есть значение действительно получается в момент вычисления.
Перепишем немного запрос:
SELECT
i
, clock_timestamp() - now() diff
, pg_sleep(0.1)
FROM
generate_series(1, 20) i
WHERE
clock_timestamp() - now() < '1 sec'::interval; -- волшебное условие
i | diff | pg_sleep
1 | 00:00:00.0002 |
2 | 00:00:00.100966 |
3 | 00:00:00.202966 |
4 | 00:00:00.303682 |
5 | 00:00:00.404945 |
6 | 00:00:00.50603 |
7 | 00:00:00.607661 |
8 | 00:00:00.7084 |
9 | 00:00:00.808655 |
10 | 00:00:00.908728 |
Смотрите-ка, PostgreSQL «спит» не ровно по 100ms, а чуть-чуть больше, зато в секунду он четко уложился, да еще и вернул при этом какой-то полезный контент — ровно что мы и хотели.
А если посложнее, и нам надо ограничить рекурсивный запрос? Настолько же просто:
WITH RECURSIVE T AS (
SELECT
0 i
, clock_timestamp() - now() diff
, NULL::void
UNION ALL
SELECT
i + 1
, clock_timestamp() - now() diff
, pg_sleep(0.1)
FROM
T
WHERE
clock_timestamp() - now() < '1 sec'::interval -- то же самое условие
)
TABLE T;
i | diff | void
0 | 00:00:00.000291 |
1 | 00:00:00.000304 |
2 | 00:00:00.101989 |
3 | 00:00:00.203279 |
4 | 00:00:00.304524 |
5 | 00:00:00.406191 |
6 | 00:00:00.507255 |
7 | 00:00:00.608043 |
8 | 00:00:00.70816 |
9 | 00:00:00.808379 |
10 | 00:00:00.90908 |
Ну, а каким контентом вы будете грузить базу вместо pg_sleep
— решайте сами.