Tôi giả sử kiểu dữ liệu text
cho các cột có liên quan.
CREATE TABLE prefix (code text, name text, price int);
CREATE TABLE num (number text, time int);
Giải pháp "đơn giản"
SELECT DISTINCT ON (1)
n.number, p.code
FROM num n
JOIN prefix p ON right(n.number, -1) LIKE (p.code || '%')
ORDER BY n.number, p.code DESC;
Các yếu tố chính:
DISTINCT ON
là một phần mở rộng Postgres của tiêu chuẩn SQL DISTINCT
. Tìm một lời giải thích chi tiết cho kỹ thuật truy vấn được sử dụng trong câu trả lời liên quan này trên SO .
ORDER BY p.code DESC
chọn trận đấu dài nhất, bởi vì '1234'
sắp xếp sau '123'
(theo thứ tự tăng dần).
Câu đố SQL đơn giản .
Không có chỉ mục, truy vấn sẽ chạy trong một thời gian rất dài (không chờ đợi để xem nó kết thúc). Để thực hiện điều này nhanh chóng, bạn cần hỗ trợ chỉ mục. Các chỉ số bát quái mà bạn đề cập, được cung cấp bởi mô-đun bổ sung pg_trgm
là một ứng cử viên tốt. Bạn phải chọn giữa chỉ số GIN và GiST. Ký tự đầu tiên của các số chỉ là nhiễu và có thể được loại trừ khỏi chỉ mục, làm cho nó trở thành một chỉ mục chức năng.
Trong các thử nghiệm của tôi, một chỉ số GIN bát quái chức năng đã giành chiến thắng trong cuộc đua về chỉ số GiST của bát quái (như mong đợi):
CREATE INDEX num_trgm_gin_idx ON num USING gin (right(number, -1) gin_trgm_ops);
Dffiddle nâng cao ở đây .
Tất cả các kết quả kiểm tra là từ bản cài đặt thử nghiệm Postgres 9.1 cục bộ với thiết lập giảm: số 17k và mã 2k:
- Tổng thời gian chạy: 1719.552 ms (trigram GiST)
- Tổng thời gian chạy: 912.329 ms (bát quái GIN)
Nhanh hơn nhiều
Thất bại với text_pattern_ops
Một khi chúng ta bỏ qua ký tự nhiễu đầu tiên gây mất tập trung, nó đi xuống khớp mẫu cơ bản neo trái . Do đó, tôi đã thử một chỉ mục cây B chức năng với lớp toán tửtext_pattern_ops
(giả sử kiểu cột text
).
CREATE INDEX num_text_pattern_idx ON num(right(number, -1) text_pattern_ops);
Điều này hoạt động xuất sắc cho các truy vấn trực tiếp với một cụm từ tìm kiếm duy nhất và làm cho chỉ số bát quái trông tệ khi so sánh:
SELECT * FROM num WHERE right(number, -1) LIKE '2345%'
- Tổng thời gian chạy: 3,816 ms (trgm_gin_idx)
- Tổng thời gian chạy: 0.147 ms (text_potype_idx)
Tuy nhiên , trình hoạch định truy vấn sẽ không xem xét chỉ số này để tham gia hai bảng. Tôi đã thấy giới hạn này trước đây. Tôi chưa có một lời giải thích có ý nghĩa cho việc này.
Các chỉ mục cây B một phần / chức năng
Việc thay thế nó để sử dụng kiểm tra đẳng thức trên các chuỗi một phần với các chỉ mục một phần. Điều này có thể được sử dụng trong một JOIN
.
Vì chúng tôi thường chỉ có một số lượng different lengths
tiền tố giới hạn , chúng tôi có thể xây dựng một giải pháp tương tự như giải pháp được trình bày ở đây với các chỉ mục một phần.
Giả sử, chúng tôi có các tiền tố từ 1 đến 5 ký tự. Tạo một số chỉ mục chức năng một phần, một cho mỗi độ dài tiền tố riêng biệt:
CREATE INDEX prefix_code_idx5 ON prefix(code) WHERE length(code) = 5;
CREATE INDEX prefix_code_idx4 ON prefix(code) WHERE length(code) = 4;
CREATE INDEX prefix_code_idx3 ON prefix(code) WHERE length(code) = 3;
CREATE INDEX prefix_code_idx2 ON prefix(code) WHERE length(code) = 2;
CREATE INDEX prefix_code_idx1 ON prefix(code) WHERE length(code) = 1;
Vì đây là các chỉ mục một phần , tất cả chúng cùng nhau chỉ lớn hơn một chỉ mục hoàn chỉnh.
Thêm chỉ mục phù hợp cho các số (lấy ký tự nhiễu hàng đầu vào tài khoản):
CREATE INDEX num_number_idx5 ON num(substring(number, 2, 5)) WHERE length(number) >= 6;
CREATE INDEX num_number_idx4 ON num(substring(number, 2, 4)) WHERE length(number) >= 5;
CREATE INDEX num_number_idx3 ON num(substring(number, 2, 3)) WHERE length(number) >= 4;
CREATE INDEX num_number_idx2 ON num(substring(number, 2, 2)) WHERE length(number) >= 3;
CREATE INDEX num_number_idx1 ON num(substring(number, 2, 1)) WHERE length(number) >= 2;
Mặc dù các chỉ mục này chỉ giữ một chuỗi con mỗi và là một phần, mỗi chỉ số bao gồm hầu hết hoặc tất cả các bảng. Vì vậy, chúng lớn hơn nhiều so với một chỉ số tổng - trừ các số dài. Và họ áp đặt nhiều công việc hơn cho các hoạt động viết. Đó là chi phí cho tốc độ đáng kinh ngạc.
Nếu chi phí đó quá cao đối với bạn (hiệu suất ghi là quan trọng / quá nhiều thao tác ghi / dung lượng ổ đĩa là một vấn đề), bạn có thể bỏ qua các chỉ mục này. Phần còn lại vẫn nhanh hơn, nếu không nhanh như nó có thể ...
Nếu các số không bao giờ ngắn hơn thì n
ký tự, bỏ các WHERE
mệnh đề thừa từ một số hoặc tất cả, và cũng bỏ WHERE
mệnh đề tương ứng từ tất cả các truy vấn sau.
CTE đệ quy
Với tất cả các thiết lập cho đến nay, tôi đã hy vọng cho giải pháp rất thanh lịch với CTE đệ quy :
WITH RECURSIVE cte AS (
SELECT n.number, p.code, 4 AS len
FROM num n
LEFT JOIN prefix p
ON substring(number, 2, 5) = p.code
AND length(n.number) >= 6 -- incl. noise character
AND length(p.code) = 5
UNION ALL
SELECT c.number, p.code, len - 1
FROM cte c
LEFT JOIN prefix p
ON substring(number, 2, c.len) = p.code
AND length(c.number) >= c.len+1 -- incl. noise character
AND length(p.code) = c.len
WHERE c.len > 0
AND c.code IS NULL
)
SELECT number, code
FROM cte
WHERE code IS NOT NULL;
- Tổng thời gian chạy: 1045.115 ms
Tuy nhiên, trong khi truy vấn này không tệ - nó hoạt động tốt như phiên bản đơn giản với chỉ số GIN trigram - nó không cung cấp những gì tôi đang hướng tới. Thuật ngữ đệ quy chỉ được lên kế hoạch một lần, vì vậy nó không thể sử dụng các chỉ mục tốt nhất. Chỉ có thuật ngữ không đệ quy có thể.
ĐOÀN TẤT CẢ
Vì chúng ta đang đối phó với một số lượng nhỏ các cuộc thu hồi, chúng ta chỉ có thể đánh vần chúng lặp đi lặp lại. Điều này cho phép các kế hoạch tối ưu hóa cho mỗi người trong số họ. (Tuy nhiên, chúng tôi mất loại trừ đệ quy các số đã thành công. Vì vậy, vẫn còn một số chỗ cần cải thiện, đặc biệt là đối với phạm vi độ dài tiền tố rộng hơn)):
SELECT DISTINCT ON (1) number, code
FROM (
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 5) = p.code
AND length(n.number) >= 6 -- incl. noise character
AND length(p.code) = 5
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 4) = p.code
AND length(n.number) >= 5
AND length(p.code) = 4
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 3) = p.code
AND length(n.number) >= 4
AND length(p.code) = 3
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 2) = p.code
AND length(n.number) >= 3
AND length(p.code) = 2
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 1) = p.code
AND length(n.number) >= 2
AND length(p.code) = 1
) x
ORDER BY number, code DESC;
- Tổng thời gian chạy: 57,578 ms (!!)
Một bước đột phá, cuối cùng!
Hàm SQL
Việc gói này vào một hàm SQL sẽ loại bỏ chi phí lập kế hoạch truy vấn để sử dụng nhiều lần:
CREATE OR REPLACE FUNCTION f_longest_prefix()
RETURNS TABLE (number text, code text) LANGUAGE sql AS
$func$
SELECT DISTINCT ON (1) number, code
FROM (
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 5) = p.code
AND length(n.number) >= 6 -- incl. noise character
AND length(p.code) = 5
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 4) = p.code
AND length(n.number) >= 5
AND length(p.code) = 4
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 3) = p.code
AND length(n.number) >= 4
AND length(p.code) = 3
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 2) = p.code
AND length(n.number) >= 3
AND length(p.code) = 2
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 1) = p.code
AND length(n.number) >= 2
AND length(p.code) = 1
) x
ORDER BY number, code DESC
$func$;
Gọi:
SELECT * FROM f_longest_prefix_sql();
- Tổng thời gian chạy: 17.138 ms (!!!)
Hàm PL / pgQuery với SQL động
Hàm plpgsql này giống như CTE đệ quy ở trên, nhưng SQL động với EXECUTE
các truy vấn phải được lên kế hoạch lại cho mỗi lần lặp. Bây giờ nó sử dụng tất cả các chỉ mục phù hợp.
Ngoài ra, điều này hoạt động cho bất kỳ phạm vi độ dài tiền tố. Hàm này có hai tham số cho phạm vi, nhưng tôi đã chuẩn bị nó với DEFAULT
các giá trị, vì vậy nó cũng hoạt động mà không có tham số rõ ràng:
CREATE OR REPLACE FUNCTION f_longest_prefix2(_min int = 1, _max int = 5)
RETURNS TABLE (number text, code text) LANGUAGE plpgsql AS
$func$
BEGIN
FOR i IN REVERSE _max .. _min LOOP -- longer matches first
RETURN QUERY EXECUTE '
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(n.number, 2, $1) = p.code
AND length(n.number) >= $1+1 -- incl. noise character
AND length(p.code) = $1'
USING i;
END LOOP;
END
$func$;
Bước cuối cùng không thể được gói vào một chức năng một cách dễ dàng.
Hoặc chỉ gọi nó như thế này:
SELECT DISTINCT ON (1)
number, code
FROM f_longest_prefix_prefix2() x
ORDER BY number, code DESC;
- Tổng thời gian chạy: 27.413 ms
Hoặc sử dụng một hàm SQL khác làm trình bao bọc:
CREATE OR REPLACE FUNCTION f_longest_prefix3(_min int = 1, _max int = 5)
RETURNS TABLE (number text, code text) LANGUAGE sql AS
$func$
SELECT DISTINCT ON (1)
number, code
FROM f_longest_prefix_prefix2($1, $2) x
ORDER BY number, code DESC
$func$;
Gọi:
SELECT * FROM f_longest_prefix3();
- Tổng thời gian chạy: 37.622 ms
Chậm hơn một chút do yêu cầu lập kế hoạch. Nhưng linh hoạt hơn SQL và ngắn hơn cho các tiền tố dài hơn.
code
trong bảng đầu tiên giống như tiền tố sau này. Bạn có thể vui lòng làm rõ nó? Và một số sửa chữa dữ liệu mẫu và đầu ra mong muốn (để dễ theo dõi vấn đề của bạn hơn) cũng sẽ được hoan nghênh.