Ich muss die Tiefe eines Nachkommens von seinem Vorfahren berechnen. Wenn ein Datensatz vorhanden ist object_id = parent_id = ancestor_id
, wird er als Stammknoten (der Vorfahr) betrachtet. Ich habe versucht, eine WITH RECURSIVE
Abfrage mit PostgreSQL 9.4 zum Laufen zu bringen .
Ich kontrolliere weder die Daten noch die Spalten. Das Daten- und Tabellenschema stammt aus einer externen Quelle. Der Tisch wächst stetig . Momentan um ca. 30.000 Datensätze pro Tag. Jeder Knoten in der Baumstruktur kann fehlen und wird irgendwann von einer externen Quelle abgerufen. Sie werden normalerweise der created_at DESC
Reihe nach abgerufen , aber die Daten werden mit asynchronen Hintergrundjobs abgerufen.
Wir hatten ursprünglich eine Codelösung für dieses Problem, aber jetzt, da wir über 5 Millionen Zeilen verfügen, dauert die Ausführung fast 30 Minuten.
Beispiel Tabellendefinition und Testdaten:
CREATE TABLE objects (
id serial NOT NULL PRIMARY KEY,
customer_id integer NOT NULL,
object_id integer NOT NULL,
parent_id integer,
ancestor_id integer,
generation integer NOT NULL DEFAULT 0
);
INSERT INTO objects(id, customer_id , object_id, parent_id, ancestor_id, generation)
VALUES (2, 1, 2, 1, 1, -1), --no parent yet
(3, 2, 3, 3, 3, -1), --root node
(4, 2, 4, 3, 3, -1), --depth 1
(5, 2, 5, 4, 3, -1), --depth 2
(6, 2, 6, 5, 3, -1), --depth 3
(7, 1, 7, 7, 7, -1), --root node
(8, 1, 8, 7, 7, -1), --depth 1
(9, 1, 9, 8, 7, -1); --depth 2
Beachten Sie, dass dies object_id
nicht eindeutig ist, die Kombination (customer_id, object_id)
jedoch eindeutig ist.
Ausführen einer Abfrage wie folgt:
WITH RECURSIVE descendants(id, customer_id, object_id, parent_id, ancestor_id, depth) AS (
SELECT id, customer_id, object_id, parent_id, ancestor_id, 0
FROM objects
WHERE object_id = parent_id
UNION
SELECT o.id, o.customer_id, o.object_id, o.parent_id, o.ancestor_id, d.depth + 1
FROM objects o
INNER JOIN descendants d ON d.parent_id = o.object_id
WHERE
d.id <> o.id
AND
d.customer_id = o.customer_id
) SELECT * FROM descendants d;
Ich möchte, dass die generation
Spalte als berechnete Tiefe festgelegt wird. Wenn ein neuer Datensatz hinzugefügt wird, wird die Generierungsspalte auf -1 gesetzt. Es gibt einige Fälle, in denen ein parent_id
möglicherweise noch nicht gezogen wurde. Wenn das parent_id
nicht vorhanden ist, sollte die Generierungsspalte auf -1 gesetzt bleiben.
Die endgültigen Daten sollten wie folgt aussehen:
id | customer_id | object_id | parent_id | ancestor_id | generation
2 1 2 1 1 -1
3 2 3 3 3 0
4 2 4 3 3 1
5 2 5 4 3 2
6 2 6 5 3 3
7 1 7 7 7 0
8 1 8 7 7 1
9 1 9 8 7 2
Das Ergebnis der Abfrage sollte sein, die Generierungsspalte auf die richtige Tiefe zu aktualisieren.
Ich begann mit den Antworten auf diese verwandte Frage zu SO .
quelle
update
mit dem Ergebnis Ihres rekursiven CTE zur Tabelle?ancestor_id
Ist das also schon eingestellt, braucht man nur die Generation von der CTE.depth zuzuordnen?Antworten:
Die Abfrage, die Sie haben, ist im Grunde richtig. Der einzige Fehler liegt im zweiten (rekursiven) Teil des CTE, in dem Sie Folgendes haben:
Es sollte umgekehrt sein:
Sie möchten die Objekte mit ihren Eltern (die bereits gefunden wurden) verbinden.
So kann die Abfrage, die die Tiefe berechnet, geschrieben werden (nichts anderes geändert, nur die Formatierung):
Für das Update ersetzen Sie einfach den letzten
SELECT
mit demUPDATE
, der das Ergebnis des CTE verbindet, zurück in die Tabelle:Getestet mit SQLfiddle
Zusätzliche Kommentare:
ancestor_id
und das müssenparent_id
nicht in der Auswahlliste enthalten sein (Vorfahr ist offensichtlich, Eltern ein bisschen schwierig herauszufinden, warum), damit Sie sie in derSELECT
Abfrage behalten können, wenn Sie möchten, aber Sie können sie sicher aus der Liste entfernenUPDATE
.(customer_id, object_id)
scheint ein Kandidat für eineUNIQUE
Einschränkung zu sein. Wenn Ihre Daten dies erfüllen, fügen Sie eine solche Einschränkung hinzu. Die im rekursiven CTE durchgeführten Verknüpfungen wären nicht sinnvoll, wenn sie nicht eindeutig wären (ein Knoten könnte andernfalls zwei Eltern haben).(customer_id, parent_id)
ein Kandidat für eineFOREIGN KEY
Einschränkung,REFERENCES
die der (eindeutige) ist(customer_id, object_id)
. Sie möchten diese FK-Einschränkung höchstwahrscheinlich nicht hinzufügen, da Sie Ihrer Beschreibung nach neue Zeilen hinzufügen und einige Zeilen auf andere verweisen können, die noch nicht hinzugefügt wurden.Das
AND o.generation = -1
abschließende Update stellt sicher, dass die Zeilen, die im ersten Durchgang aktualisiert wurden, nicht erneut aktualisiert werden, der CTE jedoch immer noch ein teurer Teil ist.Mit dem folgenden Befehl wird versucht, diese Probleme zu beheben: Verbessern Sie den CTE, um so wenig Zeilen wie möglich zu berücksichtigen, und verwenden Sie ihn
(customer_id, obejct_id)
statt(id)
zum Identifizieren von Zeilen (wird alsoid
vollständig aus der Abfrage entfernt. Er kann als erste Aktualisierung oder als nachfolgende verwendet werden:Beachten Sie, wie der CTE 3 Teile hat. Die ersten beiden sind die stabilen Teile. Im ersten Teil werden die Stammknoten gesucht, die noch nicht aktualisiert wurden, und
generation=-1
müssen daher neu hinzugefügt werden. Der 2. Teil findet Kinder (mitgeneration=-1
) von Elternknoten, die zuvor aktualisiert wurden.Der dritte, rekursive Teil findet wie bisher alle Nachkommen der ersten beiden Teile.
Getestet mit SQLfiddle-2
quelle
@ypercube bietet bereits ausführliche Erklärungen, daher komme ich auf den Punkt , was ich hinzufügen muss.
Ich nehme an, dies soll rekursiv anzuwenden, dh der Rest des Baumes immer hat
generation = -1
nach jedem fehlenden Knoten.Wenn ein Knoten im Baum (noch) fehlen kann, müssen wir Zeilen finden, bei
generation = -1
denenes sich um Stammknoten handelt
oder bei denen es sich um einen übergeordneten Knoten handelt
generation > -1
.Und von dort den Baum durchqueren. Untergeordnete Knoten dieser Auswahl müssen ebenfalls enthalten sein
generation = -1
.Nehmen Sie das
generation
von dem übergeordneten Knoten, das um eins erhöht wurde, oder fallen Sie für Stammknoten auf 0 zurück:Der nicht-rekursive Teil ist auf
SELECT
diese Weise ein einzelner , entspricht jedoch logischerweise den beiden union'ed von @ ypercubeSELECT
. Nicht sicher, welche schneller ist, müssen Sie testen.Der wesentlich wichtigere Punkt für die Leistung ist:
Index!
Wenn Sie einer großen Tabelle auf diese Weise wiederholt Zeilen hinzufügen, fügen Sie einen Teilindex hinzu :
Dies führt zu mehr Leistung als alle anderen bisher diskutierten Verbesserungen - für wiederholte kleine Ergänzungen eines großen Tisches.
Ich habe die Indexbedingung zum rekursiven Teil des CTE hinzugefügt (obwohl logisch redundant), damit der Abfrageplaner versteht, dass der Teilindex anwendbar ist.
Außerdem sollten Sie wahrscheinlich auch die bereits erwähnte
UNIQUE
Einschränkung für(object_id, customer_id)
@ypercube haben. Oder, wenn Sie aus irgendeinem Grund keine Eindeutigkeit festlegen können (warum?), Fügen Sie stattdessen einen einfachen Index hinzu. Die Reihenfolge der Indexspalten ist wichtig, übrigens:quelle
ON objects (customer_id, parent_id, object_id) WHERE generation = -1;
und vielleicht noch einerON objects (customer_id, object_id) WHERE generation > -1;
. Das Update muss auch alle aktualisierten Zeilen von einem Index auf einen anderen "umschalten". Sie sind sich also nicht sicher, ob dies für den ersten Start von UPDATE eine gute Idee ist.