ข้าม​ไป​ยัง​เนื้อหา

ทาง​เข้า — ลำดับ​ของ column, covering index และ​การ​ไม่​ต้อง​เรียง

index สอง​อัน​ที่​มี column ชุด​เดียวกันเป๊ะ ต่าง​กัน​แค่​ลำดับ ไม่ใช่​ของ​อย่าง​เดียวกัน และ​คำถาม​ข้อ​เดียวกัน​จ่าย​ราคา​ต่าง​กัน​ตาม​ว่า column ที่​ถาม​อยู่​ตัว​หน้า​หรือ​อยู่​ข้าง​หลัง

สูตร​ที่​ตัวเลข​ทั้ง​บท​เดิน​ตาม​มี​บรรทัด​เดียว คือ ต้นทุน​ของ​แผน index = k × ความ​สูง​ของ​ต้นไม้ + จำนวน​ใบ​ที่​เดิน​ผ่าน โดย k คือ​จำนวน​ค่าที่​ไม่​ซ้ำ​ของ column ที่​อยู่​หน้า column ที่​เรา​ถาม

สิ่ง​ที่​ระเบิด​ใน​สูตร​นั้น​คือ k ไม่ใช่​ตัว​อ่าน และ​บท​นี้​วัดผล​ของ​มัน​สาม​ด้าน

  • ลำดับ​ของ​คีย์​ภายใน index หนึ่ง​อัน ซึ่ง​ตัดสิน​ว่า​คำถาม​หนึ่ง​กลาย​เป็นการ​อ่าน​ต่อ​เนื่อง​ครั้ง​เดียว หรือ​การ​ลง​จากราก​ซ้ำ k ครั้ง
  • index ที่​ตอบ​คำถาม​ได้​จบ​ที่​ใบ โดย​ไม่​ต้อง​เอา​ตัว​ชี้​แถว​กลับ​ไป​เปิด heap อีกรอบ
  • การ​อาศัย​ลำดับ​ที่ index มี​อยู่​แล้ว แทน​การ​เรียง​ผลลัพธ์​เอง
📦 code ของ​บท​นี้

file ใหม่​ของ​บท​นี้​คือ qe/paths.py ซึ่ง​ไม่​ได้​สร้าง​โครงสร้าง​ใหม่​เลย​สัก​อย่าง มัน import qe/fixture.py ของ​บท 1, qe/heapfile.py ของ​บท 2 และ qe/bptree.py ของ​บท 3 มาใช้ตรงๆ

บท​นี้​ยก​มา​ห้า​จุด​ของ file นั้น ซึ่ง​เป็น​ที่​ที่​ตัวเลข​ทุก​ตัว​ใน​บท​เกิด​ขึ้น​จริง ส่วน​ที่​เหลือ​ของ file เป็นการ​ประกอบ​คำถาม​แล้ว​พิมพ์​ผลออก​มา รัน​ด้วย python3 qe/paths.py จาก folder ทำงาน​ด้าน​นอก​เหมือน​ทุก​บท

หน่วย​ของ​ต้นทุน​คือ​ค่า​อ่าน​เพจ​แบบ​เรียงตัว​หนึ่ง​เพจ = 1.0 ตาม​ที่​บท 5 ตั้ง​ไว้ ส่วน​บท​นี้​รายงาน​จำนวน​หน้าที่​อ่าน​ดิบๆ ไม่​ได้​แปลง​เป็นต้นทุน​ของ cost model หน้า​ทุก​หน้า​จึง​นับ​เป็น 1 เท่า​กัน​หมด ไม่​แยก​อ่าน​เรียง​กับ​อ่าน​สุ่ม

ตัวเลข​ทุก​ตัว​ข้าง​ล่าง​จึง​เทียบ​กันเอง​ได้ แต่​เทียบ​กับ​ราคา​ของ​บท 5 ไม่​ได้ และ​แปลง​เป็น​เวลา​ไม่​ได้

ตัวเลข​หน้าที่​แตะ​ทั้ง​บท​นับ​ภาย​ใต้​เงื่อนไข​เดียว​กับ​บท​ก่อนหน้า​ด้วย คือ​ยัง​ไม่มี buffer pool ตัว​ชี้​แถว​หนึ่ง​ตัว​จึง​เท่ากับ​หนึ่ง​หน้า​เสมอ และ​ทุก​ครั้ง​ที่​มี​เลข “หน้า​ไม่​ซ้ำ” พิมพ์​คู่​มา เลข​นั้น​คือ​ขอบ​ล่าง​ของ​กรณี​ที่ cache ได้​ทั้งหมด

ตาราง​ที่​บท​นี้​ถาม index ห้า​อัน​ที่​สร้าง​ทับ​มัน และ​วิธี​ที่ code บีบ​คีย์​ร่วม​ให้​เหลือ​เลข​ตัว​เดียว

ตาราง​ยัง​เป็น​ออเดอร์​ชุด​เดิม 200,000 แถว ที่​บท 2 เขียน​ลง heap file 3,153 หน้า การ​สแกน​ทั้ง​ตาราง​จึง​แตะ 3,153 หน้า และ​เลข​นี้​เป็น​ตัว​หาร​ของ​ทุก​อัตราส่วน​ใน​บท​นี้

จำนวน​ค่าที่​ไม่​ซ้ำ​ของ​สาม column ที่​บท​นี้​ใช้​ต่าง​กัน​คนละ​ระดับ คือ status 7 ค่า restaurant 10 ค่า และ customer_id 2,000 ค่า สาม​ตัว​นี้​คือ k ที่​จะ​โผล่​ใน​สูตร​ของ​ทุก​หัวข้อ​ข้าง​หน้า

คำ​ว่า access pathaccess pathวิธี​ที่​แผน​หนึ่ง​เดิน​เข้าไป​หยิบ​แถว เช่น สแกน heap ทั้ง​ตาราง เดิน index ช่วง​เดียว หรือ​เดิน index หลาย​ช่วง​แล้ว​ตาม​ตัว​ชี้​กลับ​ไป​อ่าน heap ใน​บท​นี้​หมาย​ถึง​วิธี​ที่​แผน​หนึ่ง​เดิน​เข้าไป​หยิบ​แถว ไม่ใช่​ตัว​โครงสร้าง​ที่​มัน​เดิน​ผ่าน index หนึ่ง​อัน​จึง​ให้​ทาง​เข้า​ได้​หลาย​ทาง และ​บท​นี้​จะ​วัด​ที​ละ​ทาง

index ทั้ง​ห้า​อัน​สร้าง​แบบ bulk load คือ​เรียง​คีย์​ให้​เสร็จ​ก่อน​แล้ว​ค่อย​ใส่ ซึ่ง​เป็น​วิธี​เดียว​กับ​ที่ CREATE INDEX ทำ

indexหน้าขนาดเทียบ heapความ​สูง
(status, restaurant)4932,019,328 B15.64 %2 ชั้น
(restaurant, status)4932,019,328 B15.64 %2 ชั้น
(customer_id, restaurant)4932,019,328 B15.64 %2 ชั้น
(restaurant)4932,019,328 B15.64 %2 ชั้น
(placed_at)4932,019,328 B15.64 %2 ชั้น

ห้า​แถว​นี้​เท่า​กัน​เป๊ะ​ทุก​ช่อง ซึ่ง​เป็น​ผล​ของ​การ​ออกแบบ ไม่ใช่​ข้อ​ค้น​พบ

qe/paths.py
def key_sr(row):
"""(status, restaurant) — status อยู่หน้า"""
return S_OF[row[2]] * NREST + R_OF[row[3]]
def key_rs(row):
"""(restaurant, status) — restaurant อยู่หน้า"""
return R_OF[row[3]] * NSTATUS + S_OF[row[2]]
def key_cr(row):
"""(customer_id, restaurant) — column หน้ามีค่าไม่ซ้ำถึง 2,000 ค่า"""
return (row[1] - 1) * NREST + R_OF[row[3]]
def key_r(row):
"""(restaurant) — index column เดียว ใช้เทียบเรื่อง covering"""
return R_OF[row[3]]
def key_t(row):
"""(placed_at) — index ของบท 3 เอามาใช้ต่อในหัวข้อการเรียง"""
return row[4]

คีย์​ร่วม​ของ index สอง column ถูก​บีบ​ให้​เหลือ uint32 ตัว​เดียว​ที่​เรียง​ตรง​กับ​ลำดับ​ของ​คู่ (column หน้า, column หลัง) เห็น​ได้​จาก key_sr ที่​คูณ​รหัส​ของ status ด้วย​จำนวน​ร้าน​ก่อน​แล้ว​ค่อย​บวก​รหัส​ร้าน

การ​คูณ​แล้ว​บวก​แบบ​นั้น​เทียบ​เท่ากับ​การ​ต่อ byte ของ​สอง column เข้า​ด้วย​กัน ซึ่ง​คือ​สิ่ง​ที่ index จริง​ทำ ค่า​ของ column หลัง​จึง​เรียง​อยู่​ภายใน​กลุ่ม​ของ column หน้า​เสมอ

S_OF กับ R_OF เก็บ​รหัส​ตาม​ลำดับ​ที่​ประกาศ​ไว้​ใน code ลำดับ​นั้น​ยืน​แทน collation ของ column จริง ไม่ใช่​ลำดับ​พจนานุกรม​ภาษา​ไทย

index ห้า​อัน​เท่า​กัน​หมด เพราะ​ของเล่น​นี้​บีบ​คีย์ ไม่ใช่​เพราะ​ของ​จริง​เป็น​แบบ​นั้น

รายการ​ใน​ใบ​ของ​ทุก index กว้าง 10 B เท่า​กัน​หมด เพราะ​คีย์​ร่วม​ถูก​บีบ​ให้​เหลือ uint32 ตัว​เดียว ขนาด​ของ file จึง​ออก​มา​เท่า​กัน​ทั้ง​ห้า​อัน

ของ​จริง​ไม่​เป็น​แบบ​นี้ index ที่​มี​สอง column กว้าง​กว่า index ที่​มี column เดียว และ​คีย์​ที่​เก็บ​ข้อความ​จริง​กว้าง​กว่า​เลขจำนวน​เต็ม​มาก

ผล​ดี​ของ​การ​บีบ​แบบ​นี้​คือ​ความ​ต่าง​ทุก​ตัวเลข​ใน​บท​นี้​มา​จาก ลำดับ ของ​คีย์​ล้วนๆ ไม่มี​ผล​ของ​ขนาด​ปน​มา​เลย ราคา​ที่​จ่าย​คือ​ตัวเลข​ขนาด​ของ file ใน​ตาราง​ข้าง​บน​ยก​ไป​ใช้​กับ index ของ​จริง​ไม่​ได้

คำถาม​เดียว​ถาม index สอง​อัน​ที่​สลับ​ลำดับ​กัน แล้ว​เทียบ​หน้าที่​แตะ​กับ​ส่วน​ต่าง​ที่​สูตร​ทำนาย​ได้

คำถาม​แรก​ของ​บท​นี้​คือ ออเดอร์​ของ​ร้าน ครัว​คุณ​แม่ มี​กี่​แถว การ​สแกน​ทั้ง​ตาราง​ตอบ​ว่า 11,934 แถว คิด​เป็น 5.97 % ของ​ตาราง และ​ทุก​แผน​ข้าง​ล่าง​ต้อง​คืนตัวเลข​เดียวกัน​นี้

เงื่อนไข restaurant = 'ครัวคุณแม่' บน index (restaurant, status) แปลง​เป็น​ช่วง​คีย์​ต่อ​เนื่อง​ช่วง​เดียว​ได้​เสมอ เพราะ​รหัส​ร้าน​คูณ​อยู่​ข้าง​หน้า ทุก​คีย์​ของ​ร้าน​นั้น​จึง​กอง​ติด​กัน

qe/paths.py
def span(prefix_code, width, lo_code=0, hi_code=None):
"""ช่วงคีย์ของ 'column หน้า = prefix_code' — คือช่วงต่อเนื่องหนึ่งช่วงเสมอ"""
hi_code = width - 1 if hi_code is None else hi_code
base = prefix_code * width
return (base + lo_code, base + hi_code)

บน index (status, restaurant) เงื่อนไข​เดียวกัน​แปลง​เป็น​ช่วง​เดียว​ไม่​ได้ เพราะ​คีย์​ของ​ร้าน​นั้น​ถูก​กระจาย​ไป​อยู่​ใน​กลุ่ม​ของ status ทั้ง 7 กลุ่ม แผน​จึง​ต้อง​อ่าน 7 ช่วง

ราคา​ของ​การ​อ่าน​หลาย​ช่วง​อยู่​ใน​ลูป​เดียว​ข้าง​ล่าง​นี้ ทุกรอบ​ของ​ลูป​คือ​การ​ลง​จากราก​ใหม่​หนึ่ง​ครั้ง

qe/paths.py
def seek_ranges(rd, ranges):
"""เดิน k ช่วงด้วย reader ตัวเดิม คืน (รายการ (key, rid), จำนวนหน้าที่แตะ)"""
rd.reset()
out = []
for lo, hi in ranges:
out.extend(rd.range(lo, hi))
return out, rd.reads
def scan_all(rd):
"""สแกน index ทั้งอัน: ลงจากรากครั้งเดียวแล้วเดินโซ่ใบยาวรวด"""
return seek_ranges(rd, [(0, 0xFFFFFFFF)])
แผนช่วง​ที่​ต้อง​อ่านindex แตะheap แตะเทียบ​สแกน 3,153 หน้า
(restaurant, status)restaurant อยู่​หน้า131 หน้า0 หน้าถูก​กว่า 101.7 เท่า
(status, restaurant)restaurant อยู่​หลัง743 หน้า0 หน้าถูก​กว่า 73.3 เท่า

31 หน้า​ของ​แผน​แรก​แตก​ได้​เป็นการ​ลง​จากราก 1 ครั้ง​ซึ่ง​กิน 2 หน้า กับ​การ​เดิน​โซ่​ใบ​ต่อ​เนื่อง​อีก 29 หน้า

43 หน้า​ของ​แผนที่​สอง​แตก​ได้​เป็นการ​ลง​จากราก 7 ครั้ง​ซึ่ง​กิน 14 หน้า กับ​การ​เดิน​โซ่​ใบ​อีก 29 หน้า​เท่า​เดิม​เป๊ะ

ใน​คำถาม​นี้​ใบ​ที่​เดิน​ผ่าน​ออก​มา​เท่า​กัน​ที่ 29 หน้า​ทั้ง​สอง​แผน ส่วน​ต่าง 12 หน้า​จึง​เป็น​ค่า​ลง​จากราก​ล้วนๆ คือ (7 − 1) × ความ​สูง 2 ชั้น

การ​อ่าน k ช่วง​ทำให้​ใบ​หัว​ท้าย​ของ​แต่ละ​ช่วง​ถูก​อ่าน​ซ้ำ​ได้ ใบ​ที่​เดิน​ผ่าน​ของ​แผน k ช่วง​จึง​เป็น​ขอบ​ล่าง ไม่ใช่​ค่า​คงที่​ที่​เท่ากับ​แผน​ช่วง​เดียว​เสมอ หัวข้อ​ข้าง​ล่าง​มี​คำถาม​ที่​มัน​ไม่​เท่า

flowchart TD
    Q["คำถามเดียว<br/>ออเดอร์ของร้าน ครัวคุณแม่ มีกี่แถว"]
    Q --> A["index (restaurant, status)<br/>restaurant อยู่หน้า"]
    Q --> B["index (status, restaurant)<br/>restaurant อยู่หลัง"]
    A --> A1["ลงจากราก 1 ครั้ง = 2 หน้า"]
    A1 --> A2["เดินโซ่ใบ 29 หน้า"]
    A2 --> A3["รวม 31 หน้า"]
    B --> B1["ลงจากราก 7 ครั้ง = 14 หน้า"]
    B1 --> B2["เดินโซ่ใบ 29 หน้า เท่ากันในคำถามนี้"]
    B2 --> B3["รวม 43 หน้า"]
    A3 --> D["ส่วนต่าง 12 หน้า<br/>= (7−1) × ความสูง 2 ชั้น"]
    B3 --> D
    D --> E["43 เทียบ 31 = 1.39 เท่า"]

คำ​บรรยาย​ภาพ: คำถาม​เดียวกัน​บน index สอง​อัน​ที่​มี column ชุด​เดียวกัน​แต่​สลับ​ลำดับ · ฝั่ง​ซ้าย restaurant อยู่​ตัว​หน้า​จึง​ได้​ช่วง​ต่อ​เนื่อง​ช่วง​เดียว ลง​จากราก​ครั้ง​เดียว · ฝั่ง​ขวา restaurant อยู่​ข้าง​หลัง​จึง​ต้อง​ลง​จากราก​ซ้ำ 7 ครั้ง​ตาม​จำนวน​ค่า​ของ status · ใน​คำถาม​นี้​ใบ​ที่​เดิน​ผ่าน​เท่า​กัน​ทั้ง​สอง​ฝั่ง​ที่ 29 หน้า ส่วน​ต่าง​จึง​เป็น​ค่า​ลง​จากราก​ล้วนๆ ซึ่ง​เป็น​ผล​ของ​ข้อมูล​ชุด​นี้ ไม่ใช่​กฎ

สลับ​ลำดับ​ที่ k = 7 แพง​ขึ้น 1.39 เท่า ยัง​ไม่ใช่​หน้าผา

สโลแกน​ที่​ว่า prefix ได้​ช่วง​เดียว ส่วน suffix ได้ k ช่วง เป็น​เรื่อง​จริง แต่​คำ​ที่​มัก​ตาม​มา​ว่า “ผิด​ลำดับ​แล้ว​พัง” ไม่​ตรง​กับ​ตัวเลข​เมื่อ k เล็ก ส่วน k จะ​ใหญ่​แค่​ไหน​ถึง​พัง​จริง หัวข้อ​ถัด​ไป​วัด​ให้​ดู

ที่ k = 7 ราคา​ต่าง​กัน 1.39 เท่า ซึ่ง​น้อย​กว่า​ที่​สโลแกน​ชวน​ให้​คิดมาก และ​แผนที่​แพง​กว่า​นั้น​ก็​ยัง​ถูก​กว่า​การ​สแกน​ทั้ง​ตาราง 73.3 เท่า​อยู่ดี

สิ่ง​ที่​ต้อง​จำ​จึง​ไม่ใช่ “อย่า​เอา column ไป​ไว้​ข้าง​หลัง” แต่​เป็น​สูตร k × ความ​สูง + ใบ​ที่​เดิน​ผ่าน ซึ่ง​บอก​ได้​ว่า​ราคา​จะ​ขยับ​เท่าไร​ก่อน​จะ​สร้าง index จริง

คำถาม​เดิม​บน index ที่ column หน้า​มี​ค่า​ไม่​ซ้ำ 2,000 ค่า และ​เงื่อนไข​ที่​ทำให้ k เท่ากับ​ตัวเลข​นั้น

คำถาม​เดิม​ทุก​ตัว​อักษร บน index (customer_id, restaurant) ซึ่ง column หน้า​มี​ค่า​ไม่​ซ้ำ​ถึง 2,000 ค่า

แผนช่วง​ที่​ต้อง​อ่านindex แตะheap แตะเทียบ​สแกน 3,153 หน้า
(customer_id, restaurant)restaurant อยู่​หลัง2,0004,023 หน้า0 หน้าแพง​กว่า 1.28 เท่า

4,023 หน้า​แตก​ได้​เป็นการ​ลง​จากราก 2,000 ครั้ง​ซึ่ง​กิน 4,000 หน้า กับ​การ​เดิน​ใบ​ต่อ​อีก​แค่ 23 หน้า

ตัวเลข 23 นั้น​เล็ก​เพราะ​แต่ละ​ช่วง​สั้น​มาก คือ​คู่​ลูกค้า​กับ​ร้าน​หนึ่ง​คู่​มี​ไม่​กี่​แถว แผน​จึง​แทบ​ไม่​ต้อง​เดิน​ต่อ​จาก​ใบ​ที่​ลง​มา​เจอ​เลย งาน​เกือบ​ทั้งหมด​เป็น​ค่า​ลง​จากราก

เทียบ​กับ​แผน prefix ของ​หัวข้อ​ก่อน ราคา​ต่าง​กัน 129.77 เท่า และ​แผน​นี้​แพง​กว่า​การ​สแกน​ทั้ง​ตาราง 1.28 เท่า ทั้ง​ที่​มัน​ไม่​แตะ heap เลย​สัก​หน้า

ตัว​อ่าน​ไม่​ได้​เปลี่ยน​เลย​ระหว่าง​สอง​หัวข้อ​นี้ มัน​เป็น seek_ranges ตัว​เดิม บน index ที่​มี​ความ​สูง​เท่า​กัน​และ​กว้าง​เท่า​กัน สิ่ง​เดียว​ที่​เปลี่ยน​คือ k จาก 7 เป็น 2,000

k ไม่ใช่​จำนวน​ค่าที่ เป็น​ไป​ได้ ของ column หน้า แต่​เป็น​จำนวน​ค่าที่ มี​อยู่​จริง ใน​ข้อมูล เพราะ​เอนจิน​ไม่​ได้​เดา​ค่า​เอง มัน​ถาม index ว่า​ค่า​ถัด​ไป​คือ​อะไร

ใน​ชุด​ข้อมูล​นี้ customer_id มี​ครบ​ทั้ง 2,000 ค่า ตัวเลข​สอง​ตัว​จึง​บังเอิญ​เท่า​กัน​พอดี ถ้า​มี​ลูกค้า​จำนวน​มาก​ที่​ไม่​เคย​สั่ง​เลย k จะ​น้อย​กว่า 2,000 และ​ราคา​จะ​ลด​ลง​ตาม​นั้น

นี่​คือ​เหตุผลตรงๆ ที่​เอนจิน​จริง​ต้อง​รู้​จำนวน​ค่าที่​ไม่​ซ้ำ​ของ column หน้า​ก่อน จึง​จะ​ยอม​เลือก​แผน k ช่วง

ตัวเลข​นั้น​ไม่​ได้​ถูก​เก็บไว้ตรงๆ แต่​หา​ได้​จาก sqlite_stat1 ของ​บท 4 ซึ่ง​เก็บ​จำนวน​แถว​ทั้งหมด​คู่​กับ​จำนวน​แถว​เฉลี่ย​ต่อ​คีย์ เอา​สอง​ตัว​นั้น​หาร​กัน​ก็ได้​จำนวน​ค่าที่​ไม่​ซ้ำ เช่น idx_status เก็บ 200000 28572 ซึ่ง​หาร​กัน​ได้ 7 พอดี

อ่าน index ทั้ง​อัน​แล้วกรอง — แผนที่​ชนะ​เฉพาะ​เมื่อ k ใหญ่

หัวข้อ​ที่​มีชื่อ​ว่า “อ่าน index ทั้ง​อัน​แล้วกรอง — แผนที่​ชนะ​เฉพาะ​เมื่อ k ใหญ่”

แผนที่​สี่​บน index อัน​เดิม ซึ่ง​เลิก seek ที​ละ​ช่วง​แล้ว​อ่าน​รวด​เดียว กับ​เหตุผล​ที่​มัน​ถูก​กว่า​แผน 2,000 ช่วง​และ​ถูก​กว่า​การ​สแกน heap

บน index (customer_id, restaurant) อัน​เดิม​นั้น ยัง​มี​แผนที่​สี่​ที่​เลิก​ไล่ seek ที​ละ​ช่วง คือ​ลง​จากราก​ครั้ง​เดียว เดิน​โซ่​ใบ​ยาว​รวด​จน​จบ แล้ว​ค่อย​กรอง​เอา​เฉพาะ​รายการ​ที่​ตรง​กับ​ร้าน​ที่​ถาม

นั่น​คือ​สิ่ง​ที่ scan_all ทำ มัน​เรียก seek_ranges ตัว​เดิม​ด้วย​ช่วง​เดียว​ที่​กว้าง​คลุม​คีย์​ทั้งหมด

แผนช่วง​ที่​ต้อง​อ่านindex แตะheap แตะเทียบ​สแกน 3,153 หน้า
สแกน index (customer_id, restaurant) ทั้ง​อัน​แล้วกรอง1492 หน้า0 หน้าถูก​กว่า 6.4 เท่า

492 หน้า​แตก​ได้​เป็นการ​ลง​จากราก 1 ครั้ง​ซึ่ง​กิน 2 หน้า กับ​การ​เดิน​โซ่​ใบ​อีก 490 หน้า รวม​กับ​ใบ​แรก​ที่​เจอ​ตอน​ลง​จากราก​ก็​คือ​ใบ​ครบ​ทั้ง 491 ใบ​ของ​ต้นไม้

แผน​นี้​อ่าน​ทุก​รายการ​ใน index โดย​ไม่​ข้าม​อะไร​เลย แต่​ยัง​ถูก​กว่า​การ​ไล่ seek 2,000 ช่วง​บน index อัน​เดียวกัน 8.18 เท่า

การ​ไล่ seek แพง​กว่า​การ​อ่าน​รวด​เดียว​เพราะ​มัน​จ่าย​ค่า​ลง​จากราก​ซ้ำ 2,000 ครั้ง ส่วน​การ​อ่าน​รวด​เดียว​จ่าย​ค่า​นั้น​ครั้ง​เดียว​แล้ว​เดิน​ตาม​โซ่​ใบ​ต่อ​ไป​เรื่อยๆ

แผน​นี้​ยัง​ถูก​กว่า​การ​สแกน heap ทั้ง​ตาราง 6.4 เท่า​ด้วย เหตุผล​อยู่​ที่​ความ​กว้าง​ของ​หนึ่ง​รายการ คือ​รายการ​ใน​ใบ​กว้าง 10 B ส่วน​แถว​จริง​ใน heap เฉลี่ย 59.99 B และ​เมื่อ​รวม slot 4 B ที่​บท 2 คิด​ให้​ทุก​แถว​เป็น 63.99 B หาร​ด้วย 10 B ได้ 6.4 พอดี

ผล​ของ​ความ​กว้าง​นั้น​คือ index ทั้ง file กิน 493 หน้า เทียบ​กับ heap 3,153 หน้า หรือ 15.64 % แผน​นี้​จึง​อ่าน​ครบ​ทุก​รายการ​ใน index เหมือน​ที่​การ​สแกน​ตาราง​อ่าน​ครบ​ทุก​แถว แต่​จ่าย​หน้า​คนละ​ระดับ​กัน

ราคา​ที่​แผน​นี้​จ่าย​คือ 492 หน้า​ซึ่ง​ไม่​ขึ้น​กับ​สัดส่วน​แถว​เลย ถาม​คำถาม​ที่​มี​คำ​ตอบ​แถว​เดียว​ก็​ยัง​จ่าย 492 หน้า​เท่า​เดิม และ​มัน​ต้อง​เทียบ​คีย์​ครบ​ทั้ง 200,000 รายการ​ที่​อยู่​ใน​ใบ

เทียบ​กับ​แผน prefix ของ​หัวข้อ​แรก​ที่​แตะ 31 หน้า แผน​นี้​จึง​แพง​กว่า 15.9 เท่า มัน​ชนะ​เฉพาะ​ตอน​ที่ k ใหญ่​จน​ค่า​ลง​จากราก​ท่วม​ทุก​อย่าง ไม่ใช่​ชนะ​ทุก​คำถาม

คำถาม​ที่​ระบุ column อีก​ตัว​บน index สอง​อัน​เดิม แล้ว​ผู้​ชนะ​สลับ​ข้าง

คำถาม​ที่​สอง​กลับ​ด้าน คือ ออเดอร์​สถานะ Rejected มี​กี่​แถว คำ​ตอบ​คือ 13,165 แถว คิด​เป็น 6.58 % ของ​ตาราง

แผนช่วง​ที่​ต้อง​อ่านindex แตะ
(status, restaurant)status อยู่​หน้า134 หน้า
(restaurant, status)status อยู่​หลัง1054 หน้า

index สอง​อัน​นี้​คือ​อัน​เดิม​สอง​อัน​จาก​คำถาม​แรก ไม่​ได้​สร้าง​ใหม่​และ​ไม่​ได้​แก้​อะไร​เลย เปลี่ยน​แค่​ว่า​คำถาม​ระบุ column ไหน แล้ว​ผู้​ชนะ​สลับ​ข้าง​ทันที

k ของ​แผนที่​สอง​เป็น 10 คราว​นี้ ไม่ใช่ 7 เพราะ column ที่​อยู่​หน้า​กลาย​เป็น restaurant ซึ่ง​มี 10 ค่า สูตร​ตัว​เดิม เปลี่ยน​แค่​ว่า​ใคร​อยู่​หน้า

แต่​ส่วน​ต่าง​จริง​คือ 54 − 34 = 20 หน้า ขณะ​ที่​ค่า​ลง​จากราก​อธิบาย​ได้ 18 หน้า คือ (10 − 1) × 2 อีก 2 หน้าที่​เหลือ​คือ​ใบ​หัว​ท้าย​ของ​ช่วง​ที่​ถูก​อ่าน​ซ้ำ ซึ่ง​คือ​เหตุผล​ที่​ใบ​ที่​เดิน​ผ่าน​ของ​แผน k ช่วง​เป็น​ขอบ​ล่าง

ลำดับ​ของ​คีย์​จึง​ไม่ใช่​คุณสมบัติ​ของ index อย่าง​เดียว มัน​เป็น​คุณสมบัติ​ของ​คู่​ระหว่าง index กับ​คำถาม การ​ถาม​ว่า (status, restaurant) หรือ (restaurant, status) ดี​กว่า​กัน​โดย​ไม่​บอก​คำถาม จึง​ตอบ​ไม่​ได้

คำถาม​สอง​ข้อ​คนละ​ปลายของ​สัดส่วน​แถว เทียบ​แผนที่​จบ​ที่​ใบ​กับ​แผนที่​ต้อง​ตาม​ตัว​ชี้​กลับ​ไป​เปิด​แถว

covering indexcovering indexindex ที่​มี​ทุก​คอลัมน์​ที่ query ขอ จึง​ตอบ​คำถาม​ได้​จบ​ที่​ใบ ไม่​ต้อง​ตาม​ตัว​ชี้​ย้อน​กลับ​ไป​อ่าน heap อีก คือ index ที่​มี​ทุก column ที่​คำถาม​ขอ แผน​จึง​ตอบ​จบ​ได้ที่​ใบ ไม่​ต้อง​เอา​ตัว​ชี้​แถว​ไป​เปิด heap อีกรอบ

คำถาม​ที่​สาม​คือ ของ​ร้าน ครัว​คุณ​แม่ แต่ละ​สถานะ​มี​กี่​แถว ซึ่ง​ขอ​สอง column คือ restaurant กับ status

แผนindex แตะheap แตะรวม
covering — index (restaurant, status)31 หน้า0 หน้า31 หน้า
non-covering — index (restaurant) แล้ว​เปิด​แถว​จริง31 หน้า11,934 หน้า11,965 หน้า
สแกน heap ทั้ง​ตาราง3,153 หน้า3,153 หน้า

ทั้ง​สอง​แผน​แตะ index เท่า​กัน​คือ 31 หน้า ส่วน​ต่าง​ทั้งหมด​อยู่​ที่ heap และ​มัน​เกิด​ขึ้น​ใน​สอง​ฟังก์ชัน​ข้าง​ล่าง​นี้

qe/paths.py
def fetch_rows(pager, rids):
"""ตามตัวชี้กลับไปหยิบแถวจริงจาก heap — หนึ่ง rid = อ่านหนึ่งหน้า (ไม่มี cache)"""
pager.reads = 0
got = [decode(read_row(pager, rid)) for rid in rids]
return got, pager.reads, len({p for p, _ in rids})
def touch_rows(pager, rids):
"""เหมือน fetch_rows แต่ไม่เก็บผล ใช้ตอนสนใจแค่ 'แตะกี่หน้า' กับชุดที่ใหญ่มาก"""
pager.reads = 0
for rid in rids:
read_row(pager, rid)
return pager.reads, len({p for p, _ in rids})

ลูปนั้น​เรียก read_row ที​ละ​ตัว​ชี้ ไม่มี​อะไร​จำ​หน้าที่​เพิ่ง​อ่าน​ไว้ ตัว​ชี้ 11,934 ตัว​จึง​กลาย​เป็นการ​อ่าน 11,934 หน้าตรงๆ

หน้า​ไม่​ซ้ำ​จริง​มี 3,079 หน้า ซึ่ง​เป็น​ขอบ​ล่าง​ของ​กรณี​ที่ cache ได้​ทั้งหมด ของ​จริง​อยู่​ระหว่าง 3,079 กับ 11,934

covering แตะ 31 หน้า ส่วน non-covering แตะ 11,965 หน้า ต่าง​กัน 11,934 หน้า คิด​เป็น​ราคา 386.0 เท่า

แต่​ประเด็น​ที่​แรง​กว่า​คือ​แผน non-covering แพง​กว่า​การ​อ่าน​ทั้ง​ตาราง​ทิ้ง 3.79 เท่า ทั้ง​ที่​มัน​เอา​แถว​มา​แค่ 5.97 %

flowchart LR
    Q["คำถาม<br/>ร้าน ครัวคุณแม่ แต่ละสถานะมีกี่แถว"]
    Q --> C1["เดิน index (restaurant, status)<br/>31 หน้า"]
    Q --> N1["เดิน index (restaurant)<br/>31 หน้า"]
    C1 --> C2["คีย์มีทั้งชื่อร้านและสถานะ<br/>ตอบจบที่ใบ"]
    C2 --> C3["รวม 31 หน้า"]
    N1 --> N2["คีย์ไม่มีสถานะ<br/>ต้องเปิดแถวจริง 11,934 ครั้ง"]
    N2 --> N3["แตะ heap 11,934 หน้า<br/>หน้าไม่ซ้ำจริง 3,079 หน้า"]
    N3 --> N4["รวม 11,965 หน้า"]
    C3 --> V{"อ่านตัวเลขสองคู่"}
    N4 --> V
    V --> V1["covering ถูกกว่า non-covering 386.0 เท่า"]
    V --> V2["non-covering แพงกว่าสแกนทั้งตาราง 3.79 เท่า"]

คำ​บรรยาย​ภาพ: สอง​เส้นทาง​ของ​คำถาม​เดียวกัน · เส้น​บน​ใช้ index ที่​มี​ทั้ง​สอง column ที่​คำถาม​ขอ จึง​จบ​ที่​ใบ​และ​ไม่​แตะ heap เลย · เส้น​ล่าง​ใช้ index ที่​รู้​แค่​ว่า​แถว​อยู่​ตรง​ไหน จึง​ต้อง​เอา​ตัว​ชี้​ไป​เปิด​แถว​จริง​ที​ละ​แถว · ลูกศร​กลับ​ไป​หา heap ใน​เส้น​ล่าง​มี 11,934 เส้น วาด​ไว้​เป็น​สัญลักษณ์​เส้น​เดียว

ปลาย​อีก​ด้าน​ของ​สัดส่วน​แถว​ให้​ผล​คนละ​แบบ คำถาม​ที่​สี่​คือ สถานะ Placed มา​จากร้าน​ไหน​บ้าง อย่าง​ละ​กี่​แถว ซึ่ง​มี​คำ​ตอบ 199 แถว คิด​เป็น 0.10 % ของ​ตาราง

แผนหน้าที่​แตะ
covering — index (status, restaurant)2 หน้า
non-covering — เดิน index แล้ว​เปิด​แถว​จริงindex 2 + heap 199 = 201 หน้า (heap ไม่​ซ้ำ 190 หน้า)
สแกน heap ทั้ง​ตาราง3,153 หน้า

ที่​สัดส่วน​แถว​ต่ำ​ขนาด​นี้ แผน non-covering ยัง​ชนะ​การ​สแกน​อยู่ 15.7 เท่า แต่ covering ยัง​ถูก​กว่า non-covering อีก 100 เท่า

กติกา​ที่​ตัดสิน​คือ​หนึ่ง​ตัว​ชี้​แถว​เท่ากับ​หนึ่ง​หน้า จุด​ตัด​จึง​เป็น​ของ​แผน​นั้น ไม่ใช่​ของ​ตาราง

ค่า​คงที่​ที่​ตัดสิน​ทั้งหมด​นี้​มี​ตัว​เดียว คือ​การ​ตาม​ตัว​ชี้​กลับ​ไป​หยิบ​แถว​เท่ากับ 1 หน้า​ต่อ 1 แถว

แผน non-covering ที่​ตัว​ชี้​แถว​กระจาย​จึง​แพง​กว่า​การ​สแกน​ทั้ง​ตาราง​ทันที​ที่​ผลลัพธ์​เกิน 3,153 แถว หรือ 1.58 % ของ​ตาราง

เลข 1.58 % นี้​เป็น​เลข​เดียว​กับ​จุด​คุ้ม​ทุน​ที่​บท 2 วัด​ไว้ ไม่ใช่​ความ​บังเอิญ ทั้ง​สอง​บท​เดิน​มา​จาก​ข้อเท็จจริง​ข้อ​เดียวกัน​คนละ​ทาง คือ​หนึ่ง​ตัว​ชี้​แถว​เท่ากับ​หนึ่ง​หน้า

แต่​ตัวเลข​นี้​เป็น​ของ​แผนที่​ตัว​ชี้​แถว​กระจาย และ​วัด​ด้วย​หน้าที่​แตะ​ดิบๆ ที่​ไม่มี buffer pool บท 5 วัด​จุด​ตัด​ของ index ที่​ไม่ cluster ด้วย cost model แล้ว​ได้ 394 แถว หรือ 0.197 % ส่วน index ที่ cluster ได้ 185,094 แถว หรือ 92.55 %

จุด​ตัด​จึง​เป็น​ตัวเลข​ของ index ตัว​นั้น​เสมอ ไม่ใช่​ของ​ตาราง คำถาม​ที่​หก​ข้าง​ล่าง​หยิบ​ครบ​ทั้ง 200,000 แถว​แต่​แตะ​หน้า​ไม่​ซ้ำ​แค่ 3,153 หน้า เพราะ index (placed_at) เรียง​ตรง​กับ​ลำดับ​ของ heap พอดี

ราคา​ของ covering ที่​ไม่​ได้​อยู่​ใน​ตาราง​ไหน​ใน​บท​นี้ คือ​ทุก column ที่​ใส่​เพิ่ม​เข้าไป​ทำให้​รายการ​ใน​ใบ​กว้าง​ขึ้น ใบ​เก็บ​ได้​น้อย​ลง และ index ทั้ง​อันโต​ขึ้น ของเล่น​ชุด​นี้​วัด​ราคา​นั้น​ไม่​ได้​เพราะ​มัน​บีบ​คีย์​เป็น uint32 หมด​แล้ว

sort avoidance — ฟรี​เมื่อ index ครอบคลุม แลก​เมื่อ​ไม่​ครอบคลุม

หัวข้อ​ที่​มีชื่อ​ว่า “sort avoidance — ฟรี​เมื่อ index ครอบคลุม แลก​เมื่อ​ไม่​ครอบคลุม”

คำถาม​สาม​ข้อ​เรื่อง​ลำดับ และ​หน่วย​ที่​หัวข้อ​นี้​ต้อง​รายงาน​คู่​กับ​หน้าที่​แตะ

sort avoidancesort avoidanceการ​อาศัย​ลำดับ​ที่ index มี​อยู่​แล้ว​เพื่อ​ไม่​ต้อง​เรียง​ผลลัพธ์ เป็นการ​แลก: ถือ​แถว​ไว้​น้อย​ลง​และ​หยุด​กลางคัน​ได้​เมื่อ​มี `LIMIT` แต่​แพง​ขึ้น​เมื่อ​ต้อง​เดิน​ผล​ทั้ง​ชุด​อยู่ดี คือ​การ​อาศัย​ลำดับ​ที่ index มี​อยู่​แล้ว เพื่อ​ไม่​ต้อง​เรียง​ผลลัพธ์​เอง ลำดับ​นั้น​เป็น​ของ​ที่​ติด​มา​กับ​โครงสร้าง ไม่ใช่​สิ่ง​ที่​ต้อง​คำนวณ

หัวข้อ​นี้​รายงาน​สอง​หน่วย​พร้อม​กัน คือ​หน้าที่​แตะ​กับ​จำนวน​แถว​ที่​ต้อง​ถือ​ไว้​พร้อม​กัน เพราะ​จำนวน​แถว​ที่​ถือ​ไว้​คือ​สิ่ง​ที่​ตัดสิน​ว่า​เรียง​ใน​หน่วย​ความ​จำ​ได้​หรือ​ต้อง​เท​ลง​ที่​เก็บ​ข้อมูล

บท 1 ประกาศ​หน่วย​ที่​สอง​ไว้​ว่า​จะ​เพิ่ม​เข้า​มา​ตอน​บท join หัวข้อ​นี้​หยิบ​มา​ใช้​ก่อน เพราะ​การ​เลี่ยง​การ​เรียง​วัด​ด้วย​หน้าที่​แตะ​อย่าง​เดียว​แล้ว​สรุป​ผิด

คำถาม​ที่​ห้า​คือ 20 ออเดอร์​แรก​สุด เขียน​เป็น ORDER BY placed_at LIMIT 20

แผนหน้าที่​แตะแถว​ที่​ถือ​ไว้​พร้อม​กัน
index (placed_at) แล้ว​หยุด​กลางคัน22 หน้า20
สแกน heap แล้ว​เก็บ 20 อันดับ​แรก3,153 หน้า20
สแกน heap แล้ว​เรียง​ทั้ง​ชุด3,153 หน้า200,000

22 หน้า​ของ​แผน​แรก​คือ index 2 หน้า​กับ heap 20 หน้า มัน​หยุด​ได้​ทันที​ที่​ครบ 20 แถว เพราะ​เดิน​ตาม​ลำดับ​คีย์​อยู่​แล้ว

การ​หยุด​กลางคัน​นั้น​เป็น​ของ​จริง​ใน code ไม่ใช่​การกรอง​ทีหลัง ลูป​ข้าง​ล่าง​เช็ก len(out) < limit ทุกรอบ​ทั้ง​ชั้น​นอก​และ​ชั้น​ใน

qe/paths.py
def scan_ordered(rd, limit):
"""เดินตามลำดับคีย์จากซ้ายสุด แล้วหยุดทันทีที่ได้ครบ limit
ใช้ _descend ของ reader ตรง ๆ เพราะ range() ไม่มีทางหยุดกลางคัน
ซึ่งประเด็นของหัวข้อ [3] คือ "หยุดกลางคันได้" นั่นแหละ
"""
rd.reset()
pno, page = rd._descend(0)
out, i = [], 0
while len(out) < limit:
_, n, nxt = header(page)
while i < n and len(out) < limit:
out.append((leaf_key(page, i), leaf_rid(page, i)))
i += 1
if len(out) >= limit or nxt == NO_NEXT:
break
page, i = rd.pager.read_page(nxt), 0
return out, rd.reads

แผนที่​สอง​ต้อง​อ่าน​ครบ​ทั้ง 3,153 หน้า​ก่อน​ถึง​จะ​รู้​ว่า​ใคร​อยู่​ใน 20 อันดับ​แรก มัน​ถือ​แถว​ไว้ 20 แถว​เท่า​กัน แต่​จ่าย​หน้า​มากกว่า 143 เท่า

LIMIT ตัด​งาน​ได้​จริง​เฉพาะ​ตอน​ที่​ลำดับ​มา​จาก index ถ้า​ลำดับ​ต้อง​คำนวณ​เอง LIMIT ไม่​ได้​ลด​งาน​ที่​ต้อง​ทำ​ก่อนหน้า​นั้น​เลย​สัก​หน้า

คำถาม​ที่​หก​ถอด LIMIT ออก เหลือ ORDER BY placed_at ทั้ง​ตาราง แล้ว​ผลกลับ​ด้าน​ทันที

แผนหน้าที่​แตะแถว​ที่​ถือ​ไว้​พร้อม​กัน
index (placed_at) แล้ว​ตาม​ไป​หยิบ​แถว​ที​ละ​แถว200,492 หน้า1
สแกน heap แล้ว​เรียง​ทั้ง​ชุด3,153 หน้า200,000

200,492 หน้า​คือ index 492 หน้า​กับ heap 200,000 หน้า แผน​นี้​เลี่ยง​การ​เรียง​ได้​จริง ผล​ไหล​ออก​ที​ละ​แถว แต่​จ่าย​หน้า​เพิ่ม 63.6 เท่า

สิ่ง​ที่​แลก​มา​คือ​หน่วย​ความ​จำ จาก 200,000 แถว​ที่​ต้อง​ถือ​ไว้​พร้อม​กัน​เหลือ 1 แถว เมื่อ index ตอบ​ไม่​จบ​ที่​ใบ​และ​ไม่มี LIMIT มา​ตัด​งาน การ​เลี่ยง​การ​เรียง​คือ​การ​ย้าย​ราคา​จาก​หน่วย​หนึ่ง​ไป​อีก​หน่วย​หนึ่ง ไม่ใช่​การ​ชนะ

ตัวเลข​ของ​คำถาม​นี้​เป็น​กรณี​ดี​ที่สุด ไม่ใช่​กรณี​ทั่วไป

แถว​ชุด​นี้​เรียง​ตาม placed_at มา​ตั้งแต่ qe/fixture.py ของ​บท 1 heap จึง​ถูก​เขียน​เรียง​ตาม​นั้น​ไป​ด้วย การ​เดิน​ตาม index (placed_at) จึง​ไล่​หน้า​ไป​ข้าง​หน้า​เรื่อยๆ ไม่​กระโดด​กลับ หน้า​ไม่​ซ้ำ​จึง​มี​แค่ 3,153 หน้า

ถ้า​มี cache พอ​เก็บ​ทั้ง file แผน​นี้​จะ​เหลือ​ราว 3,645 หน้า แทนที่​จะ​เป็น 200,492 หน้า

ถ้า​เปลี่ยน​ไป​ถาม​ด้วย column ที่​ไม่​ได้​เรียง​อยู่​แล้ว ตัวเลข​จะ​ไม่​สวย​แบบ​นี้ บท 3 วัด​ไว้​ว่า​คำถาม customer_id = 995 ได้ 97 แถว​ที่​กระจาย​อยู่​บน 93 หน้า หรือ 0.96 หน้า​ต่อ​หนึ่ง​แถว ซึ่ง​แทบ​ไม่มี​อะไร​ให้ cache ช่วย​เลย

คำถาม​ที่​เจ็ด​ผสม​สอง​เรื่อง​เข้า​ด้วย​กัน คือ ออเดอร์​ของ​ร้าน ครัว​คุณ​แม่ เรียง​ตาม status

แผนหน้าที่​แตะแถว​ที่​ถือ​ไว้​พร้อม​กัน
index (restaurant, status) — ลำดับ​ตรง​กับ​ที่​ขอ31 หน้า1
index (status, restaurant) — 7 ช่วง เดิน​ตาม​ลำดับ status43 หน้า1
index (restaurant) — index ไม่รู้จัก status31 + 11,934 หน้า11,934

แผนที่​สาม​ต้อง​หยิบ​แถว​ให้​ครบ​ทั้ง 11,934 แถว​ก่อน​ถึง​จะ​เรียง​ได้ เพราะ index ที่​มัน​ใช้​ไม่มี status อยู่​ใน​คีย์​เลย

แผน 7 ช่วง​ยัง​คืน​ผล​เรียง​ตาม status และ​ถือ​ไว้​แค่ 1 แถว​เหมือน​แผน​แรก เพราะ​เรา​เดิน​ช่วง​ตาม​ลำดับ​ของ column ที่​อยู่​หน้า​อยู่​แล้ว

สอง​แผน​บน​เป็น covering ด้วย การ​เลี่ยง​การ​เรียง​ใน​คำถาม​นี้​จึง​ชนะ​ทั้ง​สอง​หน่วย​จริง คือ 31 หน้า​กับ​ถือ​ไว้ 1 แถว เหนือ​แผนที่​สาม​ซึ่ง​จ่าย 11,965 หน้า​และ​ถือ​ไว้ 11,934 แถว เหมือน​ที่​คำถาม​ที่​ห้า​ชนะ​ทั้ง​สอง​หน่วย​เพราะ LIMIT ตัด​งาน​ให้

กติกา​ที่​แม่น​กว่า “k ช่วง​ทำลาย​ลำดับ” จึง​เป็น k ช่วง​รักษา​ลำดับ​ของ column ที่​อยู่​หน้า ไม่ใช่​ของ column ที่​อยู่​หลัง

เงื่อนไข​ที่​ทำให้​ตัวเลข​ทั้ง​บท​ออก​มา​แบบ​นี้

หัวข้อ​ที่​มีชื่อ​ว่า “เงื่อนไข​ที่​ทำให้​ตัวเลข​ทั้ง​บท​ออก​มา​แบบ​นี้”

ข้อ​จำกัด​สี่​ข้อ​ของ​ของเล่น​ชุด​นี้​ที่​ต้อง​อ่าน​คู่​กับ​ทุก​ตัวเลข​ข้าง​บน และ​สิ่ง​ที่​บท​หน้า​ทำ​ต่อ

  • ไม่มี buffer pool ตัว​ชี้​แถว​หนึ่ง​ตัว​เท่ากับ​หนึ่ง​หน้า​เสมอ เลข 11,934 กับ 200,000 หน้า​จึง​เป็น​ขอบ​บน​ของ​กรณี​ที่ cache อะไร​ไม่​ได้​เลย ส่วน​เลข 3,079 กับ 3,153 ที่​พิมพ์​คู่​มา​คือ​ขอบ​ล่าง
  • index ทุก​อัน bulk load ใบ​จึง​เต็ม​เกือบ 100 % เหมือน​ตอน CREATE INDEX เสร็จใหม่ๆ หลัง​ตาราง​ถูก​แก้​ไป​สัก​พัก ใบ​จะ​เต็ม​ราว 70 % และ​ทุก​จำนวน​หน้า​ใน​บท​นี้​จะ​โต​ราว 1.4 เท่า
  • คีย์​ร่วม​ถูก​บีบ​เป็น uint32 index ห้า​อัน​จึง​กว้าง​เท่า​กัน​หมด ความ​ต่าง​ทุก​ตัว​ใน​บท​นี้​เป็น​ผล​ของ​ลำดับ​ล้วนๆ ซึ่ง​เป็น​ข้อดี​ของ​การ​ทดลอง แต่​แปล​ว่า​ตัวเลข​ขนาด​ของ file ยก​ไป​ใช้​กับ​ของ​จริง​ไม่​ได้
  • ลำดับ​ของ​ค่า​มา​จาก code S_OF กับ R_OF ตรึง​ลำดับ​ไว้​ตาม​ที่​ประกาศ​ใน qe/fixture.py และ qe/heapfile.py มัน​ยืน​แทน collation ของ column จริง ไม่ใช่​ลำดับ​พจนานุกรม​ภาษา​ไทย

สูตร​ที่​ทั้ง​บท​นี้​เดิน​ตาม​ใช้​กับ index ตัว​เดียว​เท่านั้น พอ​มี​ตาราง​ที่​สอง​เข้า​มา คำถาม​เปลี่ยน​จาก “เข้า​ทาง​ไหน” เป็น “จับ​คู่​แถว​ของ​สอง​ตาราง​ด้วย​รูปแบบ​กา​รวน​ลูป​แบบ​ไหน”

บท 7 วัด​สี่​วิธี​จับ​คู่​แถว​นั้น และ​จะ​พบ​ว่า​สอง​แผนที่​แตะ​หน้า​เท่า​กัน​เป๊ะ​ยัง​ทำงาน​ต่าง​กัน​มาก จน​ต้อง​เพิ่ม​หน่วย​ที่​เหลือ​ของ​คอร์ส​เข้า​มา​คือ​จำนวน​ครั้ง​ที่​เทียบ​คีย์


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Query Planning — SQLite (ตรวจ 2026-08-11) — เอกสาร​ทางการ​ที่​อธิบาย​เงื่อนไข​ที่​ทำให้ index ตอบ​คำถาม​ได้​เอง​โดย​ไม่​ต้อง​กลับ​ไป​เปิด​แถว​จริง และ​เงื่อนไข​ที่​ทำให้ planner ยอม​อ่าน index หลาย​ช่วง​แทน​ช่วง​เดียว ส่วน​ตัวเลข​ทั้งหมด​ใน​บท​นี้​เป็น​ของ​โครงสร้าง​ที่​คอร์ส​นี้​สร้าง​เอง ไม่ใช่​ของ SQLite

เช็กความเข้าใจ — บทที่ 6

ข้อ 1 / 3

คำถามเดียวกันคือ ออเดอร์ของร้าน ครัวคุณแม่ มีกี่แถว บน index (restaurant, status) แตะ 31 หน้า ส่วนบน index (status, restaurant) แตะ 43 หน้า ส่วนต่าง 12 หน้ามาจากอะไร