Как стать автором
Обновить
251.87
Тензор
Разработчик системы СБИС

SQL HowTo: наперегонки со временем

Время на прочтение2 мин
Количество просмотров8.3K

В 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:

Функция

Результат

transaction_timestamp()

now()

CURRENT_TIMESTAMP

момент начала текущей транзакции

statement_timestamp()

момент начала текущего запроса

clock_timestamp()

момент вычисления

timeofday()

момент вычисления (строка)

Проверим:

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 - решайте сами.

Теги:
Хабы:
+11
Комментарии2

Публикации

Информация

Сайт
sbis.ru
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия