เก็บตารางใบเดิมกลับด้าน แล้วสแกนทั้งตารางแตะ 3320 หน้า ส่วน heap แตะ 3153
ตารางออเดอร์ที่คอร์สที่ 26 กับ 28 ใช้อยู่มี 200,000 แถว บทนี้ไม่ได้สร้างตารางใหม่ มันเก็บตารางใบเดิมลงคลังอีกใบหนึ่ง โดยวางค่าของ column เดียวกันไว้ติดกัน แทนการวางทั้งแถวไว้ติดกัน
แล้วถามคำถามเดียว การวางแนวแบบนั้นทำให้อ่านน้อยลงจริงไหม
สรุปตามตำราตอบว่าจริง ค่าที่วัดได้จากตารางใบนี้ตอบว่า ยัง
สแกนทั้งตารางบน heap แตะ 3153 หน้า ส่วนการสแกนตารางใบเดียวกันบน column storecolumn storeคลังที่วางค่าของ column เดียวกันไว้ติดกัน ต่างจาก heap ของคอร์สที่ 26 ที่วางทั้งแถวไว้ติดกัน · คอร์สนี้เก็บตารางเดิมกลับด้าน แล้วแยกผลของการวางแนวออกจากผลของการเข้ารหัส ซึ่งเป็นสองเรื่องที่สรุปตามตำรารวมไว้เป็นก้อนเดียว ที่ยังไม่เข้ารหัส และแตะครบ 6 จาก 6 column แตะ 3320 หน้า
บทนี้ตอบว่าทำไม แล้วลด 3320 ให้เหลือ 989 โดยไม่ขยับการวางแนวสักนิด
สั่ง python3 scripts/column-store/probe.py ได้เลย มันใช้ Python 3 กับ standard
library ล้วน ไม่ต้องติดตั้งอะไรก่อน และไม่มี container ให้เก็บกวาดทีหลัง
probe รันชุดข้ออ้างทั้งชุดซ้ำในการเรียกหนึ่งครั้ง แล้วเทียบผลทีละคีย์ ถ้ามีค่าไหนขยับระหว่างรอบ มันแดง
ค่าที่วัดได้ทุกตัวที่บทนี้พิมพ์มีคีย์ของตัวเองอยู่ใน
scripts/column-store/expected/column-store.txt และชื่อคีย์นั้นเขียนกำกับไว้ในทุกตาราง
ตัวเลขในบทนี้จึงอ้างอิงเป็นคู่ของทรงกับคีย์เสมอ
สนามแข่งคือตารางใบเดิม ไม่ใช่ตารางที่สร้างมาให้คลัง column ชนะ
หัวข้อที่มีชื่อว่า “สนามแข่งคือตารางใบเดิม ไม่ใช่ตารางที่สร้างมาให้คลัง column ชนะ”ตารางเดียวกัน หน้าขนาดเดียวกัน และตัวนับหน้าตัวเดียวกันกับคอร์สที่ 26
ถ้าคลังสองใบใช้ข้อมูลคนละชุด ตัวเลขสองฝั่งวางทาบกันไม่ได้ บทนี้จึงอ่านตารางใบเดิม
จาก fixture.py ของคอร์สที่ 26 และไม่แก้อะไรที่นั่นแม้แต่ byte เดียว
| ของที่ตรึงไว้ | คีย์ในผลของ probe | ค่า |
|---|---|---|
| จำนวนแถวของตาราง | แถว | 200,000 |
| ขนาดของหน้า หน่วยเป็นไบต์ | PAGE_SIZE | 4096 |
| จำนวนแถวต่อหนึ่งก้อน | row group ปริยาย | 10,000 |
| จำนวนก้อนทั้งคลัง | row group ทั้งหมด | 20 |
คลัง column ตัดแถวออกเป็นก้อน ก้อนละเท่ากัน ก้อนหนึ่งเรียกว่า row grouprow groupก้อนของแถวที่คลังแนว column ตัดออกมาเป็นหน่วยเดียวกัน ทุก column ในก้อนเดียวกันถือแถวชุดเดียวกัน · ขอบเขตของก้อนเป็นสิ่งที่ตัดสินว่า dictionary ถูกคัดลอกซ้ำกี่ใบ และ zone map ข้ามอะไรได้บ้าง และทุก column ในก้อนเดียวกันถือแถวชุดเดียวกัน
ส่วนของ column ตัวเดียวที่อยู่ภายในก้อนหนึ่ง เรียกว่า column chunkcolumn chunkส่วนของ column หนึ่งตัวภายใน row group หนึ่งก้อน · มันเลือกการเข้ารหัสและความกว้างของตัวรหัสเป็นของตัวเอง ค่าสองอย่างนั้นจึงเป็นค่าต่อก้อน ไม่ใช่ค่าต่อทั้ง column มันเลือกการเข้ารหัสของตัวเองได้ ค่านั้นจึงเป็นค่าต่อก้อน ไม่ใช่ค่าต่อทั้ง column
ศัพท์สองคำนี้มาจาก ParquetApache Parquetรูปแบบของคลังแนว column ที่วางข้อมูลเป็น row group และเก็บ min/max กับตำแหน่งของทุก column chunk ไว้ใน metadata ท้ายคลัง · คอร์สนี้อ้างมันในฐานะรูปแบบเท่านั้น สิ่งที่วัดจริงคือคลังที่ colstore.py สร้างขึ้น ซึ่งไม่ใช่ Parquet ซึ่งวางข้อมูลเป็น row group แล้วเก็บ metadata ไว้ท้ายคลัง คลังของคอร์สนี้ยืมชื่อกับรูปมา แต่มันไม่ใช่ Parquet และบทนี้ไม่ได้อ้างมันเป็นของที่วัด
หน้าที่แตะทุกตัวในบทนี้มาจาก Pager.reads ตัวเดียวกับคอร์สที่ 26 ไม่ใช่ตัวนับ
ที่คอร์สนี้เขียนขึ้นเอง
def scan(store, cols, groups=None): """สแกนเต็ม column ที่ระบุ · คืน (หน้าที่แตะ, จำนวนค่าที่ถอดรหัส)""" q = cs.Q(store) n = 0 gs = range(store.ngroups) if groups is None else groups for col in cols: for g in gs: n += len(q.chunk(col, g)) p = q.reads q.close() return p, nq.reads คือค่าที่ Pager.reads เดินไว้ หน้าที่ chunk สองอันใช้ร่วมกันถูกนับครั้งเดียว
ซึ่งเป็นการเทียบที่ยุติธรรมกับ heap เพราะการสแกน heap ก็แตะแต่ละหน้าครั้งเดียวอยู่แล้ว
คำทำนายที่สมเหตุสมผล และตัวเลขที่หักล้างมัน
หัวข้อที่มีชื่อว่า “คำทำนายที่สมเหตุสมผล และตัวเลขที่หักล้างมัน”คำถามแตะครบทั้งหก column ทั้งสองฝั่ง แล้วฝั่งที่วางกลับด้านอ่านมากกว่า
คำทำนายที่คนส่วนใหญ่เขียนออกมาก่อนวัดคือ คลัง column อ่านน้อยกว่าเสมอ เพราะมันหยิบเฉพาะสิ่งที่คำถามขอได้
บทนี้จึงเลือกคำถามที่ปิดข้อได้เปรียบข้อนั้นทิ้ง คือคำถามที่แตะครบ 6 จาก 6 column ทั้งสองฝั่ง ตัวแปรที่เหลืออยู่ตัวเดียวคือการวางแนว
| ทรงที่วัด | คีย์ในผลของ probe | หน้าที่แตะ |
|---|---|---|
| heap · สแกนทั้งตาราง | หน้าที่แตะเมื่อสแกนทั้งตาราง | 3153 |
| คลัง column ที่ยังไม่เข้ารหัส · แตะครบ 6 จาก 6 | column 6 จาก 6 · raw | 3320 |
แถวที่สองมากกว่าแถวแรก การวางแนวอย่างเดียวจึงแพ้คลังแนวแถวที่มันมาแทน
probe เก็บอัตราส่วนของคู่นี้ไว้เป็นค่าของตัวเองด้วย และค่าที่น้อยกว่าหนึ่ง อ่านว่า heap แตะน้อยกว่า ไม่ใช่มากกว่า
| คีย์ในผลของ probe | ค่า |
|---|---|
heap หารด้วย 6 column raw | 0.95 |
ประโยคที่พบทั่วไปคือ คลัง column อ่านน้อยกว่า เพราะมันไม่ต้องอ่าน column ที่ไม่ได้ถาม ครึ่งหลังของประโยคนั้นถูก และบทที่ 2 วัดให้ดูว่ามันจ่ายคืนเท่าไร
ครึ่งแรกไม่ถูก มันเป็นข้อสรุปของ การวางแนวบวกการเข้ารหัส ที่ถูกเขียนราวกับเป็น ข้อสรุปของการวางแนวอย่างเดียว
ค่า column 6 จาก 6 · raw ที่ 3320 คือค่าของการวางแนวอย่างเดียว
และมันแพ้ หน้าที่แตะเมื่อสแกนทั้งตาราง ของ heap ที่ 3153
column ที่เป็นสตริง จ่ายเต็มความยาวทุกแถว
หัวข้อที่มีชื่อว่า “column ที่เป็นสตริง จ่ายเต็มความยาวทุกแถว”ที่ทำให้ตัวเลขบวมคือทรงที่เก็บค่าตามที่มันเป็น ไม่ใช่การวางแนว
ทรงแรกของคลัง column ในบทนี้ใช้ plain encodingplain encodingการเก็บค่าตามที่มันเป็น โดยไม่มีตัวรหัสมาแทน · เป็นเส้นฐานที่คอร์สนี้เทียบการเข้ารหัสทุกแบบกลับมาหา และเป็นทรงเดียวที่ column สตริงจ่ายเต็มความยาวของสตริงทุกแถว คือเก็บค่าตามที่มันเป็น โดยไม่มีตัวรหัสมาแทน
if enc == RAW: if kind == "u32": body = b"".join(struct.pack("<I", v) for v in vals) m["fixed_width"] = 4 else: body = b"".join(bytes([len(v.encode())]) + v.encode() for v in vals) m["fixed_width"] = 0 # ความกว้างไม่คงที่ · กระโดดไปค่าเดียวไม่ได้column ที่เป็นเลขจ่ายความกว้างคงที่ทุกแถว ส่วน column ที่เป็นสตริงเขียนความยาวไว้ข้างหน้า แล้วตามด้วยตัวสตริงเต็มๆ
status เป็นสตริง คลังทรงนี้จึงเขียนคำว่า Delivered หรือ Confirmed ซ้ำใหม่ทุกแถว
ครบทั้ง 200,000 แถว ส่วน restaurant ก็เป็นสตริงเหมือนกัน
ที่เก็บ status ทั้ง column | คีย์ในผลของ probe | ไบต์ |
|---|---|---|
| heap · ตัวรหัสหนึ่ง byte ต่อแถว | ไบต์ที่ heap ใช้เก็บ status ทั้ง column | 200,000 |
| คลัง column · plain encoding | status → raw ไบต์ | 1,985,792 |
แถวแรกเท่ากับจำนวนแถวของตารางพอดี เพราะ heap จ่ายหนึ่ง byte ต่อแถวสำหรับ column ตัวนี้ เลขสองตัวที่เท่ากันจึงไม่ใช่ความบังเอิญ มันคือกลไกที่เขียนไว้ในหัวข้อถัดไป
heap ทำ dictionary encoding อยู่แล้ว ก่อนมีคลัง column ตัวไหน
หัวข้อที่มีชื่อว่า “heap ทำ dictionary encoding อยู่แล้ว ก่อนมีคลัง column ตัวไหน”กลไกที่ตำรายกให้คลัง column อยู่ในคลังแนวแถวมาก่อน และมันอยู่ในสองบรรทัดนี้
STATUS = ["Placed", "Confirmed", "Preparing", "PickedUp", "Delivered", "Rejected", "Cancelled"]FIXED = struct.Struct("<IIBIIH") # id, customer_id, status, placed_at, satang, len(name)def encode(oid, cid, st, rest, ts, satang): b = rest.encode() return FIXED.pack(oid, cid, STATUS.index(st), ts, satang, len(b)) + bB ในสตริงรูปแบบคือหนึ่ง byte ที่ไม่มีเครื่องหมาย และ STATUS.index(st) คือการแทน
สตริงด้วยตำแหน่งของมันในรายการ STATUS
นั่นคือ dictionary encodingdictionary encodingการเก็บค่าที่ไม่ซ้ำไว้ในตารางใบหนึ่ง แล้วเก็บเฉพาะตัวรหัสที่ชี้เข้าไป · heap ของคอร์สที่ 26 ทำสิ่งนี้อยู่ก่อนแล้วที่ heapfile.py:18 ด้วย STATUS.index(st) · และมันไม่ได้ทำให้เล็กลงเสมอ เงื่อนไขที่ตัดสินคือจำนวนค่าที่ไม่ซ้ำใน row group เดียว ไม่ใช่ทั้ง column ทั้งดุ้น รายการ STATUS ทำหน้าที่เป็น dictionary
และสิ่งที่ heap เขียนลงหน้าคือตัวรหัส ไม่ใช่ตัวสตริง
heap จึงไม่ใช่ฝั่งที่ยังไม่เข้ารหัสในการเทียบนี้ มันเข้ารหัส column ตัวนั้นอยู่ก่อนแล้ว ตั้งแต่คอร์สที่ 26 ส่วนคลัง column ทรง raw ต่างหากที่ยังไม่เข้ารหัส
การเทียบ heap กับคลัง column ที่พบทั่วไปจึงเทียบของที่เข้ารหัสแล้ว กับของที่ยังไม่เข้ารหัส แล้วเรียกผลต่างที่ได้ว่าผลของการวางแนว
เปิด bit packing แล้วตัวเลขพลิก โดยการวางแนวไม่ขยับ
หัวข้อที่มีชื่อว่า “เปิด bit packing แล้วตัวเลขพลิก โดยการวางแนวไม่ขยับ”คลัง column ทรงนี้วางแนวเหมือนทรง plain encoding ทุกอย่าง ต่างกันที่การเข้ารหัสอย่างเดียว
elif enc in (DICT, BITPACK): vs = sorted(set(vals)) ix = {v: i for i, v in enumerate(vs)} codes = [ix[v] for v in vals]สองการเข้ารหัสนี้สร้างรายการตัวรหัสชุดเดียวกัน ต่างกันที่ความกว้างที่ใช้เขียนตัวรหัส นั้นลงคลัง
def bits_for(k): """จำนวนบิตที่พอใส่รหัส 0..k-1 · อย่างน้อยหนึ่งบิตเสมอ""" return max(1, (k - 1).bit_length()) if k > 1 else 1bits_for ให้ความกว้างเท่าที่พอใส่รหัสตัวสุดท้าย โดยไม่ปัดขึ้นเป็น byte เต็ม
และมันคิดจากค่าที่ไม่ซ้ำใน column chunk ก้อนนั้น จึงเป็นค่าต่อก้อน
| ทรงที่วัด | คีย์ในผลของ probe | หน้าที่แตะ |
|---|---|---|
| คลัง column ที่ยังไม่เข้ารหัส · แตะครบ 6 จาก 6 | column 6 จาก 6 · raw | 3320 |
| คลัง column · bit packing · แตะครบ 6 จาก 6 | column 6 จาก 6 · bitpack | 989 |
| คีย์ในผลของ probe | ค่า |
|---|---|
heap หารด้วย 6 column bitpack | 3.19 |
สองแถวบนวางแนวเหมือนกันทุกอย่าง ตารางใบเดียวกัน row group ขนาดเดียวกัน จำนวนก้อนเท่ากัน และคำถามแตะครบ 6 จาก 6 column เหมือนกัน
สิ่งเดียวที่ต่างคือการเข้ารหัส ที่ชนะจึงเป็นการเข้ารหัส ไม่ใช่การวางแนว
ธีสิสของคอร์ส — การวางแนวกับการเข้ารหัสเป็นคนละเรื่อง
หัวข้อที่มีชื่อว่า “ธีสิสของคอร์ส — การวางแนวกับการเข้ารหัสเป็นคนละเรื่อง”ตัวเลขสามตัวในรูปเดียว และเส้นที่แยกสองเรื่องนั้นออกจากกันอยู่ตรงไหน
flowchart TB R["ตารางออเดอร์ใบเดิม · 200,000 แถว · หน้า 4096 ไบต์"] R --> H["heap ของคอร์สที่ 26 · วางทั้งแถวไว้ติดกัน · status เป็นตัวรหัสหนึ่ง byte อยู่ก่อนแล้ว"] R --> C["คลัง column · วางค่าของ column เดียวกันไว้ติดกัน · 20 row group"] H --> H1["สแกนทั้งตาราง · 3153 หน้า"] C --> C1["plain encoding · เก็บสตริงตามที่มันเป็น · 3320 หน้า"] C --> C2["bit packing · วางแนวเหมือน C1 ทุกอย่าง · 989 หน้า"]
คำบรรยายภาพ: สามทรงที่บทนี้วัด กิ่งซ้ายคือคลังแนวแถว กิ่งขวาแตกเป็นสองใบที่วางแนว เหมือนกัน ต่างกันที่การเข้ารหัสอย่างเดียว ระยะจาก 3320 ถึง 989 จึงไม่ใช่ผลของการวางแนว
สรุปตามตำรารวมสองเรื่องนี้ไว้เป็นก้อนเดียว การรวมนั้นวัดได้ว่าผิด และอีกเจ็ดบทที่เหลือ คือการแยกมันออกทีละชั้น
- การวางแนว ตัดสินว่าคำถามมีสิทธิ์ไม่อ่าน column ไหนบ้าง · บทที่ 2 วัดว่าสิทธิ์นั้น จ่ายคืนเท่าไรเมื่อคำถามแคบลง
- การเข้ารหัส ตัดสินว่าค่าหนึ่งค่ากินที่เท่าไร · บทที่ 3 ถึงบทที่ 5 ไล่ทีละแบบ และไม่มีแบบไหนชนะทุก column
- ลำดับที่เขียนแถวลงคลัง ตัดสินว่าข้ามทั้งก้อนได้ไหม · บทที่ 6 กับบทที่ 7 วัดข้อนี้
- การประกอบแถวกลับคืน คือราคาที่การวางแนวเรียกเก็บ · บทที่ 8 วัดมันแล้วปิดคอร์ส
บทนี้ไม่ได้บอกว่าคลัง column ไม่มีข้อดี มันบอกว่าข้อดีที่ตำราให้เครดิตกับการวางแนว เป็นข้อดีของการเข้ารหัส และคลังแนวแถวก็เข้ารหัสเป็นเหมือนกัน
ที่วัดได้ กับที่รูปแบบกำหนด
หัวข้อที่มีชื่อว่า “ที่วัดได้ กับที่รูปแบบกำหนด”ตัวเลขมาจากการรัน ส่วนชื่อของโครงสร้างมาจากรูปแบบ และสองอย่างนี้เป็นคนละหลักฐาน
ค่า 3320 กับ 989 เป็นสิ่งที่ probe วัดจากคลังที่ colstore.py เขียนลงจริง
ส่วน 3153 วัดจาก heap ที่ heapfile.py ของคอร์สที่ 26 เขียน และทั้งสามค่านับหน้า
ด้วย Pager.reads ตัวเดียวกัน
ส่วนคำอธิบายว่าทำไมรูปแบบแนว column ถึงเก็บ metadata ไว้ท้ายคลัง เป็นสิ่งที่เอกสาร ของ Parquet กำหนด บทนี้ไม่ได้พิสูจน์ข้อนั้น มันเพียงยืมรูปมาใช้
คลังของคอร์สนี้ไม่ใช่ Parquet ทรงของมันต่างกันตั้งแต่ระดับไบต์ เช่น plain encoding ของที่นี่เขียนความยาวของสตริงไว้ข้างหน้าด้วยความกว้างคนละขนาดกับที่ Parquet กำหนด
บทนี้ใช้หน่วยสองอย่าง คือ จำนวนหน้าที่แตะ ซึ่งนับด้วย Pager.reads ของคอร์สที่ 26
และ จำนวนไบต์ ที่การเข้ารหัสหนึ่งแบบใช้เก็บ column หนึ่งตัว
ทั้งคอร์สไม่มีหน่วยเวลาสักตัว และไม่มีประโยคว่าเร็วขึ้นกี่เท่า คอร์สนี้รับกฎข้อนั้น มาทั้งดุ้นจากคอร์สที่ 26 ของเสาเดียวกัน
หน้ารวมของคอร์สประกาศหน่วยที่ยอมรับไว้ครบทุกอย่าง อ่านได้ที่ หน้ารวมของคอร์สนี้
คำถามที่บทนี้ทิ้งไว้
หัวข้อที่มีชื่อว่า “คำถามที่บทนี้ทิ้งไว้”รู้แล้วว่าการวางแนวอย่างเดียวแพ้ ยังไม่รู้ว่ามันซื้ออะไรกลับมา
คำถามของบทนี้ถูกเลือกให้แตะครบ 6 จาก 6 column โดยตั้งใจ เพราะมันปิดสิ่งที่คลัง column ทำได้และคลังแนวแถวทำไม่ได้ทิ้งไป
คำถามจริงในงานวิเคราะห์ข้อมูลไม่ค่อยหน้าตาแบบนั้น มันแตะ column ไม่กี่ตัวจากตารางที่มี หลายสิบตัว และนั่นคือช่องที่การวางแนวเปิดไว้ให้
บทที่ 2 ถามคำถามใบเดิม แต่ให้แตะแค่ 2 จาก 6 column แล้ววัดว่าส่วนได้เปรียบ เปลี่ยนไปเท่าไร เป็นบทเดียวในคอร์สที่ยืนยันสิ่งที่ผู้อ่านเดาไว้ และมันจงใจอยู่ตรงนั้น
-
Apache Parquet — File Format (เข้าถึง 2026-09-10) — ที่มาของศัพท์ row group กับ column chunk และของข้อที่ว่า metadata ถูกเขียนไว้หลังข้อมูล เพื่อให้เขียนคลังจบในรอบเดียวได้
-
Apache Parquet — Encodings (เข้าถึง 2026-09-10) — ที่มาของข้อที่ว่า PLAIN เขียนค่าต่อกันไปตรงๆ และของข้อที่ว่า ตัวรหัสของ dictionary ถูกเขียนด้วยความกว้างที่มาจากจำนวนสมาชิกของ dictionary นั้น
-
ตัวเลขทุกตัวในบทนี้มาจาก
python3 scripts/column-store/probe.pyซึ่งสร้างคลังขึ้นใหม่ ทุกรอบจากfixture.pyของคอร์สที่ 26 · ค่าที่ตรึงไว้อยู่ที่scripts/column-store/expected/column-store.txt -
สองบรรทัดของ heap ที่ถือกลไกนี้อยู่ที่
scripts/query-engine/qe/heapfile.pyบรรทัดที่ 13 และบรรทัดที่ 18 · fence สองอันข้างบนยกมาตั้งแต่บรรทัดที่ 11 และ 16 เพื่อให้เห็นบริบท · คอร์สนี้ไม่แก้อะไรในscripts/query-engine/qe/แม้แต่ byte เดียว
เช็กความเข้าใจ — บทที่ 1
ข้อ 1 / 3สแกนทั้งตารางโดยแตะครบ 6 จาก 6 column บนคลัง column ที่ยังไม่เข้ารหัส แตะกี่หน้า เทียบกับ heap ที่แตะ 3153 หน้า