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

เก็บ​ตาราง​ใบ​เดิม​กลับ​ด้าน แล้ว​สแกน​ทั้ง​ตาราง​แตะ 3320 หน้า ส่วน heap แตะ 3153

ตา​รา​งอ​อเด​อร์​ที่​คอร์ส​ที่ 26 กับ 28 ใช้​อยู่​มี 200,000 แถว บท​นี้​ไม่​ได้​สร้าง​ตาราง​ใหม่ มัน​เก็บ​ตาราง​ใบ​เดิม​ลง​คลัง​อีก​ใบ​หนึ่ง โดย​วาง​ค่า​ของ column เดียวกัน​ไว้​ติด​กัน แทน​การ​วาง​ทั้ง​แถว​ไว้​ติด​กัน

แล้ว​ถาม​คำถาม​เดียว การ​วาง​แนว​แบบ​นั้น​ทำให้​อ่าน​น้อย​ลง​จริง​ไหม

สรุป​ตาม​ตำรา​ตอบ​ว่า​จริง ค่าที่​วัด​ได้​จาก​ตาราง​ใบ​นี้​ตอบ​ว่า ยัง

สแกน​ทั้ง​ตาราง​บน heap แตะ 3153 หน้า ส่วน​การ​สแกน​ตาราง​ใบ​เดียวกัน​บน column storecolumn storeคลัง​ที่​วาง​ค่า​ของ column เดียวกัน​ไว้​ติด​กัน ต่าง​จาก heap ของ​คอร์ส​ที่ 26 ที่​วาง​ทั้ง​แถว​ไว้​ติด​กัน · คอร์ส​นี้​เก็บ​ตาราง​เดิม​กลับ​ด้าน แล้ว​แยก​ผล​ของ​การ​วาง​แนว​ออก​จาก​ผล​ของ​การ​เข้า​รหัส ซึ่ง​เป็น​สอง​เรื่อง​ที่​สรุป​ตาม​ตำรา​รวม​ไว้​เป็น​ก้อน​เดียว ที่​ยัง​ไม่​เข้า​รหัส และ​แตะ​ครบ 6 จาก 6 column แตะ 3320 หน้า

บท​นี้​ตอบ​ว่า​ทำไม แล้ว​ลด 3320 ให้​เหลือ 989 โดย​ไม่​ขยับ​การ​วาง​แนว​สัก​นิด

📦 ตัวเลข​ทุก​ตัว​ใน​บท​นี้​รัน​เอง​ได้ ไม่​ต้อง​มี Docker และ​ไม่​ต้อง​ต่อ​เน็ต

สั่ง 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_SIZE4096
จำนวน​แถว​ต่อ​หนึ่ง​ก้อน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 ไม่ใช่​ตัว​นับ ที่​คอร์ส​นี้​เขียน​ขึ้น​เอง

scripts/column-store/probe.py:63 — ตัว​ที่​นับ​หน้า​ให้​ฝั่ง​คลัง column
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, n

q.reads คือ​ค่าที่ Pager.reads เดิน​ไว้ หน้าที่ chunk สอง​อัน​ใช้​ร่วม​กัน​ถูก​นับ​ครั้ง​เดียว ซึ่ง​เป็นการ​เทียบ​ที่​ยุติธรรม​กับ heap เพราะ​การ​สแกน heap ก็​แตะ​แต่ละ​หน้า​ครั้ง​เดียว​อยู่​แล้ว

คำ​ทำนาย​ที่​สม​เหตุ​สม​ผล และ​ตัวเลข​ที่​หัก​ล้าง​มัน

หัวข้อ​ที่​มีชื่อ​ว่า “คำ​ทำนาย​ที่​สม​เหตุ​สม​ผล และ​ตัวเลข​ที่​หัก​ล้าง​มัน”

คำถาม​แตะ​ครบ​ทั้ง​หก column ทั้ง​สอง​ฝั่ง แล้ว​ฝั่ง​ที่​วาง​กลับ​ด้าน​อ่าน​มากกว่า

คำ​ทำนาย​ที่​คน​ส่วน​ใหญ่​เขียน​ออก​มา​ก่อน​วัด​คือ คลัง column อ่าน​น้อย​กว่า​เสมอ เพราะ​มัน​หยิบ​เฉพาะ​สิ่ง​ที่​คำถาม​ขอ​ได้

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

ทรง​ที่​วัดคีย์​ใน​ผล​ของ probeหน้าที่​แตะ
heap · สแกน​ทั้ง​ตารางหน้าที่แตะเมื่อสแกนทั้งตาราง3153
คลัง column ที่​ยัง​ไม่​เข้า​รหัส · แตะ​ครบ 6 จาก 6column 6 จาก 6 · raw3320

แถว​ที่​สอง​มากกว่า​แถว​แรก การ​วาง​แนว​อย่าง​เดียว​จึง​แพ้​คลัง​แนว​แถว​ที่​มัน​มา​แทน

probe เก็บ​อัตราส่วน​ของ​คู่​นี้​ไว้​เป็น​ค่า​ของ​ตัว​เอง​ด้วย และ​ค่าที่​น้อย​กว่า​หนึ่ง อ่าน​ว่า heap แตะ​น้อย​กว่า ไม่ใช่​มากกว่า

คีย์​ใน​ผล​ของ probeค่า
heap หารด้วย 6 column raw0.95
ประโยค​ที่​บท​นี้​หัก​ล้าง

ประโยค​ที่​พบ​ทั่วไป​คือ คลัง column อ่าน​น้อย​กว่า เพราะ​มัน​ไม่​ต้อง​อ่าน column ที่​ไม่​ได้​ถาม ครึ่ง​หลัง​ของ​ประโยค​นั้น​ถูก และ​บท​ที่ 2 วัด​ให้​ดู​ว่า​มัน​จ่าย​คืน​เท่าไร

ครึ่ง​แรก​ไม่​ถูก มัน​เป็น​ข้อ​สรุป​ของ การ​วาง​แนว​บวก​การ​เข้า​รหัส ที่​ถูก​เขียน​ราวกับ​เป็น ข้อ​สรุป​ของ​การ​วาง​แนว​อย่าง​เดียว

ค่า column 6 จาก 6 · raw ที่ 3320 คือ​ค่า​ของ​การ​วาง​แนว​อย่าง​เดียว และ​มัน​แพ้ หน้าที่แตะเมื่อสแกนทั้งตาราง ของ heap ที่ 3153

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

ทรง​แรก​ของ​คลัง column ใน​บท​นี้​ใช้ plain encodingplain encodingการ​เก็บ​ค่า​ตาม​ที่​มัน​เป็น โดย​ไม่มี​ตัว​รหัส​มา​แทน · เป็น​เส้น​ฐาน​ที่​คอร์ส​นี้​เทียบ​การ​เข้า​รหัส​ทุก​แบบ​กลับ​มา​หา และ​เป็น​ทรง​เดียว​ที่ column สตริง​จ่าย​เต็ม​ความ​ยาว​ของ​สตริง​ทุก​แถว คือ​เก็บ​ค่า​ตาม​ที่​มัน​เป็น โดย​ไม่มี​ตัว​รหัส​มา​แทน

scripts/column-store/colstore.py:144 — plain encoding ของ chunk หนึ่ง​ก้อน
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 ทั้ง column200,000
คลัง column · plain encodingstatus → raw ไบต์1,985,792

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

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

scripts/query-engine/qe/heapfile.py:11 — รายการ​ค่าที่​ไม่​ซ้ำ และ​หัว​ขนาด​คงที่
STATUS = ["Placed", "Confirmed", "Preparing", "PickedUp",
"Delivered", "Rejected", "Cancelled"]
FIXED = struct.Struct("<IIBIIH") # id, customer_id, status, placed_at, satang, len(name)
scripts/query-engine/qe/heapfile.py:16 — บรรทัด​ที่​แทน​สตริง​ด้วย​ตัว​รหัส
def encode(oid, cid, st, rest, ts, satang):
b = rest.encode()
return FIXED.pack(oid, cid, STATUS.index(st), ts, satang, len(b)) + b

B ใน​สตริง​รูป​แบบ​คือ​หนึ่ง 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 ทุก​อย่าง ต่าง​กัน​ที่​การ​เข้า​รหัส​อย่าง​เดียว

scripts/column-store/colstore.py:155 — หัว​ที่ dictionary encoding กับ bit packing ใช้​ร่วม​กัน
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]

สอง​การ​เข้า​รหัส​นี้​สร้าง​รายการ​ตัว​รหัส​ชุด​เดียวกัน ต่าง​กัน​ที่​ความ​กว้าง​ที่​ใช้​เขียน​ตัว​รหัส นั้น​ลง​คลัง

scripts/column-store/colstore.py:50 — ความ​กว้าง​มา​จาก​จำนวน​ค่าที่​ไม่​ซ้ำ​ใน​ก้อน​นั้น
def bits_for(k):
"""จำนวนบิตที่พอใส่รหัส 0..k-1 · อย่างน้อยหนึ่งบิตเสมอ"""
return max(1, (k - 1).bit_length()) if k > 1 else 1

bits_for ให้​ความ​กว้าง​เท่า​ที่​พอ​ใส่​รหัส​ตัว​สุดท้าย โดย​ไม่​ปัด​ขึ้น​เป็น byte เต็ม และ​มัน​คิด​จาก​ค่าที่​ไม่​ซ้ำ​ใน column chunk ก้อน​นั้น จึง​เป็น​ค่า​ต่อ​ก้อน

ทรง​ที่​วัดคีย์​ใน​ผล​ของ probeหน้าที่​แตะ
คลัง column ที่​ยัง​ไม่​เข้า​รหัส · แตะ​ครบ 6 จาก 6column 6 จาก 6 · raw3320
คลัง column · bit packing · แตะ​ครบ 6 จาก 6column 6 จาก 6 · bitpack989
คีย์​ใน​ผล​ของ probeค่า
heap หารด้วย 6 column bitpack3.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 หน้า