Аналитические запросы теста TPC-H в PostgreSQL

В статье рассматривается использование теста TPC-H с PostgreSQL и проблемы, связанные с запросами Q17-Q20 теста.

Введение

Вместе с PostgreSQL поставляется утилит pg_bench с «TPC-B like» тестом. Кроме этого теста были созданы тесты TPC-R для отчётов, TPC-D для OLAP, TPC-W для заказов в веб-магазине, которые не получили распространения. На основе TPC-D был создан более удачный тест TPC-H для хранилищ данных и аналитических запросов («OLAP нагрузка»). В тесте используется 8 таблиц и 17 ограничений целостности. В TPC-H выделены номинации по размерам обрабатываемых данных от «до 100Гб» до 30–100Тб. Тест TPC-H предназначен для хранилищ данных, включает в себя 22 запроса, которые называют Q1 … Q22.

Запросы теста TPC-H не меняют данные в таблицах, а значит, для повторных тестирований не нужно пересоздавать или вакуумировать таблицы. В тестах TPC-B, TPC-C, TPC-E запросы довольно простые. В реальных приложениях запросы более сложные, чем в этих тестах. Поэтому для тестирования того, как СУБД выполняет запросы, которые могут встретиться в реальных приложениях, можно использовать все или отдельные запросы из теста TPC-H. Для быстрого аудита производительности различных СУБД используют вариант с 1Гб данных. В этом варианте запросы выполняются быстро, не нужно много памяти под экземпляр СУБД и много места на диске. Можно найти программы или скрипты для большинства СУБД, например, для PostgreSQL, Oracle Database, MySQL. После теста TPC-H появился тест TPC-DS с 99 запросами, но он менее популярен.

Создание таблиц теста TPC-H

mkdir gotpc
cd gotpc
wget https://raw.githubusercontent.com/pingcap/go-tpc/master/install.sh
chmod +x install.sh
./install.sh
cd $HOME/.go-tpc/bin
time ./go-tpc tpch prepare -d postgres -U postgres -p 'postgres' -D gotpch -H 127.0.0.1 -P 5432 --conn-params sslmode=disable
psql -d gotpch
\dt+
                                     List of relations
 Schema |   Name   | Type  |  Owner   | Persistence | Access method |  Size   | Description 
--------+----------+-------+----------+-------------+---------------+---------+
 public | customer | table | postgres | permanent   | heap          | 30 MB   | 
 public | lineitem | table | postgres | permanent   | heap          | 973 MB  | 
 public | nation   | table | postgres | permanent   | heap          | 40 kB   | 
 public | orders   | table | postgres | permanent   | heap          | 221 MB  | 
 public | part     | table | postgres | permanent   | heap          | 34 MB   | 
 public | partsupp | table | postgres | permanent   | heap          | 146 MB  | 
 public | region   | table | postgres | permanent   | heap          | 40 kB   | 
 public | supplier | table | postgres | permanent   | heap          | 1912 kB | 
(8 rows)
select count(*) from lineitem;
  count
---------
 6001215
(1 row)

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

Например, выбирается СУБД для внедрения. Запускается тест и на одной СУБД все запросы выполняются зам минуту, а на других проходит час-два или больше. Кто-то считает, что СУБД  «подвисла», прерывает запрос и считает, что она не способна пройти тест. Такая СУБД вряд ли будет выбрана для промышленного использования. Если СУБД не может выполнить запрос за разумное время, то вероятность того, что на реальных запросах он проделает то же самое с какой-то вероятностью. Проблемы никому не нужны. То, что запросы выполняются медленнее до полутора раз, обычно, несущественно. Существенно, чтобы не было падений экземпляра, повреждений данных, выполнения запросов на порядок дольше, ошибок при выполнении. Если, к примеру, форки Greenplum могут выполнять все запросы TPC-H, то они и будут внедряться, вместо форков PostgreSQL. В тесте TPC-H такой запрос есть, который все верcии PostgreSQL выполняют очень долго — час или больше. Это запрос Q17. Другие запросы Q18-Q20 также могли выполняться долго на старых версиях или форках PostgreSQL.

Запрос Q17

Запрос Q17 выполняется больше часа. Остальные запросы выполняются быстро — за секунды. Запросы Q18-Q20 могли выполняться долго на старых версиях или форках PostgreSQL.

В 2017 году обсуждалась проблема выполнения запроса Q20 от часа до 19 часов: https://www.postgresql.org/message-id/flat/CA%2BTgmoZJCKAp70A8kFiq5ZfZAMiA99g0wN60sbqBVR%2BLdMz%2B4g%40mail.gmail.com#4f23bdc64a9fdee184b0ea3d92c045fe

В то время использовалась версия PostgreSQL 9.5 и запрос Q20 выполнялся 19 часов.

Если запрос выполняется долго, то время выполнения не зависит от железа. Через 3 месяца обсуждений (это очень быстро для сообщества) Том Лейн пришел к выводу: Maybe not with the rest of them, but we’re certainly having an issue there with Q20.

По запросу Q17 таких обсуждений не инициировалось и запрос имеет проблемы и на 17 версии. Проблема отдельным исследователям была известна: https://www.enterprisedb.com/blog/tpc-h-performance-postgresql-83 Было написано: «We can clearly see that most of the queries (up to Q17) got faster, but then two of the queries (Q18 and Q20) got somewhat slower». https://www.postgresql.org/message-id/flat/17153-bd309320429d24b7%40postgresql.org

Другие СУБД также испытывали проблемы с этими запросами. Запросы Q17…Q20 содержат коррелированные подзапросы, которые сложны для выполнения. Было найдено решение в виде создания индекса на столбцы внешнего ключа: https://www.percona.com/blog/improving_tpc_h_like_queries_q17/

Однако, это меняет условия теста TPC-H и является читом. Был заполнен баг репорт https://www.postgresql.org/message-id/flat/17153-bd309320429d24b7%40postgresql.org на который ответили, что после того как производитель одного из тестов HammerDB увидев, что часть СУБД не способны выполнить часть запросов за разумное время включило чит создания индексов по внешним ключам, чтобы пользователи утилиты и СУБД не расстраивались и получали красивые графики или могли сравнивать работу СУБД. Если тест не может выполняться для популярных СУБД, то тест потеряет популярность.

Как запрос Q17 выполняется в Oracle Database?

Это коммерческая СУБД, ориентированная на результат и компания имеет ресурсы для доработки продукта. До версии 9.0 запрос Q17 выполнялся относительно долго — 5 минут, не часы. Начиная с версии 9i в планировщик были внесены улучшения, которые позволили выполнять запрос Q17 за секунды. Остальные запросы выполняются за доли секунды и существенно быстрее, чем у остальных СУБД. Возможно, из-за теста TPC-H, а точнее запроса Q17, пошло расхожее выражение «PostgreSQL это уровень Oracle версии 8i». С точки зрения планировщика, вероятно, так и есть — даже в последней версии PostgreSQL запрос Q17 выполняется так же как в предыдущих версиях PostgreSQL. С другой стороны, когда-то говорили, что компания Oracle специально оптимизировала планировщик под тесты TPC. Но это не так: ускорить запросы настолько, чтобы они выполнялись за доли секунд маловероятно и другие производители СУБД не смогли добиться такого результата.

Время выполнения запросов гигабайтного теста TPC-Hв Oracle Database от долей секунды до 3 секунд, даже без сбора статистики по объектам (в этом случае используется dynamicsampling) ни на одном запросе, начиная с 9 версии нет «затыков». Чтобы не было стыдно за PostgreSQL, длительность выполнения запросов TPC-H в Oracle Database скрыты:

Скрытый текст

Elapsed: 00:00:03.132
Elapsed: 00:00:00.331
Elapsed: 00:00:01.116
Elapsed: 00:00:01.582
Elapsed: 00:00:01.112
Elapsed: 00:00:00.917
Elapsed: 00:00:01.935
Elapsed: 00:00:01.109
Elapsed: 00:00:00.634
Elapsed: 00:00:02.800
Elapsed: 00:00:00.946
Elapsed: 00:00:00.651
Elapsed: 00:00:00.674
Elapsed: 00:00:01.348
Elapsed: 00:00:01.334
Elapsed: 00:00:03.534
Elapsed: 00:00:02.395
Elapsed: 00:00:03.035
Elapsed: 00:00:00.260

Выполнение запроса Q17 в Oracle database

select cast(sum(l_extendedprice) / 7.0 as dec(18,2)) as avg_yearly from lineitem,part where p_partkey = l_partkey and p_brand = 'Brand#23' and p_container = 'MED BOX' and l_quantity < (select 0.2 * avg(l_quantity) from lineitem where l_partkey = p_partkey)
-------------------------------------------------------------------------------------
| Id  | Operation               | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |           |       |       | 60383 (100)|          |
|   1 |  SORT AGGREGATE         |           |     1 |    13 |            |          |
|   2 |   VIEW                  | VM_NWVW_2 | 81599 |  1035K| 60383   (1)| 00:00:03 |
|*  3 |    FILTER               |           |       |       |            |          |
|   4 |     HASH GROUP BY       |           | 81599 |     9M| 60383   (1)| 00:00:03 |
|*  5 |      HASH JOIN          |           | 81599 |     9M| 60380   (1)| 00:00:03 |
|*  6 |       HASH JOIN         |           |  3160 |   231K| 30713   (1)| 00:00:02 |
|*  7 |        TABLE ACCESS FULL| PART      |   122 |  5978 |  1051   (1)| 00:00:01 |
|   8 |        TABLE ACCESS FULL| LINEITEM  |  6319K|   156M| 29641   (1)| 00:00:02 |
|   9 |       TABLE ACCESS FULL | LINEITEM  |  6319K|   307M| 29646   (1)| 00:00:02 |
Predicate Information (identified by operation id):
---------------------------------------------------
   3 - filter("L_QUANTITY"<.2*(SUM("L_QUANTITY")/COUNT("L_QUANTITY")))
   5 - access("P_PARTKEY"="L_PARTKEY")
   6 - access("L_PARTKEY"="P_PARTKEY")
   7 - filter(("P_BRAND"='Brand#23' AND "P_CONTAINER"='MED BOX')) 
Note
- dynamic statistics used: dynamic sampling (level=2)
Elapsed: 00:00:00.658

Запрос Q17 выполняется за 0.7 секунд с фуллсканом таблиц, без дополнительных индексов, в то время как в PostgreSQL не меньше часа, а с индексами не меньше 9 секунд.

Oracle использует трансформацию complex view merging (строка VIEW VM_NWVW_2)  https://blogs.oracle.com/optimizer/post/optimizer-transformations-view-merging-part-2

Запрос Q17, трансформированный планировщиком Oracle:

Final query after transformations:******* UNPARSED QUERY IS *******
SELECT SUM("VW_WIF_1"."ITEM_1")/7.0 "AVG_YEARLY" FROM  (SELECT CASE  WHEN "LINEITEM"."L_QUANTITY"<0.2*AVG("LINEITEM"."L_QUANTITY") OVER ( PARTITION BY "LINEITEM"."L_PARTKEY") THEN "LINEITEM"."L_EXTENDEDPRICE" END  "ITEM_1" FROM "TPCH_1GB"."PART" "PART","TPCH_1GB"."LINEITEM" "LINEITEM" WHERE "PART"."P_CONTAINER"='LG PACK' AND "PART"."P_BRAND"='Brand#43' AND "PART"."P_PARTKEY"="LINEITEM"."L_PARTKEY") "VW_WIF_1"

Если убрать двойные кавычки, то запрос можно выполнить в PostgreSQL:

SELECT SUM(VW.ITEM_1)/7.0 AVG_YEARLY
FROM (SELECT CASE  WHEN l.L_QUANTITY<0.2*AVG(l.L_QUANTITY)
      OVER ( PARTITION BY l.L_PARTKEY)
      THEN l.L_EXTENDEDPRICE END  ITEM_1 FROM PART p,
      LINEITEM l WHERE p.P_CONTAINER='LG PACK'
      AND p.P_BRAND='Brand#43' AND p.P_PARTKEY=l.L_PARTKEY) VW;

На версии оптимизатора Oracle 8i запрос выполняется 5 минут. Медленно, но гораздо быстрее, чем на PostgreSQL:

alter session set optimizer_features_enable='8.1.7';
set timing on;
select sum(l_extendedprice) / 7.0 as avg_yearly from lineitem, part where p_partkey = l_partkey and p_brand = 'Brand#43' and p_container = 'LG PACK' and l_quantity < ( select 0.2 * avg(l_quantity) from lineitem where l_partkey = p_partkey);
AVG_YEARLY
----------
309865.026
Elapsed: 00:05:19.398
SELECT * FROM table (dbms_xplan.display_cursor);
-----------------------------------------------------------------
| Id  | Operation            | Name     | Rows  | Bytes | Cost  |
|   0 | SELECT STATEMENT     |          |       |       | 17169 |
|   1 |  SORT AGGREGATE      |          |     1 |    41 |       |
|*  2 |   FILTER             |          |       |       |       |
|*  3 |    HASH JOIN         |          |  6002 |   240K| 17169 |
|*  4 |     TABLE ACCESS FULL| PART     |   202 |  5454 |   586 |
|   5 |     TABLE ACCESS FULL| LINEITEM |  6001K|    80M| 16568 |
|   6 |    SORT AGGREGATE    |          |     1 |     8 |       |
|*  7 |     TABLE ACCESS FULL| LINEITEM |    30 |   240 | 16568 |
-----------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter("L_QUANTITY"<)
   3 - access("P_PARTKEY"="L_PARTKEY")
   4 - filter(("P_BRAND"='Brand#43' AND "P_CONTAINER"='LG PACK'))
   7 - filter("L_PARTKEY"=:B1)

В 2010 году Greenplum был не способен выполнять коррелированные подзапросы и не проходил тест TPC-H. https://tholis.webnode.page/news/tpc-h-fun-with-greenplum-single-node-edition-/
Например, сейчас база данных Clickhouse не способна выполнять коррелированные подзапросы, как Greenplum 15 лет назад https://github.com/ClickHouse/ClickHouse/issues/6697

Q17 наPostgreSQL версии 17

explain (analyze, buffers on, timing off) select sum(l_extendedprice) / 7.0 as avg_yearly from lineitem, part where p_partkey = l_partkey and p_brand = 'Brand#43' and p_container = 'LG PACK' and l_quantity < ( select 0.2 * avg(l_quantity) from lineitem where l_partkey = p_partkey);
                                 QUERY PLAN
---------------------------------------------------------------------
 Aggregate  (cost=2202829.63..2202829.64 rows=1 width=32) (actual rows=1 loops=1)
   Buffers: shared hit=59294965 read=696952753
   ->  Hash Join  (cost=7350.36..2202824.90 rows=1890 width=8) (actual rows=538 loops=1)
         Hash Cond: (lineitem.l_partkey = part.p_partkey)
         Join Filter: (lineitem.l_quantity < (SubPlan 1))
         Rows Removed by Join Filter: 5535
         Buffers: shared hit=59294965 read=696952753
         ->  Seq Scan on lineitem  (cost=0.00..184517.24 rows=6001224 width=21) (actual rows=6001215 loops=1)
               Buffers: shared hit=9731 read=114774
         ->  Hash  (cost=7348.00..7348.00 rows=189 width=8) (actual rows=203 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 16kB
               Buffers: shared hit=161 read=4187
               ->  Seq Scan on part  (cost=0.00..7348.00 rows=189 width=8) (actual rows=203 loops=1)
                     Filter: ((p_brand = 'Brand#43'::bpchar) AND (p_container = 'LG PACK'::bpchar))
                     Rows Removed by Filter: 199797
                     Buffers: shared hit=161 read=4187
         SubPlan 1
           ->  Aggregate  (cost=199520.38..199520.39 rows=1 width=32) (actual rows=1 loops=6073)
                 Buffers: shared hit=59285073 read=696833792
                 ->  Seq Scan on lineitem lineitem_1  (cost=0.00..199520.30 rows=32 width=5) (actual rows=31 loops=6073)
                       Filter: (l_partkey = part.p_partkey)
                       Rows Removed by Filter: 6001184
                       Buffers: shared hit=59285073 read=696833792
 Planning:
   Buffers: shared hit=8
 Planning Time: 0.247 ms
  Execution Time: 4089201.631 ms
(30 rows)
Time: 4089203,084 ms (01:08:09,203)

Запрос Q17 выполняется больше часа.

После создания индекса

CREATE INDEX LINEITEM_PART_SUPP_FKIDX ON LINEITEM (L_PARTKEY,L_SUPPKEY);
explain (analyze, buffers, timing off) select sum(l_extendedprice) / 7.0 as avg_yearly from lineitem, part where p_partkey = l_partkey and p_brand = 'Brand#43' and p_container = 'LG PACK' and l_quantity < ( select 0.2 * avg(l_quantity) from lineitem where l_partkey = p_partkey);
                                                               QUERY PLAN
-------------------------------------------------------
 Aggregate  (cost=209175.16..209175.17 rows=1 width=32) (actual rows=1 loops=1)
   Buffers: shared hit=205522 read=129512
   ->  Hash Join  (cost=7355.50..209170.16 rows=2000 width=8) (actual rows=538 loops=1)
         Hash Cond: (lineitem.l_partkey = part.p_partkey)
         Join Filter: (lineitem.l_quantity < (SubPlan 1))
         Rows Removed by Join Filter: 5535
         Buffers: shared hit=205522 read=129512
         ->  Seq Scan on lineitem  (cost=0.00..184744.57 rows=6001057 width=21) (actual rows=6001215 loops=1)
               Buffers: shared hit=5738 read=118996
         ->  Hash  (cost=7353.00..7353.00 rows=200 width=8) (actual rows=203 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 16kB
               Buffers: shared read=4353
               ->  Seq Scan on part  (cost=0.00..7353.00 rows=200 width=8) (actual rows=203 loops=1)
                     Filter: ((p_brand = 'Brand#43'::bpchar) AND (p_container = 'LG PACK'::bpchar))
                     Rows Removed by Filter: 199797
                     Buffers: shared read=4353
         SubPlan 1
           ->  Aggregate  (cost=131.71..131.72 rows=1 width=32) (actual rows=1 loops=6073)
                 Buffers: shared hit=199784 read=6163
                 ->  Bitmap Heap Scan on lineitem lineitem_1  (cost=4.68..131.62 rows=32 width=5) (actual rows=31 loops=6073)
                       Recheck Cond: (l_partkey = part.p_partkey)
                       Heap Blocks: exact=187549
                       Buffers: shared hit=199784 read=6163
                       ->  Bitmap Index Scan on lineitem_part_supp_fkidx  (cost=0.00..4.67 rows=32 width=0) (actual rows=31 loops=6073)
                             Index Cond: (l_partkey = part.p_partkey)
                             Buffers: shared hit=18159 read=239
 Planning:
   Buffers: shared hit=4 read=12
 Planning Time: 0.325 ms
 Execution Time: 2118.095 ms
(30 rows)
Time: 2118.826 ms (00:02.119)

появился Bitmap Index Scan on lineitem_part_supp_fkidx и запрос стал выполняться за 2 секунды.

Выполнение трансформированного запроса в PostgreSQL

Планировщик Oracle трансформировал запрос

select sum(l_extendedprice) / 7.0 as avg_yearly from lineitem, part
where p_partkey = l_partkey and p_brand = 'Brand#43'
and p_container = 'LG PACK' and l_quantity < ( select 0.2 * avg(l_quantity)
                          from lineitem where l_partkey = p_partkey);

в эквивалент:

SELECT SUM(VW.ITEM_1)/7.0 AVG_YEARLY
FROM (SELECT CASE  WHEN l.L_QUANTITY<0.2*AVG(l.L_QUANTITY)
      OVER ( PARTITION BY l.L_PARTKEY)
      THEN l.L_EXTENDEDPRICE END  ITEM_1 FROM PART p,
      LINEITEM l WHERE p.P_CONTAINER='LG PACK'
      AND p.P_BRAND='Brand#43' AND p.P_PARTKEY=l.L_PARTKEY) VW;

Для просмотра запросов после трансформации в Oracle Database можно использовать команды:

alter session set events '10053 trace name context forever,level 1';
alter session set max_dump_file_size = unlimited;
запросы;
alter session set events '10053 trace name context off';

в полученном файле трассировки переписанный запрос будет идти после фразы «Final query after transformations:». Если сложно придумать, как переписать запрос, то можно использовать планировщик Oracle Database.

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

Список трансформаций в OracleDatabase:

***************************************
join predicate push-down
filter push-down
predicate move-around
complex view merging
select-project-join
set join conversion
subquery unnesting
order by elimination
star transformation
count(col) to count(*) transformation
Join Elimination
join factorization
connect by
select list pruning
distinct placement
vector transformation
Approximate Aggregate Transformation
OR-Expansion
Legacy OR-Expansion
***************************************

В списке даже есть трансформация, заменяющая count (столбец) на count (*), так как count (*) выполняется быстрее не только в Oracle Database, но и PostgreSQL, в котором такой простой трансформации пока нет. Важны не названия трансформаций, а их функционал. Трансформация complex view merging присутствовала и до 9 версии, но не справлялась с запросом Q17.

Попробуем выполнить переписанный планировщиком Oracle запрос в PostgreSQL 17:

drop index lineitem_part_supp_fkidx;
set parallel_setup_cost = 1000000000;
\timing on \\
explain (analyze, buffers, timing off) SELECT SUM(VW.ITEM_1)/7.0 "AVG_YEARLY" FROM  (SELECT CASE  WHEN l.L_QUANTITY<0.2*AVG(l.L_QUANTITY) OVER ( PARTITION BY l.L_PARTKEY) THEN l.L_EXTENDEDPRICE END  ITEM_1 FROM PART p, LINEITEM l WHERE p.P_CONTAINER='LG PACK' AND p.P_BRAND='Brand#43' AND p.P_PARTKEY=l.L_PARTKEY) VW;
                              QUERY PLAN 
----------------------------------------------------
 Aggregate  (cost=208229.18..208229.19 rows=1 width=32) (actual rows=1 loops=1)
   Buffers: shared hit=512 read=128340
   ->  WindowAgg  (cost=208009.69..208150.79 rows=6271 width=40) (actual rows=6073 loops=1)
         Buffers: shared hit=512 read=128340
         ->  Sort  (cost=208009.69..208025.37 rows=6271 width=21) (actual rows=6073 loops=1)
               Sort Key: l.l_partkey
               Sort Method: quicksort  Memory: 477kB
               Buffers: shared hit=512 read=128340
               ->  Hash Join  (cost=7349.61..207614.16 rows=6271 width=21) (actual rows=6073 loops=1)
                     Hash Cond: (l.l_partkey = p.p_partkey)
                     Buffers: shared hit=512 read=128340
                     ->  Seq Scan on lineitem l  (cost=0.00..184512.30 rows=6000730 width=21) (actual rows=6001215 loops=1)
                           Buffers: shared hit=256 read=124249
                     ->  Hash  (cost=7347.00..7347.00 rows=209 width=8) (actual rows=203 loops=1)
                           Buckets: 1024  Batches: 1  Memory Usage: 16kB
                           Buffers: shared hit=256 read=4091
                           ->  Seq Scan on part p  (cost=0.00..7347.00 rows=209 width=8) (actual rows=203 loops=1)
                                 Filter: ((p_container = 'LG PACK'::bpchar) AND (p_brand = 'Brand#43'::bpchar))
                                 Rows Removed by Filter: 199797
                                 Buffers: shared hit=256 read=4091
 Planning:
   Buffers: shared hit=8
 Planning Time: 0.201 ms
 Execution Time: 2079.081 ms
(27 rows)
Time: 2079,664 ms (00:02,080)

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

Параллельные планы отключались, чтобы не переусложнить план. С распараллеливанием (2 рабочих процесса) время выполнения запроса будет 798,561 ms. Распараллеливание в PostgreSQL эффективно.

Доработка планировщиков в виде поправок селективностей, адаптаций полезны. Трансформации есть в PostgreSQL и дают, как показано, существенный эффект. Возможно, трансформации будут улучшаться в PostgreSQL. Когда я готовил статью, мне подсказали, что на конференции сообщества PostgreSQL, которая прошла на прошлой неделе, был доклад Сергея Соловьёва, который на примере запроса Q17 показывал, как можно выполнять декорреляцию запросов. Конференция проходит каждый код и называется PgBootcamp Russia.

доклад Сергея Соловьёва на PgBootcamp 2025
доклад Сергея Соловьёва на PgBootcamp 2025

Я посмотрел доклад, но мало что понял, больше всего мне понравилась приветственная речь Брюса Момжана (один из первых разработчиков PostgreSQL). Докладчик рассказал какую бизнес-задачу решает запрос Q17, что-то похожее на «what-if». Видеозаписи докладов должны выложить в свободный доступ через какое-то время. Интересно будет посмотреть используются ли те же самые трансформации, что в планировщике Oracle или другие.

Код патча выложен в открытый доступ https://github.com/TantorLabs/meetups/tree/main/2025–04–10_Ekb/Maxim%20Milutin%2C%20Sergey%20Solovev%20-%20Hacking%20query%20planner%2C%20again…%20and%20again То есть разработчики форков PostgreSQL работают над улучшением кода планировщика.

Заключение

Запрос Q17 теста TPC-H выполняется чрезвычайно долго в PostgreSQL. Запрос относится к коррелированным подзапросам, которые встречаются в приложениях. Запрос может выполняться за секунды. В статье приведен эквивалент запроса (queryrewrite/transform), который выполняется чрезвычайно быстро. Эквивалент был сформирован планировщиком Oracle Database. У планировщика Oracle много правил переписывания запросов. Часто сложно предположить, как можно переписать запрос так, чтобы получать тот же результат без ошибки. Было показано, как можно получать переписанные планировщиком Oracle запросы. Также был приведен пример ускорения выполнения запроса Q17 путем создания индекса по столбцам. Дана ссылка на пример патча, который добавляет трансформации в планировщик PostgreSQL.

Habrahabr.ru прочитано 7950 раз