
В 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 - решайте сами.
 
           
 
Equinox
Наверное, корректней было бы diff вычислять как
clock_timestamp() - statement_timestamp()?Потому что если транзакция стартанула уже давно, то текущему запросу может вообще не хватить времени даже на один проход
Kilor Автор
Зависит от того, была ли запущена транзакция раньше в явном виде - только в этом случае
nowиstatement_timestampдадут разный результат.Но в общем случае - да, если хочется ограничить время конкретного запроса, а не транзакции в целом (хотя обычно ее и хочется, чтобы не держать блокировки дольше лимита).