Könnte jemand bizarres Verhalten bei der Ausführung von Millionen von UPDATES erklären?

8

Könnte mir jemand dieses Verhalten erklären? Ich habe die folgende Abfrage unter Postgres 9.3 ausgeführt, das nativ unter OS X ausgeführt wird. Ich habe versucht, ein Verhalten zu simulieren, bei dem die Indexgröße viel größer als die Tabellengröße werden kann, und stattdessen etwas noch Seltsameres gefunden.

CREATE TABLE test(id int);
CREATE INDEX test_idx ON test(id);

CREATE FUNCTION test_index(batch_size integer, total_batches integer) RETURNS void AS $$
DECLARE
  current_id integer := 1;
BEGIN
FOR i IN 1..total_batches LOOP
  INSERT INTO test VALUES (current_id);
  FOR j IN 1..batch_size LOOP
    UPDATE test SET id = current_id + 1 WHERE id = current_id;
    current_id := current_id + 1;
  END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT test_index(500, 10000);

Ich ließ dies ungefähr eine Stunde lang auf meinem lokalen Computer laufen, bevor ich von OS X Warnungen zu Festplattenproblemen erhielt. Ich bemerkte, dass Postgres ungefähr 10 MB / s von meiner lokalen Festplatte aufsaugte und dass die Postgres-Datenbank eine Gesamtsumme verbrauchte von 30 GB von meinem Computer. Am Ende habe ich die Abfrage abgebrochen. Unabhängig davon hat Postgres den Speicherplatz nicht an mich zurückgegeben, und ich habe die Datenbank nach Nutzungsstatistiken mit dem folgenden Ergebnis abgefragt:

test=# SELECT nspname || '.' || relname AS "relation",
    pg_size_pretty(pg_relation_size(C.oid)) AS "size"
  FROM pg_class C
  LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
  WHERE nspname NOT IN ('pg_catalog', 'information_schema')
  ORDER BY pg_relation_size(C.oid) DESC
  LIMIT 20;

           relation            |    size
-------------------------------+------------
 public.test                   | 17 GB
 public.test_idx               | 14 GB

Die Auswahl aus der Tabelle ergab jedoch keine Ergebnisse.

test=# select * from test limit 1;
 id
----
(0 rows)

Das Ausführen von 10000 500er-Batches umfasst 5.000.000 Zeilen, was eine ziemlich kleine Tabellen- / Indexgröße (auf der MB-Skala) ergeben sollte. Ich vermute, dass Postgres für jedes INSERT / UPDATE, das mit der Funktion geschieht, eine neue Version der Tabelle / des Index erstellt, aber das scheint seltsam. Die gesamte Funktion wird transaktional ausgeführt, und die Tabelle war zum Starten leer.

Irgendwelche Gedanken darüber, warum ich dieses Verhalten sehe?

Die beiden Fragen, die ich habe, lauten insbesondere: Warum wurde dieser Speicherplatz noch nicht von der Datenbank zurückgefordert, und die zweite Frage ist, warum die Datenbank überhaupt so viel Speicherplatz benötigt hat. 30 GB scheinen viel zu sein, selbst wenn MVCC berücksichtigt wird

Nikhil N.
quelle

Antworten:

7

Kurzfassung

Ihr Algorithmus sieht auf den ersten Blick nach O (n * m) aus, vergrößert jedoch effektiv O (n * m ^ 2), da alle Zeilen dieselbe ID haben. Anstelle von 5 Millionen Zeilen erhalten Sie> 1,25 G Zeilen

Lange Version

Ihre Funktion befindet sich in einer impliziten Transaktion. Aus diesem Grund werden nach dem Abbrechen Ihrer Abfrage keine Daten angezeigt. Außerdem müssen für beide Schleifen unterschiedliche Versionen der aktualisierten / eingefügten Tupel verwaltet werden.

Außerdem vermute ich, dass Sie einen Fehler in Ihrer Logik haben oder die Anzahl der vorgenommenen Aktualisierungen unterschätzen.

Erste Iteration der äußeren Schleife - current_id beginnt bei 1, fügt 1 Zeile ein, dann führt die innere Schleife 10000 Mal eine Aktualisierung für dieselbe Zeile durch und schließt mit der einzigen Zeile ab, die eine ID von 10001 und current_id mit einem Wert von 10001 zeigt. 10001 Versionen der Zeile bleiben weiterhin erhalten, da die Transaktion nicht abgeschlossen ist.

Zweite Iteration der äußeren Schleife - da current_id 10001 ist, wird eine neue Zeile mit der ID 10001 eingefügt. Jetzt haben Sie 2 Zeilen mit derselben "ID" und insgesamt 10003 Versionen beider Zeilen (10002 der ersten, 1 von der zweite). Dann aktualisiert die innere Schleife 10000 Mal BEIDE Zeilen, erstellt 20000 neue Versionen und erreicht bisher 30003 Tupel ...

Dritte Iteration der äußeren Schleife: Die aktuelle ID ist 20001, eine neue Zeile wird mit der ID 20001 eingefügt. Sie haben 3 Zeilen, alle mit derselben "ID" 20001, 30006 Zeilen- / Tupelversionen. Dann führen Sie 10000 Aktualisierungen von 3 Zeilen durch und erstellen 30000 neue Versionen, jetzt 60006 ...

...

(Wenn Ihr Speicherplatz dies erlaubt hätte) - 500. Iteration der äußeren Schleife, erstellt nur in dieser Iteration 5 Millionen Aktualisierungen von 500 Zeilen

Wie Sie sehen, haben Sie anstelle Ihrer erwarteten 5 Millionen Updates 1000 + 2000 + 3000 + ... + 4990000 + 5000000 Updates (plus Änderung) erhalten, was 10000 * (1 + 2 + 3 + ... + 499+) wäre 500), über 1,25 G Updates. Und natürlich hat eine Zeile nicht nur die Größe Ihres Int, sondern benötigt auch eine zusätzliche Struktur, sodass Ihre Tabelle und Ihr Index eine Größe von mehr als zehn Gigabyte haben.

Verwandte Fragen und Antworten:

Bruno Guardia
quelle
5

PostgreSQL gibt den Speicherplatz erst nach VACUUM FULL, nicht nach einem DELETEoder ROLLBACK(aufgrund einer Stornierung) zurück.

Die Standardform von VACUUM entfernt tote Zeilenversionen in Tabellen und Indizes und markiert den für die zukünftige Wiederverwendung verfügbaren Speicherplatz. Der Speicherplatz wird jedoch nicht an das Betriebssystem zurückgegeben, außer in dem speziellen Fall, in dem eine oder mehrere Seiten am Ende einer Tabelle vollständig frei werden und eine exklusive Tabellensperre leicht erhalten werden kann. Im Gegensatz dazu komprimiert VACUUM FULL Tabellen aktiv, indem eine vollständig neue Version der Tabellendatei ohne Totraum geschrieben wird. Dies minimiert die Größe der Tabelle, kann jedoch lange dauern. Außerdem wird zusätzlicher Speicherplatz für die neue Kopie der Tabelle benötigt, bis der Vorgang abgeschlossen ist.

Als Randnotiz scheint Ihre gesamte Funktion fraglich. Ich bin nicht sicher, was Sie testen möchten, aber wenn Sie Daten erstellen möchten, können Sie verwendengenerate_series

INSERT INTO test
SELECT x FROM generate_series(1, batch_size*total_batches) AS t(x);
Evan Carroll
quelle
Cool, das erklärt, warum die Tabelle immer noch als so datenintensiv markiert war, aber warum brauchte sie überhaupt diesen ganzen Speicherplatz? Nach meinem Verständnis von MVCC müssen unterschiedliche Versionen der aktualisierten / eingefügten Tupel für die Transaktion verwaltet werden, es sollten jedoch keine separaten Versionen für jede Iteration der Schleife verwaltet werden.
Nikhil N
1
Jede Iteration der Schleife erzeugt neue Tupel.
Evan Carroll
2
Richtig, aber ich habe den Eindruck, dass das MVCC nicht für alle Tupel, die es im Verlauf der Transaktion geändert hat, Tupel erstellen sollte. Das heißt, wenn das erste INSERT ausgeführt wird, erstellt Postgres ein einzelnes Tupel und fügt für jedes UPDATE ein einzelnes neues Tupel hinzu. Da die UPDATES 500 Mal für jede Zeile ausgeführt werden und 10000 INSERTs vorhanden sind, beträgt dies 500 * 10000 Zeilen = 5 Millionen Tupel zum Zeitpunkt des Festschreibens der Transaktion. Dies ist nur eine Schätzung, aber unabhängig von 5 MB * sagen wir 50 Bytes, um jedes Tupel zu verfolgen ~ = 250 MB, was VIEL weniger als 30 GB ist. Wo kommt das alles her?
Nikhil N
Außerdem versuche ich, das Verhalten eines Index zu testen, wenn die indizierten Felder viele Male aktualisiert werden, jedoch auf monotisch zunehmende Weise, wodurch sich ein sehr spärlicher Index ergibt, der jedoch immer an die Festplatte angehängt wird.
Nikhil N
Ich bin verwirrt darüber, was du denkst. Denken Sie, dass eine Zeile, die 18e Mal in einer Schleife aktualisiert wurde, ein Tupel oder 1e8 Tupel ist?
Evan Carroll
3

Die tatsächlichen Zahlen nach der Analyse der Funktion sind viel größer, da alle Zeilen der Tabelle denselben Wert erhalten, der in jeder Iteration mehrmals aktualisiert wird.

Wenn wir es mit Parametern ausführen nund m:

SELECT test_index(n, m);

Es gibt mZeileneinfügungen und n * (m^2 + m) / 2Aktualisierungen. Für n = 500und m = 10000muss Postgres also nur 10K-Zeilen einfügen, aber ~ 25G (25 Milliarden) Tupel-Updates durchführen.

Wenn man bedenkt, dass eine Zeile in Postgres einen Overhead von 24 Bytes hat, benötigt eine Tabelle mit nur einer einzelnen intSpalte 28 Bytes pro Zeile plus den Overhead der Seite. Für den Abschluss des Vorgangs benötigen wir also etwa 700 GB plus Speicherplatz für den Index (dies wären auch einige hundert GB).


Testen

Um die Theorie zu testen, haben wir eine weitere Tabelle test_testmit einer einzelnen Zeile erstellt.

CREATE TABLE test_test (i int not null) ;
INSERT INTO test_test (i) VALUES (0);

Dann fügen wir einen Trigger hinzu, testdamit bei jedem Update der Zähler um 1 erhöht wird (Code weggelassen). Dann führen wir die Funktion mit kleineren Werten aus n = 50und m = 100.

Unsere Theorie sagt voraus :

  • 100 Zeileneinsätze,
  • 250K-Tupel-Updates (252500 = 50 * 100 * 101/2)
  • mindestens 7 MB für die Tabelle auf der Festplatte
  • (+ Platz für den Index)

Test 1 (Originaltabelle testmit Index)

    SELECT test_index(50, 100) ;

Nach Abschluss überprüfen wir den Tabelleninhalt:

x=# SELECT COUNT(*) FROM test ;
 count 
-------
   100
(1 row)

x=# SELECT i FROM test_test ;
   i    
--------
 252500
(1 row)

Und Festplattennutzung (Abfrage unter Indexgröße / Nutzungsstatistik in der Indexpflege ):

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
test      | test_idx  |      100 | 8944 kB    | 5440 kB    | N      |           10001 |      505003
test_test |           |        1 | 8944 kB    |            | N      |                 |            

Die testTabelle hat fast 9 MB für die Tabelle und 5 MB für den Index verwendet. Beachten Sie, dass die test_testTabelle weitere 9 MB verwendet hat! Dies wird erwartet, da auch 250.000 Aktualisierungen durchgeführt wurden (unser zweiter Trigger hat die einzelne Zeile von test_testfür jede Aktualisierung einer Zeile in aktualisiert test.)

Beachten Sie auch die Anzahl der Scans in der Tabelle test(10 KB) und die gelesenen Tupel (500 KB).

Test 2 ( testTabelle ohne Index)

Genau das gleiche wie oben, außer dass die Tabelle keinen Index hat.

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
 test        |        |      100 | 8944 kB    |            | N      |                 |            
 test_test   |        |        1 | 8944 kB    |            | N      |                 |            

Wir erhalten die gleiche Größe für die Festplattennutzung der Tabelle und natürlich keine Festplattennutzung für Indizes. Die Anzahl der Scans in der Tabelle testist jedoch Null und die Tupel lesen ebenfalls.

Test 3 (mit niedrigerem Füllfaktor)

Versucht mit Füllfaktor 50 und dem niedrigstmöglichen, 10. Überhaupt keine Verbesserung. Die Datenträgernutzung war fast identisch mit den vorherigen Tests (bei denen der Standardfüllfaktor 100 Prozent verwendet wurde).

ypercubeᵀᴹ
quelle