Thuật toán tìm tiền tố dài nhất


11

Tôi có hai bàn.

Đầu tiên là một bảng có tiền tố

code name price
343  ek1   10
3435 nt     4
3432 ek2    2

Thứ hai là hồ sơ cuộc gọi với số điện thoại

number        time
834353212     10
834321242     20
834312345     30

Tôi cần viết một tập lệnh tìm tiền tố dài nhất từ ​​tiền tố cho mỗi bản ghi và ghi tất cả dữ liệu này vào bảng thứ ba, như sau:

 number        code   ....
 834353212     3435
 834321242     3432
 834312345     343

Đối với số 834353212, chúng ta phải cắt '8' và sau đó tìm mã dài nhất từ ​​bảng tiền tố, số 3435.
Chúng ta phải luôn bỏ trước '8' và tiền tố phải ở đầu.

Tôi đã giải quyết nhiệm vụ này từ lâu, với cách rất tệ. Đó là kịch bản perl khủng khiếp, thực hiện rất nhiều truy vấn cho mỗi bản ghi. Kịch bản này:

  1. Lấy một số từ bảng cuộc gọi, thực hiện chuỗi con từ độ dài (số) đến 1 => $ tiền tố trong vòng lặp

  2. Thực hiện truy vấn: chọn tính (*) từ các tiền tố trong đó mã như '$ tiền tố'

  3. Nếu đếm> 0 thì lấy tiền tố đầu tiên và ghi vào bảng

Vấn đề đầu tiên là số lượng truy vấn - đó là call_records * length(number). Vấn đề thứ hai là LIKEbiểu thức. Tôi sợ những thứ đó chậm.

Tôi đã cố gắng giải quyết vấn đề thứ hai bằng cách:

CREATE EXTENSION pg_trgm;
CREATE INDEX prefix_idx ON prefix USING gist (code gist_trgm_ops);

Điều đó tăng tốc mỗi truy vấn, nhưng không giải quyết vấn đề nói chung.

Bây giờ tôi có 20k tiền tố và 170k số, và giải pháp cũ của tôi rất tệ. Có vẻ như tôi cần một số giải pháp mới mà không cần vòng lặp.

Chỉ có một truy vấn cho mỗi bản ghi cuộc gọi hoặc một cái gì đó như thế này.


2
Tôi không thực sự chắc chắn nếu codetrong 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.
dezso

Vâng. Bạn đúng. Tôi đã quên viết về '8'. Cảm ơn bạn.
Korjavin Ivan

2
tiền tố phải ở đầu, phải không?
dezso

Đúng. Từ vị trí thứ hai. 8 tiền tố $ số
Korjavin Ivan

Cardinality của bảng của bạn là gì? Số 100k? Có bao nhiêu tiền tố?
Erwin Brandstetter

Câu trả lời:


21

Tôi giả sử kiểu dữ liệu textcho 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 ONlà 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 DESCchọ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_trgmlà 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 lengthstiề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ì nký tự, bỏ các WHEREmệnh đề thừa từ một số hoặc tất cả, và cũng bỏ WHEREmệ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 EXECUTEcá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 DEFAULTcá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.


Tôi vẫn đang kiểm tra, nhưng trông tuyệt vời! Ý tưởng của bạn "đảo ngược" như toán tử - rực rỡ. Tại sao tôi lại ngu ngốc như vậy; (
Korjavin Ivan

5
ai vậy đó là khá chỉnh sửa. tôi ước tôi có thể upvote một lần nữa.
swasheck

3
Tôi học được từ câu trả lời tuyệt vời của bạn nhiều hơn hai năm qua. 17-30 ms so với một vài giờ trong giải pháp vòng lặp của tôi? Đó là một phép thuật.
Korjavin Ivan

1
@KorjavinIvan: Vâng, như tài liệu, tôi đã thử nghiệm với thiết lập giảm các tiền tố 2k / số 17k. Nhưng điều này sẽ mở rộng khá tốt và máy thử nghiệm của tôi là một máy chủ nhỏ. Vì vậy, bạn nên ở lại dưới một giây với trường hợp thực tế của bạn.
Erwin Brandstetter

1
Câu trả lời hay ... Bạn có biết phần mở rộng tiền tố của dimitri không? Bạn có thể bao gồm điều đó trong so sánh trường hợp thử nghiệm của bạn?
MatheusOl

0

Chuỗi S là tiền tố của chuỗi T iff T nằm giữa S và SZ trong đó Z lớn hơn về mặt từ vựng so với bất kỳ chuỗi nào khác (ví dụ 99999999 với đủ 9 số để vượt quá số điện thoại dài nhất có thể trong tập dữ liệu hoặc đôi khi 0xFF sẽ hoạt động).

Tiền tố phổ biến dài nhất cho bất kỳ T đã cho nào cũng là tối đa về mặt từ vựng, do đó, một nhóm đơn giản và tối đa sẽ tìm thấy nó.

select n.number, max(p.code) 
from prefixes p
join numbers n 
on substring(n.number, 2, 255) between p.code and p.code || '99999999'
group by n.number

Nếu điều này chậm, có thể là do các biểu thức được tính toán, vì vậy bạn cũng có thể thử cụ thể hóa p.code || '999999' thành một cột trong bảng mã với chỉ mục riêng, v.v.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.