En nu komen we bij het tweede artikel in onze migratie van Oracle naar PostgreSQL-serie. Deze keer kijken we naar de START WITH/CONNECT BY
construeren.
In Oracle, START WITH/CONNECT BY
wordt gebruikt om een enkelvoudig gekoppelde lijststructuur te creëren die begint bij een bepaalde schildwachtrij. De gekoppelde lijst kan de vorm hebben van een boom en heeft geen balanceringsvereiste.
Laten we ter illustratie beginnen met een query en aannemen dat de tabel 5 rijen bevat.
SELECT * FROM person;
last_name | first_name | id | parent_id
------------+------------+----+-----------
Dunstan | Andrew | 1 | (null)
Roybal | Kirk | 2 | 1
Riggs | Simon | 3 | 1
Eisentraut | Peter | 4 | 1
Thomas | Shaun | 5 | 3
(5 rows)
Hier is de hiërarchische query van de tabel met behulp van de Oracle-syntaxis.
select id, parent_id
from person
start with parent_id IS NULL
connect by prior id = parent_id;
id | parent_id
----+-----------
1 | (null)
4 | 1
3 | 1
2 | 1
5 | 3
En hier is het weer met PostgreSQL.
WITH RECURSIVE a AS (
SELECT id, parent_id
FROM person
WHERE parent_id IS NULL
UNION ALL
SELECT d.id, d.parent_id
FROM person d
JOIN a ON a.id = d.parent_id )
SELECT id, parent_id FROM a;
id | parent_id
----+-----------
1 | (null)
4 | 1
3 | 1
2 | 1
5 | 3
(5 rows)
Deze query maakt gebruik van veel PostgreSQL-functies, dus laten we er langzaam doorheen gaan.
WITH RECURSIVE
Dit is een "Common Table Expression" (CTE). Het definieert een reeks query's die in dezelfde instructie worden uitgevoerd, niet alleen in dezelfde transactie. U kunt een willekeurig aantal uitdrukkingen tussen haakjes en een slotverklaring hebben. Voor dit gebruik hebben we er maar één nodig. Door die verklaring als RECURSIVE
. te verklaren , het wordt iteratief uitgevoerd totdat er geen rijen meer worden geretourneerd.
SELECT
UNION ALL
SELECT
Dit is een voorgeschreven zin voor een recursieve zoekopdracht. Het wordt in de documentatie gedefinieerd als de methode om het startpunt en het recursie-algoritme te onderscheiden. In Oracle-termen kun je ze zien als de START WITH-clausule die is samengevoegd met de CONNECT BY-clausule.
JOIN a ON a.id = d.parent_id
Dit is een self-join bij de CTE-instructie die de vorige rijgegevens levert aan de volgende iteratie.
Laten we een iteratie-indicator aan de query toevoegen om te illustreren hoe dit werkt.
WITH RECURSIVE a AS (
SELECT id, parent_id, 1::integer recursion_level
FROM person
WHERE parent_id IS NULL
UNION ALL
SELECT d.id, d.parent_id, a.recursion_level +1
FROM person d
JOIN a ON a.id = d.parent_id )
SELECT * FROM a;
id | parent_id | recursion_level
----+-----------+-----------------
1 | (null) | 1
4 | 1 | 2
3 | 1 | 2
2 | 1 | 2
5 | 3 | 3
(5 rows)
We initialiseren de recursieniveau-indicator met een waarde. Merk op dat in de rijen die worden geretourneerd, het eerste recursieniveau slechts één keer voorkomt. Dat komt omdat de eerste clausule maar één keer wordt uitgevoerd.
De tweede clausule is waar de iteratieve magie plaatsvindt. Hier hebben we de zichtbaarheid van de vorige rijgegevens, samen met de huidige rijgegevens. Dat stelt ons in staat om de recursieve berekeningen uit te voeren.
Simon Riggs heeft een heel mooie video over hoe je deze functie kunt gebruiken voor het ontwerpen van grafische databases. Het is zeer informatief en u zou eens een kijkje moeten nemen.
Het is u misschien opgevallen dat deze zoekopdracht kan leiden tot een circulaire voorwaarde. Dat is juist. Het is aan de ontwikkelaar om een beperkende clausule toe te voegen aan de tweede query om deze eindeloze recursie te voorkomen. Bijvoorbeeld slechts 4 niveaus diep terugkerend voordat je het gewoon opgeeft.
WITH RECURSIVE a AS (
SELECT id, parent_id, 1::integer recursion_level --<-- initialize it here
FROM person
WHERE parent_id IS NULL
UNION ALL
SELECT d.id, d.parent_id, a.recursion_level +1 --<-- iteration increment
FROM person d
JOIN a ON a.id = d.parent_id
WHERE d.recursion_level <= 4 --<-- bail out here
) SELECT * FROM a;
De kolomnamen en gegevenstypen worden bepaald door de eerste clausule. Merk op dat het voorbeeld een casting-operator gebruikt voor het recursieniveau. In een zeer diepe grafiek kan dit gegevenstype ook worden gedefinieerd als 1::bigint recursion_level
.
Deze grafiek is heel gemakkelijk te visualiseren met een klein shellscript en het hulpprogramma graphviz.
#!/bin/bash -
#===============================================================================
#
# FILE: pggraph
#
# USAGE: ./pggraph
#
# DESCRIPTION:
#
# OPTIONS: ---
# REQUIREMENTS: ---
# BUGS: ---
# NOTES: ---
# AUTHOR: Kirk Roybal (), [email protected]
# ORGANIZATION:
# CREATED: 04/21/2020 14:09
# REVISION: ---
#===============================================================================
set -o nounset # Treat unset variables as an error
dbhost=localhost
dbport=5432
dbuser=$USER
dbname=$USER
ScriptVersion="1.0"
output=$(basename $0).dot
#=== FUNCTION ================================================================
# NAME: usage
# DESCRIPTION: Display usage information.
#===============================================================================
function usage ()
{
cat <<- EOT
Usage : ${0##/*/} [options] [--]
Options:
-h|host name Database Host Name default:localhost
-n|name name Database Name default:$USER
-o|output file Output file default:$output.dot
-p|port number TCP/IP port default:5432
-u|user name User name default:$USER
-v|version Display script version
EOT
} # ---------- end of function usage ----------
#-----------------------------------------------------------------------
# Handle command line arguments
#-----------------------------------------------------------------------
while getopts ":dh:n:o:p:u:v" opt
do
case $opt in
d|debug ) set -x ;;
h|host ) dbhost="$OPTARG" ;;
n|name ) dbname="$OPTARG" ;;
o|output ) output="$OPTARG" ;;
p|port ) dbport=$OPTARG ;;
u|user ) dbuser=$OPTARG ;;
v|version ) echo "$0 -- Version $ScriptVersion"; exit 0 ;;
\? ) echo -e "\n Option does not exist : $OPTARG\n"
usage; exit 1 ;;
esac # --- end of case ---
done
shift $(($OPTIND-1))
[[ -f "$output" ]] && rm "$output"
tee "$output" <<eof< span="">
digraph g {
node [shape=rectangle]
rankdir=LR
EOF
psql -h $dbhost -U $dbuser -d $dbname -p $dbport -qtAf cte.sql |
sed -e 's/^/node/' -e 's/.*(null)|/node/' -e 's/^/\t/' -e 's/|[[:digit:]]*$//' |
sed -e 's/|/ -> node/' | tee -a "$output"
tee -a "$output" <<eof< span="">
}
EOF
dot -Tpng "$output" > "${output/dot/png}"
[[ -f "$output" ]] && rm "$output"
open "${output/dot/png}"</eof<></eof<>
Dit script vereist deze SQL-instructie in een bestand met de naam cte.sql
WITH RECURSIVE a AS (
SELECT id, parent_id, 1::integer recursion_level
FROM person
WHERE parent_id IS NULL
UNION ALL
SELECT d.id, d.parent_id, a.recursion_level +1
FROM person d
JOIN a ON a.id = d.parent_id )
SELECT parent_id, id, recursion_level FROM a;
Dan roep je het als volgt aan:
chmod +x pggraph
./pggraph
En je zult de resulterende grafiek zien.
INSERT INTO person (id, parent_id) VALUES (6,2);
Voer het hulpprogramma opnieuw uit en zie de onmiddellijke veranderingen in uw gerichte grafiek:
Dat was nu toch niet zo moeilijk?