Dan sekarang kita sampai pada artikel kedua dalam migrasi kita dari seri Oracle ke PostgreSQL. Kali ini kita akan melihat START WITH/CONNECT BY
konstruksi.
Di Oracle, START WITH/CONNECT BY
digunakan untuk membuat struktur daftar tertaut tunggal mulai dari baris sentinel tertentu. Daftar tertaut dapat berbentuk pohon, dan tidak memiliki persyaratan penyeimbangan.
Sebagai ilustrasi, mari kita mulai dengan kueri, dan anggap tabel memiliki 5 baris di dalamnya.
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)
Berikut adalah kueri hierarki tabel menggunakan sintaks 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
Dan ini dia lagi menggunakan 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)
Kueri ini menggunakan banyak fitur PostgreSQL, jadi mari kita bahas secara perlahan.
WITH RECURSIVE
Ini adalah "Ekspresi Tabel Umum" (CTE). Ini mendefinisikan satu set kueri yang akan dieksekusi dalam pernyataan yang sama, tidak hanya dalam transaksi yang sama. Anda mungkin memiliki sejumlah ekspresi tanda kurung, dan pernyataan akhir. Untuk penggunaan ini, kita hanya membutuhkan satu. Dengan mendeklarasikan pernyataan tersebut sebagai RECURSIVE
, itu akan dijalankan secara iteratif sampai tidak ada lagi baris yang dikembalikan.
SELECT
UNION ALL
SELECT
Ini adalah frasa yang ditentukan untuk kueri rekursif. Ini didefinisikan dalam dokumentasi sebagai metode untuk membedakan titik awal dan algoritma rekursi. Dalam istilah Oracle, Anda dapat menganggapnya sebagai klausa START WITH yang digabungkan dengan klausa CONNECT BY.
JOIN a ON a.id = d.parent_id
Ini adalah self-join ke pernyataan CTE yang menyediakan data baris sebelumnya ke iterasi berikutnya.
Untuk mengilustrasikan cara kerjanya, mari tambahkan indikator iterasi ke kueri.
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)
Kami menginisialisasi indikator level rekursi dengan sebuah nilai. Perhatikan bahwa di baris yang dikembalikan, tingkat rekursi pertama hanya terjadi sekali. Itu karena klausa pertama hanya dieksekusi sekali.
Klausa kedua adalah tempat keajaiban berulang terjadi. Di sini, kami memiliki visibilitas data baris sebelumnya, bersama dengan data baris saat ini. Itu memungkinkan kita untuk melakukan perhitungan rekursif.
Simon Riggs memiliki video yang sangat bagus tentang cara menggunakan fitur ini untuk desain basis data grafik. Ini sangat informatif, dan Anda harus melihatnya.
Anda mungkin telah memperhatikan bahwa kueri ini dapat menyebabkan kondisi melingkar. Itu betul. Terserah pengembang untuk menambahkan klausa pembatas ke kueri kedua untuk mencegah rekursi tanpa akhir ini. Misalnya, hanya berulang 4 level sebelum menyerah begitu saja.
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;
Nama kolom dan tipe data ditentukan oleh klausa pertama. Perhatikan bahwa contoh menggunakan operator casting untuk tingkat rekursi. Dalam grafik yang sangat dalam, jenis data ini juga dapat didefinisikan sebagai 1::bigint recursion_level
.
Grafik ini sangat mudah divisualisasikan dengan skrip shell kecil dan utilitas 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<>
Skrip ini memerlukan pernyataan SQL ini dalam file bernama 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;
Kemudian Anda memanggilnya seperti ini:
chmod +x pggraph
./pggraph
Dan Anda akan melihat grafik yang dihasilkan.
INSERT INTO person (id, parent_id) VALUES (6,2);
Jalankan utilitas lagi, dan lihat perubahan langsung pada grafik yang Anda arahkan:
Sekarang, itu tidak terlalu sulit, bukan?