Продолжаем следить за новинками 17-й версии PostgreSQL. На этот раз поговорим о том, чем запомнился сентябрьский коммитфест.


Самое интересное об июльском коммитфесте ― в предыдущей статье серии: 2023-07.


Удален параметр old_snapshot_threshold
Новый параметр event_triggers
Новые функции to_bin и to_oct
Новое системное представление pg_wait_events
EXPLAIN: счетчик времени на JIT-компиляцию преобразования кортежей
Планировщик: уточнение оценки начальной стоимости узла WindowAgg
pg_constraint: ограничения NOT NULL
Нормализация CALL, DEALLOCATE и команд управления двухфазной фиксацией
unaccent: целевое выражение правил поддерживает значения в кавычках
COPY FROM: FORCE_NOT_NULL и FORCE_NULL
Аудит подключений без аутентификации
pg_stat_subscription: новый столбец worker_type
Функция pg_promote при неуспешном переключении на реплику
Выбор метода синхронизации с диском в утилитах сервера
pg_restore: оптимизация восстановления в несколько потоков большого количества таблиц
pg_basebackup и pg_receivewal с параметром dbname
Имена параметров для ряда встроенных функций
psql: \watch min_rows



Удален параметр old_snapshot_threshold
commit: f691f5b8


В версии 9.6 появился параметр old_snapshot_threshold. Параметр определяет время для снимка данных, в течение которого гарантируется, что старые версии строк не будут удалены. По истечении этого времени очистка имеет право вычищать версии строк, видимые в снимке. При обращении к удаленным версиям строк транзакция получит ошибку, так знакомую пользователям Oracle: «Snapshot too old».


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



Новый параметр event_triggers
commit: 7750fefd


Параметр event_triggers включает/отключает триггеры событий.


\dconfig+ event_triggers

               List of configuration parameters
   Parameter    | Value | Type |  Context  | Access privileges
----------------+-------+------+-----------+-------------------
 event_triggers | on    | bool | superuser |
(1 row)

Предназначен в первую очередь для отладки триггеров. Ранее в документации к команде CREATE EVENT TRIGGER предлагалось запускать сервер в однопользовательском режиме, если ошибочно работающий триггер не удается удалить.



Новые функции to_bin и to_oct
commit: 260a1f18


К уже существующей функции to_hex для шестнадцатеричных чисел добавлены функции для перевода в двоичную и восьмеричную системы счисления:


SELECT to_bin(2), to_oct(8), to_hex(16);

 to_bin | to_oct | to_hex
--------+--------+--------
 10     | 10     | 10
(1 row)

См. также
Waiting for PostgreSQL 17 – Add to_bin() and to_oct(). – select * from depesz;



Новое системное представление pg_wait_events
commit: 1e68e43d


В документации к представлению pg_stat_activity для описания столбцов wait_event_type и wait_event прилагаются таблица с описанием типов событий ожидания и несколько таблиц с описанием событий ожидания каждого типа. Эти же описания теперь можно получить запросом к новому системному представлению pg_wait_events.


Это может быть удобно в системах мониторинга для получения описания ожиданий текущих процессов. Чем сейчас занят процесс контрольной точки?


SELECT a.pid, a.state, a.wait_event_type, a.wait_event, w.description
FROM pg_stat_activity a JOIN pg_wait_events w
     ON (a.wait_event_type = w.type AND a.wait_event = w.name)
WHERE a.backend_type = 'checkpointer'\gx

-[ RECORD 1 ]---+---------------------------------------------
pid             | 21090
state           |
wait_event_type | Activity
wait_event      | CheckpointerMain
description     | Waiting in main loop of checkpointer process


EXPLAIN: счетчик времени на JIT-компиляцию преобразования кортежей
commit: 5a3423ad


В выводе EXPLAIN ANALYZE, а также в pg_stat_statements, отдельно учитывается время, затраченное на JIT-компиляцию преобразования кортежей (tuple deforming).


В следующем запросе JIT-компиляция используется для вычисления числа ????:


EXPLAIN (analyze, summary off)
WITH pi AS (
  SELECT random() x, random() y
  FROM generate_series(1,10000000)
)
SELECT 4*sum(1-floor(x*x+y*y))/count(*) val FROM pi;

                                                                  QUERY PLAN                                                                   
-----------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=525000.00..525000.02 rows=1 width=8) (actual time=3934.330..3934.331 rows=1 loops=1)
   CTE pi
     ->  Function Scan on generate_series  (cost=0.00..150000.00 rows=10000000 width=16) (actual time=725.126..1654.054 rows=10000000 loops=1)
   ->  CTE Scan on pi  (cost=0.00..200000.00 rows=10000000 width=16) (actual time=725.129..3429.925 rows=10000000 loops=1)
 JIT:
   Functions: 6
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 0.954 ms (Deform 0.161 ms), Inlining 8.476 ms, Optimization 25.632 ms, Emission 22.098 ms, Total 57.160 ms
(8 rows)

Значение Generation в последней строке плана ― это суммарное время на компиляцию выражений (управляется параметром jit_expressions) и на компиляцию преобразования кортежей (jit_tuple_deforming). Сколько из этого времени заняла компиляция преобразования кортежей в 17-й версии отдельно указано в скобках (Deform).


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


SET jit_tuple_deforming = off;

Часть плана запроса, касающаяся JIT:


 JIT:
   Functions: 5
   Options: Inlining true, Optimization true, Expressions true, Deforming false
   Timing: Generation 0.665 ms (Deform 0.000 ms), Inlining 15.209 ms, Optimization 25.897 ms, Emission 16.664 ms, Total 58.435 ms


Планировщик: уточнение оценки начальной стоимости узла WindowAgg
commit: 3900a02c


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


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


16=> EXPLAIN (analyze, settings)
SELECT t.ticket_no, SUM(tf.amount) OVER ()
FROM tickets t JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no
LIMIT 1;

                                                                           QUERY PLAN                                                                           
----------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.56..0.86 rows=1 width=46) (actual time=30416.237..30416.239 rows=1 loops=1)
   ->  WindowAgg  (cost=0.56..2525161.21 rows=8391708 width=46) (actual time=30416.236..30416.237 rows=1 loops=1)
         ->  Nested Loop  (cost=0.56..2420264.86 rows=8391708 width=20) (actual time=0.049..27742.105 rows=8391852 loops=1)
               ->  Seq Scan on tickets t  (cost=0.00..78913.45 rows=2949845 width=14) (actual time=0.015..287.636 rows=2949857 loops=1)
               ->  Index Scan using ticket_flights_pkey on ticket_flights tf  (cost=0.56..0.76 rows=3 width=20) (actual time=0.007..0.009 rows=3 loops=2949857)
                     Index Cond: (ticket_no = t.ticket_no)
 Settings: jit = 'off', search_path = 'bookings, public'
 Planning Time: 0.456 ms
 Execution Time: 30454.862 ms
(9 rows)

Нас интересует вторая строка плана с узлом WindowAgg. Начальная стоимость равна 0.56, хотя на получение первой строки ушло ~30 секунд.


В 17-й версии оценка исправлена и планировщик выбирает более уместный hash join, что сразу отражается на скорости выполнения запроса:


17=> EXPLAIN (analyze, settings)
SELECT t.ticket_no, SUM(tf.amount) OVER ()
FROM tickets t JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no
LIMIT 1;

                                                                   QUERY PLAN                                                                    
-------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=523778.31..523778.31 rows=1 width=46) (actual time=7157.639..7157.642 rows=1 loops=1)
   ->  WindowAgg  (cost=523778.31..523778.36 rows=8392150 width=46) (actual time=7157.638..7157.640 rows=1 loops=1)
         ->  Hash Join  (cost=130215.56..418876.48 rows=8392150 width=20) (actual time=740.005..5075.390 rows=8391852 loops=1)
               Hash Cond: (tf.ticket_no = t.ticket_no)
               ->  Seq Scan on ticket_flights tf  (cost=0.00..153881.50 rows=8392150 width=20) (actual time=0.066..794.699 rows=8391852 loops=1)
               ->  Hash  (cost=78938.47..78938.47 rows=2949847 width=14) (actual time=737.972..737.973 rows=2949857 loops=1)
                     Buckets: 262144  Batches: 32  Memory Usage: 6190kB
                     ->  Seq Scan on tickets t  (cost=0.00..78938.47 rows=2949847 width=14) (actual time=0.029..307.040 rows=2949857 loops=1)
 Settings: search_path = 'bookings, public', jit = 'off'
 Planning Time: 0.331 ms
 Execution Time: 7202.957 ms
(11 rows)


pg_constraint: ограничения NOT NULL
commit: b0e96f31


В системном каталоге pg_constraint официально появился новый, но хорошо известный, тип ограничения целостности ― NOT NULL. По разным причинам ограничения NOT NULL в pg_constraint не записывались.


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


Ограничения NOT NULL в pg_constraint имеют тип contype = 'n':


CREATE TABLE t (
    col1 int NOT NULL,
    col2 int NOT NULL
);

SELECT conname, contype, conkey
FROM pg_constraint
WHERE conrelid = 't'::regclass;

     conname     | contype | conkey
-----------------+---------+--------
 t_col1_not_null | n       | {1}
 t_col2_not_null | n       | {2}
(2 rows)

Ограничение можно удалить по имени, как и любое другое:


ALTER TABLE t DROP CONSTRAINT t_col2_not_null;

Увидеть ограничения можно в отдельной секции вывода команды \d+:


postgres@postgres(17.0)=# \d+ t

                                            Table "public.t"
 Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
 col1   | integer |           | not null |         | plain   |             |              |
 col2   | integer |           |          |         | plain   |             |              |
Not-null constraints:
    "t_col1_not_null" NOT NULL "col1"
Access method: heap

В настоящий момент ограничения NOT NULL нельзя создать без проверки существующих данных (NOT VALID) или объявить отложенными (DEFERRED). Кроме того, ограничения NOT NULL для таблиц системного каталога в pg_constraint не записаны.



Нормализация CALL, DEALLOCATE и команд управления двухфазной фиксацией
commit: 11c34b34, 638d42a3, bb45156f


Механизм нормализации команд научился заменять параметры процедур в вызовах CALL на константы. Теперь вызов процедуры с разными параметрами будет учитываться в pg_stat_statements одной строкой:


CALL p(1);
CALL p(2);
CALL p(3);

SELECT queryid, query, calls                                                                               
FROM pg_stat_statements
WHERE query ILIKE 'CALL%';

       queryid       |   query    | calls
---------------------+------------+-------
 7076031282516132618 | CALL p($1) |     3
(1 row)

Кроме того, на константы заменяются идентификаторы транзакций в командах управления двухфазной фиксацией (второй коммит) и имена подготовленных операторов в DEALLOCATE (третий коммит).


См. также
Нормализация DDL и служебных команд
Нормализация DDL и служебных команд, продолжение



unaccent: целевое выражение правил поддерживает значения в кавычках
commit: 59f47fb9


Для устранения диакритических знаков встроенный полнотекстовый поиск предлагает использовать словарь unaccent, поставляемый с сервером в виде одноименного расширения.


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



COPY FROM: FORCE_NOT_NULL и FORCE_NULL
commit: f6d4c9cf


Параметры FORCE_NOT_NULL и FORCE_NULL команды COPY появились достаточно давно, в версиях 9.0 и 9.4 соответственно. Но требовали явного перечисления столбцов, даже если их нужно было применить ко всем столбцам таблицы.


Вместо перечисления всех столбцов теперь можно указать символ *.


CREATE TABLE t (col1 text NOT NULL, col2 text NOT NULL);

COPY t FROM STDIN WITH (FORMAT csv, FORCE_NOT_NULL *);

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.
>> "",""
>> \.
COPY 1

SELECT * FROM t WHERE col1 = '' and col2 = '';

 col1 | col2
------+------
      |
(1 row)


Аудит подключений без аутентификации
commit: e48b19c5


Если пользователь подключается без аутентификации, то при включенном параметре log_connections запись об этом появится в журнале сервера с указанием номера сработавшей строки (117) с методом «trust» в pg_hba.conf:


2023-10-19 11:40:07.609 MSK [111233] LOG:  connection authenticated: user="postgres" method=trust (/home/pal/master/data/pg_hba.conf:117)
2023-10-19 11:40:07.609 MSK [111233] LOG:  connection authorized: user=postgres database=demo application_name=psql

Раньше для метода «trust» в журнал записывалась только вторая строка.


Заметим, что функция SYSTEM_USER по-прежнему возвращает NULL для таких подключений.


См. также
Waiting for PostgreSQL 17 – Generate new LOG for “trust” connections under log_connections – select * from depesz;



pg_stat_subscription: новый столбец worker_type
commit: 13aeaf07


Новый столбец worker_type представления pg_stat_subscription поможет быстро определить, какую работу выполняет процесс, обслуживающий подписку. Возможные значения говорят за себя: apply, parallel apply и table synchronization.


Стоит напомнить, что сервер подписки научился применять транзакции несколькими процессами в 16-й версии.



Функция pg_promote при неуспешном переключении на реплику
commit: f593c551


Один из вариантов переключения на реплику ― это вызов функции pg_promote. Если переключиться не удалось, то функция возвращает false. И неважно, что послужило причиной:


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

Для двух последних случаев в журнал сервера записывалось предупреждение. В 17-й версии эти предупреждения превратились в ошибки, соответственно функция pg_promote будет возвращать false только при превышении таймаута, в остальных случаях завершаться ошибкой.



Выбор метода синхронизации с диском в утилитах сервера
commit: 8c16ad3b


Утилиты сервера, пишущие файлы на диск, получили параметр --synch-method. Речь о следующих утилитах: initdb, pg_basebackup, pg_checksums, pg_dump, pg_rewind и pg_upgrade.


По умолчанию используется --synch-method=fsync.


В линукс-системах можно указать syncfs для синхронизации всей файловой системы сразу, вместо вызова fsync на каждый файл. Во многих случаях это будет быстрее. Однако возможны и побочные эффекты, если с этой же файловой системой активно работают другие приложения, изменяющие файлы: в таком случае придется дополнительно синхронизировать и их запись. В документации появилось новое приложение Appendix O. syncfs() Caveats, предупреждающее об этом.



pg_restore: оптимизация восстановления в несколько потоков большого количества таблиц
commit: c103d073, 5af0263a, 9bfd44bb


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


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


Для оптимизации выбора следующей таблицы список таблиц поместили в двоичную кучу. Теперь pg_restore будет значительно быстрее восстанавливать из копий с большим количеством таблиц.



pg_basebackup и pg_receivewal с параметром dbname
commit: cca97ce6


pg_basebackup и pg_receivewal принимают параметр dbname для подключения. Вообще он не нужен этим утилитам, ведь резервные копии и WAL — это уровень всего кластера баз данных. Но в случае подключения через прокси, в частности pgbouncer, требуется указывать базу данных.


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



Имена параметров для ряда встроенных функций
commit: b575a26c


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


16=> \df string_agg

                            List of functions
   Schema   |    Name    | Result data type | Argument data types | Type
------------+------------+------------------+---------------------+------
 pg_catalog | string_agg | bytea            | bytea, bytea        | agg
 pg_catalog | string_agg | text             | text, text          | agg
(2 rows)

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


17=> \df string_agg

                                List of functions
   Schema   |    Name    | Result data type |     Argument data types      | Type
------------+------------+------------------+------------------------------+------
 pg_catalog | string_agg | bytea            | value bytea, delimiter bytea | agg
 pg_catalog | string_agg | text             | value text, delimiter text   | agg
(2 rows)


psql: \watch min_rows
commit: f347ec76


В 16-й версии команде \watch добавили параметр count, позволяющий указать количество повторений выполнения запроса.


Теперь появился еще один способ автоматически завершить работу \watch. Если запрос вернет меньше строк, чем указано в параметре min_rows, то повторение запроса прекращается. Может быть полезным для мониторинга выполнения заданий.


CREATE TABLE job (id int, status text DEFAULT 'NEW');
INSERT INTO job VALUES (1),(2),(3);

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


WITH process_job AS (
    UPDATE job SET status = 'PROCESSED'
    WHERE id = (SELECT id
                FROM job
                WHERE status = 'NEW'
                ORDER BY id LIMIT 1)
)
SELECT * FROM job WHERE status = 'NEW'
\watch min_rows=1

Mon 16 Oct 2023 05:55:53 PM MSK (every 2s)

 id | status
----+--------
  1 | NEW
  2 | NEW
  3 | NEW
(3 rows)

Mon 16 Oct 2023 05:55:55 PM MSK (every 2s)

 id | status
----+--------
  2 | NEW
  3 | NEW
(2 rows)

Mon 16 Oct 2023 05:55:57 PM MSK (every 2s)

 id | status
----+--------
  3 | NEW
(1 row)

Mon 16 Oct 2023 05:55:59 PM MSK (every 2s)

 id | status
----+--------
(0 rows)

См. также
Waiting for PostgreSQL 17 – Allow \watch queries to stop on minimum rows returned – select * from depesz;




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

Комментарии (1)


  1. Sleuthhound
    25.10.2023 06:03
    +1

    Павел, спасибо за обзор, очень интересно!