Es tut weh, wenn du das tust, also tu das nicht.
In Oracle werden Cursor als Teil des Programmierens 101 gelehrt. In vielen (wenn nicht den meisten) Fällen sind Cursor das erste, was der Oracle-Entwickler lernt. Die erste Klasse beginnt normalerweise mit:„Es gibt 13 logische Strukturen, von denen die erste die Schleife ist, die so geht …“
PostgreSQL hingegen ist nicht stark auf Cursor angewiesen. Ja, es gibt sie. Es gibt mehrere Syntaxvarianten für ihre Verwendung. Ich werde alle wichtigen Designs irgendwann in dieser Artikelserie behandeln. Aber die erste Lektion in Bezug auf PostgreSQL-Cursor ist, dass es einige (und viel bessere) algorithmische Alternativen zur Verwendung von Cursorn in PostgreSQL gibt. Tatsächlich habe ich in meiner 23-jährigen Karriere bei PostgreSQL nur zweimal die Notwendigkeit gefunden, Cursor zu verwenden. Und eines davon bedauere ich.
Cursors sind eine teure Angewohnheit.
Iterieren ist besser als Schleifen. „Was ist der Unterschied?“, werden Sie vielleicht fragen. Nun, der Unterschied liegt bei O(N) vs. O(N^2). Ok, ich sage das nochmal auf Englisch. Die Komplexität der Verwendung von Cursorn besteht darin, dass sie Datensätze nach dem gleichen Muster durchlaufen wie eine verschachtelte for-Schleife. Jeder zusätzliche Datensatz erhöht die Komplexität der Gesamtheit durch Potenzierung. Das liegt daran, dass jeder zusätzliche Datensatz effektiv eine weitere innerste Schleife erzeugt. Zwei Datensätze sind O(N^2), drei Datensätze sind O(N^3) und so weiter. Es kann kostspielig sein, sich daran zu gewöhnen, Cursor zu verwenden, wenn bessere Algorithmen zur Auswahl stehen.
Sie tun dies ohne irgendeine der Optimierungen, die für untergeordnete Funktionen der Datenbank selbst verfügbar wären. Das heißt, sie können Indizes nicht in nennenswerter Weise verwenden, sich nicht in Unterauswahlen umwandeln, in Joins hochziehen oder parallele Lesevorgänge verwenden. Sie werden auch nicht von zukünftigen Optimierungen profitieren, die die Datenbank zur Verfügung stellt. Ich hoffe, Sie sind ein Großmeister-Codierer, der immer den richtigen Algorithmus erhält und ihn beim ersten Mal perfekt codiert, denn Sie haben gerade einen der wichtigsten Vorteile einer relationalen Datenbank besiegt. Leistung, indem Sie sich auf Best Practices oder zumindest den Code einer anderen Person verlassen.
Jeder ist besser als du. Vielleicht nicht individuell, aber kollektiv mit ziemlicher Sicherheit. Abgesehen von dem deklarativen vs. imperativen Argument ermöglicht das Codieren in einer Sprache, die einmal aus der zugrunde liegenden Funktionsbibliothek entfernt wurde, allen anderen zu versuchen, Ihren Code schneller, besser und effizienter laufen zu lassen, ohne Sie zu konsultieren. Und das ist sehr, sehr gut für dich.
Erstellen wir ein paar Daten zum Spielen.
Wir beginnen mit der Einrichtung einiger Daten, mit denen wir in den nächsten Artikeln spielen können.
Inhalt von cursors.bash:
set -o nounset # Treat unset variables as an error
# This script assumes that you have PostgreSQL running locally,
# that you have a database with the same name as the local user,
# and that you can create all this structure.
# If not, then:
# sudo -iu postgres createuser -s $USER
# createdb
# Clean up from the last run
[[ -f itisPostgreSql.zip ]] && rm itisPostgreSql.zip
subdirs=$(ls -1 itisPostgreSql* | grep : | sed -e 's/://')
for sub in ${subdirs[@]}
do
rm -rf $sub
done
# Get the newest file
wget https://www.itis.gov/downloads/itisPostgreSql.zip
# Unpack it
unzip itisPostgreSql.zip
# This makes a directory with the stupidest f-ing name possible
# itisPostgreSqlDDMMYY
subdir=$(\ls -1 itisPostgreSql* | grep : | sed -e 's/://')
# The script wants to create an "ITIS" database. Let's just make that a schema.
sed -i $subdir/ITIS.sql -e '/"ITIS"/d' # Cut the lines about making the db
sed -i $subdir/ITIS.sql -e '/-- PostgreSQL database dump/s/.*/CREATE SCHEMA IF NOT EXISTS itis;/'
sed -i $subdir/ITIS.sql -e '/SET search_path = public, pg_catalog;/s/.*/SET search_path TO itis;/'
# ok, we have a schema to put the data in, let's do the import.
# timeout if we can't connect, fail on error.
PG_TIMEOUT=5 psql -v "ON_ERROR_STOP=1" -f $subdir/ITIS.sql
Dies gibt uns etwas mehr als 600.000 Datensätze, mit denen wir in der itis.hierarchy-Tabelle spielen können, die eine Taxonomie der natürlichen Welt enthält. Wir verwenden diese Daten, um verschiedene Methoden zum Umgang mit komplexen Dateninteraktionen zu veranschaulichen.
Die erste Alternative.
Mein bevorzugtes Entwurfsmuster für das Durchlaufen von Recordsets bei teuren Operationen ist der Common Table Expression (CTE).
Hier ist ein Beispiel für die Grundform:
WITH RECURSIVE fauna AS (
SELECT tsn, parent_tsn, tsn::text taxonomy
FROM itis.hierarchy
WHERE parent_tsn = 0
UNION ALL
SELECT h1.tsn, h1.parent_tsn, f.taxonomy || '.' || h1.tsn
FROM itis.hierarchy h1
JOIN fauna f
ON h1.parent_tsn = f.tsn
)
SELECT *
FROM fauna
ORDER BY taxonomy;
Was zu den folgenden Ergebnissen führt:
┌─────────┬────────┬──────────────────────────────────────────────────────────┐
│ tsn │ parent │ taxonomy │
│ │ tsn │ │
├─────────┼────────┼──────────────────────────────────────────────────────────┤
│ 202422 │ 0 │202422 │
│ 846491 │ 202422 │202422.846491 │
│ 660046 │ 846491 │202422.846491.660046 │
│ 846497 │ 660046 │202422.846491.660046.846497 │
│ 846508 │ 846497 │202422.846491.660046.846497.846508 │
│ 846553 │ 846508 │202422.846491.660046.846497.846508.846553 │
│ 954935 │ 846553 │202422.846491.660046.846497.846508.846553.954935 │
│ 5549 │ 954935 │202422.846491.660046.846497.846508.846553.954935.5549 │
│ 5550 │ 5549 │202422.846491.660046.846497.846508.846553.954935.5549.5550│
│ 954936 │ 846553 │202422.846491.660046.846497.846508.846553.954936 │
│ 954904 │ 660046 │202422.846491.660046.954904 │
│ 846509 │ 954904 │202422.846491.660046.954904.846509 │
│ 11473 │ 846509 │202422.846491.660046.954904.846509.11473 │
│ 11474 │ 11473 │202422.846491.660046.954904.846509.11473.11474 │
│ 11475 │ 11474 │202422.846491.660046.954904.846509.11473.11474.11475 │
│ ... │ │...snip... │
└─────────┴────────┴──────────────────────────────────────────────────────────┘
(601187 rows)
Diese Abfrage kann leicht geändert werden, um beliebige Berechnungen durchzuführen. Dazu gehören Datenanreicherung, komplexe Funktionen oder alles andere, was das Herz begehrt.
„Aber schau!“, rufst du. „Da steht RECURSIVE
gleich im Namen! Tut es nicht genau das, was Sie gesagt haben, nicht zu tun?“ Nun, eigentlich nein. Unter der Haube verwendet es keine Rekursion im verschachtelten Sinne oder eine Schleife, um die „Rekursion“ durchzuführen. Es ist nur ein lineares Lesen der Tabelle, bis die untergeordnete Abfrage keine neuen Ergebnisse zurückgibt. Und es funktioniert auch mit Indizes.
Sehen wir uns den Ausführungsplan an:
┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ QUERY PLAN │
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Sort (cost=211750.51..211840.16 rows=35858 width=40) │
│ Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy │
│ Sort Key: fauna.taxonomy │
│ CTE fauna │
│ -> Recursive Union (cost=1000.00..208320.69 rows=35858 width=40) │
│ -> Gather (cost=1000.00..15045.02 rows=18 width=40) │
│ Output: hierarchy.tsn, hierarchy.parent_tsn, ((hierarchy.tsn)::text) │
│ Workers Planned: 2 │
│ -> Parallel Seq Scan on itis.hierarchy (cost=0.00..14043.22 rows=8 width=40) │
│ Output: hierarchy.tsn, hierarchy.parent_tsn, (hierarchy.tsn)::text │
│ Filter: (hierarchy.parent_tsn = 0) │
│ -> Hash Join (cost=5.85..19255.85 rows=3584 width=40) │
│ Output: h1.tsn, h1.parent_tsn, ((f.taxonomy || '.'::text) || (h1.tsn)::text) │
│ Hash Cond: (h1.parent_tsn = f.tsn) │
│ -> Seq Scan on itis.hierarchy h1 (cost=0.00..16923.87 rows=601187 width=8) │
│ Output: h1.hierarchy_string, h1.tsn, h1.parent_tsn, h1.level, h1.childrencount │
│ -> Hash (cost=3.60..3.60 rows=180 width=36) │
│ Output: f.taxonomy, f.tsn │
│ -> WorkTable Scan on fauna f (cost=0.00..3.60 rows=180 width=36) │
│ Output: f.taxonomy, f.tsn │
│ -> CTE Scan on fauna (cost=0.00..717.16 rows=35858 width=40) │
│ Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy │
│ JIT: │
│ Functions: 13 │
│ Options: Inlining false, Optimization false, Expressions true, Deforming true │
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
Lassen Sie uns fortfahren und einen Index erstellen und sehen, wie das funktioniert.
CREATE UNIQUE INDEX taxonomy_parents ON itis.hierarchy (parent_tsn, tsn);
┌─────────────────────────────────────────────────────────────────────────────┐
│ QUERY PLAN │
├─────────────────────────────────────────────────────────────────────────────┤
│Sort (cost=135148.13..135237.77 rows=35858 width=40) │
│ Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy │
│ Sort Key: fauna.taxonomy │
│ CTE fauna │
│ -> Recursive Union (cost=4.56..131718.31 rows=35858 width=40) │
│ -> Bitmap Heap Scan on itis.hierarchy (cost=4.56..74.69 rows=18) │
│ Output: hierarchy.tsn, hierarchy.parent_tsn, (hierarchy.tsn) │
│ Recheck Cond: (hierarchy.parent_tsn = 0) │
│ -> Bitmap Index Scan on taxonomy_parents │
│ (cost=0.00..4.56 rows=18) │
│ Index Cond: (hierarchy.parent_tsn = 0) │
│ -> Nested Loop (cost=0.42..13092.65 rows=3584 width=40) │
│ Output: h1.tsn, h1.parent_tsn,((f.taxonomy || '.')||(h1.tsn))│
│ -> WorkTable Scan on fauna f (cost=0.00..3.60 rows=180) │
│ Output: f.tsn, f.parent_tsn, f.taxonomy │
│ -> Index Only Scan using taxonomy_parents on itis.hierarchy │
│ h1 (cost=0.42..72.32 rows=20 width=8) │
│ Output: h1.parent_tsn, h1.tsn │
│ Index Cond: (h1.parent_tsn = f.tsn) │
│ -> CTE Scan on fauna (cost=0.00..717.16 rows=35858 width=40) │
│ Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy │
│JIT: │
│ Functions: 6 │
└─────────────────────────────────────────────────────────────────────────────┘
Nun, das war befriedigend, nicht wahr? Und es wäre unerschwinglich schwer gewesen, einen Index in Kombination mit einem Cursor zu erstellen, um die gleiche Arbeit zu erledigen. Diese Struktur bringt uns weit genug, um in der Lage zu sein, eine ziemlich komplexe Baumstruktur zu durchlaufen und sie für einfache Suchen zu verwenden.
In der nächsten Ausgabe werden wir über eine andere Methode sprechen, um das gleiche Ergebnis noch schneller zu erzielen. In unserem nächsten Artikel werden wir über die Erweiterung ltree sprechen und wie man hierarchische Daten erstaunlich schnell betrachten kann. Bleiben Sie dran.