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