Och nu kommer vi till den andra artikeln i vår migrering från Oracle till PostgreSQL-serien. Den här gången ska vi ta en titt på START WITH/CONNECT BY
konstruera.
I Oracle, START WITH/CONNECT BY
används för att skapa en enkellänkad liststruktur som börjar på en given sentinel-rad. Den länkade listan kan ha formen av ett träd och har inga balanseringskrav.
För att illustrera, låt oss börja med en fråga och anta att tabellen har 5 rader i den.
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)
Här är den hierarkiska frågan i tabellen med Oracle-syntax.
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
Och här använder den PostgreSQL igen.
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)
Den här frågan använder många PostgreSQL-funktioner, så låt oss gå igenom det långsamt.
WITH RECURSIVE
Detta är ett "Common Table Expression" (CTE). Den definierar en uppsättning frågor som kommer att köras i samma programsats, inte bara i samma transaktion. Du kan ha hur många uttryck som helst inom parentes och ett slutgiltigt uttalande. För denna användning behöver vi bara en. Genom att förklara det påståendet som RECURSIVE
, kommer den att köras iterativt tills inga fler rader returneras.
SELECT
UNION ALL
SELECT
Detta är en föreskriven fras för en rekursiv fråga. Det definieras i dokumentationen som metoden för att särskilja utgångspunkt och rekursionsalgoritm. I Oracle-termer kan du se dem som START WITH-satsen kopplad till CONNECT BY-satsen.
JOIN a ON a.id = d.parent_id
Detta är en självkoppling till CTE-satsen som tillhandahåller föregående raddata till den efterföljande iterationen.
För att illustrera hur detta fungerar, låt oss lägga till en iterationsindikator i frågan.
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)
Vi initierar rekursionsnivåindikatorn med ett värde. Observera att den första rekursionsnivån endast inträffar en gång i raderna som returneras. Det beror på att den första klausulen bara körs en gång.
Den andra klausulen är där den iterativa magin sker. Här har vi synlighet av föregående raddata, tillsammans med nuvarande raddata. Det gör att vi kan utföra de rekursiva beräkningarna.
Simon Riggs har en mycket trevlig video om hur man använder den här funktionen för grafdatabasdesign. Det är mycket informativt och du borde ta en titt.
Du kanske har märkt att den här frågan kan leda till ett cirkulärt tillstånd. Det är korrekt. Det är upp till utvecklaren att lägga till en begränsningsklausul till den andra frågan för att förhindra denna ändlösa rekursion. Till exempel, bara återkommande 4 nivåer djupa innan du bara ger upp.
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;
Kolumnnamnen och datatyperna bestäms av den första satsen. Lägg märke till att exemplet använder en gjutoperator för rekursionsnivån. I ett mycket djupt diagram kan denna datatyp också definieras som 1::bigint recursion_level
.
Den här grafen är mycket lätt att visualisera med ett litet skalskript och verktyget 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<>
Detta skript kräver denna SQL-sats i en fil som heter 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;
Sedan anropar du det så här:
chmod +x pggraph
./pggraph
Och du kommer att se den resulterande grafen.
INSERT INTO person (id, parent_id) VALUES (6,2);
Kör verktyget igen och se de omedelbara ändringarna i din riktade graf:
Nu var det inte så svårt nu, eller hur?