ทำไม query ถึงช้า — และเราจะวัดมันด้วยอะไร
คำถามที่คอร์สนี้ตอบคือ ทำไมการเพิ่ม index บางครั้งถึงไม่ช่วย และเอนจินตัดสินใจเรื่องนั้นด้วยตัวเลขอะไร
ก่อนจะเถียงกันได้ว่าแผนไหนดีกว่า ต้องตกลงกันก่อนว่า “ดีกว่า” วัดด้วยอะไร คำตอบของคอร์สนี้ไม่ใช่วินาที
บทนี้จึงยังไม่ประกอบชิ้นส่วนอะไรเลย มันตั้งกติกาการวัดที่อีกเจ็ดบทจะอ้างถึง แล้วสร้างข้อมูลตั้งต้นที่ทำให้ตัวเลขทุกตัวในคอร์สนี้ผู้อ่านรันเองได้
ทั้งคอร์สเขียนด้วย 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
"""Canonical orders fixture for query-engine-from-scratch. stdlib only, seed-locked."""import random
SEED = 20260802N_ROWS = 200_000N_CUSTOMERS = 2_000EPOCH = 1735689600 # 2025-01-01T00:00:00ZDAYS = 180PROMO_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 ทำงานด้านนอก
python3 qe/fixture.pyสองบรรทัดแรกที่ออกมาคือสัญญาของทั้งคอร์ส ถ้าสองบรรทัดนี้ไม่ตรง ตัวเลขที่เหลือในอีกเจ็ดบทก็จะไม่ตรงตามไปด้วย
rows: 200000 cols: 6md5(repr) = 6796858c189960daa42506f5ed86d573ได้ตาราง 200,000 แถว 6 column คือหมายเลขออเดอร์ รหัสลูกค้า สถานะ ชื่อร้าน เวลาที่สั่ง และยอดเงิน
สี่จุดใน file นี้ที่ต้องอ่านให้ออก
หัวข้อที่มีชื่อว่า “สี่จุดใน file นี้ที่ต้องอ่านให้ออก”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 หยิบไปใช้ทั้งสามบท
มันถูกออกแบบให้เบ้ตรงจุดที่บทหลังต้องการโจทย์ ไม่ได้เบ้เพราะเลียนแบบร้านอาหารของจริง
ผลของการออกแบบแบบนี้คือทุกตัวเลขที่คอร์สนี้รายงานเป็นตัวเลขของ ข้อมูลชุดนี้ กับ โครงสร้างชุดนี้ ไม่ใช่ค่าประจำตัวของ heap file หรือของ B+tree ที่ยกไปใช้กับตารางอื่นได้ทันที
สิ่งที่ยกไปใช้ต่อได้คือวิธีวัดกับวิธีอ่านตัวเลข ไม่ใช่ตัวเลข
รูปร่างของข้อมูล — ความเบ้ที่ตั้งใจใส่ไว้
หัวข้อที่มีชื่อว่า “รูปร่างของข้อมูล — ความเบ้ที่ตั้งใจใส่ไว้”การกระจายของ
statusrestaurantcustomer_idและplaced_atที่บทหลังจะใช้เป็นโจทย์
ส่วนที่เหลือของ stdout คือรูปร่างของตาราง เริ่มที่ column status ซึ่งเบ้หนักที่สุด
| status | จำนวนแถว | สัดส่วน |
|---|---|---|
| Delivered | 163,733 | 81.87 % |
| Cancelled | 21,131 | 10.57 % |
| Rejected | 13,165 | 6.58 % |
| PickedUp | 806 | 0.40 % |
| Preparing | 583 | 0.29 % |
| Confirmed | 383 | 0.19 % |
| Placed | 199 | 0.10 % |
ค่าหัวตารางกับค่าท้ายตารางห่างกันหลายร้อยเท่า Delivered กินไป 81.87 % ของตาราง ส่วน Placed มีอยู่ 199 แถวจากสองแสนแถว คิดเป็น 0.10 %
ทิศทางของความเบ้นี้เลือกให้ตรงกับที่ผู้อ่านคาดไว้ คือออเดอร์เกือบทั้งหมดจบที่ส่งถึงมือลูกค้า ส่วนสถานะระหว่างทางมีอยู่แค่ช่วงสั้นๆ ก่อนจะถูกเปลี่ยนไปสถานะถัดไป แต่สัดส่วนที่ออกมาเป็นของ fixture ชุดนี้ ไม่ใช่ของตารางออเดอร์จริงตารางไหน
column ที่เหลือกระจายคนละแบบกัน
| column | จำนวนค่าที่ไม่ซ้ำ | ลักษณะ |
|---|---|---|
restaurant | 10 | เบ้เล็กน้อยตาม REST_W ไม่มีร้านไหนหายากผิดปกติ |
customer_id | 2,000 | กระจายเท่ากัน สุ่มแบบไม่ถ่วงน้ำหนัก |
placed_at | 196,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 และไม่มีตัวเลขประมาณการ
รูปธรรมของสิ่งที่หน่วยใหม่นี้ทำให้เห็นได้ คือคู่ตัวเลขที่บทหน้าจะวัดสดให้ดู
บท 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 / 3query เดิม เครื่องเดิม ข้อมูลชุดเดิม รันตอนบ่ายได้สามสิบมิลลิวินาที รันตอนดึกได้สามวินาที ทำไมคอร์สนี้ถึงไม่ใช้วินาทีเป็นหน่วยตัดสินแผน