การจับคู่รูปแบบด้วย LIKE, SIMILAR TO หรือนิพจน์ปกติใน PostgreSQL


94

ฉันต้องเขียนคำถามง่ายๆที่ฉันไปหาชื่อของคนที่เริ่มต้นด้วย B หรือ D:

SELECT s.name 
FROM spelers s 
WHERE s.name LIKE 'B%' OR s.name LIKE 'D%'
ORDER BY 1

ฉันสงสัยว่าถ้ามีวิธีที่จะเขียนสิ่งนี้เพื่อให้มีประสิทธิภาพมากขึ้น ดังนั้นฉันสามารถหลีกเลี่ยงorและ / หรือlike?


ทำไมคุณพยายามเขียนใหม่ ประสิทธิภาพ? ความเรียบร้อย? มีการs.nameจัดทำดัชนีหรือไม่
Martin Smith

ฉันต้องการเขียนเพื่อประสิทธิภาพ s.name ไม่ได้จัดทำดัชนี
ลูคัสคอฟฟ์แมน

8
เช่นเดียวกับที่คุณกำลังค้นหาโดยไม่ใช้สัญลักษณ์เสริมและไม่เลือกคอลัมน์เพิ่มเติมใด ๆ ที่ดัชนีnameจะมีประโยชน์ที่นี่ถ้าคุณสนใจเกี่ยวกับประสิทธิภาพ
Martin Smith

คำตอบ:


161

ข้อความค้นหาของคุณเหมาะสมที่สุด ไวยากรณ์จะไม่สั้นลงมากแบบสอบถามจะไม่เร็วขึ้น:

SELECT name
FROM   spelers
WHERE  name LIKE 'B%' OR name LIKE 'D%'
ORDER  BY 1;

หากคุณต้องการทำให้ไวยากรณ์สั้นลงจริงๆให้ใช้นิพจน์ทั่วไปที่มีสาขา :

...
WHERE  name ~ '^(B|D).*'

หรือเร็วกว่าเล็กน้อยด้วยคลาสตัวละคร :

...
WHERE  name ~ '^[BD].*'

การทดสอบอย่างรวดเร็วโดยไม่ใช้ดัชนีให้ผลลัพธ์เร็วกว่าSIMILAR TOสำหรับฉัน
ด้วยดัชนี B-Tree ที่เหมาะสมในสถานที่LIKEชนะการแข่งขันนี้โดยคำสั่งของขนาด

อ่านพื้นฐานเกี่ยวกับการจับคู่รูปแบบในคู่มือ

ดัชนีเพื่อประสิทธิภาพที่เหนือกว่า

หากคุณกังวลเกี่ยวกับประสิทธิภาพการทำงานให้สร้างดัชนีเช่นนี้สำหรับตารางที่ใหญ่กว่า:

CREATE INDEX spelers_name_special_idx ON spelers (name text_pattern_ops);

ทำให้ข้อความค้นหาประเภทนี้เร็วขึ้นตามลำดับความสำคัญ ข้อพิจารณาพิเศษใช้สำหรับลำดับการจัดเรียงเฉพาะโลแคล อ่านเพิ่มเติมเกี่ยวกับชั้นเรียนผู้ประกอบการในคู่มือ หากคุณกำลังใช้ภาษา "C" มาตรฐาน (คนส่วนใหญ่ไม่ได้) ดัชนีธรรมดา (ที่มีระดับผู้ประกอบการเริ่มต้น) จะทำ

ดัชนีดังกล่าวดีสำหรับรูปแบบการยึดซ้าย (การจับคู่จากจุดเริ่มต้นของสตริง)

SIMILAR TOหรือนิพจน์ทั่วไปที่มีนิพจน์ยึดหลักซ้ายสามารถใช้ดัชนีนี้ได้เช่นกัน แต่ไม่ใช่กับสาขา(B|D)หรือคลาสอักขระ[BD](อย่างน้อยในการทดสอบของฉันกับ PostgreSQL 9.0)

การจับคู่ Trigram หรือการค้นหาข้อความใช้ดัชนี GIN หรือ GiST พิเศษ

ภาพรวมของโอเปอเรเตอร์การจับคู่รูปแบบ

  • LIKE( ~~) นั้นง่ายและรวดเร็ว แต่มีข้อ จำกัด ในขีดความสามารถ
    ILIKE( ~~*) ตัวแปรตัวพิมพ์เล็กและใหญ่
    pg_trgm ขยายการสนับสนุนดัชนีสำหรับทั้งสอง

  • ~ (การจับคู่นิพจน์ทั่วไป) มีประสิทธิภาพ แต่ซับซ้อนกว่าและอาจช้ากว่าอะไรก็ได้สำหรับนิพจน์พื้นฐาน

  • SIMILAR TOเป็นเพียงไม่มีจุดหมาย การแบ่งครึ่งที่แปลกประหลาดLIKEและการแสดงออกปกติ ฉันไม่เคยใช้มัน ดูด้านล่าง

  • %คือ "ความคล้ายคลึงกัน" pg_trgmผู้ประกอบการที่ให้บริการโดยโมดูลเพิ่มเติม ดูด้านล่าง

  • @@เป็นผู้ดำเนินการค้นหาข้อความ ดูด้านล่าง

pg_trgm - การจับคู่ trigram

เริ่มต้นด้วยการPostgreSQL 9.1คุณสามารถอำนวยความสะดวกในการขยายpg_trgmการให้การสนับสนุนดัชนีใด ๆ LIKE / ILIKEรูปแบบ (และรูปแบบที่เรียบง่ายด้วย regexp ~) โดยใช้ GIN หรือ GIST ดัชนี

รายละเอียดตัวอย่างและลิงค์:

pg_trgmยังมีตัวดำเนินการเหล่านี้ :

  • % - ตัวดำเนินการ "ความคล้ายคลึงกัน"
  • <%(commutator %>:) - ตัวดำเนินการ "word_similarity" ใน Postgres 9.6 หรือใหม่กว่า
  • <<%(commutator %>>:) - โอเปอเรเตอร์ "เข้มงวด_word_similarity" ใน Postgres 11 หรือใหม่กว่า

ค้นหาข้อความ

เป็นการจับคู่รูปแบบพิเศษที่มีโครงสร้างพื้นฐานแยกต่างหากและประเภทดัชนี มันใช้พจนานุกรมและการกั้นและเป็นเครื่องมือที่ยอดเยี่ยมในการค้นหาคำในเอกสารโดยเฉพาะอย่างยิ่งสำหรับภาษาธรรมชาติ

รองรับการจับคู่คำนำหน้าด้วย:

เช่นเดียวกับการค้นหาวลีตั้งแต่ Postgres 9.6:

พิจารณาการแนะนำในคู่มือและภาพรวมของผู้ประกอบการและฟังก์ชั่น

เครื่องมือเพิ่มเติมสำหรับการจับคู่สตริงฟัซซี่

โมดูลfuzzystrmatchเพิ่มเติมมีตัวเลือกเพิ่มเติมบางอย่าง แต่ประสิทธิภาพโดยทั่วไปจะด้อยกว่าทุกข้อ

โดยเฉพาะอย่างยิ่งการใช้งานที่หลากหลายของlevenshtein()ฟังก์ชั่นอาจเป็นเครื่องมือ

ทำไมจึงมีการแสดงออกปกติ ( ~) เสมอเร็วกว่าSIMILAR TO?

คำตอบนั้นง่าย SIMILAR TOนิพจน์จะถูกเขียนใหม่เป็นนิพจน์ปกติภายใน ดังนั้นสำหรับทุกSIMILAR TOนิพจน์จะมีนิพจน์ทั่วไปที่เร็วกว่าอย่างน้อยหนึ่งนิพจน์ (ซึ่งจะช่วยประหยัดค่าใช้จ่ายในการเขียนนิพจน์) ไม่มีกำไรจากผลการดำเนินงานในการใช้เป็นที่เคยSIMILAR TO

และสำนวนที่เรียบง่ายที่สามารถทำได้ด้วยLIKE( ~~) จะเร็วขึ้นด้วยLIKEล่ะค่ะ

SIMILAR TOได้รับการสนับสนุนเฉพาะใน PostgreSQL เพราะสิ้นสุดในร่างแรกของมาตรฐาน SQL พวกเขายังไม่ได้กำจัดมัน แต่มีแผนการที่จะลบมันและรวมการแข่งขัน regexp แทน - หรือดังนั้นฉันได้ยิน

EXPLAIN ANALYZEเปิดเผยมัน ลองกับโต๊ะตัวเองสิ!

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name SIMILAR TO 'B%';

เผย:

...  
Seq Scan on spelers  (cost= ...  
  Filter: (name ~ '^(?:B.*)$'::text)

SIMILAR TOถูกเขียนใหม่ด้วยนิพจน์ทั่วไป ( ~)

ประสิทธิภาพสูงสุดสำหรับกรณีนี้โดยเฉพาะ

แต่EXPLAIN ANALYZEเผยให้เห็นมากขึ้น ลองด้วยดัชนีที่กล่าวถึงข้างต้น:

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name ~ '^B.*;

เผย:

...
 ->  Bitmap Heap Scan on spelers  (cost= ...
       Filter: (name ~ '^B.*'::text)
        ->  Bitmap Index Scan on spelers_name_text_pattern_ops_idx (cost= ...
              Index Cond: ((prod ~>=~ 'B'::text) AND (prod ~<~ 'C'::text))

ภายในมีดัชนีที่ไม่รู้จักสถานที่ ( text_pattern_opsหรือใช้สถานที่C) การแสดงออกทางซ้ายจอดทอดสมออยู่ที่เรียบง่ายมีการเขียนใหม่กับผู้ประกอบการเหล่านี้รูปแบบข้อความ: ~>=~, ~<=~, ,~>~ ~<~เป็นกรณีนี้สำหรับ~, ~~หรือSIMILAR TOเหมือนกัน

เช่นเดียวกับที่เป็นจริงสำหรับดัชนีในvarcharประเภทvarchar_pattern_opsหรือกับcharbpchar_pattern_ops

ดังนั้นนำไปใช้กับคำถามเดิมนี่คือวิธีที่เร็วที่สุดที่เป็นไปได้ :

SELECT name
FROM   spelers  
WHERE  name ~>=~ 'B' AND name ~<~ 'C'
    OR name ~>=~ 'D' AND name ~<~ 'E'
ORDER  BY 1;

แน่นอนหากคุณควรค้นหาชื่อย่อที่อยู่ติดกันคุณสามารถทำให้ง่ายขึ้นได้อีก:

WHERE  name ~>=~ 'B' AND name ~<~ 'D'   -- strings starting with B or C

การได้รับมากกว่าการใช้แบบธรรมดา~หรือ~~เล็ก หากผลการดำเนินงานไม่ใช่ข้อกำหนดที่สำคัญที่สุดของคุณคุณก็ควรยึดติดกับผู้ดำเนินการมาตรฐาน - มาถึงสิ่งที่คุณมีอยู่แล้วในคำถาม


OP ไม่มีดัชนีชื่อ แต่คุณรู้หรือไม่ถ้าพวกเขาทำแบบสอบถามดั้งเดิมของพวกเขาจะเกี่ยวข้องกับการค้นหา 2 ช่วงและsimilarการสแกนหรือไม่
Martin Smith

2
@MartinSmith: การทดสอบอย่างรวดเร็วด้วยการEXPLAIN ANALYZEสแกนดัชนีบิตแมป 2 รายการ การสแกนดัชนีบิตแมปหลายรายการสามารถรวมกันได้ค่อนข้างเร็ว
Erwin Brandstetter

ขอบคุณ ดังนั้นจะมีการสะสมไมล์ด้วยการแทนที่ORด้วยUNION ALLหรือแทนที่name LIKE 'B%'ด้วย name >= 'B' AND name <'C'ใน Postgres หรือไม่?
Martin Smith

1
@MartinSmith: UNIONไม่ แต่ใช่การรวมช่วงเป็นหนึ่งWHEREส่วนจะทำให้การสืบค้นเร็วขึ้น ฉันได้เพิ่มคำตอบของฉันมากขึ้น แน่นอนคุณต้องคำนึงถึงสถานที่ของคุณด้วย การค้นหาสถานที่ที่ทราบช้ากว่าเสมอ
Erwin Brandstetter

2
@a_horse_with_no_name: ฉันคาดหวังว่าจะไม่ ความสามารถใหม่ของ pg_tgrm พร้อมกับดัชนี GIN เป็นการรักษาสำหรับการค้นหาข้อความทั่วไป การค้นหาที่ติดตั้งไว้ที่จุดเริ่มต้นนั้นเร็วกว่านั้น
Erwin Brandstetter

11

วิธีการเกี่ยวกับการเพิ่มคอลัมน์ในตาราง ขึ้นอยู่กับความต้องการที่แท้จริงของคุณ:

person_name_start_with_B_or_D (Boolean)

person_name_start_with_char CHAR(1)

person_name_start_with VARCHAR(30)

PostgreSQL ไม่สนับสนุนคอลัมน์ที่คำนวณในตารางฐาน a la SQL Serverแต่คอลัมน์ใหม่สามารถรักษาได้ด้วยทริกเกอร์ เห็นได้ชัดว่าคอลัมน์ใหม่นี้จะถูกจัดทำดัชนี

อีกทางหนึ่งดัชนีในนิพจน์จะให้ค่าเท่ากันถูกกว่า เช่น:

CREATE INDEX spelers_name_initial_idx ON spelers (left(name, 1)); 

แบบสอบถามที่ตรงกับนิพจน์ในเงื่อนไขสามารถใช้ดัชนีนี้ได้

ด้วยวิธีนี้ประสิทธิภาพการทำงานจะถูกนำมาใช้เมื่อมีการสร้างหรือแก้ไขข้อมูลดังนั้นจึงอาจเหมาะสำหรับสภาพแวดล้อมของกิจกรรมที่ไม่มาก (เช่นการเขียนน้อยกว่าการอ่าน)


8

คุณสามารถลอง

SELECT s.name
FROM   spelers s
WHERE  s.name SIMILAR TO '(B|D)%' 
ORDER  BY s.name

ฉันไม่รู้เลยว่าการแสดงออกด้านบนหรือแบบดั้งเดิมของคุณนั้นน่ารำคาญใน Postgres

หากคุณสร้างดัชนีที่แนะนำจะสนใจที่จะได้ยินว่าสิ่งนี้เปรียบเทียบกับตัวเลือกอื่น ๆ ได้อย่างไร

SELECT name
FROM   spelers
WHERE  name >= 'B' AND name < 'C'
UNION ALL
SELECT name
FROM   spelers
WHERE  name >= 'D' AND name < 'E'
ORDER  BY name

1
มันใช้งานได้และฉันได้ราคา 1.19 ซึ่งฉันมี 1.25 ขอบคุณมาก!
ลูคัสคอฟฟ์แมน

2

สิ่งที่ฉันทำในอดีตต้องเผชิญกับปัญหาประสิทธิภาพคล้ายกันคือการเพิ่มตัวอักษร ASCII ของตัวอักษรตัวสุดท้ายและทำระหว่าง จากนั้นคุณจะได้รับประสิทธิภาพที่ดีที่สุดสำหรับชุดย่อยของฟังก์ชัน LIKE แน่นอนมันใช้งานได้ในบางสถานการณ์เท่านั้น แต่สำหรับชุดข้อมูลขนาดใหญ่พิเศษที่คุณกำลังค้นหาชื่อมันทำให้ประสิทธิภาพเปลี่ยนไปจากที่สุดขั้วถึงเป็นที่ยอมรับ


2

คำถามเก่ามาก แต่ฉันพบวิธีแก้ปัญหาอื่นที่รวดเร็ว:

SELECT s.name 
FROM spelers s 
WHERE ascii(s.name) in (ascii('B'),ascii('D'))
ORDER BY 1

เนื่องจาก function ascii () ดูที่อักขระตัวแรกของสตริงเท่านั้น


1
สิ่งนี้ใช้ดัชนี(name)หรือไม่?
ypercubeᵀᴹ

2

สำหรับการตรวจสอบชื่อย่อฉันมักจะใช้แคสติ้งเพื่อ"char"(ด้วยเครื่องหมายคำพูดคู่) มันไม่ได้พกพา แต่รวดเร็วมาก ภายในตัวมันจะลบข้อความและส่งกลับอักขระแรกและการดำเนินการเปรียบเทียบ "char" นั้นเร็วมากเพราะประเภทคือความยาวคงที่ 1 ไบต์:

SELECT s.name 
FROM spelers s 
WHERE s.name::"char" =ANY( ARRAY[ "char" 'B', 'D' ] )
ORDER BY 1

โปรดทราบว่าการส่งไปยัง"char"นั้นเร็วกว่าascii()slution โดย @ Sole021 แต่มันไม่รองรับ UTF8 (หรือการเข้ารหัสอื่น ๆ สำหรับเรื่องนั้น) ส่งคืนไบต์แรกเพียงอย่างเดียวดังนั้นควรใช้ในกรณีที่การเปรียบเทียบกับแบบเก่าธรรมดา 7 อักขระ ASCII บิต


1

มีสองวิธีที่ยังไม่ได้กล่าวถึงสำหรับการจัดการกับกรณีดังกล่าว:

  1. บางส่วน (หรือแบ่งพาร์ติชัน - หากสร้างขึ้นสำหรับช่วงเต็มด้วยตนเอง) ดัชนีมีประโยชน์มากที่สุดเมื่อต้องการชุดย่อยของข้อมูล (ตัวอย่างเช่นระหว่างการบำรุงรักษาหรือชั่วคราวสำหรับการรายงานบางส่วน):

    CREATE INDEX ON spelers WHERE name LIKE 'B%'
  2. การแบ่งตารางด้วยตัวเอง (ใช้อักขระตัวแรกเป็นคีย์การแบ่งพาร์ติชัน) - เทคนิคนี้มีความสำคัญอย่างยิ่งเมื่อพิจารณาใน PostgreSQL 10+ (การแบ่งพาร์ติชันที่เจ็บปวดน้อยกว่า) และ 11+ (การตัดพาร์ติชันระหว่างการประมวลผลแบบสอบถาม)

นอกจากนี้หากข้อมูลในตารางถูกจัดเรียงข้อมูลจะได้ประโยชน์จากการใช้ดัชนี BRIN (เหนืออักขระตัวแรก)


-4

อาจเร็วกว่าที่จะทำการเปรียบเทียบอักขระเดี่ยว:

SUBSTR(s.name,1,1)='B' OR SUBSTR(s.name,1,1)='D'

1
ไม่ได้จริงๆ column LIKE 'B%'จะมีประสิทธิภาพมากกว่าการใช้ฟังก์ชันสตริงย่อยในคอลัมน์
ypercubeᵀᴹ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.