PostgreSQL 18: Часть 3 или Коммитфест 2024-11

от автора


Это продолжении серии статей о принятых изменениях в PostgreSQL 18. На этот раз речь пойдет о завершившемся в ноябре коммитфесте.

А самое интересное из предшествующих коммитфестов можно прочитать здесь: 2024-07, 2024-09.

initdb: подсчет контрольных сумм включен по умолчанию
Планировщик: замена однотипных условий OR на поиск в массиве
Планировщик: перестановка выражений в DISTINCT
GROUPING SETS: HAVING -> WHERE
Очистка кеша типов данных и временные таблицы
Планировщик: инкрементальная сортировка при соединении слиянием
Новая функция array_reverse
Функции min и max для типа bytea
Статистика использования параллельных рабочих процессов
Новая функция pg_ls_summariesdir
pg_logicalsnapinspect: новый модуль contrib
Уточненные сообщения об ошибках при установке расширений
Ограничения NOT NULL в системном каталоге
Таблица TOAST для pg_index
COPY… FROM и file_fdw: ограничение на количество ошибочных строк
Недетерминированные правила сортировки: поддержка LIKE
TLS v1.3: настройка наборов шифров

initdb: подсчет контрольных сумм включен по умолчанию
commit: 983a588e0, 04bec894a

Простое, но важное изменение. При инициализации кластера утилитой initdb можно больше не указывать параметр --data-checksums или -k. Подсчет контрольных сумм теперь включен по умолчанию.

Если всё-таки контрольные суммы не нужны, то пригодится новый параметр --no-data-checksums.

Планировщик: замена однотипных условий OR на поиск в массиве
commit: d4378c000, ae4569161

План запроса в 17-й версии трижды сканирует индекс для каждого из однотипных условий:

17=# EXPLAIN (costs off) SELECT * FROM flights WHERE flight_id = 1 OR flight_id = 2 OR flight_id = 3;

                               QUERY PLAN                                 -------------------------------------------------------------------------  Bitmap Heap Scan on flights    Recheck Cond: ((flight_id = 1) OR (flight_id = 2) OR (flight_id = 3))    ->  BitmapOr          ->  Bitmap Index Scan on flights_pkey                Index Cond: (flight_id = 1)          ->  Bitmap Index Scan on flights_pkey                Index Cond: (flight_id = 2)          ->  Bitmap Index Scan on flights_pkey                Index Cond: (flight_id = 3) (9 rows)

Запрос лучше переписать так, чтобы было достаточно сходить в индекс один раз:

17=# EXPLAIN (costs off) SELECT * FROM flights WHERE flight_id = ANY (ARRAY[1,2,3]);

                       QUERY PLAN                        --------------------------------------------------------  Index Scan using flights_pkey on flights    Index Cond: (flight_id = ANY ('{1,2,3}'::integer[])) (2 rows)

Такая оптимизация требует переписывания запроса, что не всегда возможно. Особенно если запросы формирует ORM. В 18-й версии планировщик сам догадается о возможности преобразования условия первоначального запроса:

18=# EXPLAIN (costs off) SELECT * FROM flights WHERE flight_id = 1 OR flight_id = 2 OR flight_id = 3;

                       QUERY PLAN                        --------------------------------------------------------  Index Scan using flights_pkey on flights    Index Cond: (flight_id = ANY ('{1,2,3}'::integer[])) (2 rows)

Планировщик: перестановка выражений в DISTINCT
commit: a8ccf4e93

Еще один пример оптимизации, дающий свободу выбора при написании запроса.

В 17-й версии планировщик научился переставлять местами выражения в GROUP BY в соответствии с положением выражений в индексе. В таблице ticket_flights есть индекс по составному первичному ключу, нас интересует порядок столбцов в индексе.

\d ticket_flights_pkey

     Index "bookings.ticket_flights_pkey"   Column   |     Type      | Key? | Definition -----------+---------------+------+------------  ticket_no | character(13) | yes  | ticket_no  flight_id | integer       | yes  | flight_id primary key, btree, for table "bookings.ticket_flights"

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

17=# EXPLAIN (costs off) SELECT flight_id,ticket_no           FROM ticket_flights GROUP BY flight_id,ticket_no; –- столбцы в индексе 

                            QUERY PLAN                              -------------------------------------------------------------------  Group    Group Key: ticket_no, flight_id    ->  Index Only Scan using ticket_flights_pkey on ticket_flights (3 rows)

Продолжение этой оптимизации ― научить планировщик выполнять подобную перестановку для DISTINCT, включая DISTINCT ON. Что и сделано для 18-й версии:

18=# EXPLAIN (costs off) SELECT DISTINCT flight_id,ticket_no  FROM ticket_flights;

                            QUERY PLAN                              -------------------------------------------------------------------  Unique    ->  Index Only Scan using ticket_flights_pkey on ticket_flights (2 rows)

Обе оптимизации управляются конфигурационными параметрами и по умолчанию включены:

 \dconfig enable_*_reordering

  List of configuration parameters          Parameter          | Value ----------------------------+-------  enable_distinct_reordering | on  enable_group_by_reordering | on (2 rows)

GROUPING SETS: HAVING -> WHERE
commit: 67a54b9e8

В плане следующего запроса важно обратить внимание на строку Filter. Она находится под узлом Seq Scan, а не HashAggregate, как было раньше. Это значит что условие из HAVING теперь проверяется на этапе получения строк из таблицы, т. е. до агрегации.

EXPLAIN (costs off) SELECT aircraft_code, status, COUNT(*) FROM flights GROUP BY GROUPING SETS ((aircraft_code, status), (status)) HAVING status = 'Arrived';

                     QUERY PLAN                      ----------------------------------------------------  HashAggregate    Hash Key: status, aircraft_code    Hash Key: status    ->  Seq Scan on flights          Filter: ((status)::text = 'Arrived'::text) (5 rows)

Конечно, можно было бы переписать запрос и перенести условие из HAVING в WHERE. Но хорошо что планировщик учится сам находить более эффективные методы выполнения.

Очистка кеша типов данных и временные таблицы
commit: cc5ef90ed, d0f020037, b85a9d046

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

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

\timing on DO $$BEGIN   FOR i IN 1 .. 20_000 LOOP     EXECUTE format('CREATE TEMP TABLE t%s (id int)', i);     EXECUTE format('INSERT INTO t%s VALUES ( (ROW(0)::t%s).id)', i, i);   END LOOP; END; $$;  DO $$BEGIN   FOR i IN 1 .. 20_000 LOOP     EXECUTE format('DROP TABLE t%s', i);   END LOOP; END; $$;

Время выполнения каждого из блоков в 17-й версии:

Time: 12251,384 ms (00:12,251) Time: 22494,163 ms (00:22,494)

Тоже самое в 18-й версии:

Time: 2872,074 ms (00:02,872) Time: 1495,051 ms (00:01,495)

Ускорение более чем очевидно, особенно для второго анонимного блока.

Планировщик: инкрементальная сортировка при соединении слиянием
commit: 828e94c9d

Как удачно пошутил Томаш Вондра, инкрементальная сортировка добавляется инкрементально.

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

В описании патча есть такой пример:

CREATE TABLE t (a int, b int); INSERT INTO t SELECT random(1,100), random(1,100) FROM generate_series(1,100000); CREATE INDEX ON t (a); ANALYZE t;  EXPLAIN (costs off) SELECT * FROM (SELECT * FROM t ORDER BY a) t1      JOIN t t2 ON t1.a = t2.a AND t1.b = t2.b ORDER BY t1.a, t1.b;

                  QUERY PLAN                    -----------------------------------------------  Merge Join    Merge Cond: ((t.a = t2.a) AND (t.b = t2.b))    ->  Incremental Sort          Sort Key: t.a, t.b          Presorted Key: t.a          ->  Index Scan using t_a_idx on t    ->  Sort          Sort Key: t2.a, t2.b          ->  Seq Scan on t t2 (9 rows)

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

                  QUERY PLAN                    -----------------------------------------------  Merge Join    Merge Cond: ((t.a = t2.a) AND (t.b = t2.b))    ->  Sort          Sort Key: t.a, t.b          ->  Index Scan using t_a_idx on t    ->  Materialize          ->  Sort                Sort Key: t2.a, t2.b                ->  Seq Scan on t t2 (9 rows)

Новая функция array_reverse
commit: 49d6c7d8d

Назначение функции следует из названия. Функция возвращает элементы входного массива в обратном порядке:

SELECT array_reverse(string_to_array('PostgreSQL',NULL));

     array_reverse      -----------------------  {L,Q,S,e,r,g,t,s,o,P} (1 row)

См. также
Waiting for PostgreSQL 18 – Add SQL function array_reverse() (Hubert ‘depesz’ Lubaczewski)

Функции min и max для типа bytea
commit: 2d24fd942

Как и следует из названия, для сортируемого типа данных bytea добавили отсутствующие функции min и max:

WITH t AS (     SELECT book_ref::bytea AS c FROM bookings ) SELECT min(c), max(c) FROM t;

      min       |      max        ----------------+----------------  \x303030303034 | \x464646464639 (1 row)

Статистика использования параллельных рабочих процессов
commit: cf54a2c00, e7a9496de

В статистические представления pg_stat_statements (первый коммит) и pg_stat_database (второй коммит) добавлены два столбца: parallel_workers_to_launch и parallel_workers_launched. Они показывают количество запланированных параллельных рабочих процессов и сколько из них было реально запущено.

Посмотрим на столбцы в деле. Настройки параллельного выполнения не менялись:

\dconfig max_parallel_workers*|max_worker*

    List of configuration parameters             Parameter            | Value ---------------------------------+-------  max_parallel_workers            | 8  max_parallel_workers_per_gather | 2  max_worker_processes            | 8 (3 rows)

Сбросим статистику перед тестом:

SELECT pg_stat_statements_reset(); SELECT pg_stat_reset();

Запустим через pgbench 10 процессов, каждый из которых будет выполнять запросы с параллельным планом:

$ pgbench --client=10 --time=5 --file=test.sql

Файл test.sql содержит два запроса на агрегирование таблиц достаточно большого размера, чтобы был выбран параллельный план:

$ cat test.sql

SELECT count(*) FROM bookings; SELECT count(*) FROM tickets;

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

SELECT query,        parallel_workers_to_launch AS plan,        parallel_workers_launched AS fact FROM pg_stat_statements WHERE query ~ 'tickets|bookings';

             query             | plan | fact -------------------------------+------+------  SELECT count(*) FROM bookings |   84 |   42  SELECT count(*) FROM tickets  |   84 |   40 (2 rows)

А также более общую статистику базы данных:

SELECT parallel_workers_to_launch AS plan,        parallel_workers_launched AS fact FROM pg_stat_database WHERE datname = current_database();

 plan | fact ------+------   168 |   82 (1 row)

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

Новая функция pg_ls_summariesdir
commit: 4e1fad378

Для мониторинга работы инкрементального резервного копирования пригодится новая функция pg_ls_summariesdir, показывающая содержимое каталога pg_wal/summaries, включая размер файлов и дату последнего изменения.

SELECT * FROM pg_ls_summariesdir();

                       name                       | size  |      modification       --------------------------------------------------+-------+------------------------  0000000100000001412CF0980000000162404D08.summary | 17164 | 2025-02-03 15:22:33+03 (1 row)

Именно показ информации о размере файлов и дате последнего изменения отличают эту функцию от вызова pg_ls_dir('pg_wal/summaries'), которая выдает только имена файлов.

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

\df pg_ls_*dir

                                                                    List of functions    Schema   |       Name        | Result data type |                                    Argument data types                                 | Type ------------+-------------------------+------------------+-------------------------------------------------------------------------------------------+------  pg_catalog | pg_ls_archive_statusdir | SETOF record    | OUT name text, OUT size bigint, OUT modification timestamp with time zone                 | func  pg_catalog | pg_ls_dir             | SETOF text    | text                                                                                      | func  pg_catalog | pg_ls_dir             | SETOF text    | text, boolean, boolean                                                                    | func  pg_catalog | pg_ls_logdir          | SETOF record  | OUT name text, OUT size bigint, OUT modification timestamp with time zone                 | func  pg_catalog | pg_ls_logicalmapdir   | SETOF record  | OUT name text, OUT size bigint, OUT modification timestamp with time zone                 | func  pg_catalog | pg_ls_logicalsnapdir  | SETOF record  | OUT name text, OUT size bigint, OUT modification timestamp with time zone                 | func  pg_catalog | pg_ls_replslotdir     | SETOF record  | slot_name text, OUT name text, OUT size bigint, OUT modification timestamp with time zone | func  pg_catalog | pg_ls_summariesdir    | SETOF record  | OUT name text, OUT size bigint, OUT modification timestamp with time zone                 | func  pg_catalog | pg_ls_tmpdir          | SETOF record  | OUT name text, OUT size bigint, OUT modification timestamp with time zone                 | func  pg_catalog | pg_ls_tmpdir          | SETOF record  | tablespace oid, OUT name text, OUT size bigint, OUT modification timestamp with time zone | func  pg_catalog | pg_ls_waldir          | SETOF record  | OUT name text, OUT size bigint, OUT modification timestamp with time zone                 | func (11 rows)

pg_logicalsnapinspect: новый модуль contrib
commit: 7cdfeee32

Новый модуль pg_logicalsnapinspect предназначен для проверки содержимого компонентов логического декодирования. Полезен для целей отладки или изучения работы логической репликации.

В настоящий момент включает две функции pg_get_logical_snapshot_meta и pg_get_logical_snapshot_info. Функции выдают информацию о снимках данных из файлов, расположенных в $PGDATA/pg_logical/snapshots.

Уточненные сообщения об ошибках при установке расширений
commit: 14e5680ee, 774171c4f, 924e03917

Для демонстрации возьмем расширение uom, для которого доступно три версии:

SELECT name, version FROM pg_available_extension_versions WHERE name = 'uom';

 name | version ------+---------  uom  | 1.0  uom  | 1.1  uom  | 1.2 (3 rows)

В один из установочных скриптов SQL намеренно внесена ошибочная команда. Попробуем установить последнюю версию расширения:

17=# CREATE EXTENSION uom VERSION '1.2';

ERROR:  division by zero

Ошибка! Но в каком файле?

В 18-й версии сообщение и контекст ошибок при установке расширений значительно улучшены:

18=# CREATE EXTENSION uom VERSION '1.2';

ERROR:  division by zero CONTEXT:  SQL statement "SELECT 1/0" extension script file "uom--1.1--1.2.sql", near line 9

Ограничения NOT NULL в системном каталоге
commit: 14e87ffa5

Это вторая попытка записывать ограничения NOT NULL в системный каталог pg_constraint. Первая попытка была в 17-й версии, однако патч был отменен. Но описание патча осталось актуальным.

Таблица TOAST для pg_index
commit: b52c4fc3c

Для таблицы системного каталога pg_index не было таблицы TOAST. Значения столбцов не были слишком длинными. Теперь у pg_index есть TOAST:

SELECT reltoastrelid::regclass FROM pg_class WHERE oid = 'pg_index'::regclass;

     reltoastrelid       ------------------------  pg_toast.pg_toast_2610 (1 row)

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

COPY… FROM и file_fdw: ограничение на количество ошибочных строк
commit: 4ac2a9bec, a39297ec0, 6c8f67032

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

CREATE TABLE t (id int PRIMARY KEY);  COPY t FROM STDIN (on_error 'ignore', log_verbosity verbose, reject_limit 1); Enter data to be copied followed by a newline. End with a backslash and a period on a line by itself, or an EOF signal. >> 1 >> один >> два >> 3 >> \.

NOTICE:  skipping row due to data type incompatibility at line 2 for column "id": "один" NOTICE:  skipping row due to data type incompatibility at line 3 for column "id": "два" ERROR:  skipped more than REJECT_LIMIT (1) rows due to data type incompatibility CONTEXT:  COPY t, line 3, column id: "два"

Превышение ограничения приводит к ошибке.

Аналогичные изменения сделаны для расширения file_fdw (третий коммит). В предыдущей статье рассказывалось о том, что расширение научилось обрабатывать ошибки преобразования форматов. Теперь, вслед за COPY, можно установить предельное количество ошибок при помощи параметра REJECT_LIMIT.

Недетерминированные правила сортировки: поддержка LIKE
commit: 85b7efa1c

С недетерминированными правилами сортировки строки, состоящие из разных байтов, могут быть равными. В следующем примере определяется регистронезависимое правило сортировки. Для такого правила строки в разных регистрах равны:

CREATE COLLATION ignore_case   (provider = icu, locale = 'und-u-ks-level2', deterministic = false);  SELECT 'postgres' = 'POSTGRES' COLLATE "ignore_case" AS "ignore_case";

 ignore_case -------------  t (1 row)

Но вот операция LIKE для таких строк раньше не поддерживалась. Исправлено:

SELECT 'PostgreSQL' LIKE 'post%' COLLATE "ignore_case";

 ?column? ----------  t (1 row)

См. также
Waiting for PostgreSQL 18 – Support LIKE with nondeterministic collations (Hubert ‘depesz’ Lubaczewski)

TLS v1.3: настройка наборов шифров
commit: 45188c2ea, 3d1ef3a15, 6c66b7443

Добавлен новый параметр ssl_tls13_ciphers для указания списка набора шифров, когда используется TLS версии 1.3. Для предыдущих версий используется прежний параметр ssl_ciphers.


На этом пока всё. Впереди основные события январского коммитфеста.


ссылка на оригинал статьи https://habr.com/ru/articles/882578/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *