И сега стигаме до втората статия от нашата серия за миграция от Oracle към PostgreSQL. Този път ще разгледаме START WITH/CONNECT BY
конструкция.
В Oracle START WITH/CONNECT BY
се използва за създаване на едносвързана списъчна структура, започваща от даден сентинален ред. Свързаният списък може да бъде под формата на дърво и няма изискване за балансиране.
За да илюстрираме, нека започнем със заявка и да предположим, че таблицата има 5 реда.
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)
Ето йерархичната заявка на таблицата, използваща синтаксис на 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
И ето го отново с помощта на 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)
Тази заявка използва много функции на PostgreSQL, така че нека преминем през нея бавно.
WITH RECURSIVE
Това е „Общ израз на таблица“ (CTE). Той дефинира набор от заявки, които ще бъдат изпълнени в същия оператор, а не само в една и съща транзакция. Може да имате произволен брой изрази в скоби и окончателно изявление. За тази употреба ни трябва само един. Като декларирате това изявление за RECURSIVE
, той ще се изпълнява итеративно, докато не бъдат върнати повече редове.
SELECT
UNION ALL
SELECT
Това е предписана фраза за рекурсивна заявка. В документацията е дефиниран като метод за разграничаване на началната точка и рекурсивния алгоритъм. От гледна точка на Oracle, можете да ги мислите като клауза START WITH, обединена с клаузата CONNECT BY.
JOIN a ON a.id = d.parent_id
Това е самоприсъединяване към оператора CTE, който предоставя данните от предишния ред за последващата итерация.
За да илюстрираме как работи това, нека добавим индикатор за итерация към заявката.
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)
Инициализираме индикатора за ниво на рекурсия със стойност. Имайте предвид, че в редовете, които се връщат, първото ниво на рекурсия се появява само веднъж. Това е така, защото първата клауза се изпълнява само веднъж.
Втората клауза е мястото, където се случва итеративната магия. Тук имаме видимост на данните от предишния ред, заедно с данните за текущия ред. Това ни позволява да извършваме рекурсивните изчисления.
Саймън Ригс има много хубаво видео за това как да използвате тази функция за дизайн на графична база данни. Той е много информативен и трябва да го разгледате.
Може би сте забелязали, че тази заявка може да доведе до кръгово условие. Това е вярно. От разработчика зависи да добави ограничителна клауза към втората заявка, за да предотврати тази безкрайна рекурсия. Например повтаряне само на 4 нива, преди просто да се откажете.
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;
Имената на колоните и типовете данни се определят от първата клауза. Забележете, че примерът използва оператор за кастинг за нивото на рекурсия. В много дълбока графика този тип данни може също да бъде дефиниран като 1::bigint recursion_level
.
Тази графика е много лесна за визуализиране с малък шел скрипт и помощната програма 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<>
Този скрипт изисква този SQL израз във файл, наречен 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;
След това го извиквате така:
chmod +x pggraph
./pggraph
И ще видите получената графика.
INSERT INTO person (id, parent_id) VALUES (6,2);
Стартирайте отново помощната програма и вижте незабавните промени във вашата насочена графика:
Сега това не беше толкова трудно, нали?