Как планировщик использует статистику

Эта глава основывается на материалах, изложенных в разделах Использование EXPLAIN и Статистика, используемая планировщиком и более подробно описывает то, как планировщик использует системную статистику для оценки количества строк, которые может вернуть каждая часть запроса. Это существенная часть процесса планирования, обеспечивающая большую часть исходного материала для расчета стоимости запроса.

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



Примеры оценки количества строк

В приведенных ниже примерах используются таблицы в базе данных регрессионного теста QHB. Обратите также внимание, что поскольку ANALYZE при создании статистики использует случайную выборку, после каждого выполнения ANALYZE результаты будут немного меняться.

Начнем с очень простого запроса:

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)

Как планировщик определяет мощность таблицы tenk1, рассматривается в разделе Статистика, используемая планировщиком, но повторяется здесь для полноты картины. Количество страниц и строк можно узнать в pg_class:

SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';

 relpages | reltuples
----------+-----------
      358 |     10000

Это текущее количество по состоянию на последние выполнение VACUUM или ANALYZE в этой таблице. Затем планировщик извлекает фактическое текущее количество страниц в таблице (это дешевая операция, не требующая сканирования таблицы). Если это значение отличается от relpages, то reltuples пропорционально изменяется, чтобы приблизиться к текущей оценке количества строк. В приведенном выше примере значение relpages является актуальным, поэтому количество строк равно reltuples.

Давайте перейдем к примеру с диапазонным условием в предложении WHERE:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000;

                                   QUERY PLAN
--------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=24.06..394.64 rows=1007 width=244)
   Recheck Cond: (unique1 < 1000)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..23.80 rows=1007 width=0)
         Index Cond: (unique1 < 1000)

Планировщик проверяет условие предложения WHERE и ищет функцию избирательности для оператора < в pg_operator. Она хранится в столбце oprrest, и в данном случае там записано scalarltsel. Функция scalarltsel получает гистограмму для unique1 из pg_statistic. Для запросов, вводимых вручную, удобнее искать информацию в более простом представлении pg_stats:

SELECT histogram_bounds FROM pg_stats
WHERE tablename='tenk1' AND attname='unique1';

                   histogram_bounds
------------------------------------------------------
 {0,993,1997,3050,4040,5036,5957,7057,8029,9016,9995}

Далее обрабатывается отрезок гистограммы, занимаемый числами «<1000». Это и есть избирательность. Гистограмма делит диапазон на равные частотные сегменты, поэтому остается только найти сегмент, в котором находится нужное значение, и подсчитать его долю и долю всех предшествующих ему сегментов. Значение 1000 явно находится во втором сегменте (993-1997). Предполагая линейное распределение значений внутри каждого сегмента, можно вычислить избирательность как:

selectivity = (1 + (1000 - bucket[2].min)/(bucket[2].max - bucket[2].min))/num_buckets
            = (1 + (1000 - 993)/(1997 - 993))/10
            = 0.100697

то есть сумма одного целого сегмента и пропорциональной доли второго, разделенная на количество сегментов. Теперь ожидаемое количество строк можно рассчитать как произведение избирательности и мощности tenk1:

rows = rel_cardinality * selectivity
     = 10000 * 0.100697
     = 1007  (rounding off)

Далее рассмотрим пример с условием равенства в предложении WHERE:

EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'CRAAAA';

                        QUERY PLAN
----------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=30 width=244)
   Filter: (stringu1 = 'CRAAAA'::name)

Опять же, планировщик проверяет условие в WHERE и ищет функцию избирательности для =, и это функция eqsel. Для оценки равенства гистограмма бесполезна; вместо нее для определения избирательности используется список наиболее распространенных значений (most common value, MCV). Давайте рассмотрим MCV с некоторыми дополнительными столбцами, которые пригодятся позже:

SELECT null_frac, n_distinct, most_common_vals, most_common_freqs FROM pg_stats
WHERE tablename='tenk1' AND attname='stringu1';

null_frac         | 0
n_distinct        | 676
most_common_vals  | {EJAAAA,BBAAAA,CRAAAA,FCAAAA,FEAAAA,GSAAAA,JOAAAA,MCAAAA,NAAAAA,WGAAAA}
most_common_freqs | {0.00333333,0.003,0.003,0.003,0.003,0.003,0.003,0.003,0.003,0.003}

Поскольку CRAAAA находится в списке MCV, избирательность определяется просто соответствующей записью в списке наиболее распространенных частот (most common frequency, MCF):

selectivity = mcf[3]
            = 0.003

Как и прежде, расчетное количество строк будет просто произведением избирательности и кардинальности tenk1:

rows = 10000 * 0.003
     = 30

Теперь рассмотрим тот же запрос, но с константой, отсутствующей в списке MCV:

EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'xxx';

                        QUERY PLAN
----------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=15 width=244)
   Filter: (stringu1 = 'xxx'::name)

Здесь проблема в другом: как оценить избирательность, когда значение отсутствует в списке MCV. Для решения можно использовать тот факт, что искомое значение не находится в списке, в сочетании с частотой всех значений в списке MCV:

selectivity = (1 - sum(mvf))/(num_distinct - num_mcv)
            = (1 - (0.00333333 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003 +
                    0.003 + 0.003 + 0.003 + 0.003))/(676 - 10)
            = 0.0014559

Нужно сложить все частоты из списка MCV и вычесть эту сумму из единицы, а затем разделить результат на количество других уникальных значений. Это число вычисляется, исходя из предположения, что часть столбца, которая не входит в MCV, равномерно распределяется среди всех остальных уникальных значений. Обратите внимание, что здесь нет значений NULL, так что нет нужды беспокоиться о них (в противном случае мы бы вычитали из числителя и долю NULL). Затем ожидаемое количество строк вычисляется как обычно:

rows = 10000 * 0.0014559
     = 15  (rounding off)

Предыдущий пример с условием unique1 < 1000 был упрощенной версией того, что действительно делает scalarltsel. Теперь, когда мы увидели пример использования MCV, можно описать процесс более детально. В данном случае пример был верен, поскольку unique1 является уникальным столбцом, а значит, у него нет MCV (очевидно, что никакое значение не встречается чаще, чем любое другое). Для неуникального столбца обычно существуют и гистограмма, и список MCV, и гистограмма не включает значения столбца, представленные в списке MCV. Это сделано для более точной оценки. В этой ситуации scalarltsel напрямую применяет условие (например «<1000») к каждому значению списка MCV и суммирует частоты MCV, для которых это условие является истинным. Это дает точную оценку избирательности в той части таблицы, которая содержит значения MCVs. Затем, так же, как описано выше, гистограмма используется для оценки избирательности в части таблицы, не содержащей MCV, а потом эти два числа складываются для оценки общей избирательности. Например, рассмотрим

EXPLAIN SELECT * FROM tenk1 WHERE stringu1 < 'IAAAAA';

                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=3077 width=244)
   Filter: (stringu1 < 'IAAAAA'::name)

Мы уже видели информацию MCV для stringu1, а вот его гистограмма:

SELECT histogram_bounds FROM pg_stats
WHERE tablename='tenk1' AND attname='stringu1';

                                histogram_bounds
--------------------------------------------------------------------------------
 {AAAAAA,CQAAAA,FRAAAA,IBAAAA,KRAAAA,NFAAAA,PSAAAA,SGAAAA,VAAAAA,XLAAAA,ZZAAAA}

Проверяя список MCV, мы обраруживаем, что условие stringu1 < 'IAAAAA' удовлетворяется первыми шестью записями, но не последними четырьмя, поэтому избирательность для значений из множества MCV определяется как

selectivity = sum(relevant mvfs)
            = 0.00333333 + 0.003 + 0.003 + 0.003 + 0.003 + 0.003
            = 0.01833333

Суммирование всех MCF также говорит нам, что общая доля значений, представленных в списке MCV, составляет 0.03033333, и поэтому доля значений, представленная гистограммой, составляет 0.96966667 (опять же, нет значений NULL, иначе здесь их пришлось бы исключить). Видно, что значение IAAAAA расположено почти в конце третьего сегмента гистограммы. Используя довольно тривиальные предположения о частоте различных символов, планировщик получает оценку 0,298387 для части значений из гистограммы, которые меньше IAAAAA. Затем мы объединяем оценки для значений MCV и не MCV:

selectivity = mcv_selectivity + histogram_selectivity * histogram_fraction
            = 0.01833333 + 0.298387 * 0.96966667
            = 0.307669

rows        = 10000 * 0.307669
            = 3077  (rounding off)

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

Теперь рассмотрим случай с несколькими условиями в предложении WHERE:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000 AND stringu1 = 'xxx';

                                   QUERY PLAN
--------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=23.80..396.91 rows=1 width=244)
   Recheck Cond: (unique1 < 1000)
   Filter: (stringu1 = 'xxx'::name)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..23.80 rows=1007 width=0)
         Index Cond: (unique1 < 1000)

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

selectivity = selectivity(unique1 < 1000) * selectivity(stringu1 = 'xxx')
            = 0.100697 * 0.0014559
            = 0.0001466

rows        = 10000 * 0.0001466
            = 1  (rounding off)

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

Наконец мы рассмотрим запрос, включающий в себя соединение:

EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 50 AND t1.unique2 = t2.unique2;

                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Nested Loop  (cost=4.64..456.23 rows=50 width=488)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.64..142.17 rows=50 width=244)
         Recheck Cond: (unique1 < 50)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.63 rows=50 width=0)
               Index Cond: (unique1 < 50)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.00..6.27 rows=1 width=244)
         Index Cond: (unique2 = t1.unique2)

Ограничение на tenk1, unique1 < 50, вычисляется перед соединение со вложенным циклом. Все обрабатывается аналогично предыдущему примеру с диапазоном. На этот раз значение 50 попадает в первый сегмент гистограммы unique1:

selectivity = (0 + (50 - bucket[1].min)/(bucket[1].max - bucket[1].min))/num_buckets
            = (0 + (50 - 0)/(993 - 0))/10
            = 0.005035

rows        = 10000 * 0.005035
            = 50  (rounding off)

Ограничение для соединения является — t2.unique2 = t1.unique2. Оператор — уже известный =, однако функцию избирательности (eqjoinsel) мы получаем из столбца oprjoin таблицы pg_operator. Функция eqjoinsel ищет статистическую информацию как для tenk2, так и для tenk1:

SELECT tablename, null_frac,n_distinct, most_common_vals FROM pg_stats
WHERE tablename IN ('tenk1', 'tenk2') AND attname='unique2';

tablename  | null_frac | n_distinct | most_common_vals
-----------+-----------+------------+------------------
 tenk1     |         0 |         -1 |
 tenk2     |         0 |         -1 |

В этом случае отсутствует информация MCV для unique2, поскольку все значения оказываются уникальными, а значит, используется алгоритм, который полагается только на число уникальных значений для обоих отношений с учетом значений NULL:

selectivity = (1 - null_frac1) * (1 - null_frac2) * min(1/num_distinct1, 1/num_distinct2)
            = (1 - 0) * (1 - 0) / max(10000, 10000)
            = 0.0001

То есть из единицы для каждого отношения вычитается доля значений NULL, а результат делится на максимальное число уникальных значений. Количество строк, которые, скорее всего, будет получиено при соединении, вычисляется как мощность декартова произведения двух входных значений, умноженная на избирательность:

rows = (outer_cardinality * inner_cardinality) * selectivity
     = (50 * 10000) * 0.0001
     = 50

Если бы имелись списки MCV для двух столбцов, eqjoinsel использовала бы прямое сравнение со списками MCV для определения избирательности соединения части значений столбца, представленной в списке MCV. Оценка для остальных значений выполнялась бы способом, показанным выше.

Обратите внимание, что здесь inner_cardinality имеет значение 10000, то есть неизмененный размер tenk2. При проверке выходных данных EXPLAIN может показаться, что оценка строк соединения вычисляется как 50 * 1, то есть количество внешних строк умножается на предполагаемое количество строк, получаемых при каждым внутреннем индексном сканировании tenk2. Но это не так: размер отношения соединения оценивается до того, как выбирается какой-либо конкретный план соединения. Если все работает нормально, то два способа оценки размера соединения дадут примерно одинаковый результат, но из-за ошибки округления и других факторов иногда они заметно отличаются.



Примеры многомерной статистики

Функциональные зависимости

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

CREATE TABLE t (a INT, b INT);
INSERT INTO t SELECT i % 100, i % 100 FROM generate_series(1, 10000) s(i);
ANALYZE t;

Как объясняется в разделе Статистика, используемая планировщиком, планировщик может определить мощность таблицы t, используя количество страниц и строк, полученных из pg_class:

SELECT relpages, reltuples FROM pg_class WHERE relname = 't';

 relpages | reltuples
----------+-----------
       45 |     10000

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

В следующем примере показан результат оценки условия WHERE для столбца a:

EXPLAIN (ANALYZE, TIMING OFF) SELECT * FROM t WHERE a = 1;
                                 QUERY PLAN                                  
-------------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..170.00 rows=100 width=8) (actual rows=100 loops=1)
   Filter: (a = 1)
   Rows Removed by Filter: 9900

Планировщик проверяет условие и определяет, что избирательность этого предложения равна 1%. Сравнивая эту оценку и фактическое количество строк, мы видим, что эта оценка очень точна (в действительности совершенно точна, поскольку таблица очень маленькая). При изменении условия WHERE, чтобы использовался столбец b, генерируется идентичный план. Но посмотрите, что произойдет, если мы применим одно и то же условие к обоим столбцам, объединив их с помощью AND:

EXPLAIN (ANALYZE, TIMING OFF) SELECT * FROM t WHERE a = 1 AND b = 1;
                                 QUERY PLAN                                  
-----------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..195.00 rows=1 width=8) (actual rows=100 loops=1)
   Filter: ((a = 1) AND (b = 1))
   Rows Removed by Filter: 9900

Планировщик оценивает избирательность каждого условия индивидуально, приходя к тем же оценкам в 1%, что и выше. Затем он предполагает, что условия независимы, и поэтому перемножает их избирательности, выводя окончательную оценку избирательности, равную всего 0.01%. Это значительное преуменьшение, поскольку фактическое количество строк, соответствующих условиям (100), на два порядка больше.

Эту проблему можно решить путем создания объекта статистики, который предпишет команде ANALYZE вычислить многомерную статистику функциональной зависимости по двум столбцам:

CREATE STATISTICS stts (dependencies) ON a, b FROM t;
ANALYZE t;
EXPLAIN (ANALYZE, TIMING OFF) SELECT * FROM t WHERE a = 1 AND b = 1;
                                  QUERY PLAN                                   
-------------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..195.00 rows=100 width=8) (actual rows=100 loops=1)
   Filter: ((a = 1) AND (b = 1))
   Rows Removed by Filter: 9900

Многомерное количество уникальных значений

Аналогичная проблема возникает при оценке мощности наборов из нескольких столбцов, например числа групп, которые будут генерироваться предложением GROUP BY. Когда в GROUP BY задается один столбец, оценка уникальных значений (которые можно увидеть как расчетное количество строк, возвращаемых узлом HashAggregate) очень точна:

EXPLAIN (ANALYZE, TIMING OFF) SELECT COUNT(*) FROM t GROUP BY a;
                                       QUERY PLAN                                        
-----------------------------------------------------------------------------------------
 HashAggregate  (cost=195.00..196.00 rows=100 width=12) (actual rows=100 loops=1)
   Group Key: a
   ->  Seq Scan on t  (cost=0.00..145.00 rows=10000 width=4) (actual rows=10000 loops=1)

Но без многомерной статистики оценка количества групп в запросе с двумя столбцами в GROUP BY, в частности, в следующем примере, отклоняется на порядок:

EXPLAIN (ANALYZE, TIMING OFF) SELECT COUNT(*) FROM t GROUP BY a, b;
                                       QUERY PLAN                                        
--------------------------------------------------------------------------------------------
 HashAggregate  (cost=220.00..230.00 rows=1000 width=16) (actual rows=100 loops=1)
   Group Key: a, b
   ->  Seq Scan on t  (cost=0.00..145.00 rows=10000 width=8) (actual rows=10000 loops=1)

При переопределении объекта статистики, чтобы тот включал подсчет количества уникальных значений для двух столбцов, оценка значительно улучшается:

DROP STATISTICS stts;
CREATE STATISTICS stts (dependencies, ndistinct) ON a, b FROM t;
ANALYZE t;
EXPLAIN (ANALYZE, TIMING OFF) SELECT COUNT(*) FROM t GROUP BY a, b;
                                       QUERY PLAN                                        
--------------------------------------------------------------------------------------------
 HashAggregate  (cost=220.00..221.00 rows=100 width=16) (actual rows=100 loops=1)
   Group Key: a, b
   ->  Seq Scan on t  (cost=0.00..145.00 rows=10000 width=8) (actual rows=10000 loops=1)

Списки MCV

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

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

Взглянем еще раз на запрос из подраздела Функциональные зависимости, но теперь со списком MCV, созданным на том же наборе столбцов (не забудьте удалить функциональные зависимости, чтобы планировщик точно использовал только новую статистику).

DROP STATISTICS stts;
CREATE STATISTICS stts2 (mcv) ON a, b FROM t;
ANALYZE t;
EXPLAIN (ANALYZE, TIMING OFF) SELECT * FROM t WHERE a = 1 AND b = 1;
                                   QUERY PLAN
-------------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..195.00 rows=100 width=8) (actual rows=100 loops=1)
   Filter: ((a = 1) AND (b = 1))
   Rows Removed by Filter: 9900

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

Просмотреть список MCV можно с помощью возвращающей множество функции pg_mcv_list_items.

SELECT m.* FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid),
                pg_mcv_list_items(stxdmcv) m WHERE stxname = 'stts2';
 index |  values  | nulls | frequency | base_frequency
-------+----------+-------+-----------+----------------
     0 | {0, 0}   | {f,f} |      0.01 |         0.0001
     1 | {1, 1}   | {f,f} |      0.01 |         0.0001
   ...
    49 | {49, 49} | {f,f} |      0.01 |         0.0001
    50 | {50, 50} | {f,f} |      0.01 |         0.0001
   ...
    97 | {97, 97} | {f,f} |      0.01 |         0.0001
    98 | {98, 98} | {f,f} |      0.01 |         0.0001
    99 | {99, 99} | {f,f} |      0.01 |         0.0001
(100 rows)

Эта проверка подтверждает, что в двух столбцах есть 100 различных комбинаций, и все они примерно равновероятны (частота 1% для каждого из них). Базовая частота — это частота, вычисляемая из статистики по каждому столбцу независимо от статистики по нескольким столбцам. Если бы в любом из столбцов были значения NULL, это было бы указано в столбце nulls.

При оценке избирательности планировщик применяет все условия для элементов в списке MCV, а затем суммирует частоты подходящих элементов.

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

EXPLAIN (ANALYZE, TIMING OFF) SELECT * FROM t WHERE a = 1 AND b = 10;
                                 QUERY PLAN
---------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..195.00 rows=1 width=8) (actual rows=0 loops=1)
   Filter: ((a = 1) AND (b = 10))
   Rows Removed by Filter: 10000

Во-вторых, списки MCV обрабатывают более широкий диапазон типов предложений, а не только предложения с равенством, как функциональные зависимости. Например, взгляните на следующий запрос с диапазоном значений для той же таблицы:

EXPLAIN (ANALYZE, TIMING OFF) SELECT * FROM t WHERE a <= 49 AND b > 49;
                                QUERY PLAN
---------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..195.00 rows=1 width=8) (actual rows=0 loops=1)
   Filter: ((a <= 49) AND (b > 49))
   Rows Removed by Filter: 10000


Статистика планировщика и безопасность

Доступ к таблице pg_statistic имеют только суперпользователи, поэтому обычные пользователи не могут получить из нее информацию о содержании таблиц других пользователей. Некоторые функции оценки избирательности будут использовать предоставленный пользователем оператор (оператор, находящийся в запросе, или связанный оператор) для анализа сохраненной статистики. Например, чтобы определить, применимо ли сохраненное наиболее распространенное значение, оценщик избирательности должен будет выполнить соответствующий оператор =, чтобы сравнить константу в запросе с сохраненным значением. Таким образом, данные в pg_statistic потенциально могут передаваться пользовательским операторам. Соответствующим образом изготовленный оператор может «сливать» передаваемые ему операнды намеренно (например, протоколируя их в журнал или записывая в другую таблицу) или случайно, (показывая их значения в сообщениях об ошибках), в обоих случаях потенциально демонстрируя данные из pg_statistic пользователю, который не должен их видеть.

Чтобы предотвратить это, ко всем встроенным функциям оценки избирательности применяется следующая мера безопасности. Чтобы иметь возможность использовать сохраненную статистику при планировании запроса, текущий пользователь должен либо иметь право SELECT для таблицы или вовлеченных столбцов, либо использовать оператор с характеристикой LEAKPROOF (точнее, она должна быть у функции, на которой основан оператор). Иначе оценщик избирательности будет вести себя так, словно статистика недоступна, и планировщик будет продолжать использовать стандартные или резервные предположения.

Если пользователь не имеет необходимых прав для таблицы или столбцов, то во многих случаях запрос в конечном итоге получит ошибку «отказано в доступе», и тогда этот механизм не будет виден на практике. Но если пользователь читает данные из представления с барьером безопасности, то планировщик может захотеть проверить статистику нижележащей таблицы, которая в противном случае недоступна для этого пользователя. В этом случае оператор должен быть герметичным, иначе статистика не будет использоваться. Это не будет проявляться напрямую, за исключением, возможно, получения неоптимального плана запроса. При подозрении на этот вариант можно попробовать запустить запрос от имени более привилегированного пользователя, чтобы посмотреть, не выбирается ли другой план.

Это ограничение применяется только в тех случаях, когда планировщику необходимо выполнить пользовательский оператор для одного или нескольких значений из pg_statistic. Соответственно, планировщику разрешается использовать общую статистическую информацию, например долю значений NULL или количество уникальных значений в столбце, независимо от прав доступа.

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