Объяснение Необъяснимого. Часть 3

Готовясь к конференции PG Day’16, мы продолжаем знакомить вас с интересными аспектами PostgreSQL. И сегодня мы предлагаем вам перевод третьей статьи из серии объяснений.

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

Сегодня мы перейдем к более сложным операциям.



Объяснение необъяснимого.
</p><p>
 Часть 3



Функция сканирования

Пример:
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

$ explain analyze select * from generate_Series(1,10) i; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Function Scan on generate_series i (cost=0.00.10.00 rows=1000 width=4) (actual time=0.012.0.013 rows=10 loops=1) Total runtime: 0.034 ms (2 rows)

По большому счету, это настолько просто, что особо ничего объяснять не требуется.

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

Функция Scan представляет собой очень простой блок.

Он запускает функцию, которая возвращает набор записей, и все.

Она не будет запускать такие функции, как «lower()», а только те, которые потенциально возвращают несколько строк или столбцов.

Когда функция возвращает строки, они будут переданы узлу на один уровень выше Function Scan в дереве плана или клиенту.

если Function Scan является корневым узлом.

Единственная дополнительная логика, которая может здесь сыграть, — это возможность фильтровать результирующие строки, например:

$ explain analyze select * from generate_Series(1,10) i where i < 3; QUERY PLAN ------------------------------------------------------------------------------------------------------------------- Function Scan on generate_series i (cost=0.00.12.50 rows=333 width=4) (actual time=0.012.0.014 rows=2 loops=1) Filter: (i < 3) Rows Removed by Filter: 8 Total runtime: 0.030 ms (4 rows)



Сортировать

Думаю, это довольно просто понять — sort берет выбранные записи и возвращает их отсортированными определенным образом.

Пример:

$ explain analyze select * from pg_class order by relname; QUERY PLAN --------------------------------------------------------------------------------------------------------------- Sort (cost=22.88.23.61 rows=292 width=203) (actual time=0.230.0.253 rows=295 loops=1) Sort Key: relname Sort Method: quicksort Memory: 103kB -> Seq Scan on pg_class (cost=0.00.10.92 rows=292 width=203) (actual time=0.007.0.048 rows=295 loops=1) Total runtime: 0.326 ms (5 rows)

Несмотря на простоту, внутри скрыта интересная логика.

Начнём с того, что если объём памяти, необходимый для сортировки, больше значения рабочая_мемь , то произойдет переключение на дисковую сортировку:

$ explain analyze select random() as x from generate_series(1,14000) i order by x; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------ Sort (cost=62.33.64.83 rows=1000 width=0) (actual time=16.713.18.090 rows=14000 loops=1) Sort Key: (random()) Sort Method: quicksort Memory: 998kB -> Function Scan on generate_series i (cost=0.00.12.50 rows=1000 width=0) (actual time=2.036.4.533 rows=14000 loops=1) Total runtime: 18.942 ms (5 rows) $ explain analyze select random() as x from generate_series(1,15000) i order by x; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------ Sort (cost=62.33.64.83 rows=1000 width=0) (actual time=27.052.28.780 rows=15000 loops=1) Sort Key: (random()) Sort Method: external merge Disk: 264kB -> Function Scan on generate_series i (cost=0.00.12.50 rows=1000 width=0) (actual time=2.171.4.894 rows=15000 loops=1) Total runtime: 29.767 ms (5 rows)

Обратите внимание на изменение метода сортировки в приведенном выше примере.

В таких случаях Postgres использует временные файлы, хранящиеся в каталоге $PGDATA/base/pgsql_tmp/.

Разумеется, они будут удалены, как только отпадут необходимость.

Еще одним дополнительным свойством является то, что Sort может изменить свой метод работы, если он вызывается операцией Limit, например:

$ explain analyze select * from pg_class order by relfilenode limit 5; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Limit (cost=15.77.15.78 rows=5 width=203) (actual time=0.119.0.120 rows=5 loops=1) -> Sort (cost=15.77.16.50 rows=292 width=203) (actual time=0.118.0.118 rows=5 loops=1) Sort Key: relfilenode Sort Method: top-N heapsort Memory: 26kB -> Seq Scan on pg_class (cost=0.00.10.92 rows=292 width=203) (actual time=0.005.0.047 rows=295 loops=1) Total runtime: 0.161 ms (6 rows)

Обычно для сортировки выбранного набора данных необходимо обработать весь набор данных.

Но Postgres знает, что если вам нужно только небольшое количество строк, ему не нужно сортировать весь набор данных, а только первые значения.

В нотации Big O общая сортировка имеет сложность O(m * log(m)), но Top-N имеет сложность O(m * log(n)), где m — количество строк в таблице, а n — количество возвращенных строк.

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

диск для временных файлов.



Лимит

Я использовал лимит несколько раз, потому что это очень просто, но все же давайте обсудим это подробно.

Операция ограничения запускает свою подоперацию и возвращает только первые N строк того, что вернула подоперация.

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

Простой пример:

$ explain analyze select * from pg_class; QUERY PLAN --------------------------------------------------------------------------------------------------------- Seq Scan on pg_class (cost=0.00.10.92 rows=292 width=203) (actual time=0.008.0.047 rows=295 loops=1) Total runtime: 0.096 ms (2 rows) $ explain analyze select * from pg_class limit 2; QUERY PLAN ------------------------------------------------------------------------------------------------------------- Limit (cost=0.00.0.07 rows=2 width=203) (actual time=0.009.0.010 rows=2 loops=1) -> Seq Scan on pg_class (cost=0.00.10.92 rows=292 width=203) (actual time=0.008.0.009 rows=2 loops=1) Total runtime: 0.045 ms (3 rows)

Как видите, использование лимита во втором случае привело к тому, что вложенная операция Seq Scan завершила свою работу сразу после нахождения двух строк.



ХэшАгрегат

Эта операция в основном используется в тех случаях, когда вы используете GROUP BY и некоторые агрегаты, такие как sum(), avg(), min(), max() и другие.

Пример:

$ explain analyze select relkind, count(*) from pg_Class group by relkind; QUERY PLAN ------------------------------------------------------------------------------------------------------------- HashAggregate (cost=12.38.12.42 rows=4 width=1) (actual time=0.223.0.224 rows=5 loops=1) -> Seq Scan on pg_class (cost=0.00.10.92 rows=292 width=1) (actual time=0.008.0.053 rows=295 loops=1) Total runtime: 0.273 ms (3 rows)

HashAggregate делает следующее: для каждой полученной строки он находит «ключ» GROUP BY (в данном случае relkind).

Затем в хеше (ассоциативном массиве, словаре) выбранная строка помещается в корзину, обозначенную данным ключом.

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

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

Если вы это понимаете, то, вероятно, видите потенциальную проблему: что делать в ситуации, когда у вас миллионы строк? Хэш будет слишком большим, чтобы поместиться в памяти.

И здесь мы будем использовать снова рабочая_мемь .

Если сгенерированный хэш слишком велик, он будет «сброшен» на диск (снова в $PGDATA/base/pgsql_tmp).

Это значит, что если в плане есть и HashAggregate, и Sort, мы можем использовать до 2*work_mem. Этот план легко получить:

$ explain analyze select relkind, count(*) from pg_Class group by relkind order by relkind; QUERY PLAN ------------------------------------------------------------------------------------------------------------------- Sort (cost=12.46.12.47 rows=4 width=1) (actual time=0.260.0.261 rows=5 loops=1) Sort Key: relkind Sort Method: quicksort Memory: 25kB -> HashAggregate (cost=12.38.12.42 rows=4 width=1) (actual time=0.221.0.222 rows=5 loops=1) -> Seq Scan on pg_class (cost=0.00.10.92 rows=292 width=1) (actual time=0.006.0.044 rows=295 loops=1) Total runtime: 0.312 ms (6 rows)

В действительности, один запрос может использовать work_mem много раз, поскольку work_mem является ограничением операции.

Таким образом, если запрос использует 1000 HashAggregates и Sorts (и другие операции, использующие work_mem), общее потребление памяти может быть очень высоким.



Хэш-соединение/хэш

Поскольку мы только что обсудили HashAggregate, было бы логично перейти к Hash Join. Эта операция, в отличие от предыдущей, имеет два подоперации.

Один из них всегда «Хеш», а второй — что-то еще.

Как следует из названия, Hash Join используется для объединения двух наборов записей.

Например, как здесь:

$ explain analyze select * from pg_class c join pg_namespace n on c.relnamespace = n.oid; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------- Hash Join (cost=1.14.16.07 rows=292 width=316) (actual time=0.036.0.343 rows=295 loops=1) Hash Cond: (c.relnamespace = n.oid) -> Seq Scan on pg_class c (cost=0.00.10.92 rows=292 width=203) (actual time=0.007.0.044 rows=295 loops=1) -> Hash (cost=1.06.1.06 rows=6 width=117) (actual time=0.012.0.012 rows=6 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 1kB -> Seq Scan on pg_namespace n (cost=0.00.1.06 rows=6 width=117) (actual time=0.004.0.005 rows=6 loops=1) Total runtime: 0.462 ms (7 rows)

Это работает следующим образом: сначала Hash Join вызывает «Hash», который, в свою очередь, вызывает что-то еще (в нашем случае Seq Scan в pg_namespace).

Затем Hash создает хэш в памяти (или на диске — в зависимости от размера) ассоциативный массив/словарь со строками из источника, хешированными с тем, что используется для объединения данных (в нашем случае столбцом OID в pg_namespace).

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

В нотации Perl вывод Hash будет примерно таким:

{ '123' => [ { data for row with OID = 123 }, ], '256' => [ { data for row with OID = 256 }, ], .

}

Затем Hash Join запускает вторую подоперацию (в нашем случае Seq Scan by pg_class) и для каждой строки из нее выполняет следующее:
Проверяет, присутствует ли ключ соединения (в данном случае pg_class.relnamespace) в хеше, возвращаемом операцией Hash. В противном случае эта строка из подоперации игнорируется (не возвращается).

Если ключ существует, Hash Join берет строки из хеша и на основе этой строки, с одной стороны, и всех строк хеша, с другой стороны, генерирует выходные данные строк.

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

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

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

Последнее, что стоит упомянуть в связи с Hash Join/Hash, это то, что операция Hash, как и Sort и HashAggregate, будет использовать память до work_mem.

Хэш-соединение/хэш

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

Пример:

$ explain analyze select a.* from pg_class c join pg_attribute a on c.oid = a.attrelid where c.relname in ( 'pg_class', 'pg_namespace' ); QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------------------------------- Nested Loop (cost=0.28.52.32 rows=16 width=203) (actual time=0.057.0.134 rows=46 loops=1) -> Seq Scan on pg_class c (cost=0.00.11.65 rows=2 width=4) (actual time=0.043.0.080 rows=2 loops=1) Filter: (relname = ANY ('{pg_class,pg_namespace}'::name[])) Rows Removed by Filter: 291 -> Index Scan using pg_attribute_relid_attnum_index on pg_attribute a (cost=0.28.20.25 rows=8 width=203) (actual time=0.007.0.015 rows=23 loops=2) Index Cond: (attrelid = c.oid) Total runtime: 0.182 ms

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

Как и в случае с Hash Join, у Nested Loop есть два «потомка».

Сначала он запускает «Seq Scan» (в нашем примере сначала выполняется первый узел), а затем для каждой возвращаемой строки (всего 2 строки в нашем примере) он запускает вторую операцию (сканирование индекса по pg_attribute в нашем примере).

случай).

Возможно, вы заметили, что в фактической метаинформации Index Scan есть «loops=2».

Это означает, что данная операция выполнялась дважды, а остальные значения (строки, время) — это среднее значение всех запусков.

Давайте посмотрим на следующий план из объяснение.

depesz.com .

Обратите внимание, что фактическое время выполнения всех операций сканирования индекса для категорий составляет от 0,002 до 0,003 мс.

Но общее время, потраченное на этом узле, составляет 78,852 мс, поскольку сканирование индекса выполнялось более 26 тыс.

раз.

Итак, обработка выглядит так: Вложенный цикл запускает первую сторону соединения один раз.

Назовем его «А».

Для каждой строки из «А» запускается вторая операция (назовем ее «Б»).

Если «B» не возвращает ни одной строки, данные из «A» игнорируются.

Если «B» вернул строки, для каждой возвращаемой строки вложенный цикл возвращает новую строку на основе текущих строк из A и B.

Объединить

Другой метод объединения данных называется Merge Join. Он используется, если объединяемые наборы данных отсортированы (или могут быть отсортированы без особых усилий) с помощью ключа соединения.

Готового визуального примера у меня нет, поэтому я создам его искусственно, используя подзапросы, которые сортируют данные перед объединением:

$ explain analyze select * from ( select oid, * from pg_class order by oid) as c join ( select * from pg_attribute a order by attrelid) as a on c.oid = a.attrelid; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- Merge Join (cost=23.16.268.01 rows=2273 width=410) (actual time=0.658.3.779 rows=2274 loops=1) Merge Cond: (pg_class.oid = a.attrelid) -> Sort (cost=22.88.23.61 rows=292 width=207) (actual time=0.624.0.655 rows=293 loops=1) Sort Key: pg_class.oid Sort Method: quicksort Memory: 102kB -> Seq Scan on pg_class (cost=0.00.10.92 rows=292 width=207) (actual time=0.011.0.211 rows=293 loops=1) -> Materialize (cost=0.28.212.34 rows=2273 width=203) (actual time=0.028.1.264 rows=2274 loops=1) -> Index Scan using pg_attribute_relid_attnum_index on pg_attribute a (cost=0.28.183.92 rows=2273 width=203) (actual time=0.015.0.752 rows=2274 loops=1) Total runtime: 4.009 ms (9 rows)

Соединение слиянием, как и другие соединения, выполняет две подоперации (в данном случае «Сортировка» и «Материализация»).

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

Процедура выглядит следующим образом: если объединяемый столбец справа совпадает со объединяемым столбцом слева: вернуть новую объединенную строку на основе текущих строк справа и слева; возьмите следующую строку справа (или слева, если справа больше нет строк); вернуться к шагу 1; если объединяемый столбец справа «меньше», чем объединяемый столбец слева: берем следующую строку справа (если строк больше нет – завершаем обработку); вернуться к шагу 1; если объединяемый столбец справа «больше», чем объединяемый столбец слева: берем следующую строку слева (если строк больше нет – завершаем обработку); вернемся к шагу 1. Это очень крутой способ объединения наборов данных, но он работает только для отсортированных источников.

На основе текущей БД объяснение.

depesz.com , существует: 44 721 план, содержащий операцию «Вложенный цикл»; 34 305 планов с «Hash Join»; Всего существует 8889 планов, использующих «Объединение слиянием».



Модификаторы Hash Join/Nested Loop/Merge Join

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

Но это не всегда происходит. У нас могут быть левые, правые и полные (LEFT/RIGHT/FULL OUTER JOIN) внешние соединения, а также так называемые антиобъединения.

В случае левого/правого соединения названия операций изменяются на: Хэш левого соединения, Хэш-право присоединиться, Объединить левое соединение Объединить правое соединение, Вложенный цикл левого соединения.

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

Таким образом, соединение с использованием RIGHT JOIN, которое будет работать с вложенным циклом, внутренне преобразуется в LEFT JOIN, чтобы операция вложенного цикла могла работать.

Во всех этих случаях логика проста: у нас есть две стороны союза – левая и правая.

А когда в объединении упоминается сторона, она возвращает новую строку, даже если Другая сторона нет совпадающих строк.

Это происходит с такими запросами:

select * from a left join b on .



(или правое соединение).

Вся остальная информация для Hash Join/Merge Join и Nested Loop одинакова, есть лишь небольшое изменение в логике генерации строкового вывода.

Существует также версия Full Join со следующими именами операций: Хэш полного соединения, Объединение полного соединения.

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

Это происходит в случае:

select * from a full join b .



Вся обработка происходит так же, как и в предыдущих примерах.

Кроме того, существуют так называемые Anti Joins. Названия их операций следующие: Хэш-анти-соединение, Объединить анти-присоединение Антисоединение вложенного цикла.

В этих случаях Join создает строку только если правая часть не находит ни одной строки.

Это полезно, когда вы делаете что-то вроде «ГДЕ не существует()» или «левое соединение.

где правая_таблица.

столбец имеет значение null».

Как в этом примере:

$ explain analyze select * from pg_class c where not exists (select * from pg_attribute a where a.attrelid = c.oid and a.attnum = 10); QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------------------- Hash Anti Join (cost=62.27.78.66 rows=250 width=203) (actual time=0.145.0.448 rows=251 loops=1) Hash Cond: (c.oid = a.attrelid) -> Seq Scan on pg_class c (cost=0.00.10.92 rows=292 width=207) (actual time=0.009.0.195 rows=293 loops=1) -> Hash (cost=61.75.61.75 rows=42 width=4) (actual time=0.123.0.123 rows=42 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 2kB -> Index Only Scan using pg_attribute_relid_attnum_index on pg_attribute a (cost=0.28.61.75 rows=42 width=4) (actual time=0.021.0.109 rows=42 loops=1) Index Cond: (attnum = 10) Heap Fetches: 0 Total runtime: 0.521 ms (9 rows)

Здесь Pg выполнил правую часть (сканирование индекса pg_attribute), хэшировал его, а затем выполнил левую часть (сканирование Seq pg_class), возвращая только те строки, которые не встречались в хэше для данного pg_class.oid.


Материализовать

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

psql имеет множество внутренних команд. Один из них — \dTS, в котором перечислены все типы системных данных.

Внутренне \dTS выполняет этот запрос:

SELECT n.nspname as "Schema", pg_catalog.format_type(t.oid, NULL) AS "Name", pg_catalog.obj_description(t.oid, 'pg_type') as "Description" FROM pg_catalog.pg_type t LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace WHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid)) AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid) AND pg_catalog.pg_type_is_visible(t.oid) ORDER BY 1, 2;

Его план таков:

QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------------------------------- Sort (cost=2783.00.2783.16 rows=65 width=68) (actual time=3.883.3.888 rows=87 loops=1) Sort Key: n.nspname, (format_type(t.oid, NULL::integer)) Sort Method: quicksort Memory: 39kB -> Nested Loop Left Join (cost=16.32.2781.04 rows=65 width=68) (actual time=0.601.3.657 rows=87 loops=1) Join Filter: (n.oid = t.typnamespace) Rows Removed by Join Filter: 435 -> Hash Anti Join (cost=16.32.2757.70 rows=65 width=8) (actual time=0.264.0.981 rows=87 loops=1) Hash Cond: ((t.typelem = el.oid) AND (t.oid = el.typarray)) -> Seq Scan on pg_type t (cost=0.00.2740.26 rows=81 width=12) (actual time=0.012.0.662 rows=157 loops=1) Filter: (pg_type_is_visible(oid) AND ((typrelid = 0::oid) OR (SubPlan 1))) Rows Removed by Filter: 185 SubPlan 1 -> Index Scan using pg_class_oid_index on pg_class c (cost=0.15.8.17 rows=1 width=1) (actual time=0.002.0.002 rows=1 loops=98) Index Cond: (oid = t.typrelid) -> Hash (cost=11.33.11.33 rows=333 width=8) (actual time=0.241.0.241 rows=342 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 14kB -> Seq Scan on pg_type el (cost=0.00.11.33 rows=333 width=8) (actual time=0.002.0.130 rows=342 loops=1) -> Materialize (cost=0.00.1.09 rows=6 width=68) (actual time=0.000.0.001 rows=6 loops=87) -> Seq Scan on pg_namespace n (cost=0.00.1.06 rows=6 width=68) (actual time=0.002.0.003 rows=6 loops=1) Total runtime: 3.959 ms

Для удобства просмотра я также загрузил этот план на объяснение.

depesz.com .

Обратите внимание, что операция №9 — «Материализация».

Почему? Materialize вызывается вложенным циклом левого соединения — операция №2. Мы знаем, что вложенный цикл заставляет выбранную операцию выполняться несколько раз, в данном случае 87 раз.

Правая часть объединения — Seq Scan по pg_namespace. Итак, теоретически Postgres должен сканировать pg_namespace 87 раз последовательно.

Учитывая, что одно последовательное сканирование этой таблицы занимает 0,003 мс, мы можем ожидать, что общее время составит ~0,25 мс.

Но Postgres делает что-то умнее.

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

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

Он просто возьмет данные из памяти.

Благодаря этому общее время на всё (однократное чтение таблицы, подготовку образа данных в памяти и сканирование этого изображения 87 раз) составило 0,087мс.

Вы можете сказать: «Хорошо, но почему соединение слиянием материализовалось раньше, ведь оно выполняло всего лишь одно сканированиеЭ» Давайте вспомним план:

$ explain analyze select * from ( select oid, * from pg_class order by oid) as c join ( select * from pg_attribute a order by attrelid) as a on c.oid = a.attrelid; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- Merge Join (cost=23.16.268.01 rows=2273 width=410) (actual time=0.658.3.779 rows=2274 loops=1) Merge Cond: (pg_class.oid = a.attrelid) -> Sort (cost=22.88.23.61 rows=292 width=207) (actual time=0.624.0.655 rows=293 loops=1) Sort Key: pg_class.oid Sort Method: quicksort Memory: 102kB -> Seq Scan on pg_class (cost=0.00.10.92 rows=292 width=207) (actual time=0.011.0.211 rows=293 loops=1) -> Materialize (cost=0.28.212.34 rows=2273 width=203) (actual time=0.028.1.264 rows=2274 loops=1) -> Index Scan using pg_attribute_relid_attnum_index on pg_attribute a (cost=0.28.183.92 rows=2273 width=203) (actual time=0.015.0.752 rows=2274 loops=1) Total runtime: 4.009 ms (9 rows)

Да, он запускался только один раз.

Проблема в том, что источник данных для объединения слиянием должен соответствовать нескольким критериям.

Некоторые из них очевидны (данные необходимо отсортировать), а другие менее очевидны, поскольку они более технические (данные необходимо сканировать вперед и назад).

Из-за этих не столь очевидных критериев Postgres иногда приходится применять Materialize к данным, поступающим из источника (в нашем случае, Index Scan), чтобы у него были все возможности, необходимые для его использования.

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

Это все на сегодня.

Я думал, что на этом закончу, но впереди еще много операций, поэтому в этой серии будет как минимум еще два поста (оставшиеся операции и статистическая информация).

Теги: #sql #postgresql #explain #база данных #разработка веб-сайтов #postgresql #sql

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.