Na een succesvolle derde beta in augustus 2024 heeft de PostgreSQL-ontwikkelinggroep de GA-versie op 26 september uitgebracht. Onlangs bloggde ik over enkele van de sleutellogische replicatie-mogelijkheden die u in PostgreSQL 17 zult zien. In deze blog zal ik een paar nieuwe prestatiesopties beschrijven die u in Postgres 17 zult vinden, evenals een andere belangrijke logische replicatie-mogelijkheid die ik in mijn eerdere blog van deze reeks niet behandeld heb.
PostgreSQL heeft over de jaren uitzonderlijk gegroeid en met elke grote release is het een stevigere, betrouwbaardere en responsiever database voor zowel kritische als niet-kritische enterprise-toepassingen geworden. Het wereldwijde en levendige PostgreSQL-community is mede verantwoordelijk voor de succes van PostgreSQL, door zorgvuldig te controleren en te beoordelen of alle veranderingen voor hun toevoeging aan het broncodeproject geschikt zijn. Het is ook ontzettend opmerkelijk dat grote technologische merken als Microsoft, Google, Apple en anderen in Postgres investeren door in-house expertise op te bouwen en terug te geven aan de open-sourcegemeenschap.
Verbeteringen aan de logische replicatie zijn de weg vrijmakend voor het toevoegen van ondersteuning voor distributed PostgreSQL aan de kernfuncties. Distributed PostgreSQL betekent de implementatie van PostgreSQL in een distribueerde architectuur, die betekent dat er verbeterde schaalbaarheid, fault tolerance en verbeteringen in prestaties mogelijk zijn over meerdere knooppunten.
Nu zonder verdere vertraging, laten we over enkele prestatieopties van PostgreSQL 17 discusseren.
Verbeterde query-prestaties met geëncodeerde subquerytjes (Materialized CTEs).
Algemene Tabel Expressies (CTEs) in PostgreSQL zijn tijdelijke resultaat sets die binnen een SELECT
, INSERT
, UPDATE
of DELETE
statement kunnen worden gerefereerd. Ze vergroten de leesbaarheid en de organisatie van complexe query’s en kunnen recursief zijn, waardoor ze bijzonder handig zijn voor hiërarchische data. De basis syntaxis van een CTE-query is als volgt:
WITH cte_names AS
(– QUERY here )
Select * from cte_names;
Voeg het keyword WITH
toe aan een query om de CTE aan te maken; het bovenliggende query (dat het resultaat set definieert) volgt de AS
clause na de CTE-naam. Na het definiëren van de CTE, kun je de CTE aanroepen door naam om het resultaat set van de CTE te refereren en verdere bewerkingen op het resultaat set uit te voeren binnen dezelfde query.
PostgreSQL 17 brengt doorgaans performantie en mogelijkheden rond CTEs verbeteringen, inclusief verbeteringen in query planning en uitvoering. Oudere versies van Postgres behandelen CTEs als optie fences, wat betekent dat de planner de voorwaarden niet door kon drukken. Echter, vanaf PostgreSQL 12 kun je efficientere uitvoeringsplannen definiëren. U moet altijd uw queries analyseren en de uitvoeringsplannen overwegen wanneer de prestaties kritiek zijn.
Prestatie-tip: Als je meerdere keren naar dezelfde resultaatset verwijst, maak de CTE aan met het MATERIALIZED
-trefwoord. Wanneer je een materialized CTE aanmaakt, berekent en opslaat Postgres het resultaat van de hoofdquery. Zo hoeven volgende query’s niet meerdere keren complexe berekeningen uit te voeren als je meerdere keren naar de CTE verwijst.
Kolomstatistieken extraheren uit CTE-referenties, Postgres 17 verbeterde Materialized CTEs
Een gematerialiseerde CTE gedraagt zich in feite als een optimisatie-hek, wat betekent dat de buitenste query geen invloed zal hebben op het plan van de subquery zodra dat plan is gekozen. De buitenste query heeft inzicht in de geschatte breedte en rijtellingen van de CTE-resultaatset, dus het is zinvol om de kolomstatistieken van de subquery door te geven aan de planner voor de buitenste query. De buitenste query kan gebruik maken van alle beschikbare informatie, waardoor de kolomstatistieken kunnen worden doorgegeven naar het plan van de buitenste query, maar niet naar het plan van de CTE.
Deze bug gerapporteerd aan de gemeenschap bevat een eenvoudig testgeval dat de verbetering en het effect op de queryplanner als gevolg van deze verbetering kan aantonen.
Voorbeeld: Postgres 16-gedrag vergelijken met Postgres 17
Eerst creëren we onze werkruimte in Postgres 16 en voeren we ANALYZE
uit; twee tabellen en indexen:
postgres=# create table t1(a int);
CREATE TABLE
postgres=# create table t2(b int);
CREATE TABLE
postgres=# create index my_index on t1 using btree (a);
CREATE INDEX
postgres=# insert into t1 select generate_series(1, 100000) from generate_series(1, 3);
INSERT 0 300000
postgres=# insert into t2 select generate_series(1, 100) from generate_series(1, 10);
INSERT 0 1000
postgres=# analyze t1;
ANALYZE
postgres=# analyze t2;
ANALYZE
Then, we create our materialized CTE:
postgres=# explain analyze with my_cte as materialized (select b from t2) select *
from t1 where t1.a in (select b from my_cte);
The query plan from our Postgres 16 code sample contains:
QUERY PLAN
----------------------------------------------------------------------
Nested Loop (cost=37.92..856.50 rows=2966 width=4) (actual time=0.574..0.722 rows=300 loops=1)
CTE my_cte
-> Seq Scan on t2 (cost=0.00..15.00 rows=1000 width=4) (actual time=0.038..0.161 rows=1000 loops=1)
-> HashAggregate (cost=22.50..24.50 rows=200 width=4) (actual time=0.449..0.461 rows=100 loops=1)
Group Key: my_cte.b
Batches: 1 Memory Usage: 40kB
-> CTE Scan on my_cte (cost=0.00..20.00 rows=1000 width=4) (actual time=0.046..0.322 rows=1000 loops=1)
-> Index Only Scan using my_index on t1 (cost=0.42..4.06 rows=3 width=4) (actual time=0.002..0.002 rows=3 loops=1
00)
Index Cond: (a = my_cte.b)
Heap Fetches: 0
Planning Time: 1.242 ms
Execution Time: 1.051 ms
(12 rows)
Als u in het queryplan ziet, zijn de kolomstatistieken van de 200 rijen uit de sub-query foutief, wat de algemene planning beïnvloedt.
-> HashAggregate (cost=22.50..24.50 rows=200 width=4) (actual time=0.449..0.461 rows=100 loops=1)
Group Key: my_cte.b
Dan testen we dezelfde setup en query tegen PostgreSQL 17:
postgres=# explain analyze with my_cte as materialized (select b from t2) select *
from t1 where t1.a in (select b from my_cte);
QUERY PLAN
-------------------------------------------------------------------------------------------------
---------------------------------
Merge Join (cost=42.25..54.29 rows=302 width=4) (actual time=0.627..0.712 rows=300 loops=1)
Merge Cond: (t1.a = my_cte.b)
CTE my_cte
-> Seq Scan on t2 (cost=0.00..15.00 rows=1000 width=4) (actual time=0.031..0.134 rows=1000
loops=1)
-> Index Only Scan using my_index on t1 (cost=0.42..7800.42 rows=300000 width=4) (actual tim
e=0.027..0.049 rows=301 loops=1)
Heap Fetches: 0
-> Sort (cost=26.82..27.07 rows=100 width=4) (actual time=0.598..0.604 rows=100 loops=1)
Sort Key: my_cte.b
Sort Method: quicksort Memory: 25kB
-> HashAggregate (cost=22.50..23.50 rows=100 width=4) (actual time=0.484..0.494 rows=1
00 loops=1)
Group Key: my_cte.b
Batches: 1 Memory Usage: 24kB
-> CTE Scan on my_cte (cost=0.00..20.00 rows=1000 width=4) (actual time=0.033..0
.324 rows=1000 loops=1)
Planning Time: 1.066 ms
Execution Time: 0.946 ms
(15 rows)
Als u in het queryplan voor Postgres 17 ziet, worden de kolomstatistieken van de subquery correct doorgegeven aan de bovenliggende planner van de externe query. Dit helpt PostgreSQL een betere planning te kiezen die de uitvoeringstijd van de query verbeterd.
Dit is een simpele query, maar bij grotere en complexe queries kan deze verandering leiden tot een significante prestatieverschillering.
Pathkeys Overdragen Vanuit een CTE Naar een Buiten Query
Een andere interessante verbetering van de CTE-functionaliteit in Postgres 17 is de overdracht van pathkeys vanuit de sub-query naar de externe query. In PostgreSQL zijn pathkeys een onderdeel van het planningproces voor de uitvoering van een query, dat voornamelijk wordt gebruikt voor het sorteren en ordenen van rijen in queries die geordende resultaten vereisen, zoals queries met een ORDER BY
-clausule, of wanneer sorteren nodig is voor andere bewerkingen zoals merge joins.
Voor Postgres 17 was het sorteervolgorde van de geëxporteerde CTE-sub-query niet gedeeld met de externe query, zelfs als het sorteervolgorde was gegarandeerd door een indexscannode of een sortenode. Het ontbreken van een gegarandeerd sorteervolgorde laat de PostgreSQL-planner kiezen voor een minder geoptimaliseerde planning, terwijl een gegarandeerd sorteervolgorde de kans vergroterd dat een geoptimaliseerde planning wordt gekozen.
Met PostgreSQL 17 kan een CTE, indien gemanipuleerd en een specifieke volgorde heeft, door de planner worden hergebruikt in de buitenste query, waardoor de prestaties verbeteren doordat er geen redundante sorteervolgorde of inefficiënte joinmethodes meer zijn.ALS gementioneerd in de commitcommentaar door Tom Lane:
“Het code voor het verhogen van pathkeys in de buitenste query bestaat al voor normale
RTE_SUBQUERY
-subqueries, maar het werd niet gebruikt voor CTEs, misschien uit bezorgdheid voor het behouden van een optimalisatiehek tussen de CTE en de buitenste query.”
Deze eenvoudige wijziging in de Postgres broncode zou moeten resulteren in prestatieverbeteringen voor queries die complexe CTEs bevatten, vooral voor die query’s waar sorting of merge joins kunnen worden geoptimaliseerd op basis van de inherente volgorde van de CTE-resultaten.
Hier volgt een voorbeeld met de gegevens in PostgreSQL regressie:
postgres=# CREATE TABLE tenk1 (
postgres(# unique1 int4,
postgres(# unique2 int4,
postgres(# two int4,
postgres(# four int4,
postgres(# ten int4,
postgres(# twenty int4,
postgres(# hundred int4,
postgres(# thousand int4,
postgres(# twothousand int4,
postgres(# fivethous int4,
postgres(# tenthous int4,
postgres(# odd int4,
postgres(# even int4,
postgres(# stringu1 name,
postgres(# stringu2 name,
postgres(# string4 name
postgres(# );
CREATE TABLE
postgres=# CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
CREATE INDEX
postgres=# \copy tenk1 FROM '~/projects/postgres/src/test/regress/data/tenk.data';
COPY 10000
postgres=# VACUUM ANALYZE tenk1;
VACUUM
Het queryplan uit ons Postgres 16-codevoorbeeld bevat het volgende:
postgres=# explain analyze with x as materialized (select unique1 from tenk1 b order by unique1)
select count(*) from tenk1 a
where unique1 in (select * from x);
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
---
Aggregate (cost=764.29..764.30 rows=1 width=8) (actual time=21.592..21.593 rows=1 loops=1)
CTE x
-> Index Only Scan using tenk1_unique1 on tenk1 b (cost=0.29..306.29 rows=10000 width=4) (actual time=0.046..1.415 rows=10000 loops=
1)
Heap Fetches: 0
-> Nested Loop (cost=225.28..445.50 rows=5000 width=0) (actual time=7.545..20.911 rows=10000 loops=1)
-> HashAggregate (cost=225.00..227.00 rows=200 width=4) (actual time=7.535..9.051 rows=10000 loops=1)
Group Key: x.unique1
Batches: 1 Memory Usage: 929kB
-> CTE Scan on x (cost=0.00..200.00 rows=10000 width=4) (actual time=0.070..3.933 rows=10000 loops=1)
-> Index Only Scan using tenk1_unique1 on tenk1 a (cost=0.29..1.08 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=10000)
Index Cond: (unique1 = x.unique1)
Heap Fetches: 0
Planning Time: 0.806 ms
Execution Time: 21.890 ms
(14 rows)
Het queryplan uit ons Postgres 17-codevoorbeeld bevat het volgende:
postgres=# explain analyze with x as materialized (select unique1 from tenk1 b order by unique1)
select count(*) from tenk1 a
where unique1 in (select * from x);
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------- Aggregate (kosten=987,55..987,56 rijen=1 breedte=8) (actuele tijd=8,777..8,778 rijen=1 loops=1)
CTE x
-> Index Only Scan using tenk1_unique1 on tenk1 b (cost=0.29..306.29 rows=10000 width=4) (actual time=0.010..1.095 rows=100
00 loops=1)
Heap Fetches: 0
-> Merge Semi Join (cost=0.31..656.26 rows=10000 width=0) (actual time=0.037..8.024 rows=10000 loops=1)
Merge Cond: (a.unique1 = x.unique1)
-> Index Only Scan using tenk1_unique1 on tenk1 a (cost=0.29..306.29 rows=10000 width=4) (actual time=0.013..1.262 rows
=10000 loops=1)
Heap Fetches: 0
-> CTE Scan on x (cost=0.00..200.00 rows=10000 width=4) (actual time=0.016..3.678 rows=10000 loops=1)
Planning Time: 0.800 ms
Execution Time: 8.899 ms
(11 rows)
De queryplannen in Postgres 16 en Postgres 17 zijn significant verschillend door deze versie 17 verbetering. Dit is een klein voorbeeld; u kunt zien dat de prestatiewinst significant zal zijn in grotere query’s. Merk op dat deze verbetering alleen effectief is als de CTE subquery een ORDER BY
clausule heeft.
Snelle B-Tree Index Scan voor Scalair Array
In PostgreSQL is ScalarArrayOpExpr
een knooppunttype in het executieplan dat query’s behandelt die betrekking hebben aan bewerkingen zoals IN
of ANY
met arrays of lijsten van waarden. Het is bijzonder handig voor query’s waarin u een kolom vergelijkt met een set waarden, zoals: SELECT * FROM table WHERE column = ANY(ARRAY[1, 2, 3]);
.
ScalarArrayOpExpr
laat PostgreSQL optimale query’s mogelijk maken die meerdere vergelijkingen bevatten die IN
of ANY
gebruiken. PostgreSQL 17 heeft nieuwe prestatieverbeteringen geintroduceerd om deze bewerkingen nog sneller te maken.
In PostgreSQL 17 zijn significante verbeteringen gemaakt aan B-Tree index scans, die de prestatieoptimalisatie speciaal voor query’s met grote IN
lijsten of ANY
voorwaarden bevatten. Deze verbeteringen verminderen het aantal uitgevoerde indexscans door het systeem, waardoor CPU- en bufferpagina-contingenentie wordt verminderd, resulterend in snellere queryuitvoering.
Eén van de belangrijkste verbeteringen is de verbetering van het afhandelen van Scalar Array Operation Expressions (SAOP), die de efficiënte doorloop van B-tree-indexen mogelijk maakt, vooral voor multidimensionale query’s. Bijvoorbeeld, als u meerdere indexkolommen heeft (elke kolom met zijn eigen IN
-lijst), kan PostgreSQL 17 deze operaties nu effectiever in één indexscan verwerken, in plaats van meerdere scans, zoals in eerdere versies. Dit kan leiden tot performancerijsten van 20-30% bij CPU-gebonden taken, waar pagina-toegangen eerder een kleppend element waren.
Additioneel, introduceert PostgreSQL 17 betere beheer van interne locks, die de prestaties verder verbeteren voor hoge concurrency-taken, vooral bij het scannen van meerdere dimensies binnen een B-tree-index.
We kunnen dit demonstreren met een eenvoudig voorbeeld. We zullen dezelfde tenk1
-tabel en data gebruiken die we in het vorige voorbeeld uit de Postgres-regressiesuite gebruikten.
Ons voorbeeld, eerst uitgevoerd op Postgres 16:
CREATE TABLE tenk1 (
postgres(# unique1 int4,
postgres(# unique2 int4,
postgres(# two int4,
postgres(# four int4,
postgres(# ten int4,
postgres(# twenty int4,
postgres(# hundred int4,
postgres(# thousand int4,
postgres(# twothousand int4,
postgres(# fivethous int4,
postgres(# tenthous int4,
postgres(# odd int4,
postgres(# even int4,
postgres(# stringu1 name,
postgres(# stringu2 name,
postgres(# string4 name
postgres(# );
CREATE TABLE
postgres=# CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
CREATE INDEX
postgres=# \copy tenk1 FROM '~/projects/postgres/src/test/regress/data/tenk.data';
COPY 10000
postgres=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 IN (1, 2, 3);
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=14.20..330.12 rows=176 width=244) (actual time=0.138..0.153 rows=3 loops=1)
Recheck Cond: (unique1 = ANY ('{1,2,3}'::integer[]))
Heap Blocks: exact=3
Buffers: shared hit=9
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..14.16 rows=176 width=0) (actual time=0.102..0.102 rows=3 loops=1)
Index Cond: (unique1 = ANY ('{1,2,3}'::integer[]))
Buffers: shared hit=6
Planning:
Buffers: shared hit=2
Planning Time: 0.900 ms
Execution Time: 0.242 ms
(11 rows)
postgres=# SELECT idx_scan, idx_tup_fetch FROM pg_stat_user_tables WHERE relname = 'tenk1';
idx_scan | idx_tup_fetch
----------+---------------
3 | 3
(1 row)
In het vorige query kan u zien dat de shared buffer hit voor de IN
-query 9 was en dat het 3 indexscans nodig had om de resultaten te krijgen uit de indexscan. In PostgreSQL noemt men de term shared hit een specifiek type cache hit die te maken heeft met bufferbeheer. Een shared hit ontstaat als PostgreSQL een data blok of pagina uit het gedeelde bufferpoolexter bij de toegang krijgt in plaats van vanaf de schijf, waardoor de queryprestaties verbeterd worden.
Hetzelfde voorbeeld, dit keer uitgevoerd op Postgres 17:
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 IN (1, 2, 3);
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=12.88..24.08 rows=3 width=244) (actual time=0.043..0.054 rows=3 loops=1)
Recheck Cond: (unique1 = ANY ('{1,2,3}'::integer[]))
Heap Blocks: exact=3
Buffers: shared hit=5
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..12.88 rows=3 width=0) (actual time=0.026..0.026 rows=3 loops=1)
Index Cond: (unique1 = ANY ('{1,2,3}'::integer[]))
Buffers: shared hit=2
Planning:
Buffers: shared hit=59
Planning Time: 0.479 ms
Execution Time: 0.116 ms
(11 rows)
postgres=# SELECT idx_scan, idx_tup_fetch FROM pg_stat_user_tables WHERE relname = 'tenk1';
idx_scan | idx_tup_fetch
----------+---------------
1 | 3
(1 row)
Als u ziet, is de gemiddelde shared buffer hit bij Postgres 17 gereduceerd tot 5, en cruciaal is dat het er nu maar een indexscan uitvoert (terwijl dit bij Postgres 16 drie scans was). Met deze verbetering in Postgres 17 zijn de prestaties van scalair arraybewerkingen sterk verbeterd, en Postgres kan kiezen uit beter geoptimaliseerde queryplannen.
Bijbehoud van Logische Replicatieslotten en Abonnementen Tijdens Upgrade
Het bijbehoud van logische replicatieslotten en de migratie van abonnementen tijdens de grote upgrade van het systeem is een andere functie van logische replicatie die toegevoegd is aan PostgreSQL 17. Noteer dat deze functie alleen nuttig zal zijn bij het upgraden van PostgreSQL 17 naar latere versies; dit wordt niet ondersteund voor upgraden naar versies van Postgres voor Postgres 17. Replicatieslotten en replicatieoorsprongen worden gegenereerd bij het opbouwen van een logische replicatien omgeving. Hiervoor is echter specifiek informatie voor het node nodig om de replicatiesatus, toestand van de toepassing en de WAL-transmissiestatus op te nemen, zodat deze niet bij het upgradeproces worden opgenomen. Zodra de gepubliceerde node is geüpgradeerd moet de gebruiker deze objecten handmatig herbouwen.
Het pg_upgrade proces is in PostgreSQL 17 verbeterd om verwijzingen te krijgen en deze interne objecten opnieuw op te bouwen; deze functionaliteit maakt het mogelijk dat de replicatie automatisch wordt hervat bij het upgraden van een node die logische replicatie gebruikt. Voorheen moest de gebruiker bij een grote versieupgrade logische replicatieslotten verwijderen, waardoor de gegevens na de upgrade opnieuw moesten worden gesynchroniseerd met de abonnees. Dit bracht complexiteit en verlengde de downtime bij upgrades.
Volg deze stappen bij het upgraden van de publisher-cluster:
- Zorg ervoor dat alle abonementen op de publisher tijdelijk uitgeschakeld zijn door de opdracht
ALTER SUBSCRIPTION….DISABLE
uit te voeren. Deze worden weer ingeschakeld nadat de upgrade-procedure is voltooid. - Stel de
wal_level
van het nieuwe cluster in oplogical
. - Het
max_replication_slots
op het nieuwe cluster moet ingesteld zijn op een waarde groter dan of gelijk aan de replicatieslotten op het oude cluster. - De output plugins die door de slots worden gebruikt moeten geïnstalleerd zijn in het nieuwe cluster.
- Alle wijzigingen van het oude cluster zijn al geredigeerd naar het doelcluster voor de upgrade.
- Alle slots op het oude cluster moeten bruikbaar zijn; u kunt dit controleren door conflicterende kolommen in de view pg_replication_slots te controleren. Conflicterend moet false zijn voor alle slots op het oude cluster.
- Er moeten geen slots in het nieuwe cluster een waarde van
false
hebben in de kolomTemporary
van de viewpg_replication_slots
. Er moeten geen permanente logische replicatieslotten zijn in het nieuwe cluster.
Het proces van pg_upgrade
voor het upgraden van replicatieslotten zal een fout geven als een van de bovenstaande vereisten niet is ingevuld.
Conclusie
Met PostgreSQL 17 blijft de community de focus op het verbeteren van de prestaties, schaalbaarheid, beveiliging en enterprise-klaarheid van PostgreSQL. Postgres 17 verbeterd ook de ontwikkelerervaring door nieuwe functionaliteiten voor compatibiliteit toe te voegen en bestaande functionaliteiten sterker en robuuster te maken.
Buiten versie 17 zal PostgreSQL doorgroeien, verbeteren en meer prestaties bieden om aan de behoeften van enterprise-toepassingen te voldoen die schaalbare databases nodig hebben. Schaalbaarheid (zowel horizontaal als verticaal) is over de jaren verbeterd, maar er is zeker ruimte voor verbetering in de horizontale mogelijkheden door sharding functionaliteit toe te voegen aan PostgreSQL. We zullen meer verbeteringen in logische replicatie zien, en er zal nog meer komen inzake DDL-replicatie of replicatie van ontbrekende objecten (zoals sequences) en betere node-beheer. De community erkent ook de noodzaak om PostgreSQL meer compatible te maken, daarom de verbeteringen van de MERGE
-opdracht in Postgres 17, en plannen voor meer compatibiliteitseigenschappen na Postgres 17.
Source:
https://dzone.com/articles/postgresql-17-a-major-step-forward-in-performance