Pull to refresh
254.48
Postgres Professional
Разработчик СУБД Postgres Pro

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

Reading time11 min
Views2.2K


Это продолжении серии статей о принятых изменениях в 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.




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

Tags:
Hubs:
Total votes 6: ↑6 and ↓0+9
Comments0

Articles

Information

Website
www.postgrespro.ru
Registered
Founded
Employees
201–500 employees
Location
Россия
Representative
Иван Панченко