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

ทำไม query ถึง​ช้า — และ​เรา​จะ​วัด​มัน​ด้วย​อะไร

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

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

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

📦 code ของ​บท​นี้

ทั้ง​คอร์ส​เขียน​ด้วย Python 3 และ stdlib ล้วน ไม่​ต้อง​ติดตั้ง​อะไร​เพิ่ม​สัก​ตัว โมดูล​ที่​ใช้​ตลอด​คอร์สมี​แค่ struct, bisect, random, sqlite3, math, os, time, itertools, collections และ hashlib

โครง folder ที่​บท​นี้​เริ่ม​ไว้​คือ folder ทำงาน​หนึ่ง​อัน แล้ว​มี folder ย่อย​ชื่อ qe/ อยู่​ข้าง​ใน file ของ​บท​นี้​คือ qe/fixture.py file เดียว และ​คำ​สั่ง​รัน​ทุก​คำ​สั่ง​ใน​คอร์ส​นี้​เรียก​จาก folder ทำงาน​ด้าน​นอก ไม่ใช่​จาก​ใน qe/

เวลา​ที่​จับ​ได้​ไม่​ได้​วัด​แผน มัน​วัด​เครื่อง​ที่​แผน​บังเอิญ​ไป​รัน​อยู่

หัวข้อ​ที่​มีชื่อ​ว่า “เวลา​ที่​จับ​ได้​ไม่​ได้​วัด​แผน มัน​วัด​เครื่อง​ที่​แผน​บังเอิญ​ไป​รัน​อยู่”

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

query เดิม เครื่อง​เดิม ข้อมูล​ชุด​เดิม รัน​ตอน​บ่าย​ได้​สามสิบ​มิลลิ​วินาที รัน​ตอน​ดึก​ได้​สาม​วินาที ไม่มี​ใคร​แก้​อะไร​เลย​ระหว่าง​นั้น

สิ่ง​ที่​นาฬิกา​จับ​ได้​ไม่ใช่​คุณสมบัติ​ของ​แผน มัน​คือ​ผล​รวม​ของ​แผน บวก​กับ​ทุก​อย่าง​ที่​แย่ง​เครื่อง​อยู่​ใน​วินาที​นั้น

รายการ​ของ “ทุก​อย่าง” นั้น​ยาวกว่าที่​คน​ส่วน​ใหญ่​คิด

  • cache ของ OS รอบ​แรก​อ่าน​จาก disk จริง รอบ​ที่​สอง​อ่าน​จาก​หน่วย​ความ​จำ โดยที่​แผน​ไม่​ได้​เปลี่ยน​สัก​ตัว​อักษร
  • เพื่อนบ้าน​บน​เครื่อง​เดียวกัน process อื่น container อื่น หรือ VM อื่น​ที่​ใช้ disk กับ CPU ร่วม​กัน​อยู่
  • สถานะ​ภายใน​ของ​เอนจิน​เอง connection pool ที่​เพิ่ง​อุ่น แผนที่​ถูก cache ไว้​แล้ว หรือ​สถิติ​ที่​เพิ่ง​ถูก​อัปเดต
  • ตัว​เครื่อง​ที่​ผู้​อ่าน​ใช้ ซึ่ง​ไม่ใช่​เครื่อง​เดียว​กับ​ที่​ผู้​เขียน​คอร์ส​ใช้ และ​ไม่มี​ทาง​เป็น​เครื่อง​เดียวกัน​ได้

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

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

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

ชั้น​เก็บ​ข้อมูล​ไม่​เคย​ส่ง “หนึ่ง​แถว” ให้​ใคร มัน​ส่ง pagepageก้อน​ข้อมูล​ขนาด​คงที่​ที่​เป็น​หน่วย​เล็ก​ที่สุด​ที่​ชั้น​เก็บ​ข้อมูล​อ่าน​หรือ​เขียน​ได้​ใน​ครั้ง​เดียว คอร์ส​นี้​ตั้ง​ไว้​ที่ 4096 byte จึง​ไม่มี​สิ่ง​ที่​เรียก​ว่า “อ่าน​หนึ่ง​แถว” มี​แต่ “อ่าน​หนึ่ง​หน้า” ซึ่ง​เป็น​ก้อน​ขนาด​คงที่​มา​ให้​ทั้ง​ก้อน​เสมอ แม้ว่า​ใน​ก้อน​นั้น​จะ​มี​แถว​ที่​ต้องการ​อยู่​แค่​แถว​เดียว​ก็ตาม คอร์ส​นี้​ตรึง​ขนาด​ก้อน​นี้​ไว้​ที่ 4096 byte ตลอด​ทั้ง​แปดบท ส่วน​บท 2 จะ​วัดผล​ของ​การ​ตรึง​นั้น​ที​ละ​ข้อ

หน่วย​ของ​คอร์ส​นี้​จึง​เป็น​ข้อ​ตกลง​บรรทัด​เดียว: อ่าน​หนึ่ง​หน้า นับ​เป็น​หนึ่ง​ครั้ง

การ​อ่าน​ที่​ราคา​ไม่​เท่า​กัน​มี​สอง​แบบ และ​คอร์ส​นี้​แยก​สอง​แบบ​นั้น​ออก​จาก​กัน​ตั้งแต่​ต้น

  • sequential accesssequential accessการ​อ่าน​หน้าที่​อยู่​ติด​กัน​เรียง​ไป​ตาม​ไฟล์ เป็น​ราคา​ฐาน​ของ​ทั้ง​คอร์ส: อ่าน​เรียง​หนึ่ง​หน้า​มี​ต้นทุน​เท่ากับ 1.0 คือ​การ​อ่าน​หน้าที่​อยู่​ติด​กัน​ไล่​ไป​ตาม file ตัว disk กับ​ตัว OS อ่าน​ล่วงหน้า​ให้​ได้ นี่​คือ​ราคา​ฐาน​ของ​ทั้ง​คอร์ส
  • random accessrandom accessการ​อ่าน​หน้าที่​กระโดด​ไป​มา​ตาม​ตำแหน่ง​ที่​ต้องการ แพง​กว่า​อ่าน​เรียง​เพราะ​ไม่​ได้​ประโยชน์​จาก​การ​อ่าน​ล่วงหน้า และ​คอร์ส​นี้​ตั้ง​ราคา​ของ​มัน​เป็น​พารามิเตอร์​ตัว​หนึ่ง​ของ​โมเดล คือ​การกระโดด​ไป​อ่าน​หน้าที่​ต้องการ​ที​ละ​หน้า​ตาม​ตำแหน่ง ไม่​ได้​ประโยชน์​จาก​การ​อ่าน​ล่วงหน้า และ​เป็น​ราคา​ที่​คอร์ส​นี้​ตั้ง​เป็น parameter ตัว​หนึ่ง​ของ model ไม่ใช่​ค่า​คงที่​ที่​รู้​มา​ก่อน

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

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

flowchart TD
    Q["คำถามหนึ่งคำถาม<br/>ร้านนี้มีออเดอร์กี่รายการ"] --> P1["แผน ก อ่านทั้งตารางแล้วกรองทิ้ง"]
    Q --> P2["แผน ข เดินโครงสร้าง index แล้วตามตัวชี้กลับไปอ่านตาราง"]
    Q --> P3["แผน ค อ่านเฉพาะโครงสร้าง index ไม่แตะตารางเลย"]
    P1 --> U{"ตัดสินว่าแผนไหนถูกกว่า<br/>ด้วยหน่วยอะไร"}
    P2 --> U
    P3 --> U
    U --> T["เวลาที่จับด้วยนาฬิกา"]
    U --> C["จำนวนครั้งที่แตะที่เก็บข้อมูล"]
    U --> O["จำนวน opcode ที่เครื่องเสมือนเดิน"]
    T --> TX["ตัดทิ้งตั้งแต่บทนี้<br/>วัดเครื่อง cache ของ OS และเพื่อนบ้านปนมาด้วย"]
    C --> CX["หน่วยหลักของคอร์ส<br/>ใช้ตั้งแต่บท 2 ถึงบท 8"]
    O --> OX["คนละหน่วยกับหน้าที่แตะ<br/>เก็บไว้เทียบตอน capstone บท 8"]
    classDef dropped stroke-dasharray:6 4,opacity:0.65
    class T,TX dropped

คำ​บรรยาย​ภาพ: คำถาม​เดียว​มี​แผนที่​ตอบ​ได้​หลาย​แผน และ​การ​จะ​บอกว่า​แผน​ไหน​ถูก​กว่า​ต้อง​เลือก​หน่วย​วัด​ก่อน · กล่อง​เส้น​ประ​คือ​หน่วย​เวลา​ที่​คอร์ส​นี้​ตัด​ทิ้ง​ตั้งแต่​บท​แรก เพราะ​มัน​วัด​เครื่อง​กับ cache ของ OS ปน​เข้า​มา​ด้วย · หน่วย​หลัก​ที่​เหลือ​คือ​จำนวน​ครั้ง​ที่​แตะ​ที่​เก็บ​ข้อมูล ซึ่ง​บท 2 ถึง​บท 8 ใช้​ตลอด · ส่วน​จำนวน opcode ที่​เครื่อง​เสมือน​เดิน​เป็น​คนละ​หน่วย เก็บ​ไว้​เทียบ​ตอน​บท 8

ข้อมูล​ตั้งต้น​ที่​ทุก​เครื่อง​ต้อง​ได้​เหมือน​กัน

หัวข้อ​ที่​มีชื่อ​ว่า “ข้อมูล​ตั้งต้น​ที่​ทุก​เครื่อง​ต้อง​ได้​เหมือน​กัน”

ตัว qe/fixture.py ทั้ง file วิธี​รัน และ​ค่า md5 ที่​ต้อง​ตรง​กัน​ก่อน​จะ​เชื่อ​ตัวเลข​อื่น​ใน​คอร์ส​นี้​ได้

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

สร้าง folder qe/ แล้ว​วาง file ข้าง​ล่าง​นี้​ลง​ไป​ทั้ง file

qe/fixture.py
"""Canonical orders fixture for query-engine-from-scratch. stdlib only, seed-locked."""
import random
SEED = 20260802
N_ROWS = 200_000
N_CUSTOMERS = 2_000
EPOCH = 1735689600 # 2025-01-01T00:00:00Z
DAYS = 180
PROMO_DAY = 140 # one promotion spike
STATUS = ["Placed", "Confirmed", "Preparing", "PickedUp",
"Delivered", "Rejected", "Cancelled"]
RESTAURANTS = [
"ครัวคุณแม่", "ส้มตำแซ่บนัว", "ข้าวมันไก่ประตูน้ำ", "ก๋วยเตี๋ยวเรือรังสิต",
"ครัวสุขใจ", "ชาบูบ้านนา", "พิซซ่าลุงหมี", "ข้าวหมูแดงเจ๊หมวย",
"ลาบเป็ดอุดร", "กาแฟดริปซอยเก้า",
]
REST_W = [6, 16, 14, 12, 12, 9, 8, 7, 8, 8]
BASE = {"Placed": 1, "Confirmed": 2, "Preparing": 3, "PickedUp": 4,
"Delivered": 855, "Rejected": 30, "Cancelled": 105}
MIX = {r: BASE for r in RESTAURANTS}
MIX["ครัวคุณแม่"] = dict(BASE, Delivered=205, Rejected=700) # ปฏิเสธเยอะ
MIX["ครัวสุขใจ"] = dict(BASE, Delivered=885, Rejected=0) # ไม่เคยปฏิเสธ
def rows(n=N_ROWS, seed=SEED):
"""แถวเรียงตาม placed_at (ออเดอร์ถูกเขียนตามเวลาที่เข้ามา) — deterministic"""
rng = random.Random(seed)
stamps = []
for _ in range(n):
if rng.random() < 0.08: # วันโปรโมชัน
day = PROMO_DAY + rng.random()
else:
day = DAYS * (rng.random() ** 0.5) # ปริมาณไต่ขึ้นตามเวลา
stamps.append(EPOCH + int(day * 86400))
stamps.sort()
out = []
for i, ts in enumerate(stamps, start=1):
rest = rng.choices(RESTAURANTS, weights=REST_W)[0]
w = MIX[rest]
st = rng.choices(STATUS, weights=[w[s] for s in STATUS])[0]
out.append((i, rng.randint(1, N_CUSTOMERS), st, rest, ts,
rng.randrange(5_000, 200_000)))
return out
if __name__ == "__main__":
import collections, hashlib
rs = rows()
print("rows:", len(rs), "cols:", len(rs[0]))
h = hashlib.md5(repr(rs).encode()).hexdigest()
print("md5(repr) =", h)
c = collections.Counter(r[2] for r in rs)
for s, n in c.most_common():
print(f" {s:<10}{n:>8} {n/len(rs)*100:>6.2f}%")
print("distinct restaurant:", len({r[3] for r in rs}),
" distinct customer:", len({r[1] for r in rs}),
" distinct placed_at:", len({r[4] for r in rs}))
print("placed_at range:", rs[0][4], "->", rs[-1][4],
" sorted:", all(rs[i][4] <= rs[i+1][4] for i in range(len(rs)-1)))

รัน​จาก folder ทำงาน​ด้าน​นอก

Terminal window
python3 qe/fixture.py

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

rows: 200000 cols: 6
md5(repr) = 6796858c189960daa42506f5ed86d573

ได้​ตาราง 200,000 แถว 6 column คือ​หมายเลข​ออเดอร์ รหัส​ลูกค้า สถานะ ชื่อ​ร้าน เวลา​ที่​สั่ง และ​ยอด​เงิน

SEED = 20260802 กับ random.Random(seed) คือ​เหตุผล​ที่ md5 ออก​มา​เท่า​กัน​ทุก​เครื่อง code สร้าง​ตัว​สุ่ม​ของ​ตัวเอง​ขึ้น​มา​ใหม่​แทน​การ​เรียก random ระดับ​โมดูล ผล​คือ​ลำดับ​ตัวเลข​สุ่ม​ไม่​ขึ้น​กับ​ว่า​มี​ใคร​ไป​ตั้ง seed ตรง​ไหน​ไว้​ก่อนหน้า

stamps.sort() ทำให้​แถว​ทั้ง​ตาราง​เรียง​ตาม placed_at ก่อน​จะ​ถูก​เขียน ซึ่ง​ตรง​กับ​พฤติกรรม​จริง​ของ​ตารางออเดอร์​ที่​แถว​ถูก​เขียน​ตาม​เวลา​ที่​ออเดอร์​เข้า​มา

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

REST_W คือ​น้ำหนัก​การ​สุ่ม​ร้าน ทำให้​สิบ​ร้าน​มี​จำนวนออเดอร์​ไม่​เท่า​กัน แต่​เบ้​แค่​พอประมาณ ไม่​ได้​เบ้​จน​สุด​ขั้ว

MIX["ครัวสุขใจ"] ที่​ตั้ง Rejected=0 คือ​จุด​ที่​จงใจ​สร้าง​ช่องว่าง​ใน​ข้อมูล ร้าน​นี้​ไม่​มี​ออเดอร์​สถานะ Rejected เลย​แม้แต่​แถว​เดียว ทั้ง​ที่​ทั้ง​สถานะ​นั้น​และ​ร้าน​นั้น​ต่าง​ก็​มี​อยู่​จริง​ใน​ตาราง คู่​ที่​ไม่มี​อยู่​จริง​คู่​นี้​เป็น​โจทย์​ที่​บท 4 บท 5 และ​บท 8 หยิบ​ไป​ใช้​ทั้ง​สาม​บท

fixture นี้​ไม่ใช่​ข้อมูล​จริง และ​ไม่​ได้​พยายาม​จะ​เหมือน​ข้อมูล​จริง

มัน​ถูก​ออกแบบ​ให้​เบ้​ตรง​จุด​ที่​บท​หลัง​ต้องการ​โจทย์ ไม่​ได้​เบ้​เพราะ​เลียน​แบบ​ร้าน​อาหาร​ของ​จริง

ผล​ของ​การ​ออกแบบ​แบบ​นี้​คือ​ทุก​ตัวเลข​ที่​คอร์ส​นี้​รายงาน​เป็น​ตัวเลข​ของ ข้อมูล​ชุด​นี้ กับ โครงสร้าง​ชุด​นี้ ไม่ใช่​ค่า​ประจำ​ตัว​ของ heap file หรือ​ของ B+tree ที่​ยก​ไป​ใช้​กับ​ตาราง​อื่น​ได้​ทันที

สิ่ง​ที่​ยก​ไป​ใช้​ต่อ​ได้​คือ​วิธี​วัด​กับ​วิธี​อ่าน​ตัวเลข ไม่ใช่​ตัวเลข

รูปร่าง​ของ​ข้อมูล — ความ​เบ้​ที่​ตั้งใจ​ใส่​ไว้

หัวข้อ​ที่​มีชื่อ​ว่า “รูปร่าง​ของ​ข้อมูล — ความ​เบ้​ที่​ตั้งใจ​ใส่​ไว้”

การกระจาย​ของ status restaurant customer_id และ placed_at ที่​บท​หลัง​จะ​ใช้​เป็น​โจทย์

ส่วน​ที่​เหลือ​ของ stdout คือ​รูปร่าง​ของ​ตาราง เริ่ม​ที่ column status ซึ่ง​เบ้​หนัก​ที่สุด

statusจำนวน​แถวสัดส่วน
Delivered163,73381.87 %
Cancelled21,13110.57 %
Rejected13,1656.58 %
PickedUp8060.40 %
Preparing5830.29 %
Confirmed3830.19 %
Placed1990.10 %

ค่าหัว​ตาราง​กับ​ค่า​ท้าย​ตาราง​ห่าง​กัน​หลาย​ร้อย​เท่า Delivered กิน​ไป 81.87 % ของ​ตาราง ส่วน Placed มี​อยู่ 199 แถว​จาก​สอง​แสน​แถว คิด​เป็น 0.10 %

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

column ที่​เหลือกระจาย​คนละ​แบบ​กัน

columnจำนวน​ค่าที่​ไม่​ซ้ำลักษณะ
restaurant10เบ้​เล็กน้อย​ตาม REST_W ไม่มี​ร้าน​ไหน​หา​ยาก​ผิด​ปกติ
customer_id2,000กระจาย​เท่า​กัน สุ่ม​แบบ​ไม่​ถ่วง​น้ำหนัก
placed_at196,891เกือบ​ไม่​ซ้ำ​เลย และ​เรียง​อยู่​แล้ว​ทั้ง column

placed_at กิน​ช่วง​ตั้งแต่ 1735730205 ถึง 1751241515 และ stdout ยืนยัน​ด้วย​ว่า sorted: True คือ​แถว​เรียง​ตาม column นี้​จริง​ทั้ง​ตาราง

ใน​ช่วง​เวลา​นั้น​มี​วัน​โปรโมชัน​ฝัง​อยู่​หนึ่ง​วัน คือ PROMO_DAY = 140 ใน code ซึ่ง​เป็น​วัน​ที่​ปริมาณ​ออเดอร์​กระโดด​ขึ้น​จาก​วัน​รอบ​ข้าง​ชัดเจน

การกระจาย​ทั้ง​สี่​แบบ​นี้​ไม่ใช่​ของ​แถม มัน​คือ​โจทย์​ที่​บท 4 กับ​บท 5 ต้อง​แก้ เพราะ​การ​เดา​จำนวน​แถว​จะ​แม่น​หรือ​พลาด​ขึ้น​กับ​ว่า column ที่​ถาม​เบ้​แบบ​ไหน

ความหมาย​ของ​ค่า md5 ที่​คงที่ และ​สิ่ง​ที่​ต้อง​ทำ​เมื่อ​รัน​แล้ว​ได้​ไม่​ตรง

md5 ที่​คงที่​ไม่​ได้​มี​ไว้​ให้​ดู​สวย มัน​คือ​คำ​สัญญา​ว่า​ตัวเลข​ทุก​ตัว​ที่​จะ​ปรากฏ​ใน​อีก​เจ็ด​บท ผู้​อ่าน​รัน​เอง​แล้ว​ต้อง​ได้​เท่า​กัน​ทุก​หลัก

ถ้า​รัน​แล้ว​ได้ md5 ไม่​ตรง​กับ 6796858c189960daa42506f5ed86d573 ให้​ถือว่า​มี​อะไร​ผิด ไม่ใช่ “แล้ว​แต่​เครื่อง” จุด​ที่​ต้อง​กลับ​ไป​ดู​คือ SEED ถูก​แก้​หรือ​เปล่า และ file ถูกลอก​มา​ครบ​ทุก​บรรทัด​หรือ​ยัง

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

เส้น​ขอบเขต และ​คู่​ตัวเลข​ที่​บท​หน้า​จะ​วัด​ให้​ดู

หัวข้อ​ที่​มีชื่อ​ว่า “เส้น​ขอบเขต และ​คู่​ตัวเลข​ที่​บท​หน้า​จะ​วัด​ให้​ดู”

สิ่ง​ที่​คอร์ส​นี้​จงใจ​ไม่​ทำ และ​ตัวเลข​คู่​แรก​ที่​เป็น​รูปธรรม​ของ​ธีสิส​ทั้ง​คอร์ส

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

  • ไม่​ได้​สร้าง​ฐาน​ข้อมูล​ที่​ใช้งาน​ได้ ไม่มี SQL parser ไม่มี transaction ไม่มี WAL ไม่มี MVCC และ​ไม่มี concurrency ส่วน heap ของ​คอร์ส​นี้​เขียน​ต่อ​ท้าย​อย่าง​เดียว ไม่​รองรับ​การ​ลบ​หรือ​แก้​แถว
  • ไม่​ได้​แข่ง​กับ SQLite และ​ไม่​ได้​ทำซ้ำ SQLite เรา​สร้าง​สิ่ง​ที่​อธิบาย​ได้​ว่า​ทำไม​แผน​ของ SQLite ถึง​หน้าตา​แบบ​นั้น ไม่ใช่​สิ่ง​ที่​ดี​กว่า SQLite
  • ไม่​รายงาน​เวลา​เป็น​ผล​วัด ตัวเลข​วินาที​ที่​ยก​มา​ตอน​ต้นบท​คือ​อาการ​ที่​ผู้​อ่าน​เจอ ไม่ใช่​ผล​วัด​ของ​แผน ส่วน​ตัวเลข​ที่​คอร์ส​นี้​รายงาน​เอง​ไม่มี​วินาที ไม่มี​มิลลิ​วินาที และ​ไม่มี​ประโยค​ว่า​เร็ว​ขึ้น​กี่​เท่า ตลอด​ทั้ง​แปดบท
  • ไม่ใช่​คอร์ส​สอน SQL และ​ไม่ใช่​คอร์ส Postgres เรื่อง​อย่าง MVCC, WAL หรือ partitioning ปรากฏ​ได้​แค่​ใน​ฐานะ​ไดอะแกรม​กับ​การ​อ้างอิง​ที่​ลง​วัน​ที่ ไม่มี code และ​ไม่มี​ตัวเลข​ประมาณ​การ

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

0.50 % ของ​แถว จ่าย​ไป 31.72 % ของ​ราคา​การ​อ่าน​ทั้ง​ตาราง

บท 2 จะ​เขียน​ตารางออเดอร์ 200,000 แถว​นี้​ลง file จริง แล้ว​วัด​สอง​แบบ​ด้วย​ตัว​นับ​ที่​ฝัง​ไว้​ใน code เอง

  • อ่าน​ทั้ง​ตาราง​แบบ​เรียง แตะ 3,153 หน้า ได้​แถว​มา​ครบ 200,000 แถว
  • หยิบ 1,000 แถว​แบบ​สุ่ม​ด้วย​ตัว​ชี้​แถว แตะ 1,000 หน้า ได้​แถว​มา 1,000 แถว

1,000 แถว​คิด​เป็น 0.50 % ของ​ตาราง แต่ 1,000 หน้า​คิด​เป็น 31.72 % ของ​ราคา​การ​อ่าน​ทั้ง​ตาราง

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

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

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

บท 2 เริ่ม​จาก​คำถาม​ที่​ฟัง​ดูเหมือน​ไม่ใช่​คำถาม คือ “อ่าน​หนึ่ง​แถว” หมายความ​ว่า​อะไร คำ​ตอบ​คือ​มัน​ไม่มี​อยู่​จริง และ​ผล​ของ​ข้อเท็จจริง​ข้อ​เดียว​นั้น​คือ​ที่มา​ของ​ทุก​อย่าง​ที่​เหลือ​ใน​คอร์ส​นี้


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Access Path Selection in a Relational Database Management System — Selinger et al., Proc. 1979 ACM SIGMOD, หน้า 23–34 (ตรวจ PDF แล้ว 2026-08-11) — ต้นตำรับ​ของ​แนวคิด​ที่​ทั้ง​คอร์ส​นี้​เดิน​ตาม คือ​ประมาณ​ราคา​ของ​แผนที่​เป็น​ไป​ได้​ทุก​แผน​ก่อน แล้ว​ค่อย​เลือก​แผนที่​ราคา​ต่ำ​สุด แทน​การ​ให้​ผู้​เขียน query สั่ง​เอง​ว่า​จะ​เข้าถึง​ข้อมูล​ทาง​ไหน ตัวเลข​ใน​เปเปอร์​ฉบับ​นี้​เป็น​เรื่อง​ของ​บท 4

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

ข้อ 1 / 3

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