В июне 2025 г. «Тантор Лабс» представила СУБД Tantor Postgres версии 17.5. Новый релиз насыщен обновлениями во всех основных аспектах: усиление безопасности (Transparent Data Encryption с поддержкой ГОСТ-криптоалгоритмов, аудит безопасности pg_sec_check, авторизация OAuth 2.0), повышение общей производительности (pg_stat_advisor, оптимизация временных таблиц, прецизионный сбор статистики, SIMD-инструкции, семплирование), новые расширения (pgvector для векторного поиска, pg_ivm для инкрементальных представлений, pg_stat_kcache для мониторинга), а также инфраструктурные улучшения (pg_throttle с cgroups, утилита настройки диагностики).
Сегодня мы проведем обзор изменений, касающихся работы с высоконагруженными системами 1С. Новый релиз предлагает не просто несколько точечных исправлений, а целый арсенал специализированных функций, призванных существенно ускорить выполнение типичных для 1С операций, снизить нагрузку на инфраструктуру и упростить администрирование. Спектр улучшений распространился на многие ключевые узлы производительности от оптимизации работы с временными таблицами и сложными запросами RLS (row-level security) до ускорения критически важных процессов наподобие «Закрытия месяца». Обо всем этом и поговорим ниже более детально.
Улучшения, касающиеся скорости выполнения запросов:
Ускорение запросов за счет выбора оптимально подходящего индекса
Ускорение запросов с ограничением доступа на уровне записей (RLS)
Ускорение запросов к виртуальным таблицам с помощью Join Predicate Pushdown
Улучшения, касающиеся временных таблиц:
Улучшения, касающиеся сбора статистики:
Отчет по безопасности сервера баз данных
Улучшения, касающиеся скорости выполнения запросов
Ускорение запросов за счет выбора оптимально подходящего индекса
В статье «СУБД Tantor Special Edition 1C: ускоряем обновление итогов регистров накопления» был рассмотрен пример, когда планировщик выбирает индекс, не самый подходящий под условия соединения, и это приводит к значительным дополнительным затратам при выполнении запроса и к увеличению длительности его выполнения. Причина такого неоптимального поведения состоит в том, что при выборе индекса для планировщика важна не его селективность, а общая и стартовая стоимость поиска по нему. Таким образом индекс выбирается менее селективный, и поиск по нему приводит к дополнительным ресурсозатратам, в том числе при применении условия Filter
. Для решения этой проблемы в Tantor Postgres 16.6 мы добавили новый параметр enable_index_path_selectivity
. При его включении планировщик корректирует выбор индекса с учетом лучшей селективности для перебираемых индексов, но только в случае если:
ключи индексов таких путей непрерывно и последовательно покрывают предикаты соединения и/или отбора, и все такие предикаты используют только оператор "=";
индексы параметризованы одинаковым количеством соединяемых таблиц и их предикатов.
Такие индексы будут сравниваться уже по их селективности, и индекс с лучшей селективностью будет иметь более высокий приоритет.
Параметр прошел обкатку в боевых условиях, поэтому начиная с версии 17 мы включаем его по умолчанию. А вот свежий пример из «боевой» базы нашего клиента:


Выполнение запроса ускоряется с 59 до 0,5 секунд, т.е. более чем в 100 раз. Как видно, оптимизация позволяет ускорять не только запросы обновления итогов регистров, но и вообще любые запросы, в которых происходит соединение таблиц.
Ускорение запросов с ограничением доступа на уровне записей (RLS)
Сейчас сложно представить информацию систему 1С, работающую без механизмов RLS (row-level security, также нередко пишут русскими буквами «РЛС»). Этот механизм позволяет гибко управлять правами доступа групп пользователей к данным информационной системы, например, ограничить доступ к данным в зависимости от роли пользователя, его подразделения или других параметров. Это особенно важно в крупных организациях с разветвленной структурой.
При работе РЛС платформа 1С может генерировать несколько разных шаблонов запросов, отправляемых на СУБД для исполнения. Вот пример одного из таких шаблонов:
SELECT *
FROM T1
WHERE EXISTS (
SELECT 1
FROM (SELECT 1 AS SDBL_DUMMY) SDBL_DUAL
INNER JOIN T2 ON T1._IDRRef = T2.*
INNER JOIN T3 ON T2.* = T3.*
);
В рамках RnD мы доработали планировщик, чтобы он строил по таким шаблонам более эффективные планы запросов. Давайте посмотрим, как меняется план запроса, на примере из системы 1С:Документооборот:


Чем плох вариант «до» оптимизации? В нем используется подзапрос Subplan 1
, который выполняется 883 449 раз — т.е. столько раз, сколько строк выбирается по индексу reference29_7
из таблицы _reference29
. К выбранным из индекса строкам применяется фильтр, одним из условий которого как раз и является выполнение подзапроса Subplan 1
.
В варианте «после» оптимизации подзапрос не используется. Вместо него таблицы _inforg9321
и _inforg9383
сначала соединяются между собой, а затем результат их соединения через Hash Join
(по сути внутреннее соединение) соединяется с результатом выборки из таблицы _reference29
. Это более оптимальный план выполнения запроса как по затратам, так и по скорости выполнения: запрос выполняется быстрее более чем на 20%!
Такая трансформация плана запроса выполняется в соответствии с нормами реляционной алгебры. Подобное реализовано в MS SQL Server:

Здесь тоже не используется подзапрос (как и в Tantor Postgres), но запрос выполняется дольше из-за того, что ему приходится сначала отобрать данные по индексу reference29_7
, а затем обратиться к кластерному индексу(Key Lookup
) для чтения недостающих полей.
Ускорение запросов к виртуальным таблицам с помощью Join Predicate Pushdown
В статье «1С и СУБД Tantor: история одного внедрения» мы рассказывали о случае у клиента, когда запрос к виртуальной таблице ОстаткиИОбороты
на MS SQL Server выполнялся значительно быстрее, чем на Tantor Postgres, ввиду наличия одной из продвинутых техник рерайтинга (переписывания) запросов, называемой Join Predicate Pushdown
. Это такая оптимизация в SQL-запросах, когда условия фильтрации (предикаты) «проталкиваются» вниз по плану выполнения ближе к этапу сканирования таблиц, чтобы уменьшить объем обрабатываемых данных на ранних стадиях. Изучим суть данной технологии на простом примере. Предположим, у нас есть 2 таблицы:
Справочник «Менеджеры» с полями «Ссылка», «Регион». Количество записей — 1 000;
Регистр накопления «Продажи» с полями «Период», «Клиент», «Менеджер», «Сумма». Количество записей — 100 000 000.
Требуется посчитать все продажи в разрезе менеджеров региона «Южный» за июнь 2025 года (регистр накопления «Продажи» содержит 2 млн записей за этот месяц). Для этого напишем следующий запрос:

Т.к. таблица РегистрНакопления.Продажи.Обороты
является виртуальной и итоги за текущий месяц еще не рассчитаны, платформа 1С для исполнения на СУБД этот запрос по сути переписывает следующим образом (рассматриваем далее только второй запрос пакета, т.к. запрос создания временной таблицы не меняется):

Неоптимальность такого запроса в том, что мы сначала читаем все записи за июнь 2025 г. по РегистрНакопления.Продажи
, затем группируем 2 млн записей, и только потом соединяем с таблицей МенеджерыРегионаЮжный
, значительно сокращая количество выбираемых данных:

Суть технологии Join Predicate Pushdown
в том, что она позволяет «протолкнуть» селективный отбор по Менеджеру внутрь вложенного запроса. Наш запрос изменится так:

Таким, образом отбирая данные из РегистрНакопления.Продажи
, мы сразу применим селективный отбор по Менеджеру, выбрав 20 тысяч записей вместо двух миллионов, и значительно сократим как количество читаемых записей, так и записей для этапа группировки данных:

Такие запросы в 1С встречаются очень часто (обращения к любым виртуальным таблицам: среза последних/первых, остатки, обороты и т.д.), и именно они становятся наиболее частыми причинами деградации после миграции с MS SQL Server на любой из форков Postgres.
Вернемся теперь к примеру из статьи «1С и СУБД Tantor: история одного внедрения» и рассмотрим, как Join Predicate Pushdown
ускоряет выполнение запроса на реальном примере из базы клиента.

Join Predicate Pushdown
выключен
Join Predicate Pushdown
включенВремя выполнения запроса ускоряется с 2810 до 0.3 мс (ускорение в 10 тысяч раз!) за счет того, что селективный отбор применился при выборке данных из таблицы регистра накопления _AccumRg68344
. А вот план этого же запроса на MS SQL Server, где мы видим аналогичную оптимизацию:
Степень ускорения действительно впечатляет, и уже ясно, что переход с MS SQL Server на Tantor Postgres не даст потерь в производительности. Однако список полезных нововведений релиза 17.5 для систем 1С на этом не заканчивается.
Ускорение аналитических запросов с агрегацией данных 1
Для таких регламентных операций как «Закрытие месяца» характерно большое количество тяжелых аналитических запросов, которые вычисляют различные показатели почти на всех этапах расчета себестоимости.
Вот пример запроса "ДетализацияСебестоимостиПартииТоваровПостатейныеЗатраты
" и его плана:

Сам запрос представляет собой соединение нескольких таблиц с последующей группировкой данных и применения агрегатных функций. На скриншоте виден его самый проблемный узел – это сортировка, которая выполняется 145 секунд (62% от общего времени выполнения запроса). Если открыть план этого запроса по вышеуказанной ссылке и перейти во вкладку «Оригинал», то мы увидим его исходный текст, в котором никаких сортировок нет (ищем операцию «ORDER BY
»). Откуда же она тогда появляется в плане запроса?
Наличие операции Sort
—это требование алгоритма GroupAggregate
: он требует, чтобы входные данные были отсортированы по столбцам группировки. Это позволяет ему обрабатывать строки одной группы последовательно, не храня в памяти все группы одновременно. Итак, сортировка нужна. Теперь немного погрузимся в саму операцию сортировки, чтобы понять, в чем заключается оптимизация. Допустим, у нас есть такая таблица:
Склад | Номенклатура | Серия | Количество |
Центральный | Кольцо | K000002 | 1 |
Центральный | Кольцо | K000001 | 1 |
Центральный | Кольцо | K000002 | 1 |
Центральный | Серьги | S000001 | 1 |
Центральный | Серьги | S000002 | 1 |
Центральный | Серьги | S000002 | 1 |
Необходимо сгруппировать эту таблицу по полям «Склад», «Номенклатура», «Серия», применив функцию СУММА к полю «Количество». Перед группировкой, как мы выше выяснили, происходит сортировка. Чтобы сравнить первую строку (Склад: Центральный, Номенклатура: Кольцо, Серия: K000002) со второй строкой (Склад: Центральный, Номенклатура: Кольцо, Серия: K000001) и определить порядок этих строк, мы должны сравнивать значения колонок, чтобы решить, нужно ли менять строки местами или нет. Сравниваем значения попарно:
Склад: Центральный = Центральный
Номенклатура: Кольцо = Кольцо
Серия: K000001 <> K000002
Как видно, только на третьей операции сравнения можно понять, что строки не равны, и, возможно, имеет смысл поменять их местами. Получается, что первые две операции сравнения будут выполняться впустую, пока мы не дойдем до номенклатуры «Серьги». Если посчитать количество различных значений в нашей таблице, то получается, что поле «Склад» содержит одно уникальное значение, «Номенклатура» — 2, «Серия» — 4. Другими словами, поле «Серия» является самым селективным. И поскольку порядок сортировки при группировке не влияет на итоговый результат, мы можем поменять его так, чтобы минимизировать количество операций сравнения.
Если поменять порядок сравнения с «Склад, Номенклатура, Серия» на «Серия, Номенклатура, Склад», то операций сравнения придется делать значительно меньше. В этом и заключается суть данной оптимизации. А теперь давайте взглянем, как изменится план запроса с её учетом:

Длительность операции Sort
уменьшилась с 145 до 3 секунд!
Ускорение запросов с поиском по подстроке 2
Для рассмотрения этой оптимизации опять проведем аналогию с 1С. Допустим, у нас есть переменная, которой присвоено определенное текстовое значение:
Комментарий = "Tantor Postgres: пример для оптимизации оператора LIKE";
Стоит задача определить, включает ли переменная «Комментарий» слово «оператор» или нет. Рассмотрим два варианта того, как это можно сделать.
Вариант 1. Посредством использования регулярных выражений.
Комментарий = "Tantor postgres: пример для оптимизации оператора LIKE";
ПодстрокаПоиска = "оператор";
РезультатПоиска = СтрНайтиПоРегулярномуВыражению(Комментарий, ПодстрокаПоиска );
Вариант 2. С помощью функции СтрНайти.
Комментарий = "Tantor postgres: пример для оптимизации оператора LIKE";
ПодстрокаПоиска = "оператор";
РезультатПоиска = СтрНайти(Комментарий, ПодстрокаПоиска);
Второй вариант для выполнения поставленной задачи выглядит более легковесным и оптимальным, поскольку:
Поиск подстроки работает быстрее, ведь это низкоуровневая операция, заточенная под точное совпадение;
Регулярные выражения требуют компиляции паттерна и выполняют более сложный алгоритм сопоставления, а это больше нагружает процессор;
Регулярные выражения полезны, когда нужен гибкий или сложный паттерн.
Такая же оптимизация реализована и в нашей СУБД Tantor Postgres при использовании оператора LIKE
при следующих условиях:
строка шаблона подстроки поиска должна быть больше двух символов;
строка шаблона подстроки поиска должна начинаться и заканчиваться с символа "%" и не содержать символ "_".
длина строки для поиска или шаблона поиска подстроки должен превышать 100 символов.
В этом случае вместо регулярных выражений будет использована более легковесная функция поиска по подстроке.
В качестве примера возьмем типичный кейс с сотрудником казначейства, которому необходимо отобрать документы по назначению платежа. Выберем в базе 1С:ERP все документы «Поступление безналичных денежных средств», у которых поле «Назначение платежа» содержит слово «счет».


Ускорение — почти в 2 раза!
Улучшения, касающиеся временных таблиц
Уменьшение нагрузки на дисковую подсистему
Активное использование временных таблиц платформой 1С создает повышенную нагрузку на дисковую систему сервера СУБД даже если создаваемые временные таблицы полностью помещаются в оперативную память, выделяемую под temp_buffers
. Разберемся, почему это происходит, на простом примере создания временной таблицы.
Создание временной таблицы c одной колонкой типа timestamp
:
drop table if exists tt1 cascade;create temporary table tt1 (_Q_001_F_000 timestamp) without oids
На диске (в моем случае по пути /tantor-se-1c-17/data/base/pgsql_tmp
) создадутся следующие файлы:
Имя файла | Размер |
t9_39426353 | 0 KB |
У нас создался пустой файл "t9_39426353".
Вставим в данную временную таблицу десять тысяч записей:
INSERT INTO pg_temp.tt1 (_Q_001_F_000) SELECT
T1._Period
FROM _AccumRg37899 T1
LIMIT 10000
Что происходит с файлами:
Имя файла | Размер |
t9_39426353 | 360 KB |
t9_39426353_fsm | 24 KB |
Размер файла "t9_39426353" увеличился с 0 до 360 КБ. Странно, ведь у нас temp_buffers
явно превышает 360 КБ, и временная таблица должна храниться в памяти. На самом деле так и есть: временная таблица хранится в памяти, а в файле резервируется место на случай, если temp_buffers
не хватит и придется скидывать данные на диск. Сам файл хоть и занимает 360 КБ, но в него сейчас записано 45 пустых блоков по 8 КБ (8 КБ — это размер одной страницы в Tantor Postgres).
Появился файл "t9_39426353_fsm" — это служебный файл, необходимый для отслеживания свободного места в таблицах и индексах, в нашем случае — для отслеживания свободного места во временной таблице.
Таким образом, создание временной таблицы повлекло за собой создание на диске двух файлов. А сколько файлов будет создано, если мы создадим «более сложную» временную таблицу, в которой будет тип данных, который может повлечь хранение в toast, и еще добавим индекс? Попробуем это сделать и выясним.
drop table if exists tt1 cascade;create temporary table tt1 (_Q_001_F_000RRef bytea) without oids;
drop index if exists tmpind_0; create index tmpind_0 on pg_temp.tt1(_Q_001_F_000RRef );
INSERT INTO pg_temp.tt1 (_Q_001_F_000RRef ) SELECT
T1._RecorderRRef
FROM _AccumRg37899 T1
LIMIT 10000;
Созданные файлы:
Имя файла | Размер |
t51_39426371 | 392 KB |
t51_39426366_fsm | 24 KB |
t51_39426366 | 520 KB |
t51_39426370 | 8 KB |
t51_39426369 | 0 KB |
Файлы "t51_39426366" и "t51_39426366_fsm" аналогичны предыдущему случаю. Файл t51_39426371 — это резервирование под индекс временной таблицы, а два последних файла связаны с использованием во временной таблицы типа данных, для хранения которого может быть использован toast. Активное создание временными таблицами всех этих файлов и приводит к повышенной нагрузке на дисковую подсистему, и для 1С-систем эта проблема очень актуальна!
Мы решили ее следующим образом: все эти файлы будут создаваться в оперативной памяти сеанса Tantor Postgres до тех пор, пока не будет использовано все пространство, отводимое параметром temp_buffers
. Наши нагрузочные тесты показывают, что это позволяет значительно снизить нагрузку на дисковую подсистему.
Пример 1.
Нагрузочный тест на базе 1С:ERP размером 700 Гб. Эмулируется работа 350 пользователей, длительность теста — один час.
Файлы временных таблиц создаются на диске:

А вот как меняется картина, если включить отложенное размещение временных таблиц на диске:

Пример 2.
Нагрузочный тест на базе 1С:ERP размером 1000 Гб. Эмулируется работа 3225 пользователей, длительность теста 10 часов.
Файлы временных таблиц создаются на диске:

А так всё выглядит, когда отложенное размещение включено:

Оптимизация способа хранения метаданных временных таблиц
В предыдущем пункте мы увидели, что создание каждой временной таблицы в свою очередь создает дополнительную нагрузку на дисковую подсистему. Но это еще не все проблемы. Разберемся теперь, как хранятся метаданные каждой создаваемой временной таблицы.
В PostgreSQL есть таблицы системного каталога — в них хранятся данные о всех создаваемых таблицах, как временных, так и постоянных — справочники, документы, регистры и т.д. Давайте выполним команду создания временной таблицы и индекса к ней, и посмотрим, что будет происходить:
drop table if exists tt1 cascade;create temporary table tt1 (_Q_001_F_000RRef bytea) without oids;
drop index if exists tmpind_0; create index tmpind_0 on pg_temp.tt1(_Q_001_F_000RRef );
Создание временных объектов затрагивает 13 таблиц системного каталога. Посмотрим, что произошло в основных из них при выполнении вышеприведенной команды:
pg_class —
хранит информацию о названии всех таблиц, а также их внутренних идентификаторах (oid). В нашем случае добавлена информация о таблице "tt1
".pg_attribute —
хранит информацию о всех полях таблиц. В нашем случае добавлена информация о поле "_q_001_f_000rref
" таблицы "tt1
".pg_type —
хранит информацию о типах полей таблиц. В нашем случае добавлена информация, что поле "_q_001_f_000rref
" имеет тип "bytea
".pg_index —
хранит информацию о созданных индексах. В нашем случае добавлена информация об индексе "tmpind_0
".pg_depend —
хранит информацию о зависимостях между объектами БД. В нашем случае добавлена информация о том, что индекс "tmpind_0
" относится к таблице "tt1
"...и т.д.
Если провести аналогию с метаданными 1С, то это как если бы при создании на языке запросов временной таблицы в дереве метаданных появлялись бы новые объекты в дереве метаданных:

Таблицы системного каталога хранят данные о созданных временных таблицах. Проблема в том, что после удаления временных таблиц появляются старые версии строк, которые приходится удалять процессам автовакуума. Как же появляются старые версии строк? Допустим, вы удаляете из регистра сведений запись: в регистре вы ее больше не увидите, но физически она не удалена из БД, а помечена как мертвая, и физически будет удалена только когда процесс вакуума придет в таблицу данного регистра сведений. Это может приводить к следующим негативным последствиям:
Мёртвые строки увеличивают размер таблицы на диске и замедляют сканирование;
Фрагментируются индексы и поиск нужных строк по ним замедляется.
То же происходит и с таблицами системного каталога: платформа 1С закрывает соединения с СУБД в диапазоне от 30 до 60 минут и при закрытии соединения из таблиц системного каталога удаляется информация обо всех временных таблицах, созданных в рамках этого соединения. В таблицах pg_class
, pg_attribute
и др. появляется большое количество мертвых записей, и это может негативно влиять на работу всей системы в целом, ведь не просто так эти таблицы называются системным каталогом.
Вот пример из продуктивной базы ERP:

Две таблицы системного каталога (pg_depend
и pg_index
) раздулись более чем на 90%. Например, размер таблицы pg_depend
составляет 494 МБ, из которых 464 МБ —мертвые строки. При этом даже автовакуум не помогает очистить эти таблицы (скриншот сделан 30.06.25 в 13:36), потому что текущие соединения на СУБД постоянно держат на этих таблицах блокировки.
Мы плавно подошли к оптимизации, которая заключается в следующем: добавляется возможность хранить информацию о метаданных временных таблиц в локальной памяти каждого соединения к СУБД, а не в общем системном каталоге, который мы рассмотрели выше. Это дает следующие преимущества:
Нивелирует риск деградации системы из-за раздутия таблиц системного каталога;
Уменьшает нагрузку на дисковую подсистему, поскольку таблицы системного каталога хранятся на диске и в них идет постоянная запись информации о временных таблицах;
Позволяет процессу автовакуума сосредоточиться на очистке пользовательских таблиц БД, а не на служебных таблицах системного каталога.
На третьем пункте остановимся подробнее. Для тестирования этого функционала мы провели нагрузочный тест на 3225 пользователей и по итогам теста посмотрели статистику работы процесса автовакуума, а именно, сколько раз он «приходил» в таблицы системного каталога и сколько — в пользовательские таблицы (таблицы справочников, документов, регистров и т.д.):
autovacuum_count | autoanalyze_count | |
Таблицы системного каталога | 4049 | 3503 |
Пользовательские таблицы | 1090 | 1467 |
А если включить хранение информации в локальной памяти каждого соединения к СУБД, ситуация изменится так:
autovacuum_count | autoanalyze_count | |
Таблицы системного каталога | 4 | 0 |
Пользовательские таблицы | 1133 | 1562 |
Из этого можно сделать вывод, что примерно 75% времени процесс автовакуума занимается тем, что обслуживает таблицы системного каталога из-за активного использования временных таблиц.
Также данная новая возможность особенно актуальна в следующих случаях:
Для систем, работающих 24/7, ведь для них очень сложно найти технологическое окно, чтобы сделать вакуум таблиц системного каталога, а как видим по примеру выше, автовакуум с этой задачей может не справиться;
Для высоконагруженных систем, в которых активное создание временных таблиц может стать узким местом в производительности работы.
Кроме этого, перед нами открывается возможность реализовать еще несколько возможностей по повышению производительности высоконагруженных систем, работающих на платформе 1С:Предприятие.
Улучшения pg_stat_statements
Нормализация имен временных таблиц
Расширение pg_stat_statements
используется для отслеживания и сбора статистики о выполнении SQL-запросов. Оно позволяет идентифицировать медленные и часто выполняемые запросы, собирать данные о времени выполнения, количестве вызовов, количестве строк и других метриках, а также помогать в оптимизации запросов и улучшении общей производительности базы данных. Но при работе с 1С есть нюанс, который мешает видеть полную картинку происходящего —это временные таблицы, а точнее, их разные имена в запросах. Агрегируемая статистика получается неполной, ведь из-за разных имен временных таблиц хэш запроса получается разным, и при выборке данных из pg_stat_statements мы получаем «размытую» картину.
Для автоматической нормализации имен временных таблиц в СУБД Tantor Postgres реализован новый параметр mask_temp_tables.

pg_stat_statements
в платформе Tantor Имена всех временных таблиц заменяются на константу "TEMPTABLE", и это позволяет получать одинаковый хэш запросов и объективную статистику на выходе. Здорово, когда СУБД нормализует данные за вас и не нужно возиться со скриптами, не правда ли?
Семплирование запросов
Также в pg_stat_statements
было реализовано семплирование запросов. Это метод равномерной фильтрации, при котором в выборку попадает лишь некоторая доля из общего числа запросов. Рассмотрим на простом примере: представим, что pg_stat_statements
— это таблица итогов регистра накопления. Когда какой-то из документов создает движения в регистре накопления, то он также обновляет и его таблицу итогов, предварительно накладывая блокировки по измерениям регистра накопления, чтобы конкурирующая транзакция не могла изменить обновляемую запись. Это обеспечивает согласованность изменений:

По той же логике работает и pg_stat_statements
: когда выполняется SQL-запрос, его необходимо запротоколировать в pg_stat_statements
, и поскольку конкурирующие сеансы могут выполнять одинаковые запросы, то и изменить им в хэш-таблице pg_stat_statements
может понадобиться одну и ту же запись. Для согласованности изменений применяются внутренние постгресовые блокировки (LWLock, Spinlock), которые при большом количестве уникальных запросов или высокой конкурентности могут привести к падению производительности системы (более подробно о проблеме можно почитать здесь). Семплирование же позволяет протоколировать не каждый запрос, а, например, каждый второй (при sample_rate=0.5). В 1С запросы повторяются постоянно, и вероятность того, что мы упустим какой-то важный запрос, крайне мала. Общую картину нагрузки на БД мы так или иначе увидим.
Подытожим: использование семплирования в pg_stat_statements
— хороший способ снизить нагрузку без потери качества собираемой статистики.
Улучшения, касающиеся сбора статистики
Автоматическое создание статистики
Одно из преимуществ MS SQL Server при работе с 1С —более точная оценка количества выбираемых строк при планировании запроса. Она достигается в том числе за счет того, что движок MS SQL Server автоматически создает недостающую статистику на основании собранной информации по уже выполненным запросам.
В Tantor Postgres мы решили реализовать то же самое, и в версии 17.5 вышла первая версия нашего модуля pg_stat_advisor
. Модуль позволяет автоматически создать недостающую статистику, чтобы планировщик мог выбирать наиболее эффективный план исполнения запроса.
Рассмотрим подробнее на простом примере из 1С как это все происходит. Напишем запрос, который выбирает все ссылки из справочника «ОбъектыРасчетов» с отбором по конкретным контрагенту и партнеру:
ВЫБРАТЬ
ОбъектыРасчетов.Ссылка
ИЗ
Справочник.ОбъектыРасчетов КАК ОбъектыРасчетов
ГДЕ
ОбъектыРасчетов.Партнер = &Партнер
И ОбъектыРасчетов.Контрагент = &Контрагент
Получаем следующий план запроса:

В плане видим, что планировщик «думал», что будет выбрана одна строка (цифра 1 на плане) вместо 812 выбранных фактически исходя из имеющейся статистики по таблице справочника «ОбъектыРасчетов» (цифра 2 на плане). Запрос выполнился довольно быстро — за 0.4 секунды, — и ошибка в оценке строк не оказала негативного влияния. Добавим теперь в запрос соединение с таблицей документа «ЗаказКлиента», отобрав оттуда записи, подходящие по контрагенту и сумме:
ВЫБРАТЬ
ОбъектыРасчетов.Ссылка КАК Ссылка,
ЗаказКлиента.Ссылка КАК Регистратор
ИЗ
Справочник.ОбъектыРасчетов КАК ОбъектыРасчетов
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЗаказПоставщику КАК ЗаказКлиента
ПО (ЗаказКлиента.Контрагент = ОбъектыРасчетов.Контрагент)
И (ЗаказКлиента.СуммаДокумента = ОбъектыРасчетов.Сумма)
ГДЕ
ОбъектыРасчетов.Партнер = &Партнер
И ОбъектыРасчетов.Контрагент = &Контрагент
Получим следующий план:

Здесь также мы видим, что планировщик ошибся с оценкой строк по таблице справочника «ОбъектыРасчетов» (цифра 1 на плане), и это привело к выбору Nested Loop
(цифра 2 на плане). Сам по себе выбор Nested Loop
проблемой не является, но в данном случае планировщик погалал, что ему придется искать всего один раз в таблице документа «ЗаказКлиента». Это было ошибкой и привело к операции сканирования таблицы документа ЗаказКлиента аж 812 раз (цифра 3 на плане). В результате запрос выполнялся более 15 секунд.
Здесь на помощь приходит наш новый модуль pg_stat_advisor
, который анализирует планы запросов на предмет ошибки в оценке строк, автоматически создает недостающую статистику и рассчитывает ее. Выполним запрос еще раз:

Автоматически созданная статистика помогла исправить ошибку в оценке строк по таблице справочника ОбъектыРасчетов: теперь планировщик исходит из того, что там 938 строк (цифра 1 на плане), что, конечно же, гораздо более соответствует действительности (т.е. нежели одна строка). В результате планировщик выбирает совсем другой план: он читает данные по таблице документа ЗаказКлиента всего один раз (цифра 2 на плане), формирует хэш-таблицу (цифра 3 на плане) и соединяет хешированием с прочитанными данными из таблицы справочника «ОбъектыРасчетов». Это позволяет ускорить запрос с 15 до 0.4 секунд!
Повышение точности статистики без увеличения default_statistics_target
На нагрузочном тесте 1С:ERP при разборе длительных запросов обратил на себя внимание вот такой запрос:
ВЫБРАТЬ ПЕРВЫЕ 1
ПрочиеРасходы.Период КАК Период
ИЗ
РегистрНакопления.ПрочиеРасходы КАК ПрочиеРасходы
ГДЕ
ПрочиеРасходы.Регистратор = &Регистратор
УПОРЯДОЧИТЬ ПО
ПрочиеРасходы.Период
Это типичный запрос подсистемы «Даты запреты редактирования», который вычисляет минимальную дату по регистру накопления проводимого документа. С точки зрения 1С запрос написан оптимально, однако он выполняется более трех секунд:

Но если планировщик выберет индекс по регистратору (заставим его сделать это командой SET plantuner.disable_index = '_accumrg50090_1';
), то запрос выполняется всего за 1 миллисекунду:

Состав индексов:
_accumrg50090_1 | _accumrg50090_2 |
_Fld2488 _Period _RecorderTRef _RecorderRRef _LineNo | _Fld2488 _RecorderTRef _RecorderRRef _LineNo |
Почему тогда планировщик выбирает неоптимальный индекс? Все дело в наличии сортировки и инструкции "ПЕРВЫЕ 1
" в нашем запросе:
При выборе индекса _accumrg50090_1
стоимость чтения из него крайне велика — 662992.98 (выделено цифрой 1 на соответствующем плане запроса), но за счет того, что данные, выбираемые из этого индекса, упорядочены по периоду, использование операции Limit
сильно сокращает итоговую стоимость запроса — 312.32 (цифра 2 на плане), ведь планировщик думает, что прочтет из индекса только одну запись, но это не так.
При выборе же индекса _accumrg50090_2
стоимость чтения из него невелика — 3498.20 (цифра 1 на плане), но поскольку данные в этом индексе не упорядочены по полю «Период», планировщик добавляет операцию сортировки, а она требует, чтобы ей на вход нижний узел передал сразу все прочитанные строки (цифра 2 на плане). Поэтому здесь при применении операции Limit стоимость сокращается лишь немного (цифра 3 на плане).
При разборе кейса была установлено, что уникальные значения в столбцах RecorderTRef, RecorderRRef (поле «Регистратор» в 1С) распределены неравномерно, и увеличение точности статистики (default_statistics_target = 2500
вместо 100) позволяет планировщику выбрать более оптимальный индекс по регистратору за счет получения более точной оценки селективности:

Если default_statistics_target в значении 2500 решает эту проблему, то почему бы на этом не остановиться? Дело в том, что увеличение этого параметра может привести к негативным последствиям:
Распухание статистики: придется хранить не 100, а 2500 скалярных статистик и гистограмм (к слову, в том же MS SQL Server хранится всего по 200 значений скалярных статистик и гистограмм);
Может увеличиться время планирования некоторых запросов (данный вопрос рассмотрим в материалах по следующему релизу Tantor Postgres).
Увеличится время вставки во временные таблицы, т.к. после вставки платформа 1С сразу рассчитывает статистику на временной таблице.
Иначе говоря, стоит задача увеличить точность статистики не за счет увеличения количества хранимых статистик, а за счет увеличения точности статистики. Можно вручную установить количество n_distinct по конкретному столбцу, но этот вариант нам тоже не подходит, поскольку распределение данных со временем может сильно измениться, и придется этот параметр менять снова.
Поэтому мы ввели новый параметр — STATMULTIPLIER
. Он устанавливается по конкретным колонкам таблицы и при расчете статистики позволяет применять следующую формулу:
300 * default_statistics_target * STATMULTIPLIER
Статистика получается более точной без необходимости увеличивать количество хранимых статистик.
Новый параметр не является глобальным, потому что таблиц с неравномерным распределением данных в общей массе таблиц базы 1С, как правило, немного. И он должен служить своего рода «скальпелем» в умелых руках администратора БД или эксперта 1С.
Оптимизация расчета статистики 3
Статистика нужна оптимизатору запросов, чтобы выбирать оптимальные планы выполнения и исполнять запросы быстро. Она рассчитывается через команду ANALYZE
: это можно сделать явно, вызвав ее из графического интерфейса или psql, либо автоматически процессом AUTOVACUUM
при достижении определенных порогов срабатывания. Для 1С характерно наличие таблиц, которые содержат большое количество колонок, например, в типовой 1С:ERP 2.5 более тысячи таблиц с 30+ колонками. Данная оптимизация как раз и касается таких широких таблиц.
Чтобы понять суть оптимизации, проведем опять аналогию с 1С. Допустим, у нас в базе данных есть таблица «Документ.РеализацияТоваровУслуг», в которой есть поля: «Сумма», «СуммаНДС», «СуммаСНДС», «СуммаСкидки», «СписаноБаллами» и т.д. Стоит задача посчитать по таблице сумму каждого этого поля. Решить ее можно неоптимально, выполняя в цикле запрос к каждому полю таблицы:
МассивПолей = Новый Массив;
МассивПолей.Добавить("Сумма");
МассивПолей.Добавить("СуммаНДС");
МассивПолей.Добавить("СуммаСНДС");
МассивПолей.Добавить("СуммаСкидки");
МассивПолей.Добавить("СписаноБаллами");
Для Каждого Поле Из МассивПолей Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СУММА(РеализацияТоваровУслуг."+Поле+")
|ИЗ
| Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг";
РезультатЗапроса = Запрос.Выполнить();
КонецЦикла;
А можно одним запросом получить сумму сразу по всем полям:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СУММА(РеализацияТоваровУслуг.Сумма)
| СУММА(РеализацияТоваровУслуг.СуммаНДС)
| СУММА(РеализацияТоваровУслуг.СуммаСНДС)
| СУММА(РеализацияТоваровУслуг.СуммаСкидки)
| СУММА(РеализацияТоваровУслуг.СписаноБаллами)
|ИЗ
| Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг";
РезультатЗапроса = Запрос.Выполнить();
Очевидно, второй вариант окажется быстрее, т.к. мы всего один раз обращаемся к таблице и читаем данные по всем необходимым полям. И чем по большему количеству полей нужно получить данные, тем сильнее первый способ проигрывает.
В этом же и суть оптимизации команды ANALYZE
: чтобы рассчитать статистику для каждого столбца таблицы, ранее бралась выборка данных по конкретному столбцу и по нему считались все необходимые статистики (mcv
, n_distinct
и тд). После оптимизации берется выборка сразу по всем столбцам, и уже затем считаются по очереди все столбцы выборки. Да, в моменте это требует больше оперативной памяти (чтобы хранить всю выборку), но позволяет значительно ускорить расчет статистики. Для проверки эффекта был написан скрипт, который:
Создает таблицу с 256 колонками;
Вставляет в эту таблицу 1000 записей;
Запускает pgbench, который в одном потоке выполняет команду
ANALYZE
данной таблицы в течение 60 секунд.
Получили следующие результаты:
Tantor Special Edition 1C версии 16: tps = 9.228891
Tantor Special Edition 1С версии 17: tps = 123.437999
Ускорение — на порядок!
Отчет по безопасности сервера баз данных
В версии 17.5 появилась утилита pg_sec_check
, предназначенная для аудита безопасности баз данных Tantor Postgres. Она автоматизирует процесс проверки различных аспектов безопасности — от настроек сервера до параметров, специфичных для базы данных, — и предоставляет подробные отчеты об обнаруженных проблемах и рекомендации по их устранению:

Среди особенностей утилиты выделим следующие:
Гибкая система проверок: можно определять проверки, которые нужны именно вам. Если нет нужной проверки — легко добавить новую;
Модульная архитектура: поддерживает различные типы исполнителей (SQL, shell-скрипты) и валидаторы (Lua-скрипты);
Проверки с версионированием: проверки могут быть привязаны к конкретным версиям Tantor Postgres;
Локализация: поддержка нескольких языков (в настоящее время русский и английский);
Форматы отчетов: генерирует отчеты в форматах
HTML
иJSON;
Параллельное выполнение: поддерживает многопоточность для ускорения сканирования;
Защита целостности: в релизных сборках проверяет целостность своих компонентов с использованием контрольных сумм.
По каждой проверке предоставляются подробные рекомендации с пояснением, на что стоит обратить внимание и как рекомендуется делать согласно правилам информационной безопасности:

В заключение скажем, что мы в «Тантор Лабс» уделяем оптимизациям под 1С изрядную долю внимания. У нас уже составлен план задач по 1С для следующих релизов СУБД, о них мы расскажем в следующих статьях.
Примечания
1 Эта оптимизация была реализована сообществом в 17 версии Postgres. При ребейзе мы объединили ее с нашими оптимизациями планировщика для запросов 1С с агрегацией данных.
2,3 Эти оптимизации портированы в Tantor Postgres из патча фирмы «1С».