惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
V
V2EX
C
Check Point Blog
GbyAI
GbyAI
D
Docker
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog RSS Feed
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
Netflix TechBlog - Medium
T
Troy Hunt's Blog
博客园 - Franky
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Security Blog
Microsoft Security Blog
P
Privacy & Cybersecurity Law Blog
WordPress大学
WordPress大学
The Cloudflare Blog
S
SegmentFault 最新的问题
Latest news
Latest news
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
I
InfoQ
博客园 - 【当耐特】
NISL@THU
NISL@THU
A
About on SuperTechFans
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Scott Helme
Scott Helme
雷峰网
雷峰网
C
CXSECURITY Database RSS Feed - CXSecurity.com
Security Latest
Security Latest
V
Vulnerabilities – Threatpost
Security Archives - TechRepublic
Security Archives - TechRepublic
A
Arctic Wolf
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
IT之家
IT之家
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
aimingoo的专栏
aimingoo的专栏
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
SecWiki News
SecWiki News
大猫的无限游戏
大猫的无限游戏
S
Security Affairs
The Register - Security
The Register - Security
www.infosecurity-magazine.com
www.infosecurity-magazine.com
L
LINUX DO - 热门话题
T
Tor Project blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Кэш результатов запросов в Postgres Pro: как ускорить часто выполняющиеся запросы и разгрузить базу
slonik_pg (P · 2026-05-15 · via Все публикации подряд на Хабре

Расширение 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). Для штатной работы расширения разработчики настоятельно рекомендуют использовать режим консистентности, так как именно он обеспечивает автоматическую инвалидацию кэша при изменении данных. Использование кэширования функций при включенной консистентности в некоторых сценариях может сработать, однако такое поведение не гарантируется и требует обязательного самостоятельного тестирования в условиях вашего приложения.

Заключение

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