Và bây giờ chúng ta đến với bài viết thứ hai trong loạt bài chuyển từ Oracle sang PostgreSQL. Lần này chúng ta sẽ xem xét START WITH/CONNECT BY
xây dựng.
Trong Oracle, START WITH/CONNECT BY
được sử dụng để tạo một cấu trúc danh sách được liên kết đơn lẻ bắt đầu từ một hàng sentinel nhất định. Danh sách được liên kết có thể có dạng cây và không có yêu cầu cân bằng.
Để minh họa, hãy bắt đầu với một truy vấn và giả định rằng bảng có 5 hàng trong đó.
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)
Đây là truy vấn phân cấp của bảng sử dụng cú pháp 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
Và đây nó lại đang sử dụng 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)
Truy vấn này sử dụng rất nhiều tính năng của PostgreSQL, vì vậy hãy từ từ đi qua.
WITH RECURSIVE
Đây là “Biểu thức bảng chung” (CTE). Nó định nghĩa một tập hợp các truy vấn sẽ được thực hiện trong cùng một câu lệnh, không chỉ trong cùng một giao dịch. Bạn có thể có bất kỳ số lượng biểu thức dấu ngoặc nào và một câu lệnh cuối cùng. Đối với cách sử dụng này, chúng tôi chỉ cần một. Bằng cách khai báo câu lệnh đó là RECURSIVE
, nó sẽ thực thi lặp đi lặp lại cho đến khi không còn hàng nào được trả lại.
SELECT
UNION ALL
SELECT
Đây là một cụm từ quy định cho một truy vấn đệ quy. Nó được định nghĩa trong tài liệu là phương pháp để phân biệt điểm bắt đầu và thuật toán đệ quy. Theo thuật ngữ của Oracle, bạn có thể coi chúng như là mệnh đề BẮT ĐẦU VỚI liên kết với mệnh đề CONNECT BY.
JOIN a ON a.id = d.parent_id
Đây là một tự nối vào câu lệnh CTE cung cấp dữ liệu hàng trước đó cho lần lặp tiếp theo.
Để minh họa cách hoạt động của điều này, hãy thêm một chỉ báo lặp lại vào truy vấn.
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)
Chúng tôi khởi tạo chỉ báo mức đệ quy với một giá trị. Lưu ý rằng trong các hàng được trả về, mức đệ quy đầu tiên chỉ xảy ra một lần. Đó là vì mệnh đề đầu tiên chỉ được thực thi một lần.
Mệnh đề thứ hai là nơi phép thuật lặp lại xảy ra. Tại đây, chúng tôi có khả năng hiển thị dữ liệu hàng trước đó, cùng với dữ liệu hàng hiện tại. Điều đó cho phép chúng tôi thực hiện các phép tính đệ quy.
Simon Riggs có một video rất hay về cách sử dụng tính năng này để thiết kế cơ sở dữ liệu đồ thị. Nó rất giàu thông tin và bạn nên xem qua.
Bạn có thể nhận thấy rằng truy vấn này có thể dẫn đến một điều kiện vòng tròn. Đúng rồi. Nhà phát triển có thể thêm mệnh đề giới hạn vào truy vấn thứ hai để ngăn chặn quá trình đệ quy vô tận này. Ví dụ:chỉ lặp lại 4 cấp độ sâu trước khi bỏ cuộc.
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;
Tên cột và kiểu dữ liệu được xác định bởi mệnh đề đầu tiên. Lưu ý rằng ví dụ sử dụng toán tử ép kiểu cho mức đệ quy. Trong một biểu đồ rất sâu, kiểu dữ liệu này cũng có thể được định nghĩa là 1::bigint recursion_level
.
Biểu đồ này rất dễ hình dung với một tập lệnh shell nhỏ và tiện ích 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<>
Tập lệnh này yêu cầu câu lệnh SQL này trong tệp có tên 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;
Sau đó, bạn gọi nó như thế này:
chmod +x pggraph
./pggraph
Và bạn sẽ thấy biểu đồ kết quả.
INSERT INTO person (id, parent_id) VALUES (6,2);
Chạy lại tiện ích và xem các thay đổi ngay lập tức đối với biểu đồ được hướng dẫn của bạn:
Bây giờ, điều đó không quá khó phải không?