ใส่ dictionary ให้ครบทุก column แล้วสาม column ใหญ่ขึ้น · id จาก 800,360 เป็น 1,200,440 ไบต์
บทที่ 1 พิสูจน์ว่า heap แทน status ด้วยตัวรหัสหนึ่ง byte มาตั้งแต่ต้น
คำถามที่ตามมาเองคือ ถ้ากลไกนั้นดีขนาดนั้น ใส่ให้ครบทุก column เลยจะเป็นอย่างไร
บทนี้ทำแบบนั้นตรงๆ ตารางใบเดิม คลังใบเดิม การวางแนวเหมือนเดิมทุกอย่าง เปลี่ยนอย่างเดียวคือให้ทั้งหก column เก็บตัวรหัสแทนการเก็บค่าตามที่มันเป็น
คำทำนายที่สมเหตุสมผลคือทุก column เล็กลง ค่าที่วัดได้ตอบว่า สาม column ใหญ่ขึ้น
id ใช้ 800,360 ไบต์ ในทรงที่ยังไม่เข้ารหัส แล้วขึ้นเป็น 1,200,440 ไบต์
ในทรงที่ใส่ dictionary
บทนี้ตอบว่าเส้นที่แยกฝั่งที่เล็กลงออกจากฝั่งที่ใหญ่ขึ้น อยู่ตรงไหน
สั่ง python3 scripts/column-store/probe.py ได้เลย มันใช้ Python 3 กับ standard
library ล้วน ไม่ต้องติดตั้งอะไรก่อน และไม่มี container ให้เก็บกวาดทีหลัง
ค่าที่วัดได้ทุกตัวที่บทนี้พิมพ์มาจากหมวด ### 3_dict_เทียบ_raw กับหมวด ### สนามแข่ง
ของ scripts/column-store/expected/column-store.txt และชื่อคีย์เขียนกำกับไว้ในทุกตาราง
ตัวเลขที่ไม่มีคีย์ของตัวเองในผลของ probe ไม่มีอยู่ในบทนี้ อัตราส่วนที่บทนี้พิมพ์
มีคีย์ชื่อ dict หารด้วย raw และไม่ได้คำนวณขึ้นใหม่ที่นี่
คำทำนายที่สมเหตุสมผล และสาม column ที่หักล้างมัน
หัวข้อที่มีชื่อว่า “คำทำนายที่สมเหตุสมผล และสาม column ที่หักล้างมัน”ตำราจับคู่ dictionary encoding กับคำว่าเล็กลง ค่าที่วัดได้แยกหก column ออกเป็นสองฝั่งฝั่งละสาม
ทรงตั้งต้นของการเทียบคือ plain encodingplain encodingการเก็บค่าตามที่มันเป็น โดยไม่มีตัวรหัสมาแทน · เป็นเส้นฐานที่คอร์สนี้เทียบการเข้ารหัสทุกแบบกลับมาหา และเป็นทรงเดียวที่ column สตริงจ่ายเต็มความยาวของสตริงทุกแถว คือเก็บค่าตามที่มันเป็นโดยไม่มีตัวรหัสมาแทน ทรงที่สองคือ dictionary encodingdictionary encodingการเก็บค่าที่ไม่ซ้ำไว้ในตารางใบหนึ่ง แล้วเก็บเฉพาะตัวรหัสที่ชี้เข้าไป · heap ของคอร์สที่ 26 ทำสิ่งนี้อยู่ก่อนแล้วที่ heapfile.py:18 ด้วย STATUS.index(st) · และมันไม่ได้ทำให้เล็กลงเสมอ เงื่อนไขที่ตัดสินคือจำนวนค่าที่ไม่ซ้ำใน row group เดียว ไม่ใช่ทั้ง column คือเก็บรายการค่าที่ไม่ซ้ำไว้ แล้วเขียนตำแหน่งในรายการนั้นลงแทนค่าจริง
สองทรงนี้อ่านตารางใบเดียวกัน ตัดก้อนเท่ากัน และวางแนวเหมือนกันทุกอย่าง ตัวแปรที่เหลืออยู่ตัวเดียวคือการเข้ารหัส
| column | raw ไบต์ | dict ไบต์ | ส่วนต่าง (บวก = dict ใหญ่กว่า) | dict ใหญ่กว่า raw |
|---|---|---|---|---|
id | 800,360 | 1,200,440 | +400,080 | true |
placed_at | 800,360 | 1,188,004 | +387,644 | true |
total | 800,360 | 1,180,592 | +380,232 | true |
customer | 800,360 | 559,336 | -241,024 | false |
status | 1,985,792 | 201,740 | -1,784,052 | false |
restaurant | 8,398,262 | 208,680 | -8,189,582 | false |
สามแถวบนคือฝั่งที่หักล้างคำทำนาย การใส่ dictionary ให้ column พวกนั้นไม่ได้ทำให้เล็กลง
มันทำให้ใหญ่ขึ้น และ probe เก็บข้อนี้ไว้เป็นบูลีนของตัวเองที่คีย์ dict ใหญ่กว่า raw
ประโยคที่พบทั่วไปคือ dictionary encoding ทำให้ column เล็กลง เพราะมันแทนค่าที่ซ้ำกันด้วยตัวรหัสสั้นๆ
ครึ่งหลังของประโยคนั้นถูก และสามแถวล่างของตารางข้างบนคือหลักฐานว่ามันจ่ายคืนจริง
ครึ่งแรกไม่ถูก มันเติมคำว่า เสมอ เข้าไปเอง id placed_at และ total
มีค่า ส่วนต่าง (บวก = dict ใหญ่กว่า) เป็นบวกทั้งสามตัว
ตัวแปรอยู่ที่ขอบของ row group ไม่ใช่ที่ตัว column
หัวข้อที่มีชื่อว่า “ตัวแปรอยู่ที่ขอบของ row group ไม่ใช่ที่ตัว column”จำนวนค่าที่ไม่ซ้ำทั้ง column ทำนายผลไม่ได้ ตัวที่ทำนายได้คือจำนวนค่าที่ไม่ซ้ำในหนึ่งก้อน
คลัง column ตัดแถวออกเป็นก้อน ก้อนละเท่ากัน และค่าที่ตรึงไว้อยู่ในหมวด ### สนามแข่ง
ของผลของ probe
| ของที่ตรึงไว้ | คีย์ในผลของ probe | ค่า |
|---|---|---|
| จำนวนแถวต่อหนึ่งก้อน | row group ปริยาย | 10,000 |
| จำนวนก้อนทั้งคลัง | row group ทั้งหมด | 20 |
การเข้ารหัสเป็นค่าต่อก้อน ไม่ใช่ค่าต่อทั้ง column ก้อนหนึ่งจึงสร้างรายการค่าที่ไม่ซ้ำ ของตัวเองขึ้นมาใบหนึ่ง โดยไม่รู้ว่าก้อนอื่นเก็บอะไรไว้
probe เก็บทั้งสองตัวเลขไว้คนละคีย์ให้เทียบกันได้ตรงๆ
| column | ค่าที่ไม่ซ้ำทั้ง column | ค่าที่ไม่ซ้ำมากสุดใน row group เดียว | dict ใหญ่กว่า raw |
|---|---|---|---|
id | 200,000 | 10,000 | true |
placed_at | 196,891 | 9,991 | true |
total | 125,327 | 9,771 | true |
customer | 2,000 | 1,994 | false |
restaurant | 10 | 10 | false |
status | 7 | 7 | false |
total มีค่าที่ไม่ซ้ำทั้ง column 125,327 ค่า ส่วน placed_at มี 196,891 ค่า
ตัวเลขสองตัวนี้ต่างกันมาก แต่ทั้งคู่ตกฝั่งเดียวกันในตารางแรก
ค่าที่ทำนายฝั่งได้คือ column ขวา id มี 10,000 ค่าที่ไม่ซ้ำในกลุ่มเดียว
เท่ากับ row group ปริยาย พอดี ส่วน restaurant มี 10
dictionary ทั้งใบถูกคัดลอกลงทุกก้อน
หัวข้อที่มีชื่อว่า “dictionary ทั้งใบถูกคัดลอกลงทุกก้อน”กลไกอยู่ในสองส่วนของ
colstore.pyและมันเขียน dictionary ลงคลังใหม่ทุกก้อน
chunk หนึ่งอันคือหัวขนาดคงที่ ตามด้วย dictionary ของก้อนนั้น แล้วตามด้วยตัวรหัส
fence ข้างล่างสร้างสองส่วนหลัง คือ db กับ body ส่วนบรรทัดที่ 174 ของ colstore.py
เป็นบรรทัดที่ต่อทั้งสามส่วนเข้าด้วยกัน
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] db = dict_blob(vs, kind) if enc == DICT: w = 1 if len(vs) <= 256 else (2 if len(vs) <= 65536 else 4) fmt = {1: "<B", 2: "<H", 4: "<I"}[w] body = b"".join(struct.pack(fmt, c) for c in codes) bits = w * 8 m["fixed_width"] = wvs คือรายการค่าที่ไม่ซ้ำ ของก้อนนั้นก้อนเดียว เพราะ vals ที่ส่งเข้ามาคือค่าของก้อนนั้น
ส่วน codes คือตำแหน่งในรายการนั้น
dict_blob เขียนรายการ vs ลงคลังทั้งใบ คือเขียนจำนวนสมาชิกไว้ข้างหน้า แล้วตามด้วย
ค่าทุกตัวเต็มๆ ไม่ได้ย่ออะไรเลย
def dict_blob(vals, kind): if kind == "u32": return struct.pack("<I", len(vals)) + b"".join(struct.pack("<I", v) for v in vals) parts = [struct.pack("<I", len(vals))] for v in vals: b = v.encode() parts.append(bytes([len(b)]) + b) return b"".join(parts)บัญชีของ chunk จึงมีสองก้อนที่ต้องจ่าย คือตัวรหัสหนึ่งชุดต่อแถว บวก dictionary หนึ่งใบต่อก้อน ตัวรหัสแคบกว่าค่าเดิม ส่วน dictionary เป็นของที่เพิ่มเข้ามาใหม่ทั้งดุ้น
id คือขั้วที่ dictionary เป็นภาระล้วน
หัวข้อที่มีชื่อว่า “id คือขั้วที่ dictionary เป็นภาระล้วน”ค่าที่ไม่ซ้ำในกลุ่มเท่ากับจำนวนแถวของกลุ่มพอดี dictionary จึงเก็บทุกค่าซ้ำอีกรอบหนึ่ง
id เป็นเลขวิ่งของออเดอร์ ไม่มีค่าไหนซ้ำกับค่าไหน คีย์ ค่าที่ไม่ซ้ำมากสุดใน row group เดียว
ของมันจึงเป็น 10,000 ซึ่งเท่ากับ row group ปริยาย พอดี
รายการ vs ของก้อนนั้นจึงยาวเท่ากับจำนวนแถวของก้อน dictionary หนึ่งใบเก็บค่าเดิมครบทุกค่า
โดยไม่มีค่าไหนถูกใช้ซ้ำเลยแม้แต่ครั้งเดียว
| ทรงที่วัด | คีย์ในผลของ probe | ไบต์ |
|---|---|---|
id · plain encoding | raw ไบต์ | 800,360 |
id · dictionary encoding | dict ไบต์ | 1,200,440 |
| ส่วนต่างของสองแถวบน | ส่วนต่าง (บวก = dict ใหญ่กว่า) | +400,080 |
| คีย์ในผลของ probe | ค่า |
|---|---|
id → dict หารด้วย raw | 1.5 |
placed_at กับ total อยู่ฝั่งเดียวกันด้วยเหตุผลเดียวกัน คีย์
ค่าที่ไม่ซ้ำมากสุดใน row group เดียว ของสองตัวนี้คือ 9,991 และ 9,771
ทั้งคู่ยังไม่ถึง 10,000 แต่ใกล้พอที่ dictionary จะเก็บเกือบทุกแถวซ้ำอีกรอบ
ค่า dict หารด้วย raw ของมันคือ 1.484 และ 1.475
restaurant คือขั้วตรงข้าม และมันไปไกลกว่าที่ตารางอื่นชวนให้คิด
หัวข้อที่มีชื่อว่า “restaurant คือขั้วตรงข้าม และมันไปไกลกว่าที่ตารางอื่นชวนให้คิด”สิบค่าที่ไม่ซ้ำ ต่อสองแสนแถว แล้วมันลดไบต์ลงมากกว่าทุก column ที่เหลือในตารางเดียวกัน
restaurant เป็นชื่อร้าน มีค่าที่ไม่ซ้ำทั้ง column 10 ค่า และในก้อนเดียวก็ยังเป็น 10
เท่าเดิม dictionary ของทุกก้อนจึงมีสมาชิกสิบตัว
ทรง plain encoding ของ column ตัวนี้เขียนชื่อร้านเต็มๆ ใหม่ทุกแถว ซึ่งเป็นกลไกเดียวกับ
ที่บทที่ 1 วัดไว้กับ status
| ทรงที่วัด | คีย์ในผลของ probe | ไบต์ |
|---|---|---|
restaurant · plain encoding | raw ไบต์ | 8,398,262 |
restaurant · dictionary encoding | dict ไบต์ | 208,680 |
| ส่วนต่างของสองแถวบน | ส่วนต่าง (บวก = dict ใหญ่กว่า) | -8,189,582 |
| คีย์ในผลของ probe | ค่า |
|---|---|
restaurant → dict หารด้วย raw | 0.025 |
status → dict หารด้วย raw | 0.102 |
customer → dict หารด้วย raw | 0.699 |
customer เป็นตัวที่อ่านยากที่สุดในสามตัวนี้ มันมีค่าที่ไม่ซ้ำทั้ง column 2,000 ค่า
และในก้อนเดียว 1,994 ค่า ซึ่งใกล้ 2,000 มาก
แต่ก้อนหนึ่งมี 10,000 แถว ค่าหนึ่งค่าจึงถูกใช้ซ้ำหลายครั้งภายในก้อนเดียวกัน
dict ไบต์ ของมันคือ 559,336 เทียบ raw ไบต์ ที่ 800,360
ชนิดของค่าไม่ใช่ตัวตัดสิน customer เป็นเลขและมันเล็กลง ส่วน id ก็เป็นเลข
และมันใหญ่ขึ้น สองตัวนี้มี raw ไบต์ เท่ากันที่ 800,360
จำนวนค่าที่ไม่ซ้ำทั้ง column ก็ไม่ใช่ตัวตัดสิน total มี 125,327 ค่า
และ placed_at มี 196,891 ค่า ต่างกันคนละระดับ แต่ตกฝั่งเดียวกัน
ตัวที่ตัดสินคือคีย์ ค่าที่ไม่ซ้ำมากสุดใน row group เดียว เทียบกับ row group ปริยาย
ที่ 10,000 และนั่นคือขอบของก้อน ไม่ใช่คุณสมบัติของ column
สองขั้วในรูปเดียว
หัวข้อที่มีชื่อว่า “สองขั้วในรูปเดียว”ก้อนเดียวกัน กลไกเดียวกัน แล้วสิ่งที่ต่างคือรายการค่าที่ไม่ซ้ำของก้อนนั้นยาวแค่ไหน
flowchart TB G["หนึ่ง row group · 10,000 แถว · คีย์ row group ปริยาย"] G --> A["ขั้วที่ dictionary เป็นภาระ · column id"] G --> B["ขั้วที่ dictionary จ่ายคืน · column restaurant"] A --> A1["ค่าที่ไม่ซ้ำมากสุดในกลุ่มเดียว · 10,000 ค่า"] A1 --> A2["dictionary เก็บทุกค่าซ้ำอีกรอบ ไม่มีค่าไหนถูกใช้ซ้ำ"] A2 --> A3["raw 800,360 ไบต์ · dict 1,200,440 ไบต์"] B --> B1["ค่าที่ไม่ซ้ำมากสุดในกลุ่มเดียว · 10 ค่า"] B1 --> B2["dictionary สิบสมาชิก ส่วนตัวรหัสกินที่แทนชื่อร้านเต็มๆ"] B2 --> B3["raw 8,398,262 ไบต์ · dict 208,680 ไบต์"]
คำบรรยายภาพ: สองกิ่งออกจากก้อนเดียวกันและใช้กลไกเดียวกัน กิ่งซ้ายคือ column ที่รายการ ค่าที่ไม่ซ้ำยาวเท่ากับจำนวนแถวของก้อน กิ่งขวาคือ column ที่รายการนั้นยาวสิบ ระยะระหว่างสองใบล่างจึงเป็นผลของขอบก้อน ไม่ใช่ผลของการวางแนว
ที่วัดได้ กับที่รูปแบบกำหนด
หัวข้อที่มีชื่อว่า “ที่วัดได้ กับที่รูปแบบกำหนด”ไบต์มาจากการรัน ส่วนข้อที่ว่า dictionary ผูกกับก้อนไหน มาจากรูปแบบ และสองอย่างนี้เป็นคนละหลักฐาน
ค่า 800,360 กับ 1,200,440 เป็นสิ่งที่ probe วัดจากคลังที่ colstore.py เขียนลงจริง
และค่าไบต์ของทั้งหก column ในบทนี้อ่านจากหมวด ### 3_dict_เทียบ_raw ของผลชุดเดียวกัน
ส่วนข้อที่ว่า dictionary เป็นของประจำ column chunk ไม่ใช่ของประจำทั้ง column เป็นสิ่งที่รูปแบบของ Parquet กำหนดไว้ และคลังของคอร์สนี้ยืมรูปนั้นมา
รูปแบบนั้นยังกำหนดทางถอยไว้ด้วย คือตัวเขียนถอยกลับไปใช้ PLAIN เมื่อ dictionary
ของก้อนโตเกินเพดานที่ตั้งไว้ คลังของคอร์สนี้ไม่มีทางถอยนั้น มันใส่ dictionary ตามที่สั่ง
ตัวเลขของ id จึงเป็นตัวเลขของกรณีที่ตัวเขียนจริงจะหลบไปแล้ว
คำถามที่บทนี้ทิ้งไว้
หัวข้อที่มีชื่อว่า “คำถามที่บทนี้ทิ้งไว้”รู้แล้วว่าเมื่อไรควรใส่ dictionary ยังไม่รู้ว่าตัวรหัสต้องกว้างเท่าไร
fence แรกของบทนี้เลือกความกว้างของตัวรหัสที่บรรทัดซึ่งตั้งค่า w แล้วส่ง w ต่อไปให้
struct.pack ค่าที่ w รับได้ทุกค่าในบรรทัดนั้นมีหน่วยเป็น byte เต็ม
restaurant มีสมาชิกสิบตัวในก้อนหนึ่ง แต่ทรงนี้ยังจ่ายตัวรหัสเต็มความกว้างที่บรรทัดนั้นเลือกให้
ทั้งที่รหัสของสมาชิกสิบตัวไม่ต้องการที่มากขนาดนั้น
บทที่ 4 · bit ไม่ใช่ byte ถอดข้อจำกัดนั้นออก แล้ววัดว่าเหลือเท่าไร โดยไม่เปลี่ยนอย่างอื่นสักอย่าง
-
Apache Parquet — Encodings (เข้าถึง 2026-09-10) — ที่มาของข้อที่ว่า PLAIN เขียนค่าต่อกันไปตรงๆ และของข้อที่ว่า ตัวรหัสของ dictionary ถูกเขียนด้วยความกว้างที่มาจากจำนวนสมาชิกของ dictionary นั้น
-
Apache Parquet — Encodings (เข้าถึง 2026-09-10) — ที่มาของข้อที่ว่า dictionary เป็นของประจำ column chunk “The dictionary will be stored in a dictionary page per column chunk” และของข้อที่ว่า ตัวเขียนถอยกลับไปใช้ PLAIN ได้เมื่อ dictionary โตเกินเพดาน “If the dictionary grows too big … the encoding will fall back to the plain encoding” · ดึงหน้า File Format มาอ่านแล้วไม่มีคำว่า dictionary เลยสักครั้ง สองข้อนี้จึงอยู่หน้านี้
-
ตัวเลขทุกตัวในบทนี้มาจาก
python3 scripts/column-store/probe.pyซึ่งสร้างคลังขึ้นใหม่ ทุกรอบจากfixture.pyของคอร์สที่ 26 · ค่าที่ตรึงไว้อยู่ที่scripts/column-store/expected/column-store.txtหมวด### 3_dict_เทียบ_rawและหมวด### สนามแข่ง -
fence สองอันในบทนี้ยกมาจาก
scripts/column-store/colstore.pyบรรทัดที่ 155 และบรรทัดที่ 104 · คอร์สนี้ไม่แก้อะไรในscripts/query-engine/qe/แม้แต่ byte เดียว
เช็กความเข้าใจ — บทที่ 3
ข้อ 1 / 3ใส่ dictionary ให้ครบทั้งหก column แล้ว column ไหนใหญ่ขึ้นแทนที่จะเล็กลง