На апрельской конференции PG BootCamp 2025 в Екатеринбурге был представлен доклад Артёма Бугаенко о том, как сделать статистику Postgres более детализированной, не повышая значение default_statistics_target. Если посмотреть на доклад под другим углом, то ему отлично подошло бы название «Пример создания патча для PostgreSQL». Примеры правки логики планировщика есть во многих патчах, но объяснение того, куда и какой код нужно вставлять в многочисленные файлы исходного кода PostgreSQL, встречается нечасто. Также можно встретить примеры описания того, как добавить параметры конфигурации (GUC), но вот пример того, как добавить опцию в команду SQL, найти подчас затруднительно. Поэтому если вдруг вам понадобится добавить в команду PostgreSQL какую-либо свою опцию, можно использовать данные статью и доклад как своего рода руководство.

Почему понадобилось добавлять параметр 

В докладе Артёма предлагалось добавить параметр с названием STATMULTIPLIER, который можно было бы установить командой:

ALTER TABLE имя ALTER [COLUMN] столбец SET STATMULTIPLIER число;

Задача, которую решал патч из доклада, состояла в следующем. Есть параметр default_statistics_target, который по умолчанию равен 100. При сборе статистики считывается 300*100 строк таблицы. Число 300 было рассчитано в статье "Random sampling for histogram construction: how much is enough?" как минимально достаточное для построения гистограмм распределения значений по столбцам. В файле src/backend/commands/analyze.c в комментарии к функции std_typanalyze() об этом написано:

Скрытый текст

The following choice of minrows is based on the paper
"Random sampling for histogram construction: how much is enough?"
by Surajit Chaudhuri, Rajeev Motwani and Vivek Narasayya, in
Proceedings of ACM SIGMOD International Conference on Management
of Data, 1998, Pages 436-447. Their Corollary 1 to Theorem 5
says that for table size n, histogram size k, maximum relative
error in bin size f, and error probability gamma, the minimum
random sample size is
r = 4kln(2n/gamma) / f^2 Taking f = 0.5, gamma = 0.01, n = 10^6 rows, we obtain r = 305.82k Note that because of the log function, the dependence on n is quite weak; even at n = 10^12, a 300k sample gives <= 0.66
bin size error with probability 0.99. So there's no real need to
scale for n, which is a good thing because we don't necessarily
know it at this point.

Есть параметр STATISTICS, который соответствует параметру конфигурации default_statistics_target. Значение для STATISTICS можно устанавливать на уровне столбца командой:

ALTER TABLE имя ALTER [COLUMN] столбец SET STATISTICS число;

Кроме определения размера выборки для расчета статистик (300*STATISTICS строк в выборке), параметр default_statistics_target определяет число корзин в гистограммах most_common_vals и histogram_bounds. При неравномерном распределении значений на таблицах с большим числом строк (от 10 млн) значения n_distinct (число уникальных значений) и null_frac (доля пустых полей) могут быть меньше реальных на два-три порядка, что довольно существенно. Различиями в пределах порядка можно пренебречь, статистика не обязана быть абсолютно точной. Чтобы приблизить значения n_distinct и null_frac к реальным, нужно увеличить число строк выборки. Это можно сделать, увеличив значение STATISTICS.

Однако параметр STATISTICS увеличивает в гистограммах число корзин, то есть наиболее часто встречающихся значений (most_common_vals), а также массив с частотой встречаемости этих значений (most_common_freqs). Увеличение числа корзин увеличивает размер, хранимых статистик, размер таблицы системного каталога, влияет на кэш системного каталога в памяти процессов, замедляет создание плана выполнения. Хотелось бы увеличить число строк в выборке при анализе таблицы, не меняя объем хранимых статистик. Для этого Артём и решил ввести новый параметр, назвав его STATMULTIPLIER, и поменять формулу вычисления числа строк в выборке: вместо 300*STATISTICS сделать 300*STATISTICS*STATMULTIPLIER.

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

Если в таблице больше десятка миллионов строк и при увеличении параметра STATISTICS значение статистики n_distinct приближается к реальному, который можно определить запросом "select count(distinct столбец) from таблица", и при этом план выполнения меняется так, что запрос выполняется заметно быстрее, то новый параметр будет полезен. Практический пример на таблице 1C:ERP был также приведён в докладе Артёма Бугаенко, слайды и патч к которому можно посмотреть здесь.

Детализация задачи

В докладе предлагается добавить параметр с названием STATMULTIPLIER, который можно было бы установить командой:

ALTER TABLE имя ALTER [COLUMN] столбец SET STATMULTIPLIER число;

Можно было бы добавить параметр конфигурации (GUC), но это было бы нелогично: параметров достаточно много, да и в докладе рассматривается методика добавления новых параметров именно в команды. Артём Бугаенко ранее разрабатывал прошивки SSD в SK Hynix, значит, умеет писать компактный, стабильно работающий и безошибочный код. Собственно, доклад и создавался в процессе создания патча для релиза СУБД Tantor Postgres, который был призван обеспечить более высокую точность статистики без необходимости увеличивать количество хранимых статистик. Это было анонсировано в Tantor Postgres 17 версии, подробнее можно прочесть в отдельной статье.

Значение по умолчанию выбрано -1, а допустимый диапазон — от 1 до 10000. Параметр должен заменить формулу, по которой считается число строк для сбора статистики по таблице с 300*STATISTICS на 300*STATISTICS*STATMULTIPLIER.

С виду и формула, и задача просты, но на практике оказывается, что внести изменение в код PostgreSQL не так просто, есть подводные камни, которые мы скоро увидим. Например, значение STATMULTIPLIER должно храниться в столбце таблицы системного каталога pg_attribute, так как параметр  STATISTICS хранится в столбце attstattarget этой таблицы. Назовём новый параметр attstatssamplemultiplier — длинновато, но понятно.

Что менять в коде PostgreSQL

Где взять списки файлов и мест в этих файлах, в которые нужно внести изменения? Самое простое — найти закоммиченный (то есть качественный) патч, которым добавлялся аналогичный функционал. Пример такого патча был приложен к докладу, но поскольку доклад создавался в процессе разработки патча и был рассчитан на 16 версию PostgreSQL, версия на тот момент финальной не была. В PostgreSQL 17 версии организация системного каталога претерпела изменения: часть атрибутов была перенесена из фиксированной части в NULLable секцию. Однако пользуясь этой статьёй доработать патч для более новых версий PostgreSQL вполне возможно. Если же патча нет, или сложно искать, если он накладывался на предыдущие релизы PostgreSQL и сопоставить строки сложно, то можно составить список файлов, выполнив поиск по характерным словам: attstattarget, затем свериться со списком поиска по stattarget, также поискать слово STATISTICS, которое присутствует в команде, в которую требовалось добавить своё ключевое слово. Даже если свой патч создается на основе другого патча, стоит найти все файлы по этим словам и проверить, не нужно ли в них вносить изменения. В файлах стоит просматривать, как в файлах упоминаются буквосочетания stattarget и STATISTICS, по которым был поиск.

По результатам поиска остаются такие файлы для правки:

1) /src/include/catalog/pg_attribute.h — создать новый параметр attstatssamplemultiplier

2) /src/backend/commands/analyze.c — внести изменения в вычисления и формулы, в которых параметр должен учитываться, то есть поменять формулу attstattarget*300 на attstattarget*attstatssamplemultiplier*300.

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

3) /src/backend/parser/gram.y и /src/include/parser/kwlist.h — добавить ключевое слово

4) /src/include/nodes/parsenodes.h

В файлах пунктов 3 и 4  – поддержка синтаксиса команды ALTER TABLE.

5) /src/backend/commands/tablecmds.c — установить значение параметра при выполнении команды ALTER TABLE.

6) /doc/src/sgml/ref/alter_table.sgml — обязательно документировать параметр, чтобы он попал в документацию, которая генерируется из файлов sgml.

7) /src/backend/catalog/index.c, /src/backend/bootstrap/bootstrap.c, /src/backend/access/common/tupdesc.c — в эти файлы нужно добавить поддержку параметра, чтобы значение параметра могло постоянно храниться.

8) Добавить поддержку выгрузки значения параметра утилитой pg_dump, чтобы он генерировал команду ALTER TABLE с параметром.

9) Добавить поддержку параметра в psql.c — например для того, чтобы значение отображалось в команде psql "\d+".

Файлов, как видите, довольно много. Закоммитят ли патч, если предложить его в сообщество? Обоснование и формула просты и понятны, а вот правки вносятся в десяток файлов. Чтобы ревьюеры поняли, что меняется и зачем, можно описать, как осуществлялся поиск файлов для правок и что ни один файл с упоминанием параметра-примера (attstattarget) не пропущен.

Рассмотрим, что добавляется в перечисленные файлы.

Файл pg_attribute.h

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

/* statistics sample multiplier */
int16 attstatssamplemultiplier BKI_DEFAULT(-1);

В какое место добавлять?

Размер фиксированной части структуры рассчитывается макросом:

/*
 * ATTRIBUTE_FIXED_PART_SIZE is the size of the fixed-layout,
 * guaranteed-not-null part of a pg_attribute row.  This is in fact as much
 * of the row as gets copied into tuple descriptors, so don't expect you
 * can access the variable-length fields except in a real tuple!
 */
#define ATTRIBUTE_FIXED_PART_SIZE \
 (offsetof(FormData_pg_attribute,attcollation) + sizeof(Oid))

Код означает: ищется смещение до атрибута attcollation и добавляется его размер.

Атрибуты (они же параметры) переменной ширины располагаются в конце структуры. Если сделать параметр фиксированной ширины, обращение к значению будет идти по смещению. В этом случае его нужно вставить перед элементом Oid, то есть перед строкой:

Oid attcollation BKI_LOOKUP_OPT(pg_collation);

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

Если ее размер типа данных параметра или порядок параметров меняются, это придется учесть при миграции на новую мажорную версию PostgreSQL. В докладе приводится пример добавления параметра в часть фиксированной длины ввиду того что патч начинал писаться для старых версий. Лучше добавить атрибут в переменную часть сразу после логически связанного параметра attstatssample. Добавление атрибута в самый конец или в переменную часть не даёт преимуществ, ведь при миграции на новый релиз всё равно нужно предусмотреть обновление собранных статистик.

Файл kwlist.h

Для того, чтобы ключевое слово в команде распознавалось парсером, его надо добавить в файл kwlist.h. Добавить его можно после слова statistics:

PG_KEYWORD("statistics", STATISTICS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("statmultiplier", STATMULTIPLIER, UNRESERVED_KEYWORD, BARE_LABEL)

Заданы:

  1. ключевое слово;

  2. токен для парсера;

  3. UNRESERVED_KEYWORD — это значит, что парсер распознаёт его как специальное слово только в контексте команды, а не глобально;

  4. BARE_LABEL — слово можно использовать в качестве имени переменной.

Файл gram.y (POSTGRESQL BISON rules/actions)

Добавляем токен в список ключевых слов команды:

START STATEMENT STATISTICS STATMULTIPLIER  STDIN STDOUT STORAGE STORED STRICT_P STRIP_P, а также в unreserved_keyword и bare_label_keyword после STATISTICS.

Добавляем код:

 /* ALTER TABLE <name> ALTER [COLUMN] <colname> SET STATMULTIPLIER <SignedIconst> */
    ALTER opt_column ColId SET STATMULTIPLIER SignedIconst
{
 AlterTableCmd *n = makeNode(AlterTableCmd);

 n->subtype = AT_SetStatisticsSampleMultiplierColumn;
 n->name = $3;
 n->def = (Node *) makeInteger($6);
 $$ = (Node *) n;
}
SQL превращается в абстрактное синтаксическое дерево (AST)
SQL превращается в абстрактное синтаксическое дерево (AST)

ParseNodes.h

Здесь требуется добавить подтип, который передали командой

AT_SetStatisticsSampleMultiplierColumn, /* alter column set statistics sample multiplier*/

tablecmds.c

Добавление поддержки команды. Внесение этих изменений позволяет передать новое значение в переменную attstatssamplemultiplier, объявленную в pg_attribute.h.

Скопипастить текст функции ATExecSetStatistics(...) на 130 строк в новую функцию. Входные данные функции: таблица, имя столбца, переданное через абстрактное дерево значение параметра и режим блокировки таблицы на время выполнения этой команды. В теле функции проверяется правильность переданного значения: диапазон, тип, и куда значение будет записано. Выполняются проверки:

  • существует ли столбец;

  • что столбец не системный;

  • Не пытаемся обновить included column или физический столбец у индекса.

Все эти проверки обеспечивают целостность изменений. Потом обновляется само значение переменной, и вызывается «хук» постфиксных изменений. Код функции приводит к сохранению переменной и ее значения в таблице системного каталога pg_attribute. Значение можно задавать командой ALTER COLUMN. В тех местах, где упоминается case AT_SetStatistics, добавить название новой функции.

bootstrap.c

Добавить инициализацию значением по умолчанию в функцию DefineAttr(char name, char type, int attnum, int nullness):

attrtypes[attnum]->attstatssamplemultiplier = -1;

после строки аналогичного параметра

attrtypes[attnum]->attstattarget = -1;

tupdesc.c и index.c

  Добавить инициализацию значением по умолчанию в функцию TupleDescInitEntry(TupleDesc desc, namestrcpy(&(att->attname), attributeName);

att->attstatssamplemultiplier = -1;

после строки

att->attstattarget = -1;

analyze.c

  В функцию std_typanalyze(VacAttrStats *stats) добавить:

/* If the attstatssamplemultiplier column is negative, use the default value */
/* NB: it is okay to scribble on stats->attr since it's a copy */
if (attr->attstatssamplemultiplier < 1 || attr->attstatssamplemultiplier > 10000)
  attr->attstatssamplemultiplier = 1;

Можно обратить внимание, что параметр stats 32-разрядный, а в этой формуле превышение значения после перемножения не проверяется. Эту проверку нужно добавить так, чтобы значение не превышало 2 миллиарда.

/* Check for possible overflow in (attstattarget * attstatssamplemultiplier * 300) */
scaled_attstattarget = stats->attstattarget * stats->attstatssamplemultiplier;
if (scaled_attstattarget > max_scaled_attstattarget)
{
/* Adjust multiplier to prevent overflow in minrows calculation */
 stats->attstatssamplemultiplier = max_scaled_attstattarget / stats->attstattarget;
 if (stats->attstatssamplemultiplier < 1) stats->attstatssamplemultiplier = 1;
}

Если параметр выходит за границы диапазона, то использовать 1, тогда параметр не будет ни на что влиять: при умножении 1 не окажет влияния на результат.

Добавить в формулу stats->minrows = 300 * attr->attstattarget в этой функции в трёх местах параметр. Получится формула:

stats->minrows = 300 attr->attstattarget * attr->attstatssamplemultiplier;

Эта формула и есть основной функционал, ради чего параметр и добавлялся.

То же самое следует проделать в ts_typanalyze.c и rangetypes_typeanalyze.c

heap.c

Для поддержки сохраненных значений в таблицах системного каталога добавить строки по аналогии с attstatssample:

slot[slotCount]->tts_isnull[Anum_pg_attribute_attstatssamplemultiplier - 1] = true;
nullsAtt[Anum_pg_attribute_attstatssamplemultiplier - 1] = true;
replacesAtt[Anum_pg_attribute_attstatssamplemultiplier - 1] = true;

bin/psql/describe.c

Чтобы psql показывал по команде \d+ сохраненные значения нового параметра, требуется добавить строки:

/* stats multiplier target, if relevant to relkind */
if (tableinfo.relkind == RELKIND_RELATION)
{
 appendPQExpBufferStr(&buf, ",\n  CASE WHEN a.attstatssamplemultiplier=-1 THEN NULL ELSE a.attstatssamplemultiplier END AS attstatssamplemultiplier");
 attstatssamplemultiplier_col = cols++;
}

bin/psql/tab-complete.c

Чтобы psql при автозавершении команд по клавише табуляции предлагал имя нового параметра, нужно заменить:

COMPLETE_WITH("SET STATISTICS");

на

COMPLETE_WITH("SET STATISTICS","SET STATMULTIPLIER");

и добавить:

/* ALTER INDEX <name> ALTER COLUMN <column> SET STATMULTIPLIER */
else if (Matches("ALTER", "INDEX", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "STATMULTIPLIER"))
{
/* Enforce no completion here, as an integer has to be specified */
}

vacuum.h

 Добавить

int attstatssamplemultiplier; /* -1 to use default */

alter_table.sgml

Из файлов с расширением sgml создается текст документации. Добавить в них параметр и описание — дело несложное.

После строки

 ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET STATISTICS <replaceable class="parameter">integer</replaceable>

добавить строку:

ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET STATMULTIPLIER <replaceable class="parameter">integer</replaceable>

и само описание:

<varlistentry id="sql-altertable-desc-set-statmultiplier">
        <term><literal>SET STATMULTIPLIER <replaceable class="parameter">integer</replaceable></literal></term>
        <listitem>
         <para>
         This form
         sets the per-column statistics sample size multiplier for subsequent
         <command>ANALYZE</command> operations.
         The multiplier is applied to the sample size determined by the statistics target.
         The multiplier can be set to values between 1 and 10000. Alternatively, set it
         to -1 to revert to using the system default statistics sample multiplier.
         </para>
         <para>
          <literal>SET STATMULTIPLIER</literal> acquires a
          <literal>SHARE UPDATE EXCLUSIVE</literal> lock.
         </para>
         </listitem>
       </varlistentry>

Можно добавить описание и в catalogs.sgml, чтобы описание атрибута появилось в описании pg_attribute, и затем перегенерировать *.out файлы с результатами тестов.

Если изрядно утомились, то добавление кода в утилиту pg_dump (pg_dump.c и pg_dump.h) можно оставить на потом. Это относительно просто: нужно искать строки со словом attstattarget и под ними добавлять новые строки со словом attstatssamplemultiplier. Например, вы видите строку:

tbinfo->attstattarget = (int *) pg_malloc(numatts * sizeof(int));

и добавляете сразу после неё еще одну строку:

tbinfo->attstatssamplemultiplier = (int *) pg_malloc(numatts * sizeof(int));

Патч из доклада можно скачать и использовать как пример. Самое сложное — разобраться первый раз :)

Резюме

В статье рассмотрена техника создания патча на примере добавления нового колоночного атрибута в системный каталог, с возможностью установки значения через команду ALTER TABLE. Техника будет полезна тем, кто хочет лучше разобраться, как расширяется функционал PostgreSQL. Параметр, описанный в статье, был добавлен в СУБД Tantor Postgres 17.5.

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


  1. Sleuthhound
    10.07.2025 14:03

    На словах "это нужно для 1С:ERP" я было уже хотел закрыть статью, но дочитал и снимаю шляпу перед автором за труд по созданию доклада.

    А резюме тут ИМХО простое - Не нужно использовать софт (я про 1С) с той БД для которой он не писался и не будет проблем.

    Ну и дисклеймент: Помнится когда-то 1С даже и Oracle поддерживала и я даже пытался запустить сервер-приложений 1С на Linux c Oracle (лет 6-7 назад), но к сожалению даже супер-крутые ораклисты (с 20-ти летним стажем работы с оракл) помучившись и поплевавшись сказали, что никогда 1С не будет работать с Oracle нормально ибо писалась она под другую СУБД. Как говориться - гандон конечно можно натянуть на глобус, но нужно ли это?