Расширение pgpro_result_cache предназначено для сохранения результатов выполнения запросов в общей памяти экземпляра кластера баз данных, которая называется «кэш результатов» (result cache). При повторном выполнении запроса, результат которого был кэширован, запрос не выполняется, а выдаётся кэшированный результат. Запрос повторно не выполняется, что снижает нагрузку на сервер, а результат выдаётся моментально, за миллисекунды.

Пример кэширования

По умолчанию расширение выключено. Включить расширение можно на любом уровне, установив параметр конфигурации pgpro_result_cache.enable = on.

Результаты запросов будут кэшироваться только, если в запросе указана подсказка (hint) /*+result_cache*/:

/*+result_cache*/ explain (analyze) select '2+2';
...
   Action: Execute and capture to cache
 Execution Time: 0.101 ms

После первого выполнения запроса результат кэшируется. В плане запроса на это укажет строка Action: Execute and capture to cache.

При повторных выполнениях запроса результат будет выдан из кэша:

/*+result_cache*/ explain (analyze) select '2+2';
...
   Action: Read from cache
 Execution Time: 0.074 ms

На то, что результат выдан из кэша без фактического выполнения запроса указывает строка плана выполнения Action: Read from cache.

Для чего используется кэш

Кандидатами на кэширование являются запросы, обрабатывающие большой объём данных, но возвращающие небольшой, так как кэшируется результат и используемая память зависит от размера результата.

Хорошими примерами являются запросы с группировками. Например, если на веб-странице показывается статистика типа «потрачено за месяц, за год», «5 последних покупок». Чтобы не выполнять запросы при каждом заходе на страницу, результат запросов можно закэшировать.

При выдаче большого числа строк они выдаются по частям. Например, при просмотре каталога товаров результаты выдаются постранично. В конце страницы высвечивается ссылка на последнюю страницу и несколько следующих страниц. Чтобы получить число страниц, выполняется запрос select count(*)... Результат этого запроса можно кэшировать.

Также при навигации по страницам часто используют выборку с LIMIT и OFFSET. Известно, что использование классического LIMIT ... OFFSET на уровне SQL-запроса для глубокой постраничной навигации считается моветоном, так как СУБД вынуждена вхолостую сканировать и отбрасывать пропущенные строки, расходуя ресурсы. Однако расширение pgpro_result_cache предлагает эффективное решение этой проблемы: параметры limit и offset можно указывать прямо внутри хинта кэширования.

Например: 

SELECT /*+result_cache(limit 10 offset 20)*/ * FROM ...

В этом случае запрос выполняется и кэшируется целиком, а возвращаются только данные ограниченные окном limit offset. При повторном исполнении того же самого запроса с другими значениями параметров хинта будет выдано запрошенное подмножество закэшированных данных. Благодаря этому переходы между страницами ускоряются многократно, а база данных не тратит ресурсы на повторное сканирование лишних строк.

Почему не кэшировать результаты запросов на стороне приложения? Это усложняет код приложения и если используется кэширование, то на уровне сессии приложения, а не всех сессий. Общий кэш сложно поддерживать. Разработчики сталкиваются со сложностями реализации кэша на уровне приложения. Сложность реализации эквивалентна созданию реляционной базы данных на промежуточном уровне, либо использованию легковесной базы данных на стороне сервера приложения. Вместо отказа от идеи кэширования часто начинают использовать в качестве кэша временные и обычные таблицы основной базы данных на уровне хранения данных, что приводит к ещё большей нагрузке на неё. Расширение pgpro_result_cache позволяет избежать этого и реализовать кэширование прозрачно для приложения.

Кэш общий для всех сессий

Кэш общий для всего экземпляра и результат будет выдан из кэша даже для сессий другого пользователя:

Общий кэш позволяет экономить разделяемую память, используемую для кэширования результатов.

При этом сохраняется стандартная модель безопасности PostgreSQL: перед использованием кэшированного результата сервер выполняет обычные проверки прав доступа. Пользователь должен иметь необходимые права (как минимум SELECT на все задействованные отношения), иначе запрос не будет выполнен и данные из кэша не вернутся. Учтите, что кэшированные результаты игнорируют политику защиты строк (RLS), поэтому кэшировать можно только те запросы, результат которых одинаково допустим для всех пользователей, имеющих доступ к таблицам; если этого гарантировать нельзя, использовать кэш совместно с RLS не следует.

Кэширование результатов запросов к таблицам системного каталога

Важно различать встроенный кэш системного каталога PostgreSQL (syscache) и работу расширения pgpro_result_cache. Встроенный кэш каталога поддерживается сервером автоматически для каждого клиентского процесса: он хранит отдельные записи системных таблиц в локальной памяти сессии и ускоряет внутренние операции самой СУБД (например, планирование запросов).

Однако многие приложения (ORM-фреймворки, BI-системы, пулы соединений) часто выполняют собственные сложные SELECT-запросы к таблицам pg_catalog. Например, это могут быть запросы, объединяющие несколько системных таблиц для получения метаданных схемы, проверки наличия специфических привилегий или вычисления сложной иерархии ролей. Несмотря на работу встроенного syscache, выполнение таких SQL-запросов всё равно требует ресурсов процессора на парсинг, планирование, соединения (join) и агрегацию.

Именно здесь полезен pgpro_result_cache. Он позволяет закэшировать итоговый набор результатов такого запроса в разделяемой памяти. Если закэшировать результат часто выполняемого запроса к системному каталогу, то при повторном обращении из любой сессии база данных вообще не будет тратить время на его вычисление — готовый ответ вернется моментально. Результаты подобных запросов — типичные и очень эффективные кандидаты для кэширования. Использование pgpro_result_cache для пользовательских запросов никак не вмешивается в работу встроенного кэша системного каталога (он остается независимым и всегда актуальным для внутренних нужд СУБД).

Как и для любых других запросов, при рестарте экземпляра закэшированные результаты pgpro_result_cache не сохраняются, так как кэш инициализируется в разделяемой памяти только при запуске сервера. Для метаданных это не является проблемой, так как они быстро закэшируются заново при первых обращениях приложений.

Важно учитывать: кэшированные результаты игнорируют политику защиты на уровне строк (Row-Level Security). Поэтому кэшировать запросы, возвращающие данные о привилегиях конкретных пользователей, следует с осторожностью — один и тот же кэшированный результат будет возвращён всем сессиям.

Для запросов к системному каталогу рекомендуется включить автоматическое аннулирование кэша (pgpro_result_cache.consistent = on), чтобы при DDL-операциях кэшированные метаданные сбрасывались автоматически.

Сравнение pgpro_result_cache с временными таблицами

Чтобы сравнить время выполнения запросов понадобятся таблицы с большим числом строк. Воспользуемся демонстрационной базой, так как её установка проста:

wget https://edu.postgrespro.com/demo-medium.zip
zcat demo-medium.zip | psql
psql -d demo

Создадим временную таблицу с большим числом строк:

create temp table tickets1 as select * from tickets;
select count(*) from tickets;
 count  
--------
 829071
(1 row)

explain (analyze) select count(*) from tickets1;
 Aggregate  (cost=19881.60..19881.61 rows=1 width=8) (actual time=237.389..237.390 rows=1 loops=1)
   ->  Seq Scan on tickets1  (cost=0.00..18695.68 rows=474368 width=0) (actual time=0.022..146.932 rows=829071 loops=1)
 Planning Time: 0.033 ms
 Execution Time: 237.425 ms

explain (analyze) select count(*) from tickets;
 Finalize Aggregate  (cost=13012.67..13012.68 rows=1 width=8) (actual time=91.954..94.937 rows=1 loops=1)
   ->  Gather  (cost=13012.46..13012.67 rows=2 width=8) (actual time=91.858..94.930 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
...
 Planning Time: 0.056 ms
 Execution Time: 94.979 ms

Время выполнения запроса к временной таблице оказалось больше (237.425 ms), чем к обычной (94.979 ms) из-за того, что при обращении к временной таблице не использовалось распараллеливание.

Распараллеливание запросов к временным таблицам в Postgres Pro

В Postgres Pro Enterprise есть возможность использования параллельных процессов с временными таблицами, воспользуемся этим:

set enable_parallel_temptables = on;
ERROR:  parameter "enable_parallel_temptables" cannot be set after connection start

Распараллеливание при работе с временными таблицами должно быть включено до создания сессии. Включим для всех сессий и переподсоединимся:

alter system set enable_parallel_temptables = on;
select pg_reload_conf();
\c
You are now connected to database "demo" as user "postgres".

Временные таблицы существуют до конца сессии или транзакции, поэтому создадим временную таблицу ещё раз:

create temp table tickets1 as select * from tickets;
SELECT 829071
explain (analyze) select count(*) from tickets1;
 Finalize Aggregate  (cost=17422.88..17422.89 rows=1 width=8) (actual time=99.590..102.993 rows=1 loops=1)
   ->  Gather  (cost=17422.67..17422.88 rows=2 width=8) (actual time=99.498..102.986 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
...
 Planning Time: 0.040 ms
 Execution Time: 108.008 ms

Время выполнения запроса к временной таблице стало быстрее (108.008 ms). за счет использования параллельных процессов . Уменьшение времени выполнения запроса хорошо масштабируется (пропорционально числу параллельных процессов) и не сильно отличается от времени запроса к обычной таблице.

Мы познакомились с опцией параллельного выполнения запросов к временным таблицам. Использование опции простое, а результат значительный: уменьшение времени выполнения запросов к временным таблицам в несколько раз (пропорционально числу параллельных процессов).

Кэширование результатов запросов к временным таблицам

Время выполнения запросов к временным таблицам не сильно отличается от времени выполнения запросов к обычным таблицам. Поэтому, в приложениях, в которых используются временные таблицы, кэширование результатов запросов к временным таблицам может быть полезно. Выполним запрос к временной таблице:

/*+result_cache*/ explain (analyze) select count(*) from tickets1;
...
   Action: Execute and capture to cache
   ->  Finalize Aggregate  (cost=17422.88..17422.89 rows=1 width=8) (actual time=92.794..96.184 rows=1 loops=1)
         ->  Gather  (cost=17422.67..17422.88 rows=2 width=8) (actual time=92.707..96.176 rows=3 loops=1)
               Workers Planned: 2
               Workers Launched: 2
               ->  Partial Aggregate  (cost=16422.67..16422.68 rows=1 width=8) (actual time=88.329..88.330 rows=1 loops=3)
                     ->  Parallel Seq Scan on tickets1  (cost=0.00..15928.53 rows=197653 width=0) (actual time=0.100..57.200 rows=276357 loops=3)
 Planning Time: 0.049 ms
 Execution Time: 97.428 ms

Результат запроса к временной таблице был кэширован. Проверим, будет ли выдан кэшированный результат при повторном выполнении запроса:

/*+result_cache*/ explain (analyze) select count(*) from tickets1;
...
   Action: Read from cache
   ->  Finalize Aggregate  (cost=17422.88..17422.89 rows=1 width=8) (never executed)
         ->  Gather  (cost=17422.67..17422.88 rows=2 width=8) (never executed)
               Workers Planned: 2
               Workers Launched: 0
               ->  Partial Aggregate  (cost=16422.67..16422.68 rows=1 width=8) (never executed)
                     ->  Parallel Seq Scan on tickets1  (cost=0.00..15928.53 rows=197653 width=0) (never executed)
 Planning Time: 0.045 ms
 Execution Time: 1.000 ms
(11 rows)

Запрос был выполнен за миллисекунду (1.000 ms). Без кэша запрос выполнялся 97.428 ms , то есть почти в сто раз медленнее. Параллельные процессы не запускались (Workers Launched: 0), так как кэшируется целиком результат запроса и данные выдаются сразу из кэша. Использование кэша результатов помогает снизить частоту использования параллельных процессов.

Преимущество кэша результатов в том, что независимо от сложности исходного запроса данные из кэша выдаются моментально и не нагружают ни процессор, ни ввод-вывод системы хранения.

Конфликта с другими сессиями при этом не будет. В других сессиях, где нет временной таблицы, запрос выдаст ошибку:

/*+result_cache*/ explain (analyze) select count(*) from tickets1;
ERROR:  relation "tickets1" does not exist

Создадим в другой сессии таблицу с таким же именем, но с другим числом строк (limit 10):

create temp table tickets1 as select * from tickets limit 10;
/*+result_cache*/ explain (analyze) select count(*) from tickets1;
 Custom Scan (ResultCache)  (cost=14.25..14.26 rows=1 width=8) (actual time=0.02
1..0.022 rows=1 loops=1)
   Cache Key: db_id: 24618, query_id: 4641162378291284321, const_hash: 0, param_
hash: 0
   Action: Execute and capture to cache
...
 Planning Time: 0.080 ms
 Execution Time: 0.121 ms

Мониторинг кэша результатов

В представлении pgpro_result_cache_data можно отследить, что кэшированы результаты двух одинаковых запросов, но с разным query_id:

select * from pgpro_result_cache_data;
 db_id |      query_id       |      const_hash      | params_hash |          created           | exec_time_ms | hits | rows_count | data_size | query_string            
-------+---------------------+----------------------+-------------+----------------------------+--------------+------+------------+-----------+------------
------------------------
 24618 | 5185884322440896420 | -3721919477379750894 |           0 | 2025-07-23 06:29:05.108726 |     0.095727 |    2 |         10 |       334 | select oid from pg_class limit 10;
 24618 | 9093213827824582560 |                    0 |           0 | 2025-07-23 07:04:40.728514 |     96.25065 |    1 |          1 |       186 | select count(*) from tickets1;
 24618 | 4641162378291284321 |                    0 |           0 | 2025-07-23 07:07:23.700508 |     0.066993 |    1 |          1 |       186 | select count(*) from tickets1;
(3 rows)

Кэш может очищаться по сроку жизни, который задается параметром конфигурации pgpro_result_cache.ttl. Однако при использовании только TTL есть риск получить устаревшие данные: если записи в таблице изменились, кэш всё равно будет отдавать старый результат до истечения заданного времени.

Чтобы решить эту проблему и гарантировать актуальность выдаваемых данных, начиная с Postgres Pro Enterprise 17.6.1 в расширении реализована автоматическая инвалидация (режим согласованного кэша). Она включается параметром конфигурации pgpro_result_cache.consistent = on.

Когда этот параметр включен, расширение начинает строго отслеживать изменения в базе данных. При любой DML-операции (INSERT, UPDATE, DELETE, TRUNCATE) или DDL-операции (например, ALTER TABLE или DROP TABLE) расширение автоматически находит и сбрасывает при фиксации изменений все записи в кэше, которые зависят от измененных таблиц. Таким образом, приложение всегда получает только консистентные (актуальные) данные, и разработчикам не нужно вручную подбирать идеальный TTL или писать сложную логику сброса кэша на стороне приложения.

Важно учитывать, что для обеспечения таких строгих гарантий актуальности данных режим consistent накладывает ряд ограничений. При его включении расширение перестает кэшировать:

  • запросы к временным (temporary) и нежурналируемым (unlogged) таблицам;

  • запросы, использующие пользовательские функции или типы;

  • запросы, использующие изменяемые (volatile) функции;

  • транзакции (как читающие, так и пишущие), в которых есть незафиксированные изменения.

Важное примечание для партиционированных таблиц: автоматическое аннулирование не работает для секций таблиц, если их данные были неявно изменены. Это критично учитывать при проектировании кэширования для баз данных с активным использованием партицирования.

Также стоит отметить, что любое изменение параметра pgpro_result_cache.consistent (например, через ALTER SYSTEM) приводит к полному сбросу текущего кэша.

Если вы используете кластер с ведомыми узлами (standby), автоматическое аннулирование можно настроить и для них. Для этого на ведущем и всех ведомых узлах нужно включить не только consistent, но и параметр pgpro_result_cache.wal = on. В этом случае DML/DDL операции на мастере будут автоматически инвалидировать соответствующие записи в кэше на репликах через WAL-файлы.

Максимальное время кэширования результата запроса

Время жизни закэшированного результата задаётся параметром pgpro_result_cache.ttl (в секундах). По умолчанию ограничение времени жизни отсутствует (-1). Согласно документации, изменять этот параметр могут только суперпользователи .

Важно понимать, что pgpro_result_cache.ttl — это глобальный параметр, управляющий временем жизни всех записей в едином разделяемом кэше кластера. Хотя технически его можно задать на уровне сессии или транзакции командами SET и SET LOCAL, такая настройка не изолирует время жизни кэша для конкретной сессии: TTL применяется глобально, и сессия с более коротким TTL фактически «протухнет» запись, которую другая сессия намеревалась хранить дольше .

Как это работает на практике:

-- Сессия 1: кэшируем с TTL 40000 секунд
SET pgpro_result_cache.enable = on;
SET pgpro_result_cache.ttl = 40000;
EXPLAIN (ANALYZE) SELECT /*+result_cache*/ * FROM ...;
SELECT * FROM pgpro_result_cache_data;
-- запись присутствует

-- Сессия 2: устанавливаем TTL = 1 секунда
SET pgpro_result_cache.enable = on;
SET pgpro_result_cache.ttl = 1;
SELECT * FROM pgpro_result_cache_data;
-- запись исчезла — с точки зрения сессии 2 она уже «протухла»

-- Сессия 1: снова проверяем
SELECT * FROM pgpro_result_cache_data;
-- запись исчезла и для сессии 1

Это поведение — следствие единой разделяемой памяти: запись в кэше хранится глобально, а TTL при обращении к представлению pgpro_result_cache_data или при попытке использовать закэшированный результат вычисляется относительно значения ttl в текущей сессии . Если одна сессия видит запись «устаревшей», она удаляет её из общего кэша — и та пропадает для всех остальных сессий.

Поэтому pgpro_result_cache.ttl следует использовать как единое глобальное ограничение, учитывая, что все данные кэшируются в общей памяти кластера.

Если требуется гарантировать актуальность данных без ручного управления TTL — используйте механизм автоматического аннулирования. При включении параметра pgpro_result_cache.consistent = on расширение автоматически инвалидирует закэшированные результаты при каждой DML- или DDL-операции (INSERTUPDATEDELETETRUNCATEALTER TABLE и т. д.) на соответствующих таблицах . Это надёжный способ обеспечить согласованность данных без необходимости подбирать «правильный» TTL.

Автоматическое аннулирование по умолчанию выключено. Чтобы включить его, установите pgpro_result_cache.consistent = on в postgresql.conf или через ALTER SYSTEM. Изменение этого параметра сбрасывает кэш .

Идентификация запросов

Кэш хранится в общей памяти и может делать результат общим для разных сессий.

Результат идентифицируется комбинацией database_id, query_id, const_hash, params_hash и query_string.

Если эти свойства запроса совпадают для разных сессий, то результат запроса одной сессии выдается в другой сессии. Посмотрим пример.

Первая сессия:

/*+result_cache*/ explain (analyze) select 2;
                        QUERY PLAN
--------------------------------------------------------
 Custom Scan (ResultCache)  (cost=0.00..0.01 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=1)
   Cache Key: db_id: 5, query_id: 2800308901962295548, const_hash: 16618514822564815477, param_hash: 0
   Action: Read from cache
   ->  Result  (cost=0.00..0.01 rows=1 width=4) (never executed)
 Planning Time: 0.023 ms
 Execution Time: 0.086 ms
(6 rows)

Во второй сессии результат выдается сразу из кэша:

/*+result_cache*/ explain (analyze) select 2;
                        QUERY PLAN
--------------------------------------------------------
 Custom Scan (ResultCache)  (cost=0.00..0.01 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=1)
   Cache Key: db_id: 5, query_id: 2800308901962295548, const_hash: 16618514822564815477, param_hash: 0
   Action: Read from cache
   ->  Result  (cost=0.00..0.01 rows=1 width=4) (never executed)
 Planning Time: 0.029 ms
 Execution Time: 0.084 ms
(6 rows)

Быстро выполняющиеся запросы

Если запрос выполняется за доли миллисекунд, то использовать кэш не стоит, так как поиск результат в кэше может быть дольше, чем выполнение запроса:

/*+result_cache*/ explain (analyze) select now();
   Action: Execute and capture to cache
 Planning Time: 0.022 ms
 Execution Time: 0.117 ms

/*+result_cache*/ explain (analyze) select now();
   Action: Read from cache
 Planning Time: 0.043 ms
 Execution Time: 0.068 ms

explain (analyze) select now();
 Result  (cost=0.00..0.01 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=1)
 Planning Time: 0.015 ms
 Execution Time: 0.010 ms

Выполнение функции now() быстрее (0.010 ms), чем выборка результата из кэша (0.068 ms). Для таких запросов предусмотрен параметр конфигурации pgpro_result_cache.min_exec_time. Если запрос выполняется за меньшее время, то его результат не будет кэширован.

Использование памяти

Посмотрим, сколько памяти в кэше занимает результат одного запроса.

select * from pgpro_result_cache_stat;
 free_kb | entries | hits | inserts | evicts | cleanups | not_cached 
---------+---------+------+---------+--------+----------+------------
      63 |       1 |   32 |      48 |      0 |        0 |          0
(1 row)

/*+result_cache*/ explain (analyze) select count(*) from pg_class where oid=1;

select * from pgpro_result_cache_stat;
 free_kb | entries | hits | inserts | evicts | cleanups | not_cached 
---------+---------+------+---------+--------+----------+------------
      62 |       2 |   32 |      49 |      0 |        0 |          0
(1 row)

/*+result_cache*/ explain (analyze) select count(*) from pg_class where oid=2;

select * from pgpro_result_cache_stat;
 free_kb | entries | hits | inserts | evicts | cleanups | not_cached 
---------+---------+------+---------+--------+----------+------------
      61 |       3 |   32 |      50 |      0 |        0 |          0
(1 row)

Поле free_kb отображается в килобайтах (округлённо), поэтому каждая новая запись уменьшает свободную память примерно на 1 КБ — это отражает гранулярность отображения, а не точный размер записи. В кэше сохраняется сам результат запроса и его текст (query_string), но счётчики статистики (hitsinserts и т. д.) хранятся отдельно и в это значение не входят .

Выполним сто запросов с кэшированием результата:

DO                                    
$$
begin
 for i in 1..100 loop
  execute '/*+result_cache*/' || 'select ' || i::text;
 end loop;
end;
$$
LANGUAGE plpgsql;
DO
select * from pgpro_result_cache_stat;
 free_kb | entries | hits | inserts | evicts | cleanups | not_cached 
---------+---------+------+---------+--------+----------+------------
       0 |      64 |   38 |     254 |      0 |      137 |          0
(1 row)

Закэшированы результаты только 64 запросов из 100 — памяти по умолчанию (max_memory_size = 64kB) хватило лишь на них .

Параметры управления памятью кэша

Управлять объёмом и ограничениями кэша позволяют три взаимосвязанных параметра :

  • pgpro_result_cache.max_memory_size — общий размер разделяемой памяти кэша (по умолчанию 64kB). Задаётся только в postgresql.conf или через ALTER SYSTEM;

  • pgpro_result_cache.max_entries — максимальное количество кэшированных наборов результатов (по умолчанию 128). Задаётся только при запуске сервера;

  • pgpro_result_cache.max_entry_size — максимальный размер одного набора результатов (по умолчанию 16kB). Не может превышать max_memory_size / 2. Текст запроса хранится вместе с данными, поэтому значение должно быть достаточным для размещения обоих.

Оба параметра — max_memory_size и max_entries — действуют одновременно: кэш вытесняет записи по алгоритму LRU при достижении любого из этих лимитов. Нормально, если max_entry_size × max_entries больше max_memory_size: параметр max_entry_size задаёт лишь верхнюю границу размера одной записи, тогда как max_memory_size ограничивает суммарный объём всего кэша .

Значения размеров можно задавать в удобочитаемом виде — в мегабайтах или гигабайтах, не перечисляя нули вручную:

ALTER SYSTEM SET pgpro_result_cache.max_memory_size = '512MB';
SET pgpro_result_cache.max_entry_size = '10MB';

Максимально допустимое значение max_entries равно 65534 .

Кэширование подготовленных запросов

Кэш результата работает с подготовленными запросами. Очистим кэш и подготовим запрос:

select pgpro_result_cache_reset();
PREPARE q(integer) AS select /*+result_cache*/ count(*) from flights where flight_id > $1;

Запрос не выполнялся и в кэше результат нет. Выполним запрос с разными параметрами по два раза:

EXECUTE q(1000);
EXECUTE q(1000);
EXECUTE q(900);
EXECUTE q(900);


select * from pgpro_result_cache_data;
 db_id |      query_id       |     const_hash      |     params_hash      |          created           | exec_time_ms | hits | rows_count | data_size |                             query_string                             
-------+---------------------+---------------------+----------------------+----------------------------+--------------+------+------------+-----------+----------------------------------------------------------------------
 24618 | 8783690532955471044 | 3473803155467375404 |  6548121802712911902 | 2025-07-25 06:45:54.529826 |    15.425121 |    1 |          1 |       226 | select /*+result_cache*/ count(*) from flights where flight_id > $1;
 24618 | 8783690532955471044 | 4661661556901396630 | -6467605202957820427 | 2025-07-25 06:46:10.911893 |    22.682562 |    1 |          1 |       226 | select /*+result_cache*/ count(*) from flights where flight_id > $1;
(2 rows)

В представлении две строки, значит были кэшированы два результата. В столбце hits=1, это означает, что повторные запросы выдавали данные из кэша.

Кэш и курсоры в функциях

Подсказку /*+result_cache*/ можно использовать в курсорах в функциях:

select pgpro_result_cache_reset();
CREATE or replace FUNCTION f1() RETURNS text
AS $$
DECLARE
 var1 text := '';
 r record;
BEGIN
 FOR r IN (/*+result_cache*/ select distinct proowner from pg_proc)
  LOOP
   var1 := r.proowner::regrole;
  END LOOP;
 RETURN var1;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Вызовем функцию два раза:

select f1();
    f1    
----------
 postgres
(1 row)

select f1();
    f1    
----------
 postgres
(1 row)

Результат кэшировался и использовался при повторном вызове функции (hits=1):

select * from pgpro_result_cache_data;
 db_id |      query_id       | const_hash | params_hash |          created           | exec_time_ms | hits | rows_count | data_size |   
           query_string              
-------+---------------------+------------+-------------+----------------------------+--------------+------+------------+-----------+---
-------------------------------------
 24618 | 1529498166273990038 |          0 |           0 | 2025-07-25 07:13:52.391765 |     1.233447 |    1 |          1 |       190 | se
lect distinct proowner from pg_proc)
(1 row)

Рекомендации по кэшированию запросов

Для сессий к одной базе данных стоит во всех сессиях ориентироваться на схожее время жизни результатов. Например, использовать кэш для долгих запросов, которые выдают суммарные данные. Так как запрос долгий, то и TTL можно установить в ~10-100 раз дольше времени выполнения запроса. Если запрос выполняется 10 минут, то TTL установить на час.

Второй шаблон использования: кэшировать результаты запросов, которые выполняются за несколько секунд, но часто. TTL также можно установить в ~10-100 раз дольше таких запросов. Например, в одну минуту.

Если использовать кэш для обоих видов запросов в сессиях с той же самой базой данных, то установка TTL в минуту сделает бессмысленным кэширование долго выполняющихся запросов — при TTL = 60 запись считается действительной около минуты с момента создания; после истечения TTL она становится недействительной и удаляется при очередной проверке кэша.

Кэш результата запросов и функций есть в Oracle Database и если приложение использует его, то миграция с Oracle Database, на Postgres Pro Enterprise упрощается.

В приложениях, обычно, есть небольшое число запросов, которые выполняются очень часто. Например, при обращении к веб-странице проверяется, что веб-пользователь имеет право запрашивать данные, которые выдаются на странице. Сервер приложений работает из-под одного пользователя базы СУБД (one big application) и может использоваться запрос к таблице прав в приложении. Такой запрос выполняется перед основными запросами, формирующими данные. Список прав обновляется нечасто, поэтому результат такого запроса можно кэшировать, а при изменении прав сбрасывать кэш.

Чрезвычайно часто выполняющиеся запросы легко найти и оптимизировать их. Кандидатами на кэширование результатов являются запросы, выдающие не очень большой объем данных, часто выполняющиеся и выдающие один и тот же результат.

Кэширование результатов запросов снижает частоту блокировок на уровне таблиц, их индексов, а также блокировки, используемые для доступа к структурам памяти буферного кэша. Другими словами, убирает узкое место, что увеличивает общую производительность работы экземпляра Postgres.

Отдельный практический сценарий — пагинация. Указание result_cache поддерживает параметры offset и limit, поэтому можно кэшировать подходящий набор строк и затем возвращать из него нужную страницу; в документации приведены примеры с SELECT /*+result_cache(offset 5 limit 10)*/ ....

Кэш результатов как запросов, так и функций общий как в Postgres Pro Enterprise, так и в Oracle Database. В Postgres результаты выполнения функций кэшируются постольку, поскольку обращения к функциям выполняются как запрос SELECT имя_функции(параметры) или PERFORM имя_функции(параметры). При этом важно учитывать техническую особенность: кэширование результатов выполнения самих функций поддерживается только при отключенном режиме согласованного кэша (pgpro_result_cache.consistent = off). Для штатной работы расширения разработчики настоятельно рекомендуют использовать режим консистентности, так как именно он обеспечивает автоматическую инвалидацию кэша при изменении данных. Использование кэширования функций при включенной консистентности в некоторых сценариях может сработать, однако такое поведение не гарантируется и требует обязательного самостоятельного тестирования в условиях вашего приложения.

Заключение

Кэш результатов позволяет выдавать результат выполнения запросов почти моментально. Использование кэша может уменьшить нагрузку от часто выполняющихся запросов. Кандидаты на кэширование — прежде всего тяжёлые запросы, которые выполняются долго и при этом периодически повторяются с теми же параметрами. Преимущества кэша в том, что он общий для всех сессий. В приложениях обычно можно выделить несколько наиболее часто выполняющихся запросов, кэширование результатов которых ускорит работу приложения.

Комментарии (1)


  1. danolivo
    16.05.2026 08:24

    Здесь самый главный вопрос даже не инвалидация кэша, а насколько часто в БД встречаются абсолютно идентичные запросы, с идентичными константами? Если часто, то может приложению стоит просто самостоятельно запомнить результат?

    Ну и да, зависимость от вероятности попасть на коллизию хэшей врядли добавляет DBA спокойствия …