Et maintenant, nous arrivons au deuxième article de notre série sur la migration d'Oracle vers PostgreSQL. Cette fois, nous allons jeter un œil au START WITH/CONNECT BY
construire.
Dans Oracle, START WITH/CONNECT BY
est utilisé pour créer une structure de liste chaînée à partir d'une ligne sentinelle donnée. La liste liée peut prendre la forme d'un arbre et n'a aucune exigence d'équilibrage.
Pour illustrer, commençons par une requête et supposons que la table comporte 5 lignes.
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)
Voici la requête hiérarchique de la table en utilisant la 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
Et le voici à nouveau en utilisant 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)
Cette requête utilise de nombreuses fonctionnalités de PostgreSQL, alors allons-y lentement.
WITH RECURSIVE
Il s'agit d'une "expression de table commune" (CTE). Il définit un ensemble de requêtes qui seront exécutées dans la même instruction, pas seulement dans la même transaction. Vous pouvez avoir n'importe quel nombre d'expressions entre parenthèses et une déclaration finale. Pour cet usage, nous n'en avons besoin que d'un. En déclarant cette instruction RECURSIVE
, il s'exécutera de manière itérative jusqu'à ce qu'aucune autre ligne ne soit renvoyée.
SELECT
UNION ALL
SELECT
Il s'agit d'une expression prescrite pour une requête récursive. Il est défini dans la documentation comme la méthode permettant de distinguer le point de départ et l'algorithme de récursivité. En termes Oracle, vous pouvez les considérer comme la clause START WITH unie à la clause CONNECT BY.
JOIN a ON a.id = d.parent_id
Il s'agit d'une auto-jointure à l'instruction CTE qui fournit les données de la ligne précédente à l'itération suivante.
Pour illustrer comment cela fonctionne, ajoutons un indicateur d'itération à la requête.
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)
Nous initialisons l'indicateur de niveau de récursivité avec une valeur. Notez que dans les lignes renvoyées, le premier niveau de récursivité ne se produit qu'une seule fois. C'est parce que la première clause n'est exécutée qu'une seule fois.
La deuxième clause est l'endroit où la magie itérative se produit. Ici, nous avons une visibilité des données de la ligne précédente, ainsi que des données de la ligne actuelle. Cela nous permet d'effectuer les calculs récursifs.
Simon Riggs a une très belle vidéo sur l'utilisation de cette fonctionnalité pour la conception de bases de données de graphes. C'est très instructif, et vous devriez y jeter un coup d'œil.
Vous avez peut-être remarqué que cette requête pouvait conduire à une condition circulaire. C'est exact. Il appartient au développeur d'ajouter une clause limitative à la deuxième requête pour éviter cette récursivité sans fin. Par exemple, ne récurrencez que 4 niveaux de profondeur avant d'abandonner.
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;
Les noms de colonne et les types de données sont déterminés par la première clause. Notez que l'exemple utilise un opérateur de transtypage pour le niveau de récursivité. Dans un graphique très détaillé, ce type de données peut également être défini comme 1::bigint recursion_level
.
Ce graphique est très facile à visualiser avec un petit script shell et l'utilitaire 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<>
Ce script nécessite cette instruction SQL dans un fichier appelé 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;
Ensuite, vous l'invoquez comme ceci :
chmod +x pggraph
./pggraph
Et vous verrez le graphique résultant.
INSERT INTO person (id, parent_id) VALUES (6,2);
Exécutez à nouveau l'utilitaire et observez les modifications immédiates apportées à votre graphique orienté :
Maintenant, ce n'était pas si difficile maintenant, n'est-ce pas ?