PostgreSQL ค้นหาทั่วระบบ: full-text, trigram, partial indexes
เรียนรู้การออกแบบการค้นหาแบบ "ค้นหาทั่วระบบ" ใน PostgreSQL สำหรับหน้าจอภายในโดยเลือก full-text, ดัชนี trigram และ partial index เพื่อผลลัพธ์ที่เร็ว

ความหมายที่แท้จริงของ “ค้นหาทั่วระบบ” สำหรับเครื่องมือภายใน
บนหน้าจอภายใน “ค้นหาทั่วระบบ” มักหมายถึง: “ช่วยฉันหาบันทึกที่ฉันคิดไว้ ให้เร็ว แม้ฉันจะจำไม่ชัด” ผู้ใช้ไม่ได้เรียกดู พวกเขาพยายามข้ามตรงไปยังลูกค้าหนึ่งคน ตั๋ว ใบแจ้งหนี้ หรืออุปกรณ์
นั่นคือเหตุผลที่การค้นหาช้าที่ให้ความรู้สึกแย่กว่าหน้าโหลดช้า หน้าจะโหลดครั้งเดียว แต่การค้นหาเกิดขึ้นซ้ำหลายครั้ง ขณะคนกำลังคุยโทรศัพท์หรือทำการไล่ตรวจ หากผลลัพธ์ใช้เวลา 2–3 วินาที ผู้ใช้จะเปลี่ยนคำค้น กด backspace ลองคำอื่น และท้ายที่สุดก็ทำให้ระบบมีโหลดมากขึ้นและผู้ใช้หงุดหงิดมากขึ้น
จากกล่องค้นหาเดียว ผู้ใช้คาดหวังพฤติกรรมหลายอย่าง: การจับตรงส่วน ("alex" หา "Alexander"), การทนต่อการพิมพ์ผิดเล็กน้อย ("microsfot" ยังคงหา "Microsoft" ได้), การเรียงลำดับ “ผลลัพธ์ที่ดีที่สุด” อย่างสมเหตุสมผล (ID หรืออีเมลที่ตรงขึ้นลอยขึ้นด้านบน), น้ำหนักเล็กน้อยต่อความสดใหม่ และตัวกรองที่ใช้เป็นค่าตั้งต้น (ตั๋วเปิด ลูกค้าที่ใช้งานอยู่)
ความยากคืออินพุตเดียวมักซ่อนความตั้งใจหลายอย่าง เจ้าหน้าที่อาจวางหมายเลขตั๋ว พิมพ์ชื่อตัดตอน ค้นหาอีเมล หรือใส่เบอร์โทร แต่ละความตั้งใจต้องการกลยุทธ์ต่างกัน ดัชนีต่างกัน และบางครั้งกฎการจัดอันดับต่างกัน
ดังนั้นอย่าเริ่มจากดัชนี เริ่มจากการระบุความตั้งใจการค้นหาที่ผู้ใช้มีจริง ๆ แยกฟิลด์ระบุตัวตน (ID, อีเมล) ออกจากฟิลด์คลุมเครือ (ชื่อ หัวเรื่อง) และข้อความยาว (บันทึก)
เริ่มจากการตั้งชื่อข้อมูลและพฤติกรรมการค้นหา
ก่อนเลือกดัชนี จดสิ่งที่คนพิมพ์จริง “PostgreSQL search everywhere” ฟังดูเหมือนฟีเจอร์เดียว แต่จริง ๆ มักเป็นการผสมของการค้นหาที่แตกต่างกันมาก
เครื่องมือภายในผสมผสานตัวระบุ “แข็ง” (order ID, หมายเลขตั๋ว, รหัสใบแจ้งหนี้) กับข้อความ “อ่อน” (ชื่อลูกค้า, อีเมล, บันทึก, แท็ก) กลุ่มเหล่านี้ทำงานต่างกันใน PostgreSQL จึงไม่ควรปฏิบัติเหมือนกัน
แยกพฤติกรรมต่อไปนี้:
- การค้นหาตรง: ใครสักคนค้นหา
TCK-104883คาดหวังผลลัพธ์ที่ชัดเจนเพียงรายการเดียว - การค้นหาแบบคลุมเครือ: ใครสักคนพิมพ์
john smthต้องการการจับที่ยืดหยุ่นข้ามชื่อ (และอาจรวมอีเมล) และจะสแกนรายการสั้น ๆ - การค้นหาที่ขับเคลื่อนด้วยตัวกรอง: ใครสักคนเลือก “สถานะ = เปิด” และ “มอบหมายให้ = ฉัน” เป็นการกรอง; กล่องข้อความเป็นรอง
ตัดสินใจแต่ต้นว่าผลลัพธ์ต้องมีการจัดลำดับ (ผลลัพธ์ที่ดีที่สุดก่อน) หรือเพียงกรองเท่านั้น การจัดลำดับสำคัญสำหรับบันทึกและคำอธิบายยาว ๆ สำหรับ ID และอีเมล การจัดลำดับมักให้ความรู้สึกสุ่มและเพิ่มต้นทุน
รายการตรวจสอบสั้น ๆ มักเพียงพอ:
- ฟิลด์ใดถูกค้นหาทุกวัน?
- อินพุตใดเป็นตรง (ID, โค้ด), คลุมเครือ (ชื่อ), หรือข้อความยาว (บันทึก)?
- ตัวกรองใดใช้เกือบทุกการค้นหา?
- คุณต้องการการเรียงลำดับ “ผลลัพธ์ที่ดีที่สุด” หรือพอแค่เจอเท่านั้น?
- ตารางจะโตเร็วแค่ไหน: พัน รายแสน หรือเป็นล้าน?
ถ้าคุณตั้งคำตอบเหล่านี้ไว้ล่วงหน้า ตัวเลือกดัชนีจะไม่รู้สึกเหมือนการเดาอีกต่อไป
พื้นฐาน: การจับตรงและทำไม ILIKE มักทำร้ายประสิทธิภาพ
ล็อกชัยชนะง่าย ๆ ก่อน สำหรับหลายหน้าภายใน ดัชนี B-tree ธรรมดาให้ผลลัพธ์ทันทีสำหรับการจับตรงเช่น ID หมายเลขคำสั่ง อีเมล และการอ้างอิงภายนอก
ถ้าผู้ใช้วางค่าตรง ๆ ให้แน่ใจว่า query ของคุณเป็นการเปรียบเทียบตรงจริง ๆ WHERE id = ... หรือ WHERE email = ... สามารถเร็วมากกับดัชนีปกติ ดัชนี unique บน email มักคุ้มค่าเพราะทั้งความเร็วและคุณภาพข้อมูล
ปัญหาเริ่มขึ้นเมื่อ “ค้นหาทั่วระบบ” ค่อย ๆ กลายเป็น ILIKE คำสั่งอย่าง name ILIKE '%ann%' มีไวด์การ์ดนำหน้า PostgreSQL จึงไม่สามารถใช้ดัชนี B-tree ปกติได้ มันต้องตรวจสอบแถวจำนวนมากและช้าลงเมื่อฐานข้อมูลโตขึ้น
การค้นหาพร็อกซิฟิกซ์ (prefix search) ใช้งานได้ แต่เฉพาะเมื่อลวดลายผูกกับจุดเริ่มต้น: name ILIKE 'ann%' แม้กระนั้นรายละเอียดก็สำคัญ (collation, การจัดการตัวพิมพ์ และว่าคุณสร้างดัชนีจากนิพจน์เดียวกันที่ใช้ใน query หรือไม่) หาก UI ต้องไม่สนใจตัวพิมพ์ วิธีปฏิบัติทั่วไปคือใช้ lower(name) และสร้างดัชนีที่ตรงกันบน lower(name)
นอกจากนี้ ช่วยได้ถ้าตกลงกันว่า “snappy” หมายถึงอะไร:
- ประมาณ 200 ms หรือน้อยกว่าสำหรับงานฐานข้อมูลเมื่อร้อนใน cache
- ต่ำกว่า 1 วินาทีทั้งระบบ รวมเครือข่ายและการเรนเดอร์
- ไม่มีสถานะกำลังโหลดที่มองเห็นได้สำหรับการค้นหาทั่วไป
ด้วยเป้าหมายแบบนี้ จะง่ายกว่าที่จะตัดสินใจว่าควรยึดกับการจับตรงและ prefix หรือถึงเวลาที่ต้องใช้ full-text search หรือตัวดัชนี trigram
เมื่อ full-text search เป็นเครื่องมือที่เหมาะสม
Full-text search เหมาะที่สุดเมื่อผู้ใช้พิมพ์ภาษาธรรมชาติและคาดหวังว่าระบบจะหาสิ่งที่เกี่ยวข้อง ไม่ใช่แค่การจับตรง คิดถึงข้อความตั๋ว บันทึกภายใน คำอธิบายยาว บทความความรู้ และบันทึกการโทร
ข้อดีใหญ่คือการจัดลำดับความเกี่ยวข้อง แทนที่จะคืนรายการยาว ๆ ที่ผลลัพธ์ที่ดีที่สุดถูกฝังอยู่ Full-text search สามารถเรียงตามความเหมาะสมได้ ในเครื่องมือภายใน นั่นสำคัญ: ผู้ใช้ต้องการคำตอบภายในไม่กี่วินาที ไม่ใช่หลังจากสแกน 50 แถว
โดยรวมแล้ว full-text search มีส่วนประกอบสามอย่าง:
tsvector(ข้อความที่ค้นหาได้ เก็บหรือสร้างแบบ on-the-fly)tsquery(สิ่งที่ผู้ใช้พิมพ์ แปลงเป็นคำค้น)- การตั้งค่าภาษา (วิธีทำให้คำปกติลงรูป)
การตั้งค่าภาษาคือจุดที่พฤติกรรมปรากฏชัด PostgreSQL จะตัด stop words ทั่วไป (เช่น “the” หรือ “and”) และทำ stemming ดังนั้น “pay”, “paid”, และ “payment” อาจจับกันได้ นั่นดีสำหรับบันทึกและข้อความ แต่บางครั้งอาจทำให้ผู้ใช้ประหลาดใจเมื่อค้นหาคำสั้น ๆ ที่พบบ่อยแล้วไม่พบผล
คำพ้องความหมาย (synonyms) เป็นอีกจุดที่ต้องตัดสินใจ ช่วยเมื่อบริษัทใช้คำต่างกันสำหรับสิ่งเดียวกัน (เช่น “refund” กับ “chargeback”) แต่ต้องดูแลรักษาให้สั้นและอิงจากคำที่ทีมซัพพอร์ตหรือปฏิบัติการใช้จริง
ตัวอย่างเชิงปฏิบัติ: การค้นหา "can't login after reset" ควรดึงตั๋วที่มีข้อความว่า "cannot log in after password reset" ถึงแม้ถ้อยคำจะแตกต่างกัน พฤติกรรม “ค้นหาให้เจอที่เกี่ยวข้อง” นี้คือสิ่งที่ full-text search ถูกออกแบบมาให้ทำ และมักเป็นตัวเลือกที่ดีกว่าพยายามทำให้ ILIKE ทำงานเหมือนเครื่องมือค้นหา
เมื่อดัชนี trigram ชนะ
ดัชนี trigram เหมาะเมื่ ผู้ใช้พิมพ์ชิ้นส่วน พิมพ์ผิด หรือจำได้เพียงบางส่วน มันโดดเด่นบนฟิลด์สั้น ๆ ที่ full-text เข้มงวดเกินไป: ชื่อบุคคล ชื่อบริษัท หัวเรื่องตั๋ว SKU หมายเลขคำสั่ง และรหัสสินค้า
trigram คือชิ้นส่วนข้อความ 3 ตัวอักษร PostgreSQL เปรียบเทียบสตริงสองอันจากจำนวน trigram ที่แชร์กัน นั่นคือเหตุผลที่มันจับ "Jon Smth" กับ "John Smith" ได้ หรือ "ACM" กับ "ACME" และหาผลลัพธ์เมื่อคำค้นเป็นส่วนกลางของคำ
นี่มักเป็นทางที่เร็วที่สุดสู่กล่องค้นหาที่ยอมรับความผิดพลาดของมนุษย์เมื่อหน้าที่คือ “หาแถวที่ถูกต้อง” ไม่ใช่ “หาเอกสารเกี่ยวกับหัวข้อ”
จุดที่มันชนะ full-text
Full-text ดีสำหรับข้อความยาวและการจัดลำดับตามความหมาย แต่ไม่จัดการสตริงบางส่วนและการพิมพ์ผิดเล็กน้อยบนฟิลด์สั้น ๆ ได้โดยตรง Trigram ถูกสร้างมาเพื่อลักษณะความไม่แม่นยำแบบนั้น
คงต้นทุนการเขียนให้สมเหตุสมผล
ดัชนี trigram ใหญ่กว่าและเพิ่มภาระเมื่อเขียน จึงต้องเลือกเฉพาะคอลัมน์จริง ๆ ที่ผู้ใช้ใช้:
- ชื่อ อีเมล บริษัท ชื่อผู้ใช้
- ตัวระบุสั้น ๆ (SKU โค้ด อ้างอิง)
- ฟิลด์ชื่อสั้น ๆ (ไม่ใช่บันทึกขนาดใหญ่)
ถ้าคุณตั้งชื่อฟิลด์ที่ทีมพิมพ์ลงในกล่องค้นหาได้ชัดเจน คุณมักจะรักษาดัชนี trigram ให้เล็กและเร็วได้
ดัชนีบางส่วนสำหรับตัวกรองที่ผู้ใช้ใช้จริง
กล่อง “ค้นหาทั่วระบบ” มักมีค่าเริ่มต้นที่ซ่อนอยู่ ผู้ใช้ค้นหาใน workspace, เฉพาะรายการที่ใช้งาน และยกเว้นรายการที่ลบแล้ว หากตัวกรองเหล่านี้ปรากฏเกือบทุกคำขอ ให้ทำให้กรณีทั่วไปเร็วขึ้นโดยการจัดทำดัชนีเฉพาะแถวที่ตรงกัน
Partial index คือดัชนีปกติตัวหนึ่งที่มี WHERE clause PostgreSQL เก็บมันให้เล็กเพราะบันทึกเฉพาะแถวที่คุณสนใจมากที่สุด นั่นมักหมายถึงหน้าหน้าน้อยลงให้อ่านและอัตราการถูกเก็บในแคชที่ดีขึ้น
เป้าหมาย partial-index ทั่วไปได้แก่ แถวที่ใช้งาน (status = 'active'), การลบแบบนุ่ม (deleted_at IS NULL), การแยก tenant, และหน้าต่าง "ล่าสุด" (เช่น 90 วันล่าสุด)
กุญแจคือให้ตรงกับ UI หากหน้าจอซ่อนแถวที่ลบเสมอ ควรให้ query รวม deleted_at IS NULL เสมอ และ partial index ควรใช้เงื่อนไขเดียวกัน ความไม่ตรงกันเล็กน้อย เช่น ใช้ is_deleted = false ในที่หนึ่งและ deleted_at IS NULL ในอีกที่ อาจทำให้ planner ไม่ใช้ดัชนี
Partial index ทำงานร่วมกับ full-text และ trigram ได้ด้วย ตัวอย่างเช่น ดัชนีการค้นหาข้อความเฉพาะแถวที่ไม่ถูกลบจะช่วยควบคุมขนาดดัชนี
ข้อเสีย: partial index น้อยประโยชน์สำหรับคำค้นหาที่เกิดขึ้นไม่บ่อย หากบางครั้งมีการค้นหาข้ามแถวที่ลบ PostgreSQL อาจย้อนกลับไปใช้แผนช้า จัดการด้วยเส้นทางสำหรับผู้ดูแล หรือเพิ่มดัชนีที่สองเฉพาะเมื่อคำค้นหาหายากนั้นกลายเป็นเรื่องปกติ
ผสมวิธีโดยไม่ทำให้การค้นหาเป็นปริศนา
ทีมส่วนใหญ่จะผสมเทคนิคเพราะกล่องค้นหาเดียวต้องรองรับความตั้งใจต่างกัน เป้าหมายคือทำให้ลำดับการปฏิบัติชัดเจนเพื่อให้ผลลัพธ์คาดเดาได้
ลำดับความสำคัญเรียบง่ายช่วยได้ ไม่ว่าจะทำเป็นหลาย query หรือเป็น query เดียวที่มี CASE ชัดเจน
บันไดลำดับความสำคัญที่คาดเดาได้
เริ่มเข้มงวด แล้วค่อยผ่อนคลายเมื่อจำเป็น:
- Exact match ก่อน (ID, อีเมล, หมายเลขตั๋ว, SKU) โดยใช้ดัชนี B-tree
- Prefix match ถัดมาเมื่อเหมาะสม
- Trigram match หลังจากนั้นสำหรับการพิมพ์ผิดและเศษชื่อ/หัวเรื่อง
- Full-text search สุดท้ายสำหรับบันทึกยาว คำอธิบาย และเนื้อหาอิสระ
เมื่อคุณยึดตามบันไดเดียวกัน ผู้ใช้จะเรียนรู้ว่ากล่องหมายถึงอะไร พวกเขาจะไม่คิดว่าระบบพังเมื่อ “12345” เจอตั๋วทันทีในขณะที่ “refund policy” ต้องค้นหาไฟล์ข้อความยาว
กรองก่อน แล้วค่อยคลุมเครือ
การค้นหาแบบคลุมเครือแพงเมื่อมันต้องพิจารณาทั้งตาราง จำกัดชุดผู้สมัครด้วยตัวกรองที่ผู้ใช้ใช้จริง (สถานะ ทีมที่มอบหมาย ช่วงวันที่ บัญชี) แล้วค่อยรัน trigram หรือ full-text บนที่เหลือ แม้ดัชนี trigram ที่เร็วก็จะรู้สึกช้าเมื่อคุณให้มันสกอร์หลายล้านแถว
คุ้มค่าที่จะเขียนกฎหนึ่งย่อหน้าที่เพื่อนร่วมทีมที่ไม่ใช่เทคนิคอ่านเข้าใจได้ เช่น: “เราจับหมายเลขตั๋วแบบตรงก่อน แล้วชื่อผู้ใช้ที่ยอมรับการพิมพ์ผิด แล้วค่อยค้นหาบันทึก” คำจำกัดความร่วมนี้ป้องกันข้อถกเถียงภายหลังว่าทำไมแถวหนึ่งจึงโผล่ขึ้น
ขั้นตอนทีละขั้น: เลือกแนวทางและนำไปใช้อย่างปลอดภัย
กล่องค้นหาที่เร็วคือชุดของการตัดสินใจเล็ก ๆ เขียนมันก่อน แล้วงานฐานข้อมูลจะง่ายขึ้น
- กำหนดอินพุต: มันเป็นกล่องเดียวเท่านั้น หรือกล่องเดียวพร้อมตัวกรอง (สถานะ เจ้าของ ช่วงวันที่)?
- เลือกประเภทการจับต่อฟิลด์: ID/โค้ดต้องการจับตรง ชื่อและอีเมลมักต้อง prefix หรือการจับแบบคลุมเครือ ข้อความยาวควรใช้การค้นหาภาษาธรรมชาติ
- เพิ่มดัชนีที่ถูกต้องและยืนยันว่าใช้: สร้างดัชนี แล้วตรวจสอบ query จริงด้วย
EXPLAIN (ANALYZE, BUFFERS) - เพิ่มการจัดลำดับหรือการเรียงที่ตรงกับความตั้งใจ: ถ้าผู้ใช้พิมพ์ “invoice 1042” ผลลัพธ์ตรงควรขึ้นก่อน ถ้าพิมพ์ชื่อสะกดผิด การจัดลำดับตามความใกล้เคียงควรชนะ
- ทดสอบด้วยคำค้นจริง: ลองพิมพ์ผิด คำสั้นมาก (เช่น “al”), ข้อความยาวที่วางมา, อินพุตว่าง, และโหมด “เฉพาะตัวกรอง”
เพื่อปล่อยอย่างปลอดภัย เปลี่ยนทีละอย่างและเตรียมย้อนกลับง่าย สำหรับดัชนีใหม่บนตารางใหญ่ ให้ใช้ CREATE INDEX CONCURRENTLY เพื่อไม่บล็อกการเขียน หากเป็นไปได้ ปล่อยหลัง feature flag และเปรียบเทียบความหน่วงก่อน-หลัง
รูปแบบปฏิบัติที่ใช้ได้จริงสำหรับ “PostgreSQL search everywhere” คือ: exact match ก่อน (เร็วและแม่นยำ), trigram สำหรับฟิลด์ที่คนพิมพ์ผิด, และ full-text สำหรับข้อความยาวที่ได้ประโยชน์จากการจัดลำดับ
ตัวอย่างสมจริง: กล่องค้นหาเดียวในแผงแอดมินซัพพอร์ต
จินตนาการแผงแอดมินซัพพอร์ตที่ทีมมีแค่กล่องค้นหาเดียว แต่คาดหวังให้มันหาได้ทั้งลูกค้า ตั๋ว และบันทึก นี่คือปัญหา "อินพุตเดียว หลายความหมาย"
ชัยชนะแรกคือทำให้ความตั้งใจปรากฏโดยไม่เพิ่มความฝืด หากคำค้นดูเหมือนอีเมลหรือเบอร์โทร ให้จัดเส้นทางเป็นการค้นหาผู้ใช้ หากดูเหมือนหมายเลขตั๋ว (เช่น "TKT-10482") ส่งไปที่ตั๋วโดยตรง ทุกอย่างที่เหลือตกไปเป็นการค้นหาข้อความในหัวเรื่องและบันทึกตั๋ว
สำหรับการค้นหาลูกค้า ดัชนี trigram มักให้ความรู้สึกดีที่สุด ชื่อกับบริษัทมีความยุ่งเหยิงและผู้ใช้พิมพ์ชิ้นส่วน ดัชนี trigram ทำให้การค้นหาอย่าง “jon smi” หรือ “acm” เร็วและยืดหยุ่น
สำหรับบันทึกตั๋ว ให้ใช้ full-text search บันทึกเป็นประโยคจริงและคุณมักต้องการการจับที่เกี่ยวข้อง ไม่ใช่แค่ "มีสตริงย่อยนี้" การจัดลำดับช่วยเมื่อหลายตั๋วพูดถึงคำเดียวกัน
ตัวกรองสำคัญกว่าที่ทีมส่วนใหญ่นึก หากเจ้าหน้าที่อยู่ใน “ตั๋วเปิด” เสมอ ให้เพิ่ม partial index ที่ครอบคลุมเฉพาะแถวเปิด ทำแบบเดียวกันสำหรับ “ลูกค้าที่ใช้งาน” มันทำให้ดัชนีเล็กลงและทำให้เส้นทางปกติเร็ว
คำค้นสั้น ๆ สมควรได้กฎ มิฉะนั้นฐานข้อมูลจะทำงานหนักเกินความจำเป็น:
- 1–2 ตัวอักษร: แสดงตั๋วเปิดล่าสุดและลูกค้าที่อัปเดตล่าสุด
- 3+ ตัวอักษร: รัน trigram สำหรับฟิลด์ลูกค้า และ full-text สำหรับข้อความตั๋ว
- ไม่มีความตั้งใจชัดเจน: แสดงรายการผสม แต่จำกัดแต่ละกลุ่ม (เช่น 10 ลูกค้า และ 10 ตั๋ว)
ความผิดพลาดทั่วไปที่ทำให้การค้นาหรือช้าหรือสับสน
บั๊กส่วนใหญ่ของ “ทำไมการค้นหาช้าจัง?” เกิดจากการกระทำของทีมเอง เป้าหมายไม่ใช่การจัดทำดัชนีทุกอย่าง แต่จัดทำดัชนีสิ่งที่ผู้ใช้ทำจริง
กับดักทั่วไปคือเพิ่มดัชนีบนหลายคอลัมน์ “กันไว้ก่อน” การอ่านอาจดีขึ้น แต่การแทรกและอัปเดตแต่ละครั้งต้องทำงานมากขึ้น ในเครื่องมือภายในที่บันทึกเปลี่ยนตลอดวัน (ตั๋ว คำสั่ง ผู้ใช้) ความเร็วเขียนมีความสำคัญ
ข้อผิดพลาดอีกอย่างคือใช้ full-text เมื่อสิ่งที่ต้องการจริง ๆ คือการค้นหาทนต่อการพิมพ์ผิดบนชื่อหรืออีเมล Full-text ดีสำหรับเอกสารและคำอธิบาย แต่มันไม่ใช่การแก้ปัญหาทั่วไปสำหรับ “Jon” กับ “John” หรือ “gmail.con” กับ “gmail.com” นั่นมักเป็นกรณีของ trigram
ตัวกรองยังสามารถทำลายแผนได้เงียบ ๆ หากการค้นหาส่วนใหญ่เกิดขึ้นพร้อมตัวกรองคงที่ (เช่น status = 'open' หรือ org_id = 42) ดัชนีที่ดีที่สุดอาจเป็น partial index ที่ตรงกับเงื่อนไขนั้น ถ้าคุณลืม สิ่งที่คาดว่าจะใช้ดัชนีอาจกลายเป็นการสแกนแถวมากกว่าที่คิด
บางความผิดพลาดที่เห็นบ่อย:
- เพิ่มดัชนีมากเกินไปโดยไม่วัดต้นทุนการเขียน
- คาดหวังว่า full-text จะทำงานเหมือน autocomplete ที่ทนต่อการพิมพ์ผิด
- มองข้ามว่าตัวกรองทั่วไปเปลี่ยนดัชนีที่ถูกใช้
- ทดสอบบนข้อมูลเล็ก ๆ แทนที่จะเป็นความถี่คำจริง (คำที่พบบ่อยเทียบกับ ID หายาก)
- การเรียงลำดับโดยคอลัมน์ที่ไม่มีดัชนีรองรับ บังคับให้เกิด sort ช้า
ตัวอย่าง: หน้าซัพพอร์ตค้นหาตั๋วโดยหัวเรื่อง ชื่อลูกค้า และหมายเลขตั๋ว แล้วเรียงโดยกิจกรรมล่าสุด หาก latest_activity_at ไม่มีดัชนีสำหรับชุดที่ถูกกรอง (เช่น ตั๋วเปิด) การ sort นั้นอาจลบความเร็วที่คุณได้จากดัชนีการค้นหา
เช็ครวดเร็วก่อนปล่อย
ก่อนเรียกฟีเจอร์ “ค้นหาทั่วระบบ” เสร็จ ให้ระบุพฤติกรรมที่คุณสัญญาต่อผู้ใช้ชัดเจน
- ผู้ใช้พยายามหาบันทึกโดยตัวระบุตรง (หมายเลขตั๋ว อีเมล)?
- พวกเขาคาดหวังการแมทช์แบบคลุมเครือสำหรับการพิมพ์ผิด?
- ต้องการผลลัพธ์มีการจัดลำดับจากบันทึกยาวหรือไม่?
หากคุณผสมโหมด หลักแหลมว่าฝ่ายใดชนะเมื่อขัดแย้ง
จากนั้นระบุ 2–3 ฟิลด์ที่ขับเคลื่อนการค้นหาส่วนใหญ่ หาก 80% ของการค้นหาคือโดยอีเมล ชื่อ และหมายเลขตั๋ว ปรับจูนสิ่งเหล่านั้นก่อน และพิจารณาที่เหลือเป็นรอง
รายการตรวจสอบก่อนปล่อยสั้น ๆ:
- ยืนยันโหมดการจับหลักต่อฟิลด์ (จับตรง, แมทช์คลุมเครือ, หรือข้อความจัดลำดับ)
- ระบุฟิลเตอร์ที่ผู้ใช้ใช้ประจำและตรวจสอบว่าดัชนีตรงกับการรวมกันเหล่านั้น
- ตัดสินใจว่าจะจัดการการค้นหาแนวสั้นและว่างอย่างไร (เช่น กำหนดให้ fuzzy ต้องมี 2–3 ตัวอักษร; แสดง “ล่าสุด” เมื่อว่าง)
- ทำให้การจัดลำดับอธิบายได้: ล่าสุดที่สุด, ผลการจับข้อความที่ดีที่สุด, หรือกฎผสมเรียบง่าย
สุดท้าย ทดสอบด้วยขนาดข้อมูลและเวลาเชิงสมจริง ไม่ใช่แค่ความถูกต้อง คิวรีที่รู้สึกทันทีกับ 1,000 แถวอาจลากเมื่อมี 1,000,000 แถว
ขั้นตอนต่อไป: เปลี่ยนแผนเป็นหน้าค้นหาภายในที่เร็ว
กล่องค้นหาจะยังเร็วเมื่อทีมตกลงกันว่ามันควรทำอะไร เขียนกฎเป็นภาษาปกติ: “การแมทช์” หมายถึงอะไร (ตรง, prefix, ทนต่อการพิมพ์ผิด), ฟิลด์ใดถูกค้นหา และตัวกรองเปลี่ยนผลอย่างไร
เก็บชุดทดสอบสั้น ๆ ของการค้นหาจริงและใช้เป็นชุด regression สิบถึงยี่สิบคำค้นมักพอ: ชื่อที่พบบ่อยสองสามคำ, อีเมลบางส่วน, การพิมพ์ผิด, ชิ้นข้อความยาวที่วางมา, และกรณี “ไม่มีผลลัพธ์” รันก่อนและหลังการเปลี่ยนแปลงเพื่อไม่ให้งานปรับปรุงประสิทธิภาพทำให้ความเกี่ยวข้องเปลี่ยนโดยไม่รู้ตัว
ถ้าคุณสร้างเครื่องมือภายในด้วย AppMaster (appmaster.io) จะช่วยให้กำหนดกฎการค้นหาไปพร้อมกับแบบจำลองข้อมูลและตรรกะธุรกิจ เพื่อพฤติกรรม UI และทางเลือกฐานข้อมูลจะไม่เบี่ยงเบนเมื่อความต้องการเปลี่ยน
คำถามที่พบบ่อย
มองเป็น “หาบันทึกที่ฉันคิดไว้ ให้เร็วที่สุด” มากกว่าการเรียกดูทั่วไป เริ่มจากเขียนความตั้งใจจริงของผู้ใช้ (ค้นหาด้วย ID, ค้นหาชื่อ/อีเมลที่ยอมรับการพิมพ์ผิดได้, ค้นหาข้อความยาว) และตัวกรองเริ่มต้นที่พวกเขาใช้บ่อย ตัดสินใจเหล่านี้จะบอกคุณว่าควรรันคำค้นแบบไหนและควรลงทุนกับดัชนีใด
ILIKE '%term%' มีไวด์การ์ดนำหน้า ดังนั้น PostgreSQL มักจะไม่ใช้ดัชนี B-tree ปกติและต้องสแกนหลายแถว มันอาจดูโอเคกับตารางเล็ก ๆ แต่จะช้าลงอย่างมากเมื่อข้อมูลโตขึ้น หากต้องการการจับสตริงย่อยหรือการทนต่อการพิมพ์ผิด ให้วางแผนใช้ trigram หรือ full-text แทนการคาดหวังว่า ILIKE จะขยายตัวได้
ใช้การเปรียบเทียบแบบตรง เช่น WHERE id = $1 หรือ WHERE email = $1 และรองรับด้วยดัชนี B-tree (โดยปกติอีเมลมักเป็น unique) การค้นหาแบบตรงคือการค้นหาที่ถูกที่สุดและทำให้ผลลัพธ์น่าเชื่อถือ หากผู้ใช้วางหมายเลขตั๋วหรืออีเมลทั้งตัว ให้ส่งคำค้นไปที่เส้นทางนี้ก่อน
ชอบรูปแบบพ้องต้นแบบที่ผูกกับจุดเริ่มต้นเช่น name ILIKE 'ann%' และทำให้สอดคล้องกับดัชนีของคุณ สำหรับพฤติกรรมไม่สนใจตัวพิมพ์ ตัวอย่างที่ใช้กันบ่อยคือค้นหา lower(name) และสร้างดัชนีบนนิพจน์เดียวกันเพื่อให้ query planner ใช้งานได้ หากไม่ได้ปักลวดลายที่จุดเริ่มต้น prefix search จะไม่พอ
ใช้ดัชนี trigram เมื่อผู้ใช้พิมพ์เศษข้อความ พิมพ์ผิดเล็กน้อย หรือจำได้เพียง "ประมาณนั้น" โดยเฉพาะบนฟิลด์สั้น ๆ เช่น ชื่อ บริษัท หัวเรื่อง SKU โค้ด และชื่อผู้ใช้ มันจับชิ้นส่วน 3 ตัวอักษรและเปรียบเทียบความคล้ายคลึงกัน คัดเลือกคอลัมน์ที่ผู้ใช้จริง ๆ ใช้ เพราะดัชนี trigram ใหญ่กว่าและเพิ่มค่าใช้จ่ายเมื่อเขียน
ใช้ full-text search เมื่อผู้ใช้ค้นหาประโยคหรือคีย์เวิร์ดในเนื้อหาที่ยาวกว่า เช่น บันทึก ข้อความ คำอธิบาย หรือเอกสารความรู้ ข้อได้เปรียบหลักคือการคัดอันดับความเกี่ยวข้อง เพื่อให้ผลลัพธ์ที่ดีที่สุดอยู่บนสุด แต่อย่าลืมพฤติกรรมของภาษา เช่น สเตมมิงและการตัด stop-word ซึ่งเป็นประโยชน์กับเนื้อหาเชิงประโยคแต่บางครั้งอาจทำให้คำสั้น ๆ ไม่คืนค่าอะไร
เพิ่ม partial index เมื่อการค้นหาส่วนใหญ่มีตัวกรองเดียวกัน เช่น deleted_at IS NULL, status = 'open' หรือข้อจำกัด tenant/workspace ดัชนีจะครอบคลุมเฉพาะชุดที่ใช้งานบ่อย ทำให้ขนาดเล็กลงและเร็วขึ้น ตรวจสอบให้คำค้นของคุณใช้เงื่อนไขเดียวกับ partial index เป๊ะ ๆ มิฉะนั้น PostgreSQL อาจไม่ใช้มัน
ใช้บันไดลำดับความสำคัญที่สม่ำเสมอเพื่อให้ผลลัพธ์คาดเดาได้: exact match ก่อนสำหรับ ID/อีเมล, ตามด้วย prefix ถ้าเหมาะ, แล้ว trigram สำหรับชื่อ/หัวเรื่องที่ยอมรับการพิมพ์ผิด, และ full-text สำหรับบันทึกหรือคำอธิบายยาว ๆ ใช้ตัวกรองเริ่มต้นก่อนเพื่อลดจำนวนแถวที่การค้นหาแบบคลุมเครือต้องพิจารณา
ตั้งกฎง่าย ๆ เช่น ให้ต้องมี 3+ ตัวอักษรก่อนจะรัน fuzzy search และใช้การค้นหาสั้น ๆ เพื่อแสดงรายการล่าสุดหรือรายการที่เข้าถึงบ่อย ๆ การป้อน 1–2 ตัวอักษรสร้างสัญญาณรบกวนมากและอาจทำงานหนักโดยไม่คุ้มค่า กำหนดด้วยว่าจะแสดงอะไรเมื่อช่องว่างว่าง
สร้างดัชนีแล้วตรวจสอบ query จริงด้วย EXPLAIN (ANALYZE, BUFFERS) บนขนาดข้อมูลที่สมจริง มีกระบวนการปล่อยทีละอย่างเพื่อให้ย้อนกลับง่าย บนตารางใหญ่ ให้สร้างดัชนีด้วย CREATE INDEX CONCURRENTLY เพื่อไม่บล็อกการเขียน และถ้าเป็นไปได้ ทำงานนี้หลัง feature flag เพื่อตรวจวัดเวลาเฉลี่ยก่อน-หลัง หากคุณสร้างหน้าจอใน AppMaster (appmaster.io) ให้กำหนดกฎการค้นหาควบคู่กับแบบจำลองข้อมูลและตรรกะธุรกิจเพื่อป้องกันความคลาดเคลื่อนเมื่อความต้องการเปลี่ยน


