Eine der bekanntesten Schwächen von PostgreSQL war lange Zeit die Möglichkeit, Abfragen zu parallelisieren. Mit der Veröffentlichung von Version 9.6 wird dies kein Problem mehr sein. Zu diesem Thema wurde großartige Arbeit geleistet, beginnend mit dem Commit 80558c1, der Einführung des parallelen sequentiellen Scans, das wir im Laufe dieses Artikels sehen werden.
Zunächst müssen Sie beachten:Die Entwicklung dieser Funktion war kontinuierlich und einige Parameter haben ihre Namen zwischen einem Commit und einem anderen geändert. Dieser Artikel wurde mit einem Checkout am 17. Juni geschrieben und einige hier dargestellte Funktionen sind nur in Version 9.6 beta2 vorhanden.
Im Vergleich zur Version 9.5 wurden neue Parameter in die Konfigurationsdatei eingeführt. Diese sind:
- max_parallel_workers_per_gather :die Anzahl der Worker, die einen sequentiellen Scan einer Tabelle unterstützen können;
- min_parallel_relation_size :die Mindestgröße, die eine Beziehung haben muss, damit der Planer den Einsatz zusätzlicher Arbeitskräfte in Betracht ziehen kann;
- parallel_setup_cost :der Planer-Parameter, der die Kosten für die Instanziierung eines Workers schätzt;
- parallel_tuple_cost :der Planer-Parameter, der die Kosten für die Übertragung eines Tupels von einem Worker zu einem anderen schätzt;
- force_parallel_mode :Parameter, der zum Testen nützlich ist, starke Parallelität und auch eine Abfrage, in der der Planer auf andere Weise arbeiten würde.
Mal sehen, wie die zusätzlichen Worker verwendet werden können, um unsere Abfragen zu beschleunigen. Wir erstellen eine Testtabelle mit einem INT-Feld und hundert Millionen Datensätzen:
postgres=# CREATE TABLE test (i int);
CREATE TABLE
postgres=# INSERT INTO test SELECT generate_series(1,100000000);
INSERT 0 100000000
postgres=# ANALYSE test;
ANALYZE
PostgreSQL hat max_parallel_workers_per_gather
standardmäßig auf 2 gesetzt, wofür zwei Worker während eines sequentiellen Scans aktiviert werden.
Ein einfacher sequentieller Scan bringt keine Neuerungen:
postgres=# EXPLAIN ANALYSE SELECT * FROM test;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
Seq Scan on test (cost=0.00..1442478.32 rows=100000032 width=4) (actual time=0.081..21051.918 rows=100000000 loops=1)
Planning time: 0.077 ms
Execution time: 28055.993 ms
(3 rows)
Tatsächlich das Vorhandensein eines WHERE
-Klausel ist für die Parallelisierung erforderlich:
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i=1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..964311.60 rows=1 width=4) (actual time=3.381..9799.942 rows=1 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on test (cost=0.00..963311.50 rows=0 width=4) (actual time=6525.595..9791.066 rows=0 loops=3)
Filter: (i = 1)
Rows Removed by Filter: 33333333
Planning time: 0.130 ms
Execution time: 9804.484 ms
(8 rows)
Wir können zur vorherigen Aktion zurückkehren und die Unterschiede bei der Einstellung max_parallel_workers_per_gather
beobachten bis 0:
postgres=# SET max_parallel_workers_per_gather TO 0;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i=1;
QUERY PLAN
--------------------------------------------------------------------------------------------------------
Seq Scan on test (cost=0.00..1692478.40 rows=1 width=4) (actual time=0.123..25003.221 rows=1 loops=1)
Filter: (i = 1)
Rows Removed by Filter: 99999999
Planning time: 0.105 ms
Execution time: 25003.263 ms
(5 rows)
Eine 2,5-mal größere Zeit.
Nicht immer hält der Planer einen parallel sequentiellen Scan für die beste Option. Wenn eine Abfrage nicht selektiv genug ist und viele Tupel von Worker zu Worker übertragen werden müssen, bevorzugt sie möglicherweise einen „klassischen“ sequentiellen Scan:
postgres=# SET max_parallel_workers_per_gather TO 2;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i<90000000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
Seq Scan on test (cost=0.00..1692478.40 rows=90116088 width=4) (actual time=0.073..31410.276 rows=89999999 loops=1)
Filter: (i < 90000000)
Rows Removed by Filter: 10000001
Planning time: 0.133 ms
Execution time: 37939.401 ms
(5 rows)
Tatsächlich erhalten wir ein schlechteres Ergebnis, wenn wir versuchen, einen parallelen sequentiellen Scan zu erzwingen:
postgres=# SET parallel_tuple_cost TO 0;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i<90000000;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..964311.50 rows=90116088 width=4) (actual time=0.454..75546.078 rows=89999999 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on test (cost=0.00..1338795.20 rows=37548370 width=4) (actual time=0.088..20294.670 rows=30000000 loops=3)
Filter: (i < 90000000)
Rows Removed by Filter: 3333334
Planning time: 0.128 ms
Execution time: 83423.577 ms
(8 rows)
Die Anzahl der Worker kann bis auf max_worker_processes
erhöht werden (Standard:8). Wir stellen den Wert von parallel_tuple_cost
wieder her und wir sehen, was passiert, wenn wir max_parallel_workers_per_gather
erhöhen bis 8.
postgres=# SET parallel_tuple_cost TO DEFAULT ;
SET
postgres=# SET max_parallel_workers_per_gather TO 8;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i=1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..651811.50 rows=1 width=4) (actual time=3.684..8248.307 rows=1 loops=1)
Workers Planned: 6
Workers Launched: 6
-> Parallel Seq Scan on test (cost=0.00..650811.40 rows=0 width=4) (actual time=7053.761..8231.174 rows=0 loops=7)
Filter: (i = 1)
Rows Removed by Filter: 14285714
Planning time: 0.124 ms
Execution time: 8250.461 ms
(8 rows)
Obwohl PostgreSQL bis zu 8 Worker verwenden könnte, hat es nur sechs instanziiert. Dies liegt daran, dass Postgres die Anzahl der Worker auch nach der Größe der Tabelle und der min_parallel_relation_size
optimiert . Die Anzahl der von Postgres bereitgestellten Worker basiert auf einer geometrischen Progression mit 3 als gemeinsamem Verhältnis 3 und min_parallel_relation_size
als Skalierungsfaktor. Hier ist ein Beispiel. Unter Berücksichtigung der 8 MB des Standardparameters:
Größe | Arbeiter |
---|---|
<8MB | 0 |
<24 MB | 1 |
<72 MB | 2 |
<216 MB | 3 |
<648 MB | 4 |
<1944 MB | 5 |
<5822MB | 6 |
… | … |
Unsere Tabellengröße beträgt 3458 MB, also ist 6 die maximale Anzahl verfügbarer Worker.
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+----------+---------+-------------
public | test | table | postgres | 3458 MB |
(1 row)
Abschließend werde ich eine kurze Demonstration der Verbesserungen geben, die durch diesen Patch erreicht wurden. Wenn wir unsere Abfrage mit einer wachsenden Zahl wachsender Worker ausführen, erhalten wir die folgenden Ergebnisse:
Arbeiter | Zeit |
---|---|
0 | 24767,848 ms |
1 | 14855,961 ms |
2 | 10415,661 ms |
3 | 8041,187 ms |
4 | 8090,855 ms |
5 | 8082,937 ms |
6 | 8061,939 ms |
Wir können sehen, dass sich die Zeiten dramatisch verbessern, bis Sie ein Drittel des Ausgangswerts erreichen. Es ist auch einfach zu erklären, dass wir keine Verbesserungen zwischen der Verwendung von 3 und 6 Workern sehen:Die Maschine, auf der der Test ausgeführt wurde, hat 4 CPUs, sodass die Ergebnisse stabil sind, nachdem dem ursprünglichen Prozess 3 weitere Worker hinzugefügt wurden .
Schließlich hat PostgreSQL 9.6 die Voraussetzungen für die Abfrageparallelisierung geschaffen, bei der der parallele sequentielle Scan nur das erste großartige Ergebnis ist. Wir werden auch sehen, dass in 9.6 Aggregationen parallelisiert wurden, aber das sind Informationen für einen anderen Artikel, der in den kommenden Wochen veröffentlicht wird!