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

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

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

Примеры оценок строк

В приведенных ниже примерах используются таблицы в базе данных регрессионного теста QHB. Обратите также внимание, что поскольку 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 в списке наиболее распространенных частот (MCFs):

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, равномерно распределена между всеми другими различными значениями. Обратите внимание, что нет никаких нулевых значений, поэтому нам не нужно беспокоиться о них (в противном случае мы бы вычитали нулевую дробь из числителя). Приблизительное число строк затем вычисляется как обычно:

rows = 10000 * 0.0014559
     = 15  (rounding off)

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

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 удовлетворяется первыми шестью записями, а не последними четырьмя, поэтому селективность для значений из множества MCV определяется как

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

Суммирование всех MCFs также говорит нам, что общая доля значений из множества MCVs, составляет 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 незначительна, потому что распределение столбцов на самом деле довольно плоское (статистика, показывающая эти конкретные значения как более распространенные, чем другие, в основном из-за ошибки выборки). В более типичном случае, когда некоторые значения являются значительно более распространенными, чем другие, этот сложный процесс повышает точность, поскольку селективность для MVC найдена точно.

Теперь рассмотрим случай с несколькими условиями в 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.eqjoinsel) находится в столбце oprjoin таблицы pg_operator. Эта функция ищет статистическую информацию для обоих 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 поскольку все значения выглядят уникальными, а значит используется алгоритм, который полагается только на число различных значений для обоих отношений вместе с их нулевыми фракциями:

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 для определения селективности соединения для той части значений, что содержится в MCVs. Оценка для остальной части данных следует по такому же подходу.

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

Для тех, кто заинтересован в более подробной информации, оценка размера таблицы (перед любым условием WHERE) делается в src/backend/ optimizer/util/plancat.с. Общая логика для селективности условий находится в src/backend/optimizer/path/clausesel.с. Специфичные для операторов функции селективности можно найти внутри src/backend/utils/adt/selfuncs.с.

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

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

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

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% Сравнивая эту оценку и фактическое число строк, мы видим, что оценка является очень точной (фактической, так как таблица очень мала). При использования столбца b в условии WHERE генерируется идентичный план. Но обратите внимание, что произойдет, если мы применим одно и то же условие к обоим столбцам, объединив их с 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% для каждого из них). base_frequency — это частота, вычисляемая из статистики по каждому столбцу независимо от статистики по нескольким столбцам. Если бы в любом из столбцов были какие-либо нулевые значения, это было бы указано в столбце nulls.

При оценке селективности планировщик применяет все условия для элементов в списке MCV, а затем суммирует частоты эквивалентных элементов. За подробностями можно обратиться к функции mcv_clauselist_selectivity в файле src/backend/статистика/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 ограничивается суперпользователями, так что обычные пользователи не могут узнать о содержании таблиц других пользователей из него. Некоторые функции оценки селективности будут использовать предоставленный пользователем оператор (либо оператор, появляющийся в запросе, либо связанный с ним оператор) для анализа сохраненной статистики. Например, чтобы определить, применимо ли сохраненное MCV, оценщик селективности должен будет выполнить соответствующее оператор = для сравнения константы в запросе с сохраненным значением. Таким образом, данные в pg_statistic потенциально могут передаваться пользовательским операторам. Правильно созданный оператор может намеренно пропускать переданные операнды (например, регистрируя их или записывая в другую таблицу), или случайно пропускать их, показывая их значения в сообщениях об ошибках, в обоих случаях возможно получить данные из pg_statistic для пользователя, который не должен видеть его.

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

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

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

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