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

ตัว​รหัส​ของ status ลง​เหลือ 3 บิต​ต่อ​ค่า แล้ว​การ​เข้า​รหัส​ที่​ตำรา​จับ​คู่​กับ​ข้อ​มูล​ซ้ำๆ ก็​แพ้​มัน

บท​ที่ 3 จบ​ลง​ที่ dictionary encoding ซึ่ง​แทน​ค่า​ด้วย​ตัว​รหัส แล้ว​เขียน​ตัว​รหัส​นั้น​ลง​คลัง บท​นี้​ถาม​คำถาม​ถัด​ไป ตัว​รหัส​ต้อง​กว้าง​หนึ่ง byte จริง​ไหม

status มี​ค่าที่​ไม่​ซ้ำ​เจ็ด​ค่า และ​ค่า​เดิม​ซ้ำ​ต่อ​กัน​เป็น​ช่วง​ยาว ตำรา​จับ​คู่​ข้อมูล​รูป​นี้​กับ run-length encoding และ​ผู้​เขียน probe ก็​ทำนาย​ว่า​ทรง​นั้น​คือ​คำ​ตอบ

การ​วัด​ตอบ​ครึ่ง​เดียว RLE ชนะ​ทรง​ที่​ใช้ byte เต็ม​จริง ที่ 190,386 เทียบ 201,740 ไบต์ แต่​มัน​แพ้​การ​นับ​บิต​ล้วนๆ ที่ 76,740 ไบต์ บน​ข้อมูล​ชุด​เดียวกัน

📦 ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก​คีย์​ของ​ตัว​เอง​ใน expected/column-store.txt

สั่ง python3 scripts/column-store/probe.py ได้​เลย มัน​ใช้ Python 3 กับ standard library ล้วน ไม่​ต้อง​มี Docker และ​ไม่​ต้อง​ต่อ​เน็ต

ค่าที่​บท​นี้​พิมพ์​มา​จาก​สี่​หมวด​ของ​ผล​ที่ probe พิมพ์​ออก​มา สอง​หมวด​ที่​แบก​บท​คือ ### 5_bit_packing กับ ### 4_run_length_บน_status ส่วน​อีก​สอง​หมวด​ให้​ค่า​คนละ​ตัว คือ ### 3_dict_เทียบ_raw ที่​คีย์ ค่าที่ไม่ซ้ำมากสุดใน row group เดียว และ ### สนามแข่ง ที่​คีย์ แถว · ทุก​ตาราง​เขียน​ชื่อ​คีย์​กำกับ​ไว้

ตัวเลข​ที่​ไม่มี​คีย์ บท​นี้​ไม่​พิมพ์ อัตราส่วน​ระหว่าง​ทรง RLE กับ​ทรง bit packing ไม่มี​คีย์​ใน scripts/column-store/expected/column-store.txt บท​นี้​จึง​วาง​ไบต์​ทั้ง​สอง​ตัว​ลง​ไป​แทน

สอง​การ​เข้า​รหัส​นี้​สร้าง​รายการ​ตัว​รหัส​ชุด​เดียวกัน แล้ว​เลือก​ความ​กว้าง​คนละ​แบบ

scripts/column-store/colstore.py:155 — สอง​ทรง​ที่​แยก​กัน​ตรง​บรรทัด​ที่​เลือก​ความ​กว้าง
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"] = w
else:
bits = bits_for(len(vs))
body = pack_bits(codes, bits)
m["fixed_width"] = 0

กิ่ง​บน​คือ dictionary encoding มัน​เลือก w เป็น​จำนวน byte แล้ว​คูณ​แปด ความ​กว้าง​ที่​ได้ จึง​ปัด​ขึ้น​เป็น byte เต็ม​เสมอ

กิ่ง​ล่าง​คือ bit packingbit packingการ​ให้​ตัว​รหัส​กว้าง​เท่า​ที่​จำเป็น แทน​การ​ปัด​ขึ้น​เป็น byte เต็ม · status ใช้ 3 บิต​ต่อ​ค่า​แล้ว​ได้ 76,740 ไบต์ และ restaurant ใช้ 4 บิต​ต่อ​ค่า​แล้ว​ได้ 108,680 ไบต์ เทียบ​กับ dictionary ที่​ใช้ 8 บิต​ต่อ​ค่า​ทั้ง​สอง​ตัว มัน​เรียก bits_for แล้ว​เขียน​ตัว​รหัส​ต่อ​กัน​ไป​โดย​ไม่​สน​ขอบ​ของ byte ค่าที่ bits_for คืน​มา​เรียก​ว่า bit widthbit widthจำนวน​บิต​ที่​ใช้​ต่อ​หนึ่ง​ค่า และ​ที่มา​ของ​มัน​ต่าง​กัน​ตาม​การ​เข้า​รหัส · bit packing เอา​มา​จาก​จำนวน​ค่าที่​ไม่​ซ้ำ​ใน column chunk นั้น ส่วน frame of reference เอา​มา​จาก​ระยะ​ระหว่าง​ค่าที่​มาก​ที่สุด​กับ​ค่าที่​น้อย​ที่สุด​ใน​ก้อน​เดียวกัน · ทั้ง​สอง​แบบ​เป็น​ค่า​ต่อ row group ไม่ใช่​ค่า​ต่อ​ทั้ง column ดัง​นั้น column ตัว​เดียวกัน​มี​ความ​กว้าง​ไม่​เท่า​กัน​ใน​แต่ละ​ก้อน​ได้ และ​มัน​มา​จาก​จำนวน​ค่าที่​ไม่​ซ้ำ​ใน column chunk ก้อน​นั้น

ที่​วัดหมวด​ใน​ผล​ของ probeคีย์ค่า
status · bit packing### 5_bit_packingบิตต่อค่า3
status · dictionary### 5_bit_packingบิตต่อค่าของ dict (ไบต์เต็ม)8
restaurant · bit packing### 5_bit_packingบิตต่อค่า4
restaurant · dictionary### 5_bit_packingบิตต่อค่าของ dict (ไบต์เต็ม)8

ความ​กว้าง​สอง​ค่า​นี้​ตาม​จำนวน​ค่าที่​ไม่​ซ้ำ​ที่​บท​ที่ 3 วัด​ไว้ คือ​คีย์ ค่าที่ไม่ซ้ำมากสุดใน row group เดียว ใน​หมวด ### 3_dict_เทียบ_raw ซึ่ง​เป็น 7 สำหรับ status และ 10 สำหรับ restaurant

เมื่อ​ความ​กว้าง​ลง​มา​ถึง​ระดับ​นี้ ส่วน​ที่​เหลือ​ของ chunk ก็​อ่าน​ออก​ได้​ว่า​ใคร​กิน​ที่

probe แยก​ไบต์​ของ​ทรง bit packing ออก​มา​ให้​สอง​ส่วน ส่วน​แรก​คือ​ตัว​รหัส​ของ​ทุก​แถว ส่วน​ที่​สอง​คือ dictionary ที่​แต่ละ row group ถือ​ของ​ตัว​เอง

สอง​ส่วน​นี้​บวก​กัน​แล้ว​ยัง​ไม่​เท่ากับ​ไบต์​ทั้ง column เพราะ​ทุก chunk เขียน​หัว​ของ​ตัว​เอง​เพิ่ม​ด้วย ตาม​ที่ CHUNK_HDR ใน colstore.py กำหนด

ที่​วัดหมวด​ใน​ผล​ของ probeคีย์ไบต์
status · ส่วน​ที่​เป็น​ตัว​รหัส### 5_bit_packingไบต์ที่เป็นตัวรหัส75,000
status · ส่วน​ที่​เป็น dictionary### 5_bit_packingไบต์ที่เป็น dictionary1,380
restaurant · ส่วน​ที่​เป็น​ตัว​รหัส### 5_bit_packingไบต์ที่เป็นตัวรหัส100,000
restaurant · ส่วน​ที่​เป็น dictionary### 5_bit_packingไบต์ที่เป็น dictionary8,320

restaurant จ่าย​ค่าตัว​รหัส​มากกว่า status ทั้ง​ที่​แถว​เท่า​กัน เพราะ​ความ​กว้าง​ของ​มัน เป็น 4 บิต ไม่ใช่ 3 บิต ความ​กว้าง​จึง​เป็น​ค่าที่​การ​เข้า​รหัส​เลือก​ให้​แต่ละ column chunk ไม่ใช่​หนึ่ง byte ตายตัว

ทรง​เดียว​กับ​ที่​ตำรา​แนะนำ บน​ข้อมูล​ที่​ตรง​เงื่อนไข​ที่​ตำรา​ระบุ แล้ว​มัน​ยัง​แพ้

run-length encodingrun-length encoding (RLE)การ​เก็บ​ค่า​หนึ่ง​ค่า​พร้อม​จำนวน​ครั้ง​ที่​มัน​ซ้ำ​ติด​กัน · บน status ตาม​ที่​เก็บ​อยู่​ได้ 190,386 ไบต์ ซึ่ง​เล็ก​กว่า dictionary ที่ 201,740 ไบต์ แต่​ใหญ่​กว่า bit packing ที่ 76,740 ไบต์ · เรียง​ตาม status แล้ว​ได้ 772 ไบต์ ขณะ​ที่ bit packing ใน​การ​เรียง​เดียวกัน​ได้ 29,444 ไบต์ เก็บ​ค่า​หนึ่ง​ค่า​พร้อม​จำนวน​ครั้ง​ที่​มัน​ซ้ำ​ติด​กัน ช่วง​ของ​ค่าที่​เหมือน​กัน ติด​กัน​หนึ่ง​ช่วง​เรียก​ว่า​หนึ่ง runrunช่วง​ของ​ค่าที่​เหมือน​กัน​ติด​กัน​หนึ่ง​ช่วง · status ตาม​ที่​เก็บ​อยู่​มี 62,882 run เทียบ​จุด​คุ้ม​ทุน​ของ run ที่ 66,666 run ใน​ทรง​ที่​ใช้ 3 ไบต์​ต่อ run · เรียง​ตาม status แล้ว​เหลือ 26 run

scripts/column-store/colstore.py:176 — หนึ่ง run เขียน​ลง​คลัง​เป็น​ตัว​รหัส​บวก​จำนวน​ครั้ง
elif enc == RLE:
vs = sorted(set(vals))
ix = {v: i for i, v in enumerate(vs)}
db = dict_blob(vs, kind)
w = 1 if len(vs) <= 256 else (2 if len(vs) <= 65536 else 4)
fmt = {1: "<B", 2: "<H", 4: "<I"}[w]
runs = []
cur = None
cnt = 0
for v in vals:
c = ix[v]
if c == cur and cnt < 65535:
cnt += 1
else:
if cur is not None:
runs.append((cur, cnt))
cur, cnt = c, 1
if cur is not None:
runs.append((cur, cnt))
body = b"".join(struct.pack(fmt, c) + struct.pack("<H", k) for c, k in runs)

บรรทัด​สุดท้าย​คือ​ทรง​ที่​ตัดสิน​ทุก​อย่าง หนึ่ง run เขียน​ตัว​รหัส​หนึ่ง​ตัว​ตาม​ด้วย​จำนวน​ครั้ง แบบ <H ค่าที่ RLE จ่าย​จึง​คิด​ต่อ run ไม่ใช่​ต่อ​แถว

ทรง​ที่​วัดคีย์​ใน​กลุ่ม ตามที่เก็บ (เรียงตาม placed_at)ไบต์
เก็บ​สตริง​ตาม​ที่​มัน​เป็นraw1,985,792
dictionary encoding · ตัว​รหัส​ไบต์​เต็มdict201,740
run-length encodingrle190,386
bit packingbitpack76,740

ทั้ง​สี่​แถว​มา​จาก​หมวด ### 4_run_length_บน_status กลุ่ม ตามที่เก็บ (เรียงตาม placed_at) คือ status ใน​ลำดับ​ที่​คลัง​เก็บ​มัน​ไว้​จริง ไม่​ได้​เรียง​ใหม่

คีย์​ใน​กลุ่ม​เดียวกันค่า
rle หารด้วย dict0.9437
rle เล็กกว่า dicttrue
ประโยค​ที่​บท​นี้​หัก​ล้าง

ประโยค​ที่​พบ​ทั่วไป​คือ ข้อมูล​ที่​ค่า​ซ้ำ​ติด​กัน​ยาวๆ ให้​ใช้ run-length encoding คำ​ทำนาย​ที่​ตาม​มา​คือ RLE คือ​ทรง​ที่​เล็ก​ที่สุด​ของ status

ครึ่ง​แรก​ของ​ประโยค​จริง​ตาม​ที่​วัด​ได้ RLE เล็ก​กว่า​ทรง dict ตาม​คีย์ rle เล็กกว่า dict

คำ​ทำนาย​ที่​ตาม​มา​ผิด ทรง rle ได้ 190,386 ไบต์ ส่วน​ทรง bitpack ได้ 76,740 ไบต์ บน status ชุด​เดียวกัน​และ​ลำดับ​เดียวกัน การ​นับ​บิต​ล้วนๆ ชนะ​การ​เข้า​รหัส​ที่​ตำรา​จับ​คู่​ไว้​กับ​ข้อมูล​รูป​นี้

เหตุผล​อยู่​ที่​จำนวน run เทียบ​จุด​คุ้ม​ทุน​ของ​มัน

หัวข้อ​ที่​มีชื่อ​ว่า “เหตุผล​อยู่​ที่​จำนวน run เทียบ​จุด​คุ้ม​ทุน​ของ​มัน”

RLE จ่าย​ต่อ run ทรง​จึง​คุ้ม​ก็​ต่อ​เมื่อ run น้อย​พอ และ​ที่​นี่​มัน​เฉียด​เส้น​นั้น

flowchart TB
  S["status · 200,000 แถว · ค่าที่ไม่ซ้ำ 7 ค่า"]
  S --> D["dictionary encoding · ตัวรหัสกว้าง 8 บิต · จ่ายต่อแถว · 201,740 ไบต์"]
  S --> R["run-length encoding · จ่ายต่อ run · 190,386 ไบต์"]
  S --> B["bit packing · ตัวรหัสกว้าง 3 บิต · จ่ายต่อแถว · 76,740 ไบต์"]
  R --> R1["จำนวน run ที่วัดได้ · 62,882"]
  R --> R2["จุดคุ้มทุนของ run ที่ 3 ไบต์ต่อ run · 66,666"]

คำ​บรรยาย​ภาพ: สาม​ทรง​บน status ชุด​เดียวกัน กิ่ง​กลาง​จ่าย​ต่อ run ส่วน​อีก​สอง​กิ่ง​จ่าย​ต่อ​แถว สอง​ใบ​ล่าง​คือ​ค่าที่​ตัดสิน​กิ่ง​กลาง คือ​จำนวน run ที่​วัด​ได้ เทียบ​จุด​คุ้ม​ทุน​ที่​ทรง​นี้​ต้อง​อยู่​ใต้​มัน

ที่​วัดคีย์​ใน​กลุ่ม ตามที่เก็บ (เรียงตาม placed_at)ค่า
จำนวน run ของ​ทั้ง columnจำนวน run62,882
จำนวน run ของ row group ก้อน​แรกrun ต่อ row group3046
เส้น​ที่​ทรง​นี้​ต้อง​อยู่​ใต้จุดคุ้มทุนของ run (ไบต์ต่อ run = 3)66,666

62,882 อยู่​ใต้ 66,666 แค่​นิด​เดียว RLE จึง​ชนะ​ทรง dict แบบ​เฉียด​ฉิว ตาม​คีย์ rle หารด้วย dict ที่ 0.9437

ส่วน bit packing ไม่มี​ค่า​ใช้​จ่าย​ต่อ run เลย มัน​จ่าย​ความ​กว้าง​เท่า​เดิม​ทุก​แถว เส้น​จุด​คุ้ม​ทุน​ของ RLE จึง​ไม่ใช่​เส้น​ที่ bit packing ต้อง​ข้าม

ตรง​นี้​คือ​ที่​ที่​การ​อ่าน​ผิด​เกิด​ง่าย​ที่สุด เพราะ​การ​เรียง​ช่วย​ทั้ง​สอง​ทรง ไม่ใช่​ช่วย​เฉพาะ RLE

probe สร้าง​คลัง​ใบ​ที่​สอง​จาก​แถว​ชุด​เดิม โดย​เรียง​ตาม status ก่อน​เขียน แล้ว​วัด​ทั้ง​สี่​ทรง​ซ้ำ ผลอ​ยู่​ใน​กลุ่ม เรียงตาม status แล้ว ของ​หมวด​เดียวกัน

คีย์ตามที่เก็บ (เรียงตาม placed_at)เรียงตาม status แล้ว
จำนวน run62,88226
run ต่อ row group ก้อน​แรก30461
rle190,386 ไบต์772 ไบต์
bitpack76,740 ไบต์29,444 ไบต์
dict201,740 ไบต์200,694 ไบต์
rle หารด้วย dict0.94370.0038
คีย์​ของ​หมวดค่า
จำนวน run ลดลงกี่เท่าเมื่อเรียง2418.5

RLE ได้​ประโยชน์​มาก​ที่สุด​จาก​การ​เรียง นั่น​คือ​ข้อ​ที่​คาด​ไว้​ได้ และ​ตัวเลข​ก็​ยืนยัน

ข้อ​ที่​คาด​ไว้​ไม่​ได้​อยู่​ที่​แถว bitpack ทรง​ที่​ไม่มี​แนวคิด​เรื่อง run อยู่​ใน​ตัว​เลย ก็​ลง​จาก 76,740 เหลือ 29,444 ไบต์​ใน​การ​เรียง​ชุด​เดียวกัน

ทำไม bit packing ถึง​เล็ก​ลง​ทั้ง​ที่​มัน​ไม่รู้​จัก run

ความ​กว้าง​ของ bit packing เป็น​ค่า​ต่อ row group ตาม​ที่​หัวข้อ​แรก​วาง​ไว้ มัน​มา​จาก​จำนวน ค่าที่​ไม่​ซ้ำ​ใน column chunk ก้อน​นั้น ไม่ใช่​ของ​ทั้ง column

เรียง​ตาม status แล้ว ก้อน​หนึ่ง​จึง​ถือ​ค่าที่​ไม่​ซ้ำ​น้อย​ลง และ​ความ​กว้าง​ของ​ก้อน​นั้น​ก็​ลด​ตาม

การ​อ่าน​ที่​ผิด​คือ “การ​เรียง​เป็น​ของ​ที่ RLE ได้​ประโยชน์​อยู่​ทรง​เดียว” ตัวเลข​ที่​วัด​ได้​บอก​ว่า การ​เรียง​เป็น​สิ่ง​ที่​ทั้ง​สอง​ทรง​กิน​ได้ และ​หลัง​เรียง​แล้ว​ทรง​ที่​เล็ก​ที่สุด​ก็​ยัง​เปลี่ยน​มือ​ได้ ตาม​คีย์ rle ที่ 772 ไบต์ เทียบ​คีย์ bitpack ที่ 29,444 ไบต์

ตาราง​เรียง​ได้​ลำดับ​เดียว และ​บท​นี้​วัด​เฉพาะ​ฝั่ง​ที่​ได้ ยัง​ไม่​ได้​วัด​ฝั่ง​ที่​เสีย

ตัวเลข​ใน​หัวข้อ​ที่​แล้ว​เป็น​ตัวเลข​ของ status ตัว​เดียว การ​เรียง​ใหม่​ตาม​ค่า​ของ column หนึ่ง เปลี่ยน​ขนาด​ของ column ตัว​อื่น​ด้วย และ​บท​นี้​ไม่​ได้​วัด​ข้อ​นั้น

🔁 บท​ที่​ต่อ​จาก​ตรง​นี้

🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Apache Parquet — Encodings (เข้า​ถึง 2026-09-10) — หน้า​นี้​ระบุ​ว่า​ความ​กว้าง​ของ​ตัว​รหัส​ถูก เก็บ ไว้​หนึ่ง byte · มัน​ไม่​ได้​บอก​ว่า ความ​กว้าง​นั้น​คำนวณ​มา​จาก​จำนวน​สมาชิก​ของ dictionary ซึ่ง​เป็น​สิ่ง​ที่ colstore.py:50 ทำ · และ​รูป​แบบ​นี้​พับ bit packing ไว้​ใน RLE hybrid ของ​มัน​เอง ส่วน​การ​แยก​สอง​อย่าง​ออก​จาก​กัน เป็น​ของ​คอร์ส​นี้ เพื่อ​ให้​วัด​ที​ละ​อย่าง​ได้

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก python3 scripts/column-store/probe.py หมวด ### 5_bit_packing หมวด ### 4_run_length_บน_status หมวด ### 3_dict_เทียบ_raw และ​หมวด ### สนามแข่ง · ค่าที่​ตรึง​ไว้​อยู่​ที่ scripts/column-store/expected/column-store.txt

  • fence สอง​อัน​ใน​บท​นี้​ยก​มา​จาก scripts/column-store/colstore.py บรรทัด​ที่ 155 และ 176 · คลัง​ที่ probe วัด​คือ​คลัง​ที่ colstore.py เขียน ไม่ใช่ Parquet

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

ข้อ 1 / 3

ตัวรหัสของ status กว้างกี่บิตต่อค่า ในทรง bit packing เทียบกับทรง dictionary