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

s2gskc525ap1kyzgbkhva63_cxo.png

Эта статья открывает цикл о новостях будущей, 18-ой, версии PostgreSQL. Рассмотрим следующие возможности попавшие в июльский коммитфест.

Планировщик: поддержка правого полусоединения хешированием
Планировщик: материализация внутреннего набора строк для соединения вложенными циклами в параллельном плане
Вспомогательные функции планировщика для generate_series
EXPLAIN (analyze): статистика рабочих процессов узла Parallel Bitmap Heap Scan
Функции min и max для составных типов
Имена параметров для функций regexp*
Режим отладки в pgbench
pg_get_backend_memory_contexts: столбец path вместо parent и новый столбец type
Функция pg_get_acl
pg_upgrade: оптимизация работы pg_dump
Предопределенная роль pg_signal_autovacuum_worker

Планировщик: поддержка правого полусоединения хешированием
commit: aa86129e1

В плане следующего запроса в 17-й версии используется соединение хешированием и последовательное сканирование обеих таблиц:

17=# EXPLAIN (costs off, analyze)
SELECT * FROM flights
WHERE flight_id IN (SELECT flight_id FROM ticket_flights);
                                           QUERY PLAN                                           
------------------------------------------------------------------------------------------------
 Hash Join (actual time=2133.122..2195.619 rows=150588 loops=1)
   Hash Cond: (flights.flight_id = ticket_flights.flight_id)
   ->  Seq Scan on flights (actual time=0.018..10.301 rows=214867 loops=1)
   ->  Hash (actual time=2132.969..2132.970 rows=150588 loops=1)
         Buckets: 262144 (originally 131072)  Batches: 1 (originally 1)  Memory Usage: 7343kB
         ->  HashAggregate (actual time=1821.476..2114.218 rows=150588 loops=1)
               Group Key: ticket_flights.flight_id
               Batches: 5  Memory Usage: 10289kB  Disk Usage: 69384kB
               ->  Seq Scan on ticket_flights (actual time=7.200..655.356 rows=8391852 loops=1)
 Planning Time: 0.325 ms
 Execution Time: 2258.237 ms
(11 rows)

Но хеш строится по таблице большего размера ticket_flights, что очевидно требует больше ресурсов, чем строить хеш по небольшой таблице flights.

В 18-й версии замена таблицы для хеширования станет возможной, а в плане запроса на 4-й строке мы увидим Hash Right Semi Join, в этом случае еще и в параллельном режиме. Как следствие, время выполнения заметно сокращается:

                                            QUERY PLAN                                             
---------------------------------------------------------------------------------------------------
 Gather (actual time=56.771..943.233 rows=150590 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Parallel Hash Right Semi Join (actual time=41.754..909.894 rows=50197 loops=3)
         Hash Cond: (ticket_flights.flight_id = flights.flight_id)
         ->  Parallel Seq Scan on ticket_flights (actual time=0.047..221.511 rows=2797284 loops=3)
         ->  Parallel Hash (actual time=40.309..40.309 rows=71622 loops=3)
               Buckets: 262144  Batches: 1  Memory Usage: 23808kB
               ->  Parallel Seq Scan on flights (actual time=0.008..6.631 rows=71622 loops=3)
 Planning Time: 0.555 ms
 Execution Time: 949.831 ms
(11 rows)

Планировщик: материализация внутреннего набора строк для соединения вложенными циклами в параллельном плане
commit: 22d946b0f

До 18-й версии планировщик не рассматривал возможность материализации внутреннего набора строк для соединения вложенными циклами в параллельном режиме.

17=# EXPLAIN (costs off)
SELECT *
FROM ticket_flights tf
     JOIN flights f USING (flight_id)
WHERE f.flight_id = 12345;
                     QUERY PLAN                     
----------------------------------------------------
 Nested Loop
   ->  Index Scan using flights_pkey on flights f
         Index Cond: (flight_id = 12345)
   ->  Gather
         Workers Planned: 2
         ->  Parallel Seq Scan on ticket_flights tf
               Filter: (flight_id = 12345)
(7 rows)

План этого же запроса в 18-й версии использует материализацию рейсов:

                          QUERY PLAN                          
--------------------------------------------------------------
 Gather
   Workers Planned: 2
   ->  Nested Loop
         ->  Parallel Seq Scan on ticket_flights tf
               Filter: (flight_id = 12345)
         ->  Materialize
               ->  Index Scan using flights_pkey on flights f
                     Index Cond: (flight_id = 12345)
(8 rows)

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

Вспомогательные функции планировщика для generate_series
commit: 036bdcec9

Новая вспомогательная функция подсказывает планировщику, сколько строк вернет generate_series для диапазона дат и времени:

EXPLAIN
SELECT *
FROM generate_series(current_date, current_date + '1 day'::interval, '1 hour');
                             QUERY PLAN                              
---------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.01..0.26 rows=25 width=8)
(1 row)

В предыдущих версиях количество строк всегда оценивалось в 1000 (значение по умолчанию параметра ROWS для функций).

Заметим, что аналогичная вспомогательная функция для целых чисел уже давно существует:

EXPLAIN
SELECT *
FROM generate_series(1, 42, 2);
                             QUERY PLAN                              
---------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..0.21 rows=21 width=4)
(1 row)

EXPLAIN (analyze): статистика рабочих процессов узла Parallel Bitmap Heap Scan
commit: 5a1e6df3b

Команда EXPLAIN теперь показывает статистику (количество точных и неточных фрагментов) для каждого рабочего процесса, участвующего в параллельном сканировании по битовой карте. В следующем примере это строки, начинающиеся с Worker 0 и Worker 1.

EXPLAIN (analyze, costs off, timing off, summary off)
SELECT count(*) FROM bookings
  WHERE total_amount < 20000 AND book_date > '2017-07-15';
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Finalize Aggregate (actual rows=1 loops=1)
   ->  Gather (actual rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate (actual rows=1 loops=3)
               ->  Parallel Bitmap Heap Scan on bookings (actual rows=6415 loops=3)
                     Recheck Cond: (total_amount < '20000'::numeric)
                     Filter: (book_date > '2017-07-15 00:00:00+03'::timestamp with time zone)
                     Rows Removed by Filter: 67072
                     Heap Blocks: exact=6345
                     Worker 0:  Heap Blocks: exact=3632
                     Worker 1:  Heap Blocks: exact=3470
                     ->  Bitmap Index Scan on bookings_total_amount_idx (actual rows=220460 loops=1)
                           Index Cond: (total_amount < '20000'::numeric)

Функции min и max для составных типов
commit: a0f1fce80

Значения составных типов уже давно можно сравнивать между собой. Сравнение выполняется поэлементно. Например отсортируем строки в следующем примере:

CREATE TABLE seats(
    line text,
    number integer
);

INSERT INTO seats VALUES
    ('A', 42), ('B',  1), ('C', 27);

SELECT * FROM seats s ORDER BY s DESC;
 line | number
------+--------
 C    |     27
 B    |      1
 A    |     42
(3 rows)

Однако агрегатных функций min и max для составных типов не было. Их и добавили:

SELECT min(s.*), max(s.*) FROM seats s;
  min   |  max   
--------+--------
 (A,42) | (C,27)
(1 row)

Имена параметров для функций regexp*
commit: 580f8727

Функции для работы с регулярными выражениями обзавелись именами параметров для большей наглядности:

SELECT regexp_replace(
    pattern => '$',
    replacement => 'Postgres!',
    string => 'All You Need Is '
);
      regexp_replace       
---------------------------
 All You Need Is Postgres!

Имена параметров можно найти в документации или в выводе команды \df в psql. Раньше был доступен только позиционный способ передачи параметров.

Режим отладки в pgbench
commit: 3ff01b2b

Большинство утилит сервера используют параметр -d для указания базы данных для подключения. Однако в pgbench этот параметр включал режим отладки. Для унификации параметры pgbench изменили: теперь задать базу данных можно в -d или --dbname, а режим отладки включается только с полным именем --debug.

Изменение доступно с версии 17, хотя формально завершено в июльском коммитфесте 18-й версии.

pg_get_backend_memory_contexts: столбец path вместо parent и новый столбец type
commit: 32d3ed81, f0d11275, 12227a1d

Разработчики сервера и любители исследовать использование памяти оценят изменения в представлении pg_backend_memory_contexts.

Стало удобнее и надежнее связывать родительские и дочерние контексты между собой. Для этого добавлен столбец path, представляющий собой массив идентификаторов контекстов. Первый элемент массива ― всегда контекст верхнего уровня TopMemoryContext, а последний элемент ― идентификатор контекста текущей строки. Также, для удобства написания запросов поправили нумерацию в столбце level, теперь она начинается с 1, а не 0.

Пример запроса, показывающего контекст памяти TopPortalContext и все его дочерние контексты.

WITH memory_contexts AS (
    SELECT * FROM pg_backend_memory_contexts
)
SELECT child.name, child.type, child.level, child.path, child.total_bytes
FROM memory_contexts parent, memory_contexts child
WHERE parent.name = 'TopPortalContext' AND
      child.path[parent.level] = parent.path[parent.level];
              name              |    type    | level |         path          | total_bytes
--------------------------------+------------+-------+-----------------------+-------------
 TopPortalContext               | AllocSet   |     2 | {1,20}                |        8192
 PortalContext                  | AllocSet   |     3 | {1,20,31}             |        1024
 ExecutorState                  | AllocSet   |     4 | {1,20,31,136}         |       65632
 tuplestore tuples              | Generation |     5 | {1,20,31,136,138}     |       32768
 printtup                       | AllocSet   |     5 | {1,20,31,136,139}     |        8192
 ExprContext                    | AllocSet   |     5 | {1,20,31,136,140}     |        8192
 ExprContext                    | AllocSet   |     5 | {1,20,31,136,141}     |        8192
 tuplestore tuples              | Generation |     5 | {1,20,31,136,142}     |        8192
 ExprContext                    | AllocSet   |     5 | {1,20,31,136,143}     |        8192
 Table function arguments       | AllocSet   |     5 | {1,20,31,136,144}     |        8192
 ExprContext                    | AllocSet   |     5 | {1,20,31,136,145}     |       32768
 pg_get_backend_memory_contexts | AllocSet   |     6 | {1,20,31,136,145,146} |       16384
(12 rows)

Раньше для связывания контекстов использовался столбец parent (удален во втором коммите), указывающий на name. Требовался более сложный рекурсивный запрос. Но более важно, что эта связка работала менее надежно, т. к. имена контекстов могли быть не уникальными.

Кроме того, в представление добавлен столбец type (третий коммит). Сейчас используются четыре типа контекста: AllocSet, Generation, Slab, Bump. Подробнее о контекстах памяти можно прочитать в исходном коде: src/backend/utils/mmgr/README.

Функция pg_get_acl
commit: 4564f1ce, e311c6e5, d898665b

После выполнения следующих команд появляется зависимость таблицы test от роли alice.

CREATE ROLE alice;
CREATE TABLE test (id int);
GRANT SELECT ON test TO alice;

Теперь роль alice нельзя удалить, без удаления зависимости от таблицы:

DROP ROLE alice;
ERROR:  role "alice" cannot be dropped because some objects depend on it
DETAIL:  privileges for table test

Информация о зависимостях объектов хранится в двух системных каталогах pg_depend (между объектами внутри одной БД) и pg_shdepend, когда в зависимостях участвуют общие объекты кластера, например роли, как в нашем случае.

Сейчас в pg_shdepend ровно одна строка об этой зависимости:

SELECT * FROM pg_shdepend\gx
-[ RECORD 1 ]-----
dbid       | 5
classid    | 1259
objid      | 16528
objsubid   | 0
refclassid | 1260
refobjid   | 16527
deptype    | a

Расшифровать информацию из pg_shdepend не так просто, поскольку объекты в ней представлены в виде идентификаторов: идентификатор таблицы системного каталога (classid), идентификатор строки в этом системном каталоге (objid) и порядковый номер столбца для таблиц (objsubid). Объекты, на которые есть ссылки, представлены соответственно refclassid и refobjid (ссылок на столбцы таблиц не бывает, поэтому нет столбца refobjsubid).

Для удобства работы с объектами по идентификаторам предусмотрен ряд функций. Например функция pg_identify_object выводит запись об объекте:

SELECT * FROM pg_identify_object(1259,16528,0);
 type  | schema | name |  identity   
-------+--------+------+-------------
 table | public | test | public.test

Объект, от которого таблица зависит:

SELECT * FROM pg_identify_object(1260,16527,0);
 type | schema | name  | identity
------+--------+-------+----------
 role |        | alice | alice

В этом семействе функций произошло пополнение. Новая функция pg_get_acl показывает права доступа к объекту, без необходимости запроса к конкретному системному каталогу.

Следующий запрос покажет, какие именно привилегии выданы alice:

SELECT *,
    pg_identify_object(classid, objid, objsubid) AS object,
    pg_identify_object(refclassid, refobjid, 0) AS ref_object,
    pg_get_acl(classid, objid, objsubid)
FROM pg_shdepend\gx
-[ RECORD 1 ]---------------------------------------------
dbid       | 5
classid    | 1259
objid      | 16528
objsubid   | 0
refclassid | 1260
refobjid   | 16527
deptype    | a
object     | (table,public,test,public.test)
ref_object | (role,,alice,alice)
pg_get_acl | {postgres=arwdDxtm/postgres,alice=r/postgres}

pg_upgrade: оптимизация работы pg_dump
commit: 6e16b1e4, 64f34eb2e, 2329cad1b, 23687e925, 68e962998, c8b06bb96, bd15b7db4, 6ebeeae29

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

Первая заключается в том, что pg_upgrade будет запускать pg_dump с параметром --no-sync. При обновлении сервера не требуется гарантия записи файлов на диск, ведь если что-то пойдет не так, то всю процедуру нужно повторять заново. А сэкономить на синхронизации файлов с диском можно.

Благодаря второй оптимизации, команды на создание баз данных будут использовать стратегию FILE_COPY вместо используемой по умолчанию WAL_LOG:

CREATE DATABASE .. STRATEGY = FILE_COPY;

При этой стратегии содержимое шаблонной базы не записывается в журнал, а надежность гарантируется выполнением контрольных точек перед копированием файлов и после него. При обычной работе это менее эффективно, но в режиме двоичного обновления (binary upgrade) сервер не будет выполнять лишние контрольные точки (потребуется только одна, после создания шаблонной базы template0).

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

Третья оптимизация ускоряет выгрузку описания таблиц в режиме pg_dump --binary-upgrade. Именно в этом режиме pg_upgrade запускает pg_dump. Ускорение достигается за счет того, что в начале своей работы pg_dump одним запросом к pg_class получает информацию о всех таблицах и сохраняет ее в упорядоченном массиве в памяти. В предыдущих версиях при выгрузке каждой таблицы выполняется отдельный запрос к pg_class, на что тратится много ресурсов.

Четвертая оптимизация ускоряет выгрузку последовательностей. Тем же способом, что и выгрузку таблиц. В начале работы pg_dump сохраняет информацию о последовательностях в упорядоченном массиве. В дальнейшем обращаться к системному каталогу не требуется. Но эта оптимизация работает не только в режиме --binary-upgrade, но и в обычном.

Третья и четвертая оптимизации не совсем бесплатны. Для хранения информации о таблицах и последовательностях придется выделять дополнительную оперативную память. Но разработчики уверены, что такие накладные расходы вполне оправданы.

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

Для проверки создадим в отдельной базе данных 17-й версии 100 000 таблиц со столбцом, к которому привязана последовательность. Повторим эти же действия в базе данных 18-й версии:

$ psql -d test -o /dev/null <

А теперь измерим, сколько времени потребуется на работу pg_dump в режиме --binary-upgrade.

Версия 17:

$ time pg_dump -d test --schema-only --binary-upgrade > /dev/null
real    1m40,677s
user    0m5,403s
sys    0m3,265s

Версия 18:

$ time pg_dump -d test --schema-only --binary-upgrade > /dev/null
real    0m7,427s
user    0m2,220s
sys    0m0,264s

Ускорение более чем на порядок.

Предопределенная роль pg_signal_autovacuum_worker
commit: ccd38024b

Роли, включенные в предопределенную роль pg_signal_backend, могут отправлять процессам сервера сигнал на прерывание текущего запроса (pg_cancel_backend) или даже сигнал на прерывание работы (pg_terminate_backend). С одним ограничением. Эти сигналы нельзя посылать процессам суперпользователя.

Однако при сопровождении сервера могут возникать ситуации, когда требуется прервать работу процессов автоочистки. Члены новой предопределенной роли pg_signal_autovacuum_worker, не являясь суперпользователями, смогут отправлять рабочим процессам автоочистки сигнал на завершение очистки текущей таблицы или сигнал на прекращение работы, используя эти же две функции.

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

© Habrahabr.ru