- แนะนำ 3 แนวทางสร้างสรรค์ สำหรับ เพิ่มประสิทธิภาพคิวรี PostgreSQL โดยหลุดจากกรอบเดิม แทนการเพิ่มดัชนีหรือเขียนคิวรีใหม่
- ได้แก่ การตัด full table scan ด้วย CHECK constraint, การปรับแต่งข้อมูล cardinality ต่ำด้วย function-based index, และ การทำ unique constraint ด้วย Hash index
- 1. ใช้การตั้งค่า
constraint_exclusion เพื่อป้องกันการสแกนที่ไม่จำเป็นในคิวรีที่มีเงื่อนไขผิด
- 2. ใช้ function-based index และ virtual generated column เพื่อลดขนาดดัชนีและทำให้คิวรีมีความสม่ำเสมอ
- 3. ผสาน Hash index กับ exclusion constraint เพื่อทำ unique constraint สำหรับคอลัมน์ข้อความขนาดใหญ่ได้อย่างมีประสิทธิภาพ และประหยัดพื้นที่จัดเก็บอย่างมาก
ตัด full table scan ด้วย CHECK constraint
- แม้คอลัมน์
plan จะมี CHECK constraint ที่ยอมรับเฉพาะค่า 'free', 'pro' เท่านั้น แต่ถ้ารันคิวรีผิดเป็น 'Pro' PostgreSQL ก็ยังสแกนทั้งตาราง
- ใน execution plan จะอ่านครบ 100,000 แถว ทั้งที่ผลลัพธ์จริงมี 0 แถว
- หากตั้งค่าพารามิเตอร์
constraint_exclusion เป็น 'on' PostgreSQL จะพิจารณา constraint และ ข้ามการสแกนทั้งหมด
- เวลารันลดจาก 7.4ms เหลือ 0.008ms
- ค่าเริ่มต้นคือ
'partition' และในคิวรีง่าย ๆ อาจมี planning overhead เพิ่มขึ้นแทน
- แต่ในสภาพแวดล้อมแบบ BI และรายงาน ที่ผู้ใช้มักใส่เงื่อนไขผิดบ่อย การตั้งเป็น
'on' มีประโยชน์
ปรับแต่งข้อมูล cardinality ต่ำด้วย function-based index
- ในตาราง
sale ที่มีข้อมูลการขาย 10 ล้านรายการ เมื่อต้องรัน คิวรีสรุปยอดขายรายวัน จะใช้เวลา 627ms หากสแกนทั้งตาราง
- หากเพิ่ม B-Tree index ที่คอลัมน์
sold_at เวลาจะลดเหลือ 187ms แต่ดัชนีมีขนาดใหญ่ถึง 214MB
- หากสร้าง function-based index บน expression
date_trunc('day', sold_at) ขนาดจะลดเหลือ 66MB และเวลาเหลือ 145ms ซึ่งเร็วขึ้นอีก
- เป็นไปได้เพราะข้อมูล cardinality ต่ำทำให้เกิด index deduplication ได้
- แต่ expression ในคิวรีต้องตรงกับนิยามดัชนีแบบเป๊ะ จึงต้อง รักษาความสม่ำเสมอของ expression
- วิธีหนึ่งคือสร้าง VIEW ที่มี expression เดียวกันไว้ หรือ
- เพิ่ม virtual generated column ที่รองรับตั้งแต่ PostgreSQL 18 เพื่อทำให้ความสม่ำเสมอเป็นอัตโนมัติ
- เมื่อใช้ virtual generated column ดัชนีจะถูกใช้งานโดยอัตโนมัติ ทำให้ได้ทั้ง ดัชนีขนาดเล็ก คิวรีที่เร็ว และ expression ที่สม่ำเสมอ
- อย่างไรก็ตาม ใน PostgreSQL 18 ยัง ไม่รองรับการสร้างดัชนีบน virtual column และคาดว่าจะรองรับในเวอร์ชัน 19
ทำ unique constraint ด้วย Hash index
- ในตาราง
urls ที่เก็บ URL ยาว ๆ หากสร้าง unique index แบบ B-Tree เพื่อ ป้องกัน URL ซ้ำ ดัชนีจะมีขนาดถึง 154MB
- Hash index มีขนาดเล็กกว่ามาก เพราะไม่ได้เก็บค่าจริง แต่เก็บเฉพาะค่าแฮช
- โดยปกติ PostgreSQL ไม่รองรับ unique Hash index โดยตรง แต่
- สามารถใช้ exclusion constraint ในรูปแบบ
EXCLUDE USING HASH (url WITH =) เพื่อ อ้อมไปทำ unique constraint ได้
- วิธีนี้ยังคงเกิดข้อผิดพลาดเมื่อพยายาม insert ข้อมูลซ้ำ และ ประสิทธิภาพคิวรีก็เร็วกว่า B-Tree ด้วย (0.022ms vs 0.046ms)
- ดัชนีมีขนาด 32MB ซึ่ง เล็กกว่า B-Tree มากกว่า 5 เท่า
- ข้อเสีย:
- อ้างอิงด้วย foreign key ไม่ได้ (ใช้ constraint
REFERENCES ไม่ได้)
- มีข้อจำกัดด้านความเข้ากันได้กับคำสั่ง
INSERT ... ON CONFLICT
- สามารถใช้
ON CONFLICT ON CONSTRAINT หรือ MERGE แทนได้
- Hash index เหมาะกับการ รับประกันความไม่ซ้ำในคอลัมน์ข้อความขนาดใหญ่ และเป็น ทางเลือกที่ประหยัดพื้นที่ เมื่อไม่ต้องใช้ foreign key
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ดัชนีมีขนาด 214MB ซึ่งประมาณครึ่งหนึ่งของขนาดทั้งตาราง
จากมุมมองนักวิเคราะห์ถือว่าดี แต่ในแง่ประสิทธิภาพการเขียนจะเกิดปัญหา write amplification
การออกแบบดัชนีจะแตกต่างกันไปตามสัดส่วนการอ่าน/เขียน และนี่จึงเป็นเหตุผลที่มี data warehouse หรือ read replica
ถ้าต้องรองรับผู้ใช้จำนวนมากมาก ก็ควรหลีกเลี่ยงการมีดัชนี BI/OLAP บน OLTP DB
ถ้าแพตเทิร์นการเข้าถึงตารางคงที่ ตัวตารางเองก็อาจกลายเป็นดัชนีและได้ประสิทธิภาพโดยไม่มี write amplification
คิดว่าตัวอย่างแรกน่าจะนิยาม
Planเป็น enum type จะดีกว่ามันเบากว่าข้อความ และเมื่อมีการใส่ตัวกรองผิดก็จะให้ error แทนที่จะคืนผลลัพธ์ว่าง ทำให้ปลอดภัยกว่า
เป็นบทความที่ยอดเยี่ยม ใช้ PostgreSQL กับ MySQL มาหลายสิบปีแล้ว แต่พออ่านบทความนี้ก็ยังรู้สึกว่าตัวเองรู้เพียง ส่วนเล็ก ๆ ของความเป็นไปได้ทั้งหมด
สิ่งที่น่าสนใจที่สุดคือคำสั่ง
MERGEที่กล่าวถึงตอนท้ายบทความปกติจะใช้
INSERT ... ON CONFLICT DO UPDATEเพื่อทำ upsert แต่MERGEดูเหมือนจะทรงพลังกว่าและใช้ได้ในหลายสถานการณ์มากกว่าMERGEอยู่ในมาตรฐาน SQL มานานแล้ว แต่ Postgres เลื่อนการนำมาใช้เพราะ ปัญหาความไม่เป็นอะตอมของโมเดล MVCCมีอธิบายไว้ใน บทความบล็อกของ pganalyze ด้วย
ส่วนตัวฉันชอบ
INSERT ... ON CONFLICTมากกว่า และจะใช้MERGEเฉพาะตอนที่จำเป็นจริง ๆ พร้อมระวังเรื่อง การจัดการ error เป็นพิเศษINSERT ... ON CONFLICTคาดเดาพฤติกรรมได้มากกว่าดู บทความเปรียบเทียบบน modern-sql.com
COPY INTOในรูปแบบ binary format จะเร็วที่สุด แทบไม่มี overhead ฝั่งเซิร์ฟเวอร์เลยBRIN index ที่บทความไม่ได้พูดถึงก็น่าสนใจ
ถ้าข้อมูลเพิ่มขึ้นแบบโมโนโทนิก มันเป็นดัชนีที่เล็กและเร็วมากอย่างเหมาะสม
เช่น ข้อมูล timestamp ที่เซิร์ฟเวอร์รับเข้ามา ซึ่งอาจมีลำดับคลาดเคลื่อนเล็กน้อยก็ยังเหมาะ
สำหรับ UUIDv7 อาจต้องปรับ
pages_per_rangeรู้สึกเสียดายเสมอที่ hash index ตั้ง unique constraint ไม่ได้
ดูเหมือนถ้ามีแค่ glue code สำหรับแปลงเป็น exclusion constraint ก็น่าจะแก้ได้ เลยสงสัยว่าทำไมยังไม่มี
การตรวจสอบความเป็นเอกลักษณ์ด้วยแฮชไม่ได้รองรับในดัชนีเพราะ จัดการ collision ไม่ได้
วิธีแก้ที่เสนอมาก็เจอปัญหาเดียวกัน
Postgres จะถือว่าซ้ำก็ต่อเมื่อทั้งแฮชและค่าจริงตรงกัน
เนื้อหาในบทความสดใหม่ดี virtual column กับ hash index น่าสนใจ แต่ยังให้ความรู้สึกว่ายังไม่ถูกรวมเข้ากับ ecosystem อย่างสมบูรณ์
ส่วน hash index มีข้อจำกัดมานาน แต่ก็ค่อย ๆ ดีขึ้น และ automatic unique constraint คือโจทย์ที่ยังเหลืออยู่
คิดว่าใช้ stored generated column แล้วสร้างดัชนีได้ทันทีไม่ใช่หรือ
แม้จะรองรับตั้งแต่ PostgreSQL 14 แต่ผลลัพธ์จะถูก เก็บไว้จริงในเชิงกายภาพและกินพื้นที่จัดเก็บเพิ่ม
หลังย้ายขึ้นคลาวด์แล้ว ก็แทบไม่ได้ จัดการ pgsql โดยตรง ในสภาพแวดล้อมเซิร์ฟเวอร์คงที่อีกเลย
เลยสงสัยว่า การไฮไลต์ไวยากรณ์ SQL ที่อยู่ในบทความเป็นฟีเจอร์ในตัวหรือเป็นเครื่องมือแยก
แต่เวลาคัดลอกคิวรียาว ๆ แล้วมีการขึ้นบรรทัดใหม่ มันจะเติมช่องว่างให้อัตโนมัติ ซึ่งค่อนข้างน่ารำคาญ