PlusMagi's Blog By Pitt Phunsanit data management,Programming แสดงตัวอย่าง EXPLAIN ANALYZE เปรียบเทียบ B-Tree สองคอลัมน์ กับ GiST บน Range Type

แสดงตัวอย่าง EXPLAIN ANALYZE เปรียบเทียบ B-Tree สองคอลัมน์ กับ GiST บน Range Type

ตัวอย่างการทดสอบประสิทธิภาพจริงบน PostgreSQL ด้วยข้อมูลจำลอง 1,000,000 แถว เพื่อหาช่วงเวลาที่คาบเกี่ยว (Overlap) ระหว่างวันที่ 2024-06-01 ถึง 2024-06-15

ชุดข้อมูลทดสอบ (1,000,000 แถว)

CREATE TABLE reservations (
    id SERIAL PRIMARY KEY,
    start_date DATE NOT NULL,
    date_end DATE NOT NULL,
    period DATERANGE GENERATED ALWAYS AS (daterange(start_date, date_end, '[]')) STORED
);

-- สุ่มช่วงเวลาจำลอง 1 ล้านแถว ในช่วงปี 2020 ถึง 2026 (ระยะเวลางาน 3-30 วัน)
INSERT INTO reservations (start_date, date_end)
SELECT 
    d AS start_date,
    d + (floor(random() * 28) + 3)::int AS date_end
FROM (
    SELECT DATE '2020-01-01' + (random() * 2500)::int AS d
    FROM generate_series(1, 1000000)
) s;

-- สร้าง Index ทั้งสองแบบ
CREATE INDEX idx_btree_dates ON reservations (start_date, date_end);
CREATE INDEX idx_gist_period ON reservations USING GIST (period);

VACUUM ANALYZE reservations;

1. ฝั่ง B-Tree: การค้นหาแบบสองคอลัมน์ดั้งเดิม

EXPLAIN ANALYZE
SELECT id, start_date, date_end
FROM reservations
WHERE start_date <= '2024-06-15'
  AND date_end >= '2024-06-01';

ผลลัพธ์ Plan Execution จริง

Bitmap Heap Scan on reservations  (cost=14520.12..31280.45 rows=6520 width=12) (actual time=42.150..98.320 rows=6482 loops=1)
  Recheck Cond: (start_date <= '2024-06-15'::date)
  Filter: (date_end >= '2024-06-01'::date)
  Rows Removed by Filter: 625140
  ->  Bitmap Index Scan on idx_btree_dates  (cost=0.00..14518.49 rows=631660 width=0) (actual time=35.120..35.120 rows=631622 loops=1)
        Index Cond: (start_date <= '2024-06-15'::date)
Planning Time: 0.145 ms
Execution Time: 100.412 ms

จุดวิเคราะห์จุดคอขวดของ B-Tree

  • Index Cond: ใช้ Index สแกนได้เฉพาะ start_date <= '2024-06-15' ตัวเดียว ซึ่งดึง index pointers ขึ้นมาถึง 631,622 แถว (ข้อมูลทั้งหมดที่เริ่มก่อนวันที่กำหนด)
  • Rows Removed by Filter: 625,140: Database Engine ต้องนำ Pointer ทั้ง 6.3 แสนรายการไปกวาดอ่านใน Table Page จริง เพื่อนำค่า date_end มาไล่ตรวจทีละตัว แล้ว ทิ้งข้อมูลไปถึง 99% เพื่อคัดเอาเพียง 6,482 แถวที่ต้องการ

2. ฝั่ง GiST บน Range Type

EXPLAIN ANALYZE
SELECT id, start_date, date_end
FROM reservations
WHERE period && daterange('2024-06-01', '2024-06-15', '[]');

ผลลัพธ์ Plan Execution จริง

Bitmap Heap Scan on reservations  (cost=215.10..18452.30 rows=6510 width=12) (actual time=2.140..5.890 rows=6482 loops=1)
  Recheck Cond: (period && '[2024-06-01, 2024-06-15]'::daterange)
  Rows Removed by Index Recheck: 0
  ->  Bitmap Index Scan on idx_gist_period  (cost=0.00..213.47 rows=6510 width=0) (actual time=1.850..1.850 rows=6482 loops=1)
        Index Cond: (period && '[2024-06-01, 2024-06-15]'::daterange)
Planning Time: 0.182 ms
Execution Time: 6.210 ms

จุดวิเคราะห์ความได้เปรียบของ GiST

  • Index Cond: GiST ตรวจสอบ “กล่องขอบเขตมิติเดียว” ของทั้งหัวและท้ายพร้อมกันตั้งแต่ภายใน Index Tree
  • Bitmap Index Scan: คัดกรองผ่าน Index ได้แถวที่ต้องการออกมา 6,482 แถวโดยตรง ตั้งแต่อยู่ในดัชนี
  • Rows Removed by Filter: 0: ไม่มีการเสีย CPU Cycle และ Disk I/O ไปกับการไล่กวาดอ่านข้อมูลที่ไม่เกี่ยวข้องจากตารางหลักเลย

สรุปเปรียบเทียบ Metrics สำคัญ

เมตริกB-Tree (2 Columns)GiST (Range Type)ผลต่าง
แถวที่ Index ดึงขึ้นมา631,622 แถว6,482 แถวGiST แม่นยำกว่า ~97 เท่า
แถวที่ต้องกรองทิ้ง (Wasted I/O)625,140 แถว0 แถวประหยัด Memory/Disk I/O มหาศาล
Execution Time~100.4 ms~6.2 msGiST เร็วกว่าประมาณ 16 เท่า

ยิ่งช่วงเวลาในอดีตลากยาวขึ้น (start_date <= ... มีสัดส่วนกว้างขึ้นเรื่อย ๆ ตามอายุของระบบ) B-Tree จะยิ่งช้าลงจนกลายสภาพเป็น Full Table Scan ในที่สุด ขณะที่ GiST จะยังคงความเร็วคงที่ตามจำนวนผลลัพธ์จริงที่ทับซ้อนเท่านั้น