อัลกอริทึมสำหรับการค้นหาคำนำหน้าที่ยาวที่สุด


11

ฉันมีสองตาราง

สิ่งแรกคือตารางที่มีคำนำหน้า

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

ประการที่สองคือบันทึกการโทรพร้อมหมายเลขโทรศัพท์

number        time
834353212     10
834321242     20
834312345     30

ฉันต้องการเขียนสคริปต์ที่ค้นหาคำนำหน้าที่ยาวที่สุดจากคำนำหน้าสำหรับแต่ละระเบียนและเขียนข้อมูลทั้งหมดนี้ไปยังตารางที่สามเช่นนี้

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

สำหรับหมายเลข 834353212 เราจะต้องตัด '8' แล้วหารหัสที่ยาวที่สุดจากตารางคำนำหน้า 3435
เราจะต้องวางแรก '8' และคำนำหน้าจะต้องอยู่ในจุดเริ่มต้น

ฉันแก้ไขงานนี้เมื่อนานมาแล้วด้วยวิธีที่ไม่ดีมาก มันเป็นสคริปต์ Perl ที่แย่มากซึ่งทำแบบสอบถามจำนวนมากสำหรับแต่ละเร็กคอร์ด สคริปต์นี้:

  1. ใช้หมายเลขจากตารางการโทรทำสตริงย่อยจากความยาว (หมายเลข) ถึง 1 => คำนำหน้า $ ในลูป

  2. ทำแบบสอบถาม: select count (*) จากส่วนนำหน้าโดยที่โค้ดเช่น '$ prefix'

  3. หากนับ> 0 ให้นำส่วนนำหน้าแรกและเขียนลงในตาราง

ปัญหาแรกคือนับแบบสอบถาม - call_records * length(number)มัน ปัญหาที่สองคือLIKEการแสดงออก ฉันเกรงว่าสิ่งเหล่านั้นจะช้า

ฉันพยายามที่จะแก้ปัญหาที่สองโดย:

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

ที่เพิ่มความเร็วแต่ละแบบสอบถาม แต่ไม่ได้แก้ปัญหาโดยทั่วไป

ตอนนี้ฉันมีส่วนนำหน้า20kและตัวเลข170kและโซลูชันเก่าของฉันไม่ดี ดูเหมือนว่าฉันต้องการโซลูชันใหม่โดยไม่มีลูป

มีเพียงหนึ่งแบบสอบถามสำหรับบันทึกการโทรแต่ละครั้งหรืออะไรทำนองนี้


2
ฉันไม่แน่ใจว่าถ้าcodeในตารางแรกเหมือนกับคำนำหน้าในภายหลัง คุณช่วยอธิบายให้ฟังหน่อยได้ไหม? และการแก้ไขข้อมูลตัวอย่างและผลลัพธ์ที่ต้องการ (เพื่อให้ง่ายต่อการติดตามปัญหาของคุณ) จะได้รับการต้อนรับ
dezso

อ๋อ คุณพูดถูก ฉันลืมเขียนเกี่ยวกับ '8' ขอขอบคุณ.
Korjavin Ivan

2
คำนำหน้าต้องอยู่ที่จุดเริ่มต้นใช่มั้ย
dezso

ใช่. จากสถานที่ที่สอง 8 $ นำหน้าหมายเลข $
Korjavin Ivan

cardinality ของตารางของคุณคืออะไร? ตัวเลข 100k มีคำนำหน้ากี่คำ
Erwin Brandstetter

คำตอบ:


21

ฉันกำลังสมมติประเภทข้อมูลtextสำหรับคอลัมน์ที่เกี่ยวข้อง

CREATE TABLE prefix (code text, name text, price int);
CREATE TABLE num (number text, time int);

โซลูชัน "แบบง่าย"

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;

องค์ประกอบสำคัญ:

DISTINCT ONเป็นส่วนขยายของ Postgres DISTINCTมาตรฐานของ หาคำอธิบายรายละเอียดสำหรับเทคนิคที่ใช้ในแบบสอบถามนี้คำตอบที่เกี่ยวข้องในดังนั้น
ORDER BY p.code DESCเลือกการจับคู่ที่ยาวที่สุดเพราะ'1234'เรียงลำดับหลัง'123'(เรียงตามลำดับจากมากไปหาน้อย)

ง่ายซอ SQL

โดยไม่ต้องดัชนีแบบสอบถามจะวิ่งไปหามากเวลานาน (ไม่รอที่จะเห็นมันจบ) เพื่อให้เร็วขึ้นคุณต้องมีการสนับสนุนดัชนี ดัชนี trigram ที่คุณพูดถึงซึ่งจัดหาโดยโมดูลเพิ่มเติมpg_trgmนั้นเป็นตัวเลือกที่ดี คุณต้องเลือกระหว่างดัชนี GIN และ GiST ตัวอักษรตัวแรกของตัวเลขเป็นเพียงเสียงรบกวนและสามารถแยกออกจากดัชนีได้ทำให้เป็นดัชนีการทำงานเพิ่มเติม
ในการทดสอบของฉันดัชนี GIN ของ trigram ที่ใช้งานได้ชนะการแข่งขันมากกว่าดัชนี GiST Trigram (ตามที่คาดไว้):

CREATE INDEX num_trgm_gin_idx ON num USING gin (right(number, -1) gin_trgm_ops);

ขั้นสูงdbfiddleที่นี่

ผลการทดสอบทั้งหมดมาจากการติดตั้งแบบทดสอบท้องถิ่น Postgres 9.1 พร้อมการตั้งค่าที่ลดลง: หมายเลข 17k และรหัส 2k:

  • รันไทม์ทั้งหมด: 1719.552 ms (trigram GiST)
  • รันไทม์ทั้งหมด: 912.329 ms (trigram GIN)

เร็วขึ้นมาก

ไม่สามารถลองด้วย text_pattern_ops

เมื่อเราเพิกเฉยต่อตัวอักษรแรกที่ทำให้เสียสมาธิมันจะเข้าสู่รูปแบบพื้นฐานที่จับคู่ซ้าย ดังนั้นฉันจึงลองใช้ดัชนีต้นไม้แบบ B ที่ใช้งานได้กับ คลาสโอเปอเรเตอร์text_pattern_ops (สมมติว่าเป็นประเภทคอลัมน์text)

CREATE INDEX num_text_pattern_idx ON num(right(number, -1) text_pattern_ops);

วิธีนี้ใช้งานได้อย่างยอดเยี่ยมสำหรับการค้นหาโดยตรงด้วยคำค้นหาเดียวและทำให้ดัชนี trigram ดูไม่ดีเมื่อเปรียบเทียบ:

SELECT * FROM num WHERE right(number, -1) LIKE '2345%'
  • จำนวนรันไทม์ทั้งหมด: 3.816 ms (trgm_gin_idx)
  • จำนวนรันไทม์ทั้งหมด: 0.147 ms (text_pattern_idx)

อย่างไรก็ตามตัววางแผนคิวรีจะไม่พิจารณาดัชนีนี้สำหรับการเข้าร่วมสองตาราง ฉันเคยเห็นข้อ จำกัด นี้มาก่อน ฉันยังไม่มีคำอธิบายที่มีความหมายสำหรับสิ่งนี้

ดัชนี B-tree บางส่วน / การทำงาน

ทางเลือกที่จะใช้การตรวจสอบความเท่าเทียมกันในสตริงบางส่วนกับดัชนีบางส่วน นี้สามารถJOINนำมาใช้ใน

เนื่องจากโดยทั่วไปเรามีจำนวน จำกัดdifferent lengthsสำหรับคำนำหน้าเท่านั้นเราจึงสามารถสร้างโซลูชันที่คล้ายกับที่นำเสนอที่นี่ด้วยดัชนีบางส่วน

สมมติว่าเรามีคำนำหน้าตั้งแต่1ถึง5ตัวอักษร สร้างดัชนีการทำงานบางส่วนจำนวนหนึ่งสำหรับทุกความยาวของคำนำหน้า:

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;

เนื่องจากสิ่งเหล่านี้เป็นดัชนีบางส่วนพวกเขาทั้งหมดจึงมีขนาดใหญ่กว่าดัชนีสมบูรณ์แบบเดียว

เพิ่มดัชนีที่ตรงกันสำหรับตัวเลข (คำนึงถึงตัวอักษรนำเสียงเข้าบัญชี):

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;

ในขณะที่ดัชนีเหล่านี้เก็บซับสตริงแต่ละอันเท่านั้นและเป็นบางส่วนแต่ละส่วนครอบคลุมตารางส่วนใหญ่หรือทั้งหมด ดังนั้นพวกมันจึงมีขนาดใหญ่กว่าดัชนีรวมทั้งหมดยกเว้นจำนวนยาว และพวกเขากำหนดงานเพิ่มเติมสำหรับการเขียน นั่นคือค่าใช้จ่ายสำหรับความเร็วที่น่าอัศจรรย์

หากค่าใช้จ่ายนั้นสูงเกินไปสำหรับคุณ (ประสิทธิภาพการเขียนมีความสำคัญ / มีปัญหาในการเขียน / เนื้อที่ดิสก์มากเกินไป) คุณสามารถข้ามดัชนีเหล่านี้ได้ ที่เหลือก็ยังเร็วกว่านี้ถ้าไม่เร็วอย่างที่ควรจะเป็น ...

หากตัวเลขไม่สั้นลงดังนั้นnให้วางWHEREคำสั่งที่ซ้ำซ้อนจากบางส่วนหรือทั้งหมดและปล่อยWHEREประโยคที่เกี่ยวข้องจากแบบสอบถามต่อไปนี้ทั้งหมด

CTE แบบเรียกซ้ำ

ด้วยการตั้งค่าทั้งหมดจนถึงตอนนี้ฉันหวังว่าโซลูชันที่หรูหรามากพร้อมCTE แบบเรียกซ้ำ :

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;
  • รันไทม์ทั้งหมด: 1045.115 ms

อย่างไรก็ตามในขณะที่การค้นหานี้ไม่ได้เลวร้าย - มันทำงานได้ดีเหมือนกับเวอร์ชันง่าย ๆ ที่มีดัชนี GIN Trigram - มันไม่ได้ส่งมอบสิ่งที่ฉันต้องการ คำที่เกิดซ้ำมีการวางแผนเพียงครั้งเดียวดังนั้นจึงไม่สามารถใช้ดัชนีที่ดีที่สุดได้ เฉพาะคำศัพท์ที่ไม่เกิดซ้ำได้เท่านั้น

ยูเนี่ยนทั้งหมด

เนื่องจากเราจัดการกับการสอบถามซ้ำจำนวนน้อยเราจึงสามารถสะกดพวกเขาซ้ำ ๆ สิ่งนี้ช่วยให้แผนที่เหมาะสมที่สุดสำหรับแต่ละแผน (เราสูญเสียการแยกแบบเรียกซ้ำสำหรับตัวเลขที่ประสบความสำเร็จอยู่แล้วดังนั้นยังมีห้องพักสำหรับการปรับปรุงโดยเฉพาะอย่างยิ่งสำหรับช่วงความยาวของคำนำหน้าที่กว้างขึ้น)):

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;
  • จำนวนรันไทม์ทั้งหมด: 57.578 ms (!!)

ความก้าวหน้าในที่สุด!

ฟังก์ชัน SQL

การตัดสิ่งนี้ลงในฟังก์ชั่น SQL จะลบค่าใช้จ่ายในการวางแผนคิวรีเพื่อใช้ซ้ำ:

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$;

โทร:

SELECT * FROM f_longest_prefix_sql();
  • จำนวนรันไทม์ทั้งหมด: 17.138 ms (!!!)

ฟังก์ชัน PL / pgSQL พร้อม SQL แบบไดนามิก

ฟังก์ชัน plpgsql นี้คล้ายกับ recursive CTE ด้านบน แต่ SQL แบบไดนามิกที่มีการEXECUTEบังคับให้เคียวรีถูกวางแผนใหม่สำหรับการวนซ้ำทุกครั้ง ตอนนี้มันใช้ประโยชน์จากดัชนีที่ปรับแต่งแล้วทั้งหมด

นอกจากนี้ยังใช้ได้กับความยาวของช่วงนำหน้าด้วย ฟังก์ชั่นรับพารามิเตอร์สองตัวสำหรับช่วง แต่ฉันเตรียมไว้พร้อมกับDEFAULTค่าดังนั้นจึงใช้งานได้โดยไม่มีพารามิเตอร์ที่ชัดเจนเช่นกัน:

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$;

ขั้นตอนสุดท้ายไม่สามารถห่อเป็นฟังก์ชันเดียวได้อย่างง่ายดาย ทั้งเรียกมันว่าอย่างนี้:

SELECT DISTINCT ON (1)
       number, code
FROM   f_longest_prefix_prefix2() x
ORDER  BY number, code DESC;
  • รันไทม์ทั้งหมด: 27.413 ms

หรือใช้ฟังก์ชัน SQL อื่นเป็น wrapper:

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$;

โทร:

SELECT * FROM f_longest_prefix3();
  • รันไทม์ทั้งหมด: 37.622 ms

ช้าลงเล็กน้อยเนื่องจากค่าใช้จ่ายในการวางแผนที่จำเป็น แต่หลากหลายกว่า SQL และสั้นกว่าสำหรับคำนำหน้าอีกต่อไป


ฉันยังตรวจสอบอยู่ แต่ดูดีมาก! ความคิดของคุณ "ย้อนกลับ" เหมือนโอเปอเรเตอร์ - ยอดเยี่ยม ทำไมฉันถึงโง่มาก (
โครวินอีวาน

5
whoah! ที่ค่อนข้างแก้ไข ฉันหวังว่าฉันจะสามารถโหวตได้อีกครั้ง
swasheck

3
ฉันเรียนรู้จากคำตอบที่น่าทึ่งของคุณมากกว่าสองปีที่ผ่านมา 17-30 ms เทียบกับหลายชั่วโมงในโซลูชันลูปของฉัน นั่นเป็นเวทมนต์
Korjavin Ivan

1
@KorjavinIvan: ในฐานะที่เป็นเอกสารฉันทดสอบด้วยการตั้งค่าลดจำนวนคำนำหน้า 2k / หมายเลข 17k แต่นี่น่าจะปรับขนาดได้ค่อนข้างดีและเครื่องทดสอบของฉันเป็นเซิร์ฟเวอร์ขนาดเล็ก ดังนั้นคุณควรพักอย่างน้อยวินาทีกับกรณีชีวิตจริงของคุณ
Erwin Brandstetter

1
คำตอบที่ดี ... คุณรู้จักส่วนขยายคำนำหน้าของดิมิทรีหรือไม่? คุณสามารถรวมไว้ในการเปรียบเทียบกรณีทดสอบของคุณ?
MatheusOl

0

สตริง S เป็นคำนำหน้าของสตริง T iff T อยู่ระหว่าง S และ SZ โดยที่ Z มีขนาดเล็กกว่าสตริงอื่น ๆ (เช่น 99999999 โดยมี 9 เพียงพอที่จะใช้หมายเลขโทรศัพท์ที่ยาวที่สุดในชุดข้อมูลหรือบางครั้ง 0xFF จะทำงาน)

คำนำหน้าทั่วไปที่ยาวที่สุดสำหรับ T ที่กำหนดใด ๆ ก็เป็นจำนวนสูงสุดด้วยพจนานุกรมดังนั้นกลุ่มแบบง่าย ๆ โดยและ max จะค้นหาได้

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

หากสิ่งนี้ช้าอาจเป็นไปได้เนื่องจากนิพจน์ที่คำนวณดังนั้นคุณยังสามารถลองใช้การสร้าง material p.code || '999999' ลงในคอลัมน์ในตารางรหัสด้วยดัชนีของตัวเองและอื่น ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.