A nyní se dostáváme k druhému článku naší série o migraci z Oracle na PostgreSQL. Tentokrát se podíváme na START WITH/CONNECT BY
konstrukce.
V Oracle START WITH/CONNECT BY
se používá k vytvoření jednoduše propojené struktury seznamu začínající na daném řádku kontroly. Propojený seznam může mít podobu stromu a nemá žádné požadavky na vyvážení.
Pro ilustraci začněme dotazem a předpokládejme, že tabulka má 5 řádků.
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)
Zde je hierarchický dotaz na tabulku pomocí syntaxe Oracle.
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
A tady je to opět pomocí 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)
Tento dotaz využívá mnoho funkcí PostgreSQL, takže si jej projdeme pomalu.
WITH RECURSIVE
Toto je „Common Table Expression“ (CTE). Definuje sadu dotazů, které budou provedeny ve stejném příkazu, nikoli pouze ve stejné transakci. Můžete mít libovolný počet výrazů v závorkách a závěrečné prohlášení. Pro toto použití potřebujeme pouze jeden. Deklarováním tohoto příkazu jako RECURSIVE
, bude se opakovat, dokud nebudou vráceny žádné další řádky.
SELECT
UNION ALL
SELECT
Toto je předepsaná fráze pro rekurzivní dotaz. V dokumentaci je definována jako metoda pro rozlišení výchozího bodu a rekurzního algoritmu. V podmínkách Oracle si je můžete představit jako klauzuli START WITH spojenou s klauzulí CONNECT BY.
JOIN a ON a.id = d.parent_id
Toto je vlastní připojení k příkazu CTE, které poskytuje data předchozího řádku do následující iterace.
Abychom ilustrovali, jak to funguje, přidáme k dotazu indikátor iterace.
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)
Indikátor úrovně rekurze inicializujeme hodnotou. Všimněte si, že ve vrácených řádcích se první úroveň rekurze vyskytuje pouze jednou. Je to proto, že první klauzule je provedena pouze jednou.
Druhá věta je místo, kde se děje iterativní magie. Zde máme viditelnost dat předchozího řádku spolu s daty aktuálního řádku. To nám umožňuje provádět rekurzivní výpočty.
Simon Riggs má velmi pěkné video o tom, jak použít tuto funkci pro návrh databáze grafů. Je to velmi informativní a měli byste se na to podívat.
Možná jste si všimli, že tento dotaz může vést k kruhové podmínce. To je správně. Je na vývojáři, aby do druhého dotazu přidal omezující klauzuli, aby zabránil této nekonečné rekurzi. Například stačí opakovat 4 úrovně hluboko, než to prostě vzdát.
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;
Názvy sloupců a datové typy jsou určeny první klauzulí. Všimněte si, že příklad používá operátor přetypování pro úroveň rekurze. Ve velmi hlubokém grafu lze tento datový typ definovat také jako 1::bigint recursion_level
.
Tento graf lze velmi snadno vizualizovat pomocí malého skriptu shellu a nástroje 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<>
Tento skript vyžaduje tento příkaz SQL v souboru s názvem 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;
Potom jej vyvoláte takto:
chmod +x pggraph
./pggraph
A uvidíte výsledný graf.
INSERT INTO person (id, parent_id) VALUES (6,2);
Spusťte nástroj znovu a podívejte se na okamžité změny vašeho orientovaného grafu:
Teď to nebylo tak těžké, že?