- ดัชนีของ PostgreSQL เป็นโครงสร้างสำคัญที่ช่วยเพิ่มความเร็วในการเข้าถึงข้อมูล โดยลดปริมาณข้อมูลที่ต้องอ่านจากดิสก์เพื่อ ปรับปรุงประสิทธิภาพของคิวรี
- ดัชนีมีให้เลือกหลายรูปแบบ เช่น Btree, Hash, BRIN, GIN, GiST, SP-GiST ซึ่งแต่ละแบบถูกปรับให้เหมาะกับลักษณะข้อมูลและรูปแบบคิวรีที่ต่างกัน
- ดัชนีมีต้นทุนหลายด้าน เช่น พื้นที่ดิสก์, ประสิทธิภาพการเขียน, ความซับซ้อนของ query planner, การใช้หน่วยความจำ
- ฟีเจอร์ขั้นสูงอย่าง partial index, multicolumn index, covering index, expression index สามารถช่วยเพิ่มประสิทธิภาพสูงสุดในบางสถานการณ์ได้
- การเลือกและจัดการดัชนีอย่างเหมาะสมถูกเน้นว่าเป็น องค์ประกอบสำคัญของการปรับแต่งประสิทธิภาพ PostgreSQL
แนวคิดพื้นฐานของดัชนี
- ดัชนีคือโครงสร้างที่ช่วยให้ฐานข้อมูล ลดปริมาณข้อมูลที่ต้องอ่านจากดิสก์ เพื่อให้คิวรีทำงานได้เร็วขึ้น
- Primary key, unique key, exclusion constraint ก็ถูกทำให้ทำงานผ่านดัชนีเช่นกัน
- ดัชนีมีประสิทธิภาพเมื่อผลลัพธ์ของคิวรีมีน้อยกว่า 15~20% ของทั้งตาราง และหากมากกว่านั้น sequential scan อาจมีประสิทธิภาพดีกว่า
- PostgreSQL มี ประเภทดัชนี 6 แบบ มาให้โดยค่าเริ่มต้น และสามารถใช้ประเภทเพิ่มเติมได้ผ่านส่วนขยาย
- ดัชนีแต่ละตัวจะเชื่อมค่าคีย์เข้ากับตำแหน่งข้อมูลที่เกี่ยวข้อง (TID)
โครงสร้างข้อมูลที่จัดเก็บบนดิสก์
- ตารางของ PostgreSQL ถูกเก็บเป็นไฟล์ heap และประกอบด้วยหน้า (page) ขนาด 8KB
- แต่ละแถว (tuple) ถูกเก็บโดยไม่มีลำดับตายตัว และมีที่อยู่ภายในระบุด้วย ctid (current tuple id)
- ตัวอย่าง:
(0,1) หมายถึงทูเพิลตัวแรกของหน้า 0
- ดัชนีจะเชื่อมตำแหน่งเหล่านี้ของ heap (
ctid) ด้วยโครงสร้างต้นไม้เพื่อรองรับการค้นหาอย่างรวดเร็ว
วิธีที่ดัชนีช่วยเร่งการเข้าถึงข้อมูล
- หากไม่มีดัชนี PostgreSQL จะทำ sequential scan โดยอ่านทุกหน้า
- ในตัวอย่างคิวรีเมื่อต้องค้นหา
name='Ronaldo' จะต้องอ่าน 6272 หน้าและใช้เวลา 265ms
- เมื่อเพิ่มดัชนี จะเปลี่ยนเป็น Index Scan อ่านเพียง 4 หน้าและเสร็จใน 0.077ms
- ดัชนีจะจับคู่ค่ากับ
ctid เพื่อค้นหาเฉพาะแถวที่ต้องการได้อย่างรวดเร็ว
- ขนาดไฟล์ดัชนีอาจใกล้เคียงกับขนาดตารางได้ (เช่น ตาราง 30MB → ดัชนี 30MB)
ต้นทุนของดัชนี
- นอกจากช่วยเพิ่มประสิทธิภาพแล้ว ดัชนียังมาพร้อม ภาระต้นทุน หลายด้าน
พื้นที่ดิสก์
- ดัชนีใช้พื้นที่จัดเก็บแยกต่างหาก และ อาจมีขนาดใหญ่กว่าตาราง
- ทำให้มีต้นทุนเพิ่มเติมในการสำรองข้อมูล การทำสำเนา และการกู้คืนจากความเสียหาย
- สามารถปรับปรุงประสิทธิภาพด้านพื้นที่ได้ด้วย partial index, multicolumn index, BRIN เป็นต้น
งานเขียนข้อมูล
- เมื่อมี
UPDATE, INSERT, DELETE แล้วคอลัมน์ที่อยู่ในดัชนีถูกเปลี่ยน จะเกิด overhead จากการอัปเดตดัชนี
Query planner
- ยิ่งมีดัชนีมากขึ้น ตัว planner ก็ต้องพิจารณาทางเลือกมากขึ้น ทำให้ใช้เวลาสร้างแผนคิวรีนานขึ้น
การใช้หน่วยความจำ
- หน้าของดัชนีจะถูกโหลดไว้ใน shared buffer เพื่อแคช และยิ่งมีดัชนีมากก็ยิ่งเพิ่มภาระหน่วยความจำ
- เนื่องจากมี ข้อจำกัดขนาดโหนดของ btree คอลัมน์ที่มีขนาดใหญ่จะทำให้ต้นไม้ลึกขึ้น
- ในการ sort, multicolumn scan, vacuum, reindex ก็ยังมีการใช้ work memory เพิ่มเติมด้วย
ประเภทหลักของดัชนี
Btree
- เป็น โครงสร้างดัชนีเริ่มต้น ของ PostgreSQL และเป็นดัชนีอเนกประสงค์ที่ใช้ใน DBMS ส่วนใหญ่
- รองรับการค้นหาอย่างรวดเร็วด้วยความซับซ้อนเวลา O(log n)
- เป็น โครงสร้างต้นไม้สมดุล ที่ทุก leaf node มีความลึกเท่ากัน
- เหมาะกับการทำงานของ ORDER BY, JOIN และใช้กับข้อกำหนด primary key และ unique key
- โหนดภายในเก็บตัวชี้ไปยังโหนดย่อย ส่วน leaf node จะเก็บคีย์และ heap pointer
- สามารถค้นหาได้สองทิศทางผ่าน left/right node pointer
การใช้หลายดัชนีร่วมกัน
- PostgreSQL สามารถรวมหลายดัชนีด้วย bitmap AND/OR เพื่อจัดการเงื่อนไขแบบผสม
- ตัวอย่าง: เมื่อมีเงื่อนไข
age=30 AND login_count=100 จะรวมบิตแมปจากดัชนีสองตัวเข้าด้วยกัน
ดัชนีหลายคอลัมน์
- สามารถรวมหลายคอลัมน์ไว้ในดัชนีเดียวเพื่อ ประหยัดพื้นที่และเพิ่มความเร็ว
- อย่างไรก็ตาม ลำดับของคอลัมน์มีความสำคัญ และจะใช้ดัชนีได้เฉพาะเงื่อนไขที่ตรงจากด้านซ้ายก่อนเท่านั้น
Partial index
- ใช้เงื่อนไขเพื่อทำดัชนีเฉพาะบางแถว
- ลดขนาดดัชนี ช่วยให้เหมาะกับ RAM มากขึ้น และเพิ่มความเร็วในการค้นหา
- ตัวอย่าง:
create index on rules(status) where status='enabled';
- มีประโยชน์เมื่อการกระจายค่ามีความไม่สมดุล (เช่น
status <> 'TODO')
Covering index
- หากคอลัมน์ทั้งหมดที่คิวรีต้องใช้ถูกรวมอยู่ในดัชนี ก็จะ คืนผลลัพธ์ได้โดยไม่ต้องเข้าถึง heap (index-only scan)
create index abc_cov_idx on bar(a, b) including c;
- ใช้พื้นที่ได้มีประสิทธิภาพกว่า multicolumn index
Expression index
- เป็นการทำดัชนีจาก ผลลัพธ์ของฟังก์ชันหรือ expression แทนค่าคอลัมน์โดยตรง
- ตัวอย่าง:
CREATE INDEX idx_lower_name ON customers (lower(name));
- มีประโยชน์เมื่อค้นหาด้วยค่าที่แปลงแล้วอย่าง
LOWER(name)
- ใช้ได้เฉพาะฟังก์ชันแบบ immutable เท่านั้น
Hash
- เป็นดัชนีที่อิงกับ โครงสร้าง hash map และ ประหยัดพื้นที่ สำหรับข้อมูลอย่างสตริงยาวหรือ UUID
- เก็บ hash code ขนาด 32 บิตเพื่อลดขนาด
- รองรับเฉพาะการเปรียบเทียบแบบ เท่ากัน (=) เท่านั้น และไม่รองรับการ sort หรือดัชนีหลายคอลัมน์
- หากมีการกระจาย hash อย่างสม่ำเสมอ อาจให้ ประสิทธิภาพการอ่านที่เร็วกว่า Btree ได้
- ตามเอกสารทางการ hash index สามารถ เข้าถึง bucket page ได้โดยตรง จึงช่วยลด I/O ในตารางขนาดใหญ่
BRIN (Block Range Index)
- เป็นดัชนีที่เก็บเพียง ค่าต่ำสุดและค่าสูงสุดของแต่ละช่วงบล็อก
- กะทัดรัดมากและเป็นมิตรกับแคช
- เหมาะกับ ข้อมูลขนาดใหญ่, append-only, time-series
- หากมีการอัปเดตแถวบ่อย ประสิทธิภาพจะลดลงจาก การเก็บข้อมูลซ้ำเพราะ MVCC
- สามารถปรับสมดุลระหว่าง ความแม่นยำกับขนาด ได้ด้วยการตั้งค่า
pages_per_range
GIN (Generalized Inverted Index)
- เป็นดัชนีที่เหมาะกับ การค้นหาในข้อมูลเชิงประกอบ
- รองรับการค้นหาองค์ประกอบเฉพาะในข้อความ, อาร์เรย์, JSONB เป็นต้น
- ใช้ กลยุทธ์เฉพาะ (opclass) ตามชนิดข้อมูล
- สำหรับ JSON แนะนำให้ใช้กับ คอลัมน์ JSONB ส่วนข้อความแนะนำให้ใช้ร่วมกับ tsvector หรือส่วนขยาย pg_trgm
GiST & SP-GiST
- Generalized Search Tree (GiST) และ Space-Partitioned GiST (SP-GiST) เป็นเฟรมเวิร์กสำหรับสร้างดัชนีของข้อมูลบางประเภท
- GiST รองรับ ต้นไม้สมดุล ส่วน SP-GiST รองรับ โครงสร้างไม่สมดุล
- ใช้กับ ข้อมูลภูมิสารสนเทศ, inet, range, text vector เป็นต้น
- GIN เด่นด้านการค้นหาที่รวดเร็ว ขณะที่ GiST มีต้นทุนในการสร้างและดูแลต่ำกว่า
- สำหรับ full-text search ควรเลือกให้เหมาะกับความต้องการระหว่างสองแบบนี้
บทสรุป
- ดัชนีคือหัวใจของการปรับแต่งประสิทธิภาพ PostgreSQL โดย ต้องสร้างสมดุลระหว่างความเร็วในการอ่านกับต้นทุนด้านการเขียนและการจัดเก็บ
- หากเลือกประเภทดัชนีให้เหมาะกับลักษณะข้อมูลและรูปแบบคิวรี ก็จะช่วยให้ ระบบฐานข้อมูลทำงานได้รวดเร็วและมีประสิทธิภาพ
- การออกแบบดัชนีอย่างเหมาะสมเป็นองค์ประกอบสำคัญในการ รองรับการขยายระบบและรักษาเสถียรภาพของระบบขนาดใหญ่
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เอกสารทางการของ PostgreSQL เขียนได้ดีมากและอ่านสนุกด้วย เลยอยากแชร์
เอกสารแนะนำ PostgreSQL Indexes
ส่วนของดัชนีหลายคอลัมน์แทบจะเหมือนกับที่ฉันเคยเรียนมา
แต่ก็สงสัยว่าใน PostgreSQL เวอร์ชันล่าสุดยังเป็นแบบนั้นอยู่หรือไม่
ก่อนหน้านี้เคยเห็นมีการใช้ bitmap index scan กับคิวรีที่คล้ายกับตัวอย่างที่สาม เลยทำให้เริ่มกลับมาทบทวน “ความเข้าใจแบบเดิม” อีกครั้ง
สำหรับเรื่องดัชนี ฉันคิดว่าเว็บไซต์และหนังสือ Use The Index, Luke เป็น งานคลาสสิก ที่ทั้งทีมควรอ่าน
ในเวอร์ชันก่อนหน้าก็ทำได้เหมือนกัน แต่ตอนนั้นต้องสแกนทั้งดัชนีจึงไม่มีประสิทธิภาพ
วิดีโอที่เกี่ยวข้อง: ลิงก์ YouTube
คิดว่าน่าจะดีถ้า PostgreSQL รองรับ incremental view maintenance เป็นความสามารถพื้นฐาน
แนวคิดนี้จะอัปเดตอัตโนมัติเมื่อข้อมูลต้นทางเปลี่ยน เหมือนดัชนี แต่ไม่ได้จำกัดอยู่แค่วิวบางประเภทและสามารถใช้กับวิวใดก็ได้
มีหลายโปรเจกต์ที่เกี่ยวข้อง เช่น Noria, Materialize, Apache Flink, GCP Continuous Queries, Spark Streaming Tables, Delta Tables, ClickHouse streaming tables, TimescaleDB, ksqlDB, StreamSQL
ฝั่ง PostgreSQL ช่วงหลังมีส่วนขยายชื่อ pg_ivm ที่เริ่มเข้ามาจัดการปัญหานี้
ประเด็น B-tree vs Hash อินเด็กซ์ น่าสนใจ
หลายคนคิดว่าคอลัมน์ ID เหมาะกับ hash มากกว่า แต่ในความเป็นจริง B-tree แบบปกติมักมีประสิทธิภาพดีกว่า
โดยเฉพาะการแทรกค่าที่เกือบเป็นลำดับต่อเนื่อง โครงสร้างแบบต้นไม้จะได้เปรียบกว่า
อย่างไรก็ตาม บล็อกโพสต์ที่อ้างถึงครั้งนี้กลับบอกว่า hash ชนะในการ benchmark
จังหวะของบทความนี้ดีมาก
กฎ leading column ของดัชนีหลายคอลัมน์เป็นเรื่องที่สับสนอยู่เสมอ แต่ด้วย bitmap index scan ทำให้มันไม่ร้ายแรงเหมือนเมื่อก่อน
ฟีเจอร์ skip scan ใน PostgreSQL 18 เปลี่ยนความเข้าใจเดิมไปมาก ดังนั้นถ้าเรียนมาจากเวอร์ชันเก่า ก็คงต้อง อัปเดต mental model กันใหม่
คิดว่าเป็นบทความที่ยอดเยี่ยมมากสำหรับ PostgreSQL
เรื่อง B-tree index เองก็อ้างอิง Use The Index, Luke มานานแล้วบ่อยครั้ง
คิดว่าเป็นงานที่ควรอ่าน
ไม่ได้เป็นแค่บทความเกริ่นนำแบบผิวเผิน แต่ลึกพอสมควรและยังอ่านได้ไม่ยาก ตราบใดที่ยังไม่ลงไปถึงโครงสร้างภายใน
ชอบ สไตล์การเขียนที่เรียบง่ายและถ่อมตัว แบบนี้
เป็นวิธีถ่ายทอดความรู้ที่ตรงไปตรงมาดี