Кеширование анонимных prepared statements

PgDoorman кеширует анонимные сообщения Parse в transaction pooling. Многие драйверы отправляют короткие параметризованные запросы как Parse с пустым именем. Без подмены PostgreSQL заново строит план на каждом Bind, поэтому горячие OLTP-запросы каждый раз тратят CPU планировщика.

PgDoorman прозрачно переписывает каждый анонимный Parse в служебное имя DOORMAN_<N> на серверном соединении. План попадает в реестр именованных prepared statements и переиспользуется между Bind одного клиента и между разными клиентами одного пула. Главный эффект — меньше CPU на планирование и меньше повторных Parse для одинаковых форм запроса.

Подмена прозрачна для драйвера: клиент шлёт и получает пустые имена точно так же, как при работе с обычным PostgreSQL.

PgBouncer (1.21+) и Odyssey поддерживают prepared statements в transaction mode, но только для именованных statements. Анонимный Parse они передают без изменений. Отличие PgDoorman — подмена анонимного Parse. Ограничения кеша, LRU, TTL и метрики нужны, чтобы эта подмена не превращалась в рост памяти на динамическом SQL.

Что ускоряется

Кеш анонимного Parse убирает повторную работу на горячих параметризованных запросах:

  • бэкенд PostgreSQL не получает одинаковый Parse при каждом повторном обращении к уже известной форме запроса;
  • PostgreSQL может использовать prepared statement, который уже создан на этом серверном соединении;
  • разные клиенты одного пула используют одну пуловую запись, а не прогревают одинаковый запрос независимо друг от друга;
  • при попадании в кеш бэкенда PgDoorman синтезирует ParseComplete без обращения к PostgreSQL.

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

Базовая модель PostgreSQL

В сообщении Parse имя prepared statement задаёт его тип: пустое имя соответствует анонимному statement, любое непустое — именованному:

                          Время жизни в PG        Кеширование плана
  ─────────────────────   ─────────────────       ─────────────────────
  Анонимный (name="")     До следующего           Нет именованной
                          анонимного Parse        записи; каждый новый
                          или конца сессии        анонимный Parse
                                                  строит план заново
  Именованный             До Close /              Сначала custom-планы;
   (name="stmt_42")       DEALLOCATE /            после 5 custom-
                          конца сессии            выполнений возможен
                                                  generic-план

Большинство современных драйверов по умолчанию используют анонимные prepared для разовых параметризованных запросов: lib/pq (Go), libpq PQexecParams (C), часть режимов в pgjdbc и psycopg. Прикладной код выглядит как обычный параметризованный запрос, но в wire protocol уходит пустое имя.

Почему это проблема в transaction pooling

В transaction pooling одно серверное соединение по очереди обслуживает разных клиентов. Если пулер отправляет пустой Parse как есть, каждый Bind клиента приходит на соединение, у которого плана для этого запроса нет. Горячие OLTP-пути платят CPU планировщика на каждом обращении.

Именованные prepared решают проблему производительности планирования, но перекладывают учёт на пулер:

  • Пулер должен помнить именованные statements каждого клиента до его отключения, даже если общий кеш пула уже выселил запись.
  • На каждом Bind пулер должен проверить, знает ли текущее серверное соединение это имя, и при необходимости заново сделать Parse.
  • При отключении клиента пулер должен отправить Close или DEALLOCATE на правильное серверное соединение.
  • Драйверы, которые генерируют отдельное имя stmt_<seq> под каждый уникальный запрос, раздувают клиентский кеш: сотни записей на клиента, при тысячах подключений превращающиеся в миллионы записей в памяти.

Остаются два варианта: отказаться от кеширования плана для анонимных запросов или взять на себя полный учёт именованных. PgDoorman выбирает третий путь.

Что делает PgDoorman

На каждый анонимный Parse от клиента PgDoorman:

  1. Считает хеш по тексту запроса, OID типов параметров и короткому отпечатку параметров планировщика, которые клиент закрепил при подключении (search_path, default_transaction_isolation, default_transaction_read_only, default_text_search_config, role). Поэтому два клиента с одинаковым запросом и OID параметров, но с разными значениями search_path в StartupMessage, получат разные записи кэша и разные планы PostgreSQL. Другие параметры, которые тоже могут влиять на план (TimeZone, DateStyle, plan_cache_mode, enable_*, настройки стоимости JIT), в этот отпечаток не входят. Перед тем как использовать один и тот же prepared-запрос с разными значениями таких параметров, посмотрите ограничения в справке по sync_server_parameters.
  2. Ищет хеш в общем кеше пула. При промахе выделяет новое имя DOORMAN_<counter> и регистрирует запись Arc<Parse>.
  3. Записывает в клиентский кеш ключ Anonymous(hash), чтобы следующий Bind нашёл тот же DOORMAN_<N>.
  4. Отправляет Parse на бэкенд с переписанным именем.
  5. На соответствующем Bind (с пустым именем) переписывает имя statement в DOORMAN_<N> и проверяет, что текущий бэкенд уже держит запись; если нет, отправляет Parse повторно.

Клиент никогда не видит DOORMAN_<N>: имя живёт только на участке между PgDoorman и бэкендом. Когда нужный бэкенд уже держит запись, PgDoorman синтезирует ParseComplete сам и не делает round-trip.

Пример wire-protocol

Go-приложение, выполняющее

db.Query("SELECT * FROM t WHERE name = $1", "vasya")

через lib/pq, отправляет такой обмен:

  Клиент                  PgDoorman                  Backend
  ──────                  ─────────                  ───────
  Parse("", q)        ───►│ hash, miss → DOORMAN_42
                           │ pool_cache[hash] = Arc<Parse>
                           │ client_cache[Anon(hash)] = ...
                           │            Parse("DOORMAN_42") ─────►
                           │                   ◄── ParseComplete
                      ◄────│ ParseComplete
  Bind("", "vasya")   ───►│ rewrite "" → "DOORMAN_42"
                           │            Bind("DOORMAN_42") ──────►
                           │            Execute, Sync ───────────►
                           │               ◄── BindComplete, ...
                           │               ◄── ReadyForQuery
                      ◄────│ BindComplete, ...

Второй клиент с тем же запросом в том же пуле попадает в кеш пула и не отправляет Parse на бэкенд:

  Клиент B           PgDoorman                       Backend (тот же)
  ────────           ─────────                       ────────────────
  Parse("", q)  ───►│ hash hit → DOORMAN_42
                     │ server_cache содержит "DOORMAN_42"
                ◄────│ синтетический ParseComplete   (на бэкенд ничего)
  Bind("", v)   ───►│ rewrite "" → "DOORMAN_42"
                     │            Bind("DOORMAN_42") ────►
                     │            ...

Слои кеша

PgDoorman держит состояние prepared statements на трёх уровнях:

  Pool-level    DashMap<hash, CacheEntry>
                Один на пул. Хранит Arc<Parse> с именем DOORMAN_N.
                Размер:    prepared_statements_cache_size (по умолчанию 8192).
                Выселение: приближённый LRU.

  Client-level  Named:     AHashMap<String, CachedStatement>, без лимита.
                Anonymous: LruCache<u64, CachedStatement> ограничен
                           client_anonymous_prepared_cache_size (если не задан,
                           наследует prepared_statements_cache_size),
                           или AHashMap при размере 0.
                Выселение Anonymous локальное: Arc<Parse> отбрасывается,
                DOORMAN_<N> на бэкенде остаётся.

  Server-level  LruCache<String, ()>, на серверное соединение.
                Запоминает, какие DOORMAN_N этот бэкенд уже держит.
                Точный LRU; при выселении отправляет Close на бэкенд.

При вытеснении записи из Anonymous LRU PgDoorman отбрасывает локальную ссылку и не отправляет Close на бэкенд. Соответствующий DOORMAN_<N> будет переиспользован server-level LRU или закроется по server_lifetime (по умолчанию 20 минут) — что наступит раньше.

Текст запроса интернируется через Arc<str>: десять клиентов с одним и тем же анонимным запросом делят одну аллокацию в памяти.

Когда подмена помогает

  • API-нагрузки с малым набором горячих запросов. Десяток уникальных форм SELECT / INSERT на тысячи клиентов. Доля попаданий в кеш пула близка к 100 %, планировщик работает один раз на серверное соединение для каждой формы запроса, а следующие обращения идут через Bind к уже подготовленному statement.
  • Драйверы, привязанные к анонимным prepared. lib/pq, libpq PQexecParams, pgjdbc до достижения prepareThreshold. Без подмены они каждый раз перепланируют.
  • Смешанные пулы, где именованные и анонимные statements соседствуют. Анонимные получают тот же выигрыш от кеша планов, что и именованные, без роста клиентского кеша именованных statements.

Когда подмена не помогает

  • Разовый / OLAP-трафик. Каждый запрос уникален. Когда кеш пула заполнен, каждая новая форма запроса ищет старую запись для вытеснения через O(N)-обход. Если инстанс обслуживает только такую нагрузку, отключите remap через prepared_statements: false.
  • Скрипты с одним statement. Цепочка connect → Parse → 1 Bind → disconnect не накапливает достаточно попаданий, чтобы окупить учёт. Накладные расходы на Parse ~700 нс — небольшие, но измеримые.
  • Асинхронные драйверы в режиме pipeline. Каждая сессия получает уникальное имя DOORMAN_async_<N>, чтобы избежать коллизий между одновременными незавершёнными операциями. Серверный кеш между сессиями не переиспользуется. Кеш пула по-прежнему делит текст запроса между сессиями; планировщик на бэкенде срабатывает один раз на сессию.

Эффективность этого ускорения измеряйте по rate(pg_doorman_servers_prepared_hits_total[5m]) и rate(pg_doorman_servers_prepared_misses_total[5m]). Устойчивая доля промахов выше 30 % означает, что подмена расходует CPU и память, но почти не даёт переиспользования планов. Тогда либо отключите её, либо увеличьте prepared_statements_cache_size.

Как это устроено у других пулеров

ПулерКеш Parse/плана для анонимного prepared statement
PgDoormanДа: прозрачная подмена на DOORMAN_<N>
PgBouncer 1.21+Нет: только named, анонимный проходит as-is
OdysseyНет: только named, pool_reserve_prepared_statement
PgCatНет: только named

В PgBouncer поддержка prepared statements появилась в 1.21, но ограничена именованными: анонимный Parse проходит без изменений, и каждый Bind запускает планировщик. Флаг pool_reserve_prepared_statement в Odyssey требует именованных statement; на анонимный трафик он не влияет. PgCat ведёт себя так же.

Кешировать план анонимных prepared сегодня умеет только PgDoorman.

Конфигурация

ПараметрПо умолчаниюЭффект
prepared_statementstrueВключает подмену и кеширование prepared statements. false отключает функцию.
prepared_statements_cache_size8192Размер кеша пула в записях. Должен быть больше 0, пока prepared_statements = true.
server_prepared_statements_cache_sizeнаследует prepared_statements_cache_sizeРазмер LRU на серверное соединение для имён DOORMAN_<N>. 0 отключает хранение на бэкенде, но не подмену на уровне пула.
client_anonymous_prepared_cache_sizeнаследует prepared_statements_cache_sizeРазмер Anonymous LRU на клиента. 0 снимает ограничение. Named-часть всегда без лимита.

Named-часть клиентского кеша всегда без лимита и не зависит от client_anonymous_prepared_cache_size.

Полностью отключить подмену prepared statements (редко, для установок только с OLAP):

general:
  prepared_statements: false

Отдельного переключателя только для анонимных statements нет. Не используйте prepared_statements_cache_size: 0 как выключатель: pg_doorman отклоняет такое общее значение, пока prepared_statements включён.

Отличия от семантики PostgreSQL

Подмена меняет несколько протокольных деталей, на которые могут полагаться строгие приложения:

  • Один и тот же анонимный Parse, отправленный дважды, не стирает предыдущий. Каждая пара (query, param_types) живёт независимо в кеше пула под своим DOORMAN_<N>.
  • Close с пустым именем ничего не делает с кешами PgDoorman. Соответствующий DOORMAN_<N> живёт до выселения из LRU кеша пула или до закрытия пула.
  • Решение PostgreSQL между custom- и generic-планом становится общим для всех клиентов, использующих один DOORMAN_<N>. Именованный statement сначала получает custom-планы; после пяти custom-выполнений PostgreSQL может перейти на generic-план, если его оценочная стоимость достаточно близка. С PgDoorman эти выполнения могут прийти от разных клиентов, поэтому выбор generic-плана может отражать смешанное распределение параметров.

Приложения, которые опираются на PG-семантику "анонимный Parse стирает предыдущий", должны переключиться на именованные statement с явным Close.

Тюнинг

Размер кеша

Кеш prepared statements в PgDoorman состоит из трёх слоёв. Ими управляют три связанных параметра:

  • prepared_statements_cache_size (по умолчанию 8192) задаёт размер общего кеша пула — одна хеш-таблица на пул, ключом служит хеш запроса. Это верхняя граница на число различных форм запроса, которые пул помнит сразу для всех клиентов. Приближённый LRU: вытеснение проходит за O(N) по всему кешу и не отправляет Close на бэкенд (другие клиенты могут ещё держать Arc).
  • server_prepared_statements_cache_size (по умолчанию наследует prepared_statements_cache_size) задаёт размер серверного кеша — отдельный LRU на каждое серверное соединение, ключом служит имя DOORMAN_<N>. Это верхняя граница на число prepared statements, которое PgDoorman позволит держать одному бэкенду PostgreSQL. Точный LRU за O(1); при выселении в очередь бэкенда кладётся Close, который отправляется ближайшим Sync или Flush — представление pg_prepared_statements может временно показывать больше строк, чем потолок, пока не придёт следующий Sync.
  • client_anonymous_prepared_cache_size (по умолчанию наследует prepared_statements_cache_size) задаёт размер Anonymous LRU на клиента. 0 отключает LRU и снимает потолок: кеш растёт без ограничения. Любое положительное число задаёт лимит независимо от размера кеша пула.

Пуловый и серверный лимиты можно переопределить на уровне пула:

general:
  prepared_statements_cache_size: 8192
  server_prepared_statements_cache_size: 1024  # потолок на бэкенд жёстче

pools:
  oltp:
    # наследует оба значения из general
    pool_mode: "transaction"
  reporting:
    # у этого пула шире разнообразие запросов; пусть серверный кеш
    # вмещает больше
    server_prepared_statements_cache_size: 4096
    pool_mode: "transaction"

prepared_statements: false отключает подмену целиком и заодно обнуляет кеши на уровне пула и серверного соединения. Указать server_prepared_statements_cache_size: 0 при положительном размере кеша пула допустимо, но смысла мало: серверный кеш превратится в pass-through, и каждое попадание на другой бэкенд приведёт к повторному Parse.

Когда уменьшать server_prepared_statements_cache_size ниже размера кеша пула:

  • На бэкендах копится слишком много строк DOORMAN_<N> (pg_prepared_statements упирается в потолок, память планов растёт).
  • Хочется ускорить вытеснение через Close, не урезая попадания в кеш пула.

Когда оставить значения равными (поведение по умолчанию):

  • Нет измеренной проблемы с памятью на бэкендах. Достаточно наследования.

Размер client_anonymous_prepared_cache_size

Если параметр не задан, Anonymous LRU на клиента наследует вычисленный prepared_statements_cache_size пула (по умолчанию 8192). Явное значение перекрывает наследование: 0 отключает LRU, и кеш растёт без ограничения; любое положительное число задаёт потолок LRU.

Каждая запись хранит лёгкую структуру (hash, async_name?, Arc<Parse>) — сам Arc<Parse> делится с кешем пула, поэтому накладные расходы на клиента ≈ 80 байт на запись. На 10 000 подключённых клиентов × 256 записей × ~80 байт получаем около 200 МБ предсказуемого потолка на пулере.

Поднимайте лимит, когда:

  • ORM или генератор SQL выдаёт stmt_<seq> под каждый запрос и Anonymous LRU постоянно вытесняет записи (видно по устойчиво ненулевой скорости pg_doorman_clients_prepared_anonymous_evictions_total).
  • Приложение заведомо имеет широкое рабочее множество в одной сессии и скорость вытеснений соответствует этой нагрузке.

Уменьшайте лимит при очень больших числах подключений (50 000+ клиентов): на таком масштабе clients × cache_size × 80 байт учётной памяти на пулере может пересечь 1 ГБ, и урезание лимита уполовинивает её. max_memory_usage не ограничивает служебную память кеша prepared statements; этот параметр защищает буферы запросов в полёте.

Named всегда без лимита

Named-часть клиентского кеша не ограничена. PgDoorman держит Arc<Parse> для каждого именованного statement, который создал клиент, до его отключения или явного DEALLOCATE / DEALLOCATE ALL. Это согласуется с собственным контрактом PostgreSQL — именованные prepared живут до конца сессии — и исключает сценарий, при котором вытеснение Named-записи под нагрузкой ломает следующий Bind ошибкой prepared statement does not exist.

Обратная сторона: драйверы, генерирующие отдельное имя под каждый запрос (часть режимов pgjdbc и Hibernate, отдельные конфигурации .NET Npgsql), могут раздуть Named-часть без потолка. PgDoorman не может ограничить её безопасно; ответственность за переиспользование имён или явный DEALLOCATE лежит на приложении.

Сигнал давления есть только для Anonymous LRU — счётчик вытеснений pg_doorman_clients_prepared_anonymous_evictions_total. Для Named такого сигнала нет: следите за колонкой client_named_count в SHOW POOLS_MEMORY и метрикой pg_doorman_clients_prepared_named_entries на предмет неожиданного роста.

Окно роста памяти на бэкенде

При вытеснении записи из Anonymous LRU на стороне клиента PgDoorman отбрасывает только локальный Arc<Parse>. Соответствующий DOORMAN_<N> остаётся живым на каждом бэкенде, который когда-либо его обслуживал. Очищают его два механизма:

  • Server-level LRU. На каждом бэкенде ведётся свой LruCache<String, ()> имён DOORMAN_<N>, ограниченный server_prepared_statements_cache_size (или prepared_statements_cache_size, если отдельный серверный лимит не задан). При достижении лимита бэкенд отправляет Close на наименее давно использованное имя и освобождает план.
  • Ротация бэкенда. Бэкенд достигает server_lifetime (по умолчанию 20 мин), и pg_doorman закрывает его; новый бэкенд стартует с пустым кешем планов.

Худший случай по памяти на одном бэкенде — это server_prepared_statements_cache_size × средний размер плана (8192 × ~100 КБ — около 800 МБ) на стороне PostgreSQL. Чтобы сжать окно:

  • Снизьте server_prepared_statements_cache_size, чтобы серверный LRU быстрее вытесняла планы.
  • Снизьте server_lifetime, чтобы бэкенды ротировались чаще.

Системное представление pg_prepared_statements в PostgreSQL показывает имена, которые держит текущий бэкенд. Подсчёт строк там показывает, насколько близко бэкенд подошёл к лимиту.

Мониторинг

Команды администратора:

  • SHOW PREPARED_STATEMENTS — pool, hash, name, query, count_used, kind. Топ записей по count_used показывает горячие запросы, на которых кеш окупается. Колонка kind — последняя в наборе и принимает значения named, anonymous или mixed в зависимости от того, как клиенты использовали запись за её жизнь.

    Пример:

     pool         | hash               | name        | query             | count_used | kind
    --------------+--------------------+-------------+-------------------+------------+-----------
     sharded.user | 1234567890123456   | DOORMAN_1   | SELECT * FROM t1  |     150234 | anonymous
     sharded.user | 2345678901234567   | DOORMAN_2   | INSERT INTO t2 .. |      87654 | named
     sharded.user | 3456789012345678   | DOORMAN_3   | SELECT * FROM t3  |      45678 | mixed
    
  • SHOW POOLS_MEMORYpool_prepared_count, client_prepared_count, pool_prepared_bytes, client_prepared_bytes плюс разбивка по kind: client_named_count, client_anonymous_count, client_anonymous_evictions_alive. Последняя колонка суммирует счётчики вытеснений только по подключённым сейчас клиентам — отключившиеся клиенты выпадают из суммы, поэтому колонка не монотонна. Для накопительного счётчика читайте Prometheus-метрику pg_doorman_clients_prepared_anonymous_evictions_total.

Prometheus-метрики (полный список в Prometheus):

  • pg_doorman_pool_prepared_cache_entries{user, database}
  • pg_doorman_pool_prepared_cache_bytes
  • pg_doorman_clients_prepared_cache_entries
  • pg_doorman_clients_prepared_cache_bytes
  • pg_doorman_clients_prepared_named_entries{user, database}
  • pg_doorman_clients_prepared_anonymous_entries{user, database}
  • pg_doorman_clients_prepared_anonymous_evictions_total{user, database}
  • pg_doorman_servers_prepared_hits{user, database}
  • pg_doorman_servers_prepared_misses{user, database}
  • pg_doorman_servers_prepared_hits_total{user, database}
  • pg_doorman_servers_prepared_misses_total{user, database}
  • pg_doorman_async_clients_count

Для rate() и алертов используйте метрики с суффиксом _total. Метрики серверного кеша без _total — текущая сумма по живым бэкендам; она может уменьшаться при ротации бэкендов.

Алертинг

Скорость вытеснений в Anonymous LRU

Устойчиво ненулевая скорость на счётчике вытеснений означает, что LRU вытесняет записи быстрее, чем приложение переиспользует их. Шаблон алерта:

rate(pg_doorman_clients_prepared_anonymous_evictions_total[5m]) > 10
  for 10m

Порог в 10 вытеснений/с на пул — отправная точка, реальное значение зависит от формы трафика и числа подключений. Срабатывание алерта читайте как "лимит слишком мал или рабочее множество приложения шире, чем ожидалось"; решение — либо поднять client_anonymous_prepared_cache_size, либо разобраться, не генерирует ли приложение уникальные запросы на горячем пути.

Интерпретация kind = mixed

Каждая запись кеша пула помнит, использовали ли её клиенты как именованный statement, как анонимный, или обоими способами. kind = mixed означает, что одна и та же пара (query, param_types) была обработана хотя бы одним клиентом как named и хотя бы одним другим как anonymous за её текущую жизнь. У большинства нагрузок строк mixed нет; если в пуле их большинство, у клиентов разные драйверы или разные конфигурации драйверов против одной БД, и эту разнородность стоит проверить — иногда она задумана, иногда сигналит, что один из клиентов настроен неверно.

Число prepared statements на бэкенде

PostgreSQL отдаёт pg_prepared_statements для текущего бэкенда. Если память пулера в норме, но RSS бэкендов PostgreSQL растёт, посчитайте строки на каждом бэкенде:

SELECT count(*) FROM pg_prepared_statements;

Цифры около server_prepared_statements_cache_size означают, что серверный LRU работает на потолке, а ротация — второй способ освободить память планов. Если серверный лимит наследует prepared_statements_cache_size, используйте это значение как потолок. Снижение серверного лимита или server_lifetime уменьшает давление на память планов ценой более частых повторных Parse на бэкенде.

Ограниченный query interner

Глобальный интернер, в котором дедуплицируются тексты Parse, разделён на две независимые хеш-таблицы.

  • NAMED — тексты именованных prepared. Запись жива, пока пуловой или клиентский кеш держит ссылку через Arc<str>. Сборщик мусора убирает запись, когда её перестали удерживать снаружи интернера; двухтактовая отсрочка защищает запись, на которую сослались между двумя циклами обхода.
  • ANON — тексты анонимных prepared. Запись истекает по query_interner_anon_idle_ttl_seconds бездействия (60 секунд по умолчанию). 0 отключает TTL и возвращает поведение pg_doorman до 3.7 — оставлено для существующих установок.

Если anonymous Bind или Describe приходит после того, как pg_doorman потерял соответствующее состояние anonymous prepared, pg_doorman отвечает ERROR: unnamed prepared statement does not exist (SQLSTATE 26000). Типичные причины: вытеснение из клиентского Anonymous LRU, RESET INTERNER, TTL-вытеснение из interner или паттерн драйвера, который переиспользует unnamed prepared statements между пачками. Это та же ошибка, что PostgreSQL отдаёт напрямую в аналогичной ситуации; стандартные драйверы реагируют повторным Parse.

Бинарное обновление (SIGUSR2) переносит и NAMED, и ANON в новый процесс. Анонимные записи попадают в новый ANON-интернер со свежим last_used, и TTL отсчитывается заново с момента обновления.

Команды для оператора

SHOW INTERNER (через admin-сессию) выводит количество и объём текста для каждой половины:

kind      | entries | bytes
named     |     420 |    87654
anonymous |    1337 |   234567

SHOW INTERNER N показывает N самых тяжёлых записей: hash, kind, bytes, idle_ms (-1 для named — там нет последнего-использования, вместо него отслеживается состояние сборщика) и 120 первых символов текста запроса.

RESET INTERNER чистит обе половины. Активные клиенты заново сделают Parse при следующем использовании. Команда диагностическая.

Метрики Prometheus отражают SHOW INTERNER плюс гистограмму длительности обхода и счётчик синтетических 26000. Увеличивайте query_interner_anon_idle_ttl_seconds только когда synthetic misses совпадают с TTL-вытеснениями anonymous-записей или с подтверждённым паттерном cross-batch unnamed statements. Если промахи идут вместе с pg_doorman_clients_prepared_anonymous_evictions_total, увеличивайте client_anonymous_prepared_cache_size.

Справочник