Посмотрим на статистику на примере таблицы рейсов flights. Значение параметра, управляющего размером статистики, по умолчанию равно 100:
=> SHOW default_statistics_target;
default_statistics_target --------------------------- 100 (1 row)
Уменьшим его до 10 в целях демонстрации:
=> SET default_statistics_target = 10;
SET
=> ANALYZE;
ANALYZE
Поскольку при анализе таблицы учитывается 300*default_statistics_target строк, то оценки, как правило, не будут абсолютно точными.
Теперь посмотрим на оценку кардинальности в простом случае - запрос без предикатов.
=> EXPLAIN SELECT * FROM flights;
QUERY PLAN ---------------------------------------------------------------- Seq Scan on flights (cost=0.00..4564.67 rows=214867 width=63) (1 row)
Точное значение:
=> SELECT count(*) FROM flights;
count -------- 214867 (1 row)
Оптимизатор получает значение из pg_class:
=> SELECT reltuples, relpages FROM pg_class WHERE relname = 'flights';
reltuples | relpages -----------+---------- 214867 | 2416 (1 row)
Часть рейсов еще не отправились, поэтому время вылета для них не определено:
=> EXPLAIN SELECT * FROM flights WHERE actual_departure IS NULL;
QUERY PLAN --------------------------------------------------------------- Seq Scan on flights (cost=0.00..4564.67 rows=17261 width=63) Filter: (actual_departure IS NULL) (2 rows)
Точное значение:
=> SELECT count(*) FROM flights WHERE actual_departure IS NULL;
count ------- 16348 (1 row)
Оценка оптимизатора получена как общее число строк, умноженное на долю NULL-значений:
=> SELECT 214867 * null_frac FROM pg_stats WHERE tablename = 'flights' AND attname = 'actual_departure';
?column? ---------------- 17260.98156064 (1 row)
Если значение есть в списке наиболее частых значений, то селективность можно узнать непосредственно из статистики. Пример (Шереметьево):
=> EXPLAIN SELECT * FROM flights WHERE departure_airport = 'SVO';
QUERY PLAN --------------------------------------------------------------- Seq Scan on flights (cost=0.00..5101.84 rows=19195 width=63) Filter: (departure_airport = 'SVO'::bpchar) (2 rows)
Точное значение:
=> SELECT count(*) FROM flights WHERE departure_airport = 'SVO';
count ------- 19347 (1 row)
Вот как выглядит список наиболее частых значений и частота их встречаемости:
=> SELECT most_common_vals, most_common_freqs FROM pg_stats WHERE tablename = 'flights' AND attname = 'departure_airport' \gx
-[ RECORD 1 ]-----+-------------------------------------------------------------------------------------- most_common_vals | {DME,SVO,VKO,LED,OVB,KJA,ROV,NUX,AER,BZK} most_common_freqs | {0.0986667,0.0893333,0.0543333,0.0533333,0.031,0.023,0.0203333,0.0193333,0.019,0.019}
Максимальное число значений определяется параметром default_statistics_target, который в нашем случае равен 10. Значения по умолчанию 100 немного не хватило бы, чтобы хранить все 104 значения.
Кардинальность вычисляется как число строк, умноженное на частоту значения:
=> SELECT 214867 * s.most_common_freqs[array_position((s.most_common_vals::text::text[]),'SVO')] FROM pg_stats s WHERE s.tablename = 'flights' AND s.attname = 'departure_airport';
?column? ----------------- 19194.785277836 (1 row)
Если же указанного значения нет в списке наиболее частых, то оно вычисляется исходя из предположения, что все данные (кроме наиболее частых) распределены равномерно.
Например, в списке частых значений нет Владивостока.
=> EXPLAIN SELECT * FROM flights WHERE departure_airport = 'VVO';
QUERY PLAN -------------------------------------------------------------- Seq Scan on flights (cost=0.00..5101.84 rows=1323 width=63) Filter: (departure_airport = 'VVO'::bpchar) (2 rows)
Точное значение:
=> SELECT count(*) FROM flights WHERE departure_airport = 'VVO';
count ------- 1188 (1 row)
Для получения оценки вычислим сумму частот наиболее частых значений:
=> SELECT sum(f) FROM pg_stats s, unnest(s.most_common_freqs) f WHERE s.tablename = 'flights' AND s.attname = 'departure_airport';
sum ---------- 0.427333 (1 row)
На менее частые значения приходятся оставшиеся строки. Поскольку мы исходим из предположения о равномерности распределения менее частых значений, селективность будет равна 1/nd, где nd - число уникальных значений:
=> SELECT n_distinct FROM pg_stats s WHERE s.tablename = 'flights' AND s.attname = 'departure_airport';
n_distinct ------------ 103 (1 row)
Учитывая, что из этих значений 10 входят в список наиболее частых, и нет неопределенных значений, получаем следующую оценку:
=> SELECT 214867 * (1 - 0.427333) / (103 - 10);
?column? ----------------------- 1323.0886052580645161 (1 row)
При условиях "больше" и "меньше" для оценки будет использоваться список наиболее частых значений, или гистограмма, или оба способа вместе. Гистограмма строится так, чтобы не включать наиболее частые значения и NULL:
=> SELECT histogram_bounds FROM pg_stats s WHERE s.tablename = 'flights' AND s.attname = 'departure_airport';
histogram_bounds ----------------------------------------------- {AAQ,CSY,HMA,KRO,MJZ,NOZ,PEE,SCW,TBW,ULY,YKS} (1 row)
Число корзин гистограммы определяется параметром default_statistics_target, а границы выбираются так, чтобы в каждой корзине находилось примерно одинаковое количество значений.
Рассмотрим пример:
=> EXPLAIN SELECT * FROM flights WHERE departure_airport < 'HMA';
QUERY PLAN --------------------------------------------------------------- Seq Scan on flights (cost=0.00..5101.84 rows=53975 width=63) Filter: (departure_airport < 'HMA'::bpchar) (2 rows)
Точное значение:
=> SELECT count(*) FROM flights WHERE departure_airport < 'HMA';
count ------- 50972 (1 row)
Как получена оценка?
Учтем частоту наиболее частых значения, попадающих в указанный интервал:
=> SELECT sum( s.most_common_freqs[array_position((s.most_common_vals::text::text[]),v)] ) FROM pg_stats s, unnest(s.most_common_vals::text::text[]) v WHERE s.tablename = 'flights' AND s.attname = 'departure_airport' AND v < 'HMA';
sum ---------- 0.136667 (1 row)
Указанный интервал занимает ровно 2 корзины гистограммы из 10, а неопределенных значений в данном столбце нет, получаем следующую оценку:
=> SELECT 214867 * (1 - 0.427333) * (2.0 / 10.0) + 214867 * 0.136667;
?column? ---------------------------------- 53974.67634680000000000000000000 (1 row)
В общем случае учитываются и не полностью занятые корзины (с помощью линейной аппроксимации).
Рассмотрим запрос с двумя условиями:
=> SELECT count(*) FROM flights WHERE flight_no = 'PG0007' AND departure_airport = 'VKO';
count ------- 396 (1 row)
Оценка оказывается сильно заниженной:
=> EXPLAIN SELECT * FROM flights WHERE flight_no = 'PG0007' AND departure_airport = 'VKO';
QUERY PLAN ----------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on flights (cost=10.77..883.79 rows=17 width=63) Recheck Cond: (flight_no = 'PG0007'::bpchar) Filter: (departure_airport = 'VKO'::bpchar) -> Bitmap Index Scan on flights_flight_no_scheduled_departure_key (cost=0.00..10.77 rows=313 width=0) Index Cond: (flight_no = 'PG0007'::bpchar) (5 rows)
Причина в том, что планировщик полагается на то, что предикаты не коррелированы и считает общую селективность как произведение селективностей условий, объединенных логическим "и". Это хорошо видно в приведенном плане: оценка в узле Bitmap Index Scan (условие на flight_no) одна, а после фильтрации в узле Bitmap Heap Scan (условие на departure_airport) - другая.
Однако мы понимаем, что номер рейса однозначно определяет аэропорт отправления: фактически, второе условие избыточно (конечно, считая, что аэропорт указан правильно).
Начиная с версии PostgreSQL 10, это можно объяснить и планировщику с помощью статистики по функциональной зависимости:
=> CREATE STATISTICS flights1(dependencies) ON flight_no, departure_airport FROM flights;
CREATE STATISTICS
=> ANALYZE flights;
ANALYZE
=> EXPLAIN SELECT * FROM flights WHERE flight_no = 'PG0007' AND departure_airport = 'VKO';
QUERY PLAN ----------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on flights (cost=10.82..878.93 rows=310 width=63) Recheck Cond: (flight_no = 'PG0007'::bpchar) Filter: (departure_airport = 'VKO'::bpchar) -> Bitmap Index Scan on flights_flight_no_scheduled_departure_key (cost=0.00..10.74 rows=310 width=0) Index Cond: (flight_no = 'PG0007'::bpchar) (5 rows)
Теперь оценка улучшилась.
Другая ситуация, в которой планировщик ошибается с оценкой, связана с группировкой:
=> SELECT count(*) FROM ( SELECT DISTINCT departure_airport, arrival_airport FROM flights ) t;
count ------- 618 (1 row)
=> EXPLAIN SELECT DISTINCT departure_airport, arrival_airport FROM flights;
QUERY PLAN --------------------------------------------------------------------- HashAggregate (cost=5639.01..5745.10 rows=10609 width=8) Group Key: departure_airport, arrival_airport -> Seq Scan on flights (cost=0.00..4564.67 rows=214867 width=8) (3 rows)
Расширенная статистика позволяет исправить и эту оценку:
=> CREATE STATISTICS flights2(ndistinct) ON departure_airport, arrival_airport FROM flights;
CREATE STATISTICS
=> ANALYZE flights;
ANALYZE
=> EXPLAIN SELECT DISTINCT departure_airport, arrival_airport FROM flights;
QUERY PLAN --------------------------------------------------------------------- HashAggregate (cost=5639.01..5644.72 rows=571 width=8) Group Key: departure_airport, arrival_airport -> Seq Scan on flights (cost=0.00..4564.67 rows=214867 width=8) (3 rows)
При необходимости можно создавать статистику сразу и по зависимостям, и по числу уникальных значений.
Собранная статистика находится в отдельной таблице:
=> SELECT * FROM pg_statistic_ext \gx
-[ RECORD 1 ]---+----------------------------------------- stxrelid | 16415 stxname | flights2 stxnamespace | 16385 stxowner | 10 stxkeys | 5 6 stxkind | {d} stxndistinct | {"5, 6": 571} stxdependencies | -[ RECORD 2 ]---+----------------------------------------- stxrelid | 16415 stxname | flights1 stxnamespace | 16385 stxowner | 10 stxkeys | 2 5 stxkind | {f} stxndistinct | stxdependencies | {"2 => 5": 1.000000, "5 => 2": 0.015000}
Если в условиях используются обращения к функциям, планировщик не учитывает возможных значений. Например, рейсов, совершенных в январе, будет примерно 1/12 от общего количества:
=> SELECT count(*) FROM flights WHERE extract(month FROM scheduled_departure) = 1;
count ------- 16831 (1 row)
Однако планировщик не понимает смысла функции extract и использует фиксированную селективность 0,5%:
=> EXPLAIN SELECT * FROM flights WHERE extract(month FROM scheduled_departure) = 1;
QUERY PLAN ----------------------------------------------------------------------------------------- Gather (cost=1000.00..5419.29 rows=1074 width=63) Workers Planned: 1 -> Parallel Seq Scan on flights (cost=0.00..4311.89 rows=632 width=63) Filter: (date_part('month'::text, scheduled_departure) = '1'::double precision) (4 rows)
=> SELECT 214867 * 0.005;
?column? ---------- 1074.335 (1 row)
Ситуацию можно исправить, построив индекс по выражению, так как для таких индексов собирается собственная статистика. Однако в данном случае придется написать функцию-обертку с классом изменчивости IMMUTABLE, указав тем самым, что она гарантированно возвращает одно и то же значение при одних и тех же значениях параметров.
=> CREATE FUNCTION get_month(t timestamptz) RETURNS integer AS $$ SELECT extract(month FROM t)::integer $$ IMMUTABLE LANGUAGE SQL;
CREATE FUNCTION
=> CREATE INDEX ON flights(get_month(scheduled_departure));
CREATE INDEX
=> ANALYZE flights;
ANALYZE
=> EXPLAIN SELECT * FROM flights WHERE get_month(scheduled_departure) = 1;
QUERY PLAN ------------------------------------------------------------------------------------------ Bitmap Heap Scan on flights (cost=276.64..7035.71 rows=16545 width=63) Recheck Cond: (get_month(scheduled_departure) = 1) -> Bitmap Index Scan on flights_get_month_idx (cost=0.00..272.51 rows=16545 width=0) Index Cond: (get_month(scheduled_departure) = 1) (4 rows)
Оценка исправилась.
Статистика для индексов по выражению хранится вместе со статистикой для таблиц:
=> SELECT n_distinct FROM pg_stats WHERE tablename = 'flights_get_month_idx';
n_distinct ------------ 12 (1 row)
Селективность соединения - доля строк от декартового произведения двух таблиц. Рассмотрим пример:
=> EXPLAIN SELECT * FROM flights f JOIN aircrafts a ON a.aircraft_code = f.aircraft_code;
QUERY PLAN ------------------------------------------------------------------------------ Hash Join (cost=1.20..59649.41 rows=214867 width=103) Hash Cond: (f.aircraft_code = ml.aircraft_code) -> Seq Scan on flights f (cost=0.00..4564.67 rows=214867 width=63) -> Hash (cost=1.09..1.09 rows=9 width=72) -> Seq Scan on aircrafts_data ml (cost=0.00..1.09 rows=9 width=72) (5 rows)
Точное значение:
=> SELECT count(*) FROM flights f JOIN aircrafts a ON a.aircraft_code = f.aircraft_code;
count -------- 214867 (1 row)
"Базовая" формула для расчета селективности соединения (в предположении равномерного распределения) - минимальное из значений 1/nd1 и 1/nd2, где
=> SELECT s1.n_distinct, s2.n_distinct FROM pg_stats s1, pg_stats s2 WHERE s1.tablename = 'flights' and s1.attname = 'aircraft_code' AND s2.tablename = 'aircrafts_data' and s2.attname = 'aircraft_code';
n_distinct | n_distinct ------------+------------ 8 | -1 (1 row)
В данном случае получаем:
=> SELECT 214867 * 9 * least(1.0/8, 1.0/9);
?column? ----------------------------- 214866.99999999999999785133 (1 row)
В более сложных случаях приведенная формула дала бы неправильный результат. Например, рейсы совершают разные модели самолетов с разной вместимостью, и для соединения рейсов с местами получили бы:
=> SELECT 214867 * 1339 * least(1.0/8, 1.0/8);
?column? ------------------------------- 35963364.12500000000000000000 (1 row)
При этом точное значение:
=> SELECT count(*) FROM flights f JOIN seats s ON f.aircraft_code = s.aircraft_code;
count ---------- 16518865 (1 row)
Однако планировщик умеет учитывать списки наиболее частых значений и гистограммы, и получает практически точную оценку:
=> EXPLAIN SELECT * FROM flights f JOIN seats s ON f.aircraft_code = s.aircraft_code;
QUERY PLAN ------------------------------------------------------------------------ Hash Join (cost=38.13..281145.22 rows=16803459 width=78) Hash Cond: (f.aircraft_code = s.aircraft_code) -> Seq Scan on flights f (cost=0.00..4564.67 rows=214867 width=63) -> Hash (cost=21.39..21.39 rows=1339 width=15) -> Seq Scan on seats s (cost=0.00..21.39 rows=1339 width=15) (5 rows)
К сожалению, ситуация ухудшается, когда соединяются несколько таблиц. Например, добавим в предыдущий запрос таблицу самолетов - это никак не повлияет на общее количество строк в выборке:
=> SELECT count(*) FROM flights f JOIN aircrafts a ON a.aircraft_code = f.aircraft_code JOIN seats s ON a.aircraft_code = s.aircraft_code;
count ---------- 16518865 (1 row)
Однако теперь планировщик ошибается:
=> EXPLAIN SELECT * FROM flights f JOIN aircrafts a ON a.aircraft_code = f.aircraft_code JOIN seats s ON a.aircraft_code = s.aircraft_code;
QUERY PLAN ------------------------------------------------------------------------------ Hash Join (cost=39.33..8417803.78 rows=31967435 width=118) Hash Cond: (f.aircraft_code = ml.aircraft_code) -> Hash Join (cost=38.13..281145.22 rows=16803459 width=78) Hash Cond: (f.aircraft_code = s.aircraft_code) -> Seq Scan on flights f (cost=0.00..4564.67 rows=214867 width=63) -> Hash (cost=21.39..21.39 rows=1339 width=15) -> Seq Scan on seats s (cost=0.00..21.39 rows=1339 width=15) -> Hash (cost=1.09..1.09 rows=9 width=72) -> Seq Scan on aircrafts_data ml (cost=0.00..1.09 rows=9 width=72) (9 rows)
Причина в том, что, соединив первые две таблицы, планировщик не имеет детальной статистики о результирующем наборе строк.
Конец демонстрации.