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

คำถาม​ใบ​เดิม​ที่​แตะ​แค่ 2 จาก 6 column แตะ 304 หน้า ส่วน heap ต้อง​อ่าน 3153

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

บท​นี้​เปิด​มัน​กลับ คำถาม​ยัง​เป็น​คำถาม​ใบ​เดิม ตาราง​ยัง​เป็น​ตาราง​ใบ​เดิม แต่​คราว​นี้​ให้​มัน​แตะ แค่ 2 จาก 6 column

คลัง column ทรง bit packing ตอบ​คำถาม​นั้น​ด้วย 304 หน้า ส่วน heap ต้อง​อ่าน 3153 หน้า

📌 บท​เดียว​ใน​คอร์ส​ที่​ยืนยัน​สิ่ง​ที่​คุณ​เดา​ไว้ และ​มัน​จงใจ​อยู่​ตรง​นี้

อีก​เจ็ด​บท​มี​อยู่​เพราะ​การ​วัด​หัก​ล้าง​คำ​ทำนาย​ที่​สม​เหตุ​สม​ผล บท​นี้​ไม่ใช่

ถ้า​คอร์ส​ข้าม​บท​นี้​ไป ผู้​อ่าน​จะ​สรุป​จาก​บท​ที่ 1 ว่า​คลัง column ไม่มี​ข้อดี​เลย ซึ่ง​ผิด สิ่ง​ที่​บท​ที่ 1 วัด​คือ การ​วาง​แนว​อย่าง​เดียว​แพ้ ไม่ใช่​ว่าการ​วาง​แนว​ไม่​ซื้อ​อะไร​กลับ​มา

บท​นี้​วัด​ว่า​มัน​ซื้อ​อะไร​กลับ​มา และ​จ่าย​คืน​มาก​ที่สุด​ตอน​ไหน

คำถาม​ใบ​เดียวกัน​ทั้ง​สอง​ฝั่ง ต่าง​กัน​ที่​ฝั่ง​คลัง column อ่าน​แค่ 2 จาก 6 column

คำถาม​ที่ probe ถาม​อยู่​ที่​คีย์ คำถาม ใน​หมวด ### 1_ฉายภาพ column ของ​ผล​ที่​มัน​พิมพ์​ออก​มา คือ SELECT total WHERE restaurant = ครัวคุณแม่

คำถาม​นี้​ต้องการ​ค่า​จาก column สอง​ตัว คือ restaurant สำหรับ​เงื่อนไข และ total สำหรับ​ผลลัพธ์ อีก​สี่​ตัว​ที่​เหลือ​ไม่มี​ใคร​ขอ

scripts/column-store/probe.py:276 — สอง​ทรง​เดียวกัน ต่าง​กัน​ที่​ราย​ชื่อ column ที่​ส่ง​เข้าไป
# ---------- 1. query ที่แตะ 2 จาก 6 column เทียบกับ 6 จาก 6 ----------
two = ("restaurant", "total")
p2_raw, v2 = scan(st_raw, two)
p2_bp, _ = scan(st_bp, two)
p6_raw, v6 = scan(st_raw, COLS)
p6_bp, _ = scan(st_bp, COLS)

st_bp เป็น​คลัง​ใบ​เดียว​ที่​ถูก​เรียก​ทั้ง​สอง​รอบ สิ่ง​ที่​ต่าง​กัน​คือ two กับ COLS

ทรง​ที่​วัดคีย์​ใน​ผล​ของ probeหน้าที่​แตะ
heap · ตอบ​คำถาม​ใบ​นี้heap ต้องอ่าน3153
คลัง column · bit packing · แตะ 2 จาก 6column 2 จาก 6 · bitpack304
คีย์​ใน​ผล​ของ probeค่า
heap หารด้วย 2 column bitpack10.37

heap อ่าน​เท่า​เดิม​เพราะ​หนึ่ง​หน้า​ของ​มัน​ถือ​ทั้ง​แถว

หัวข้อ​ที่​มีชื่อ​ว่า “heap อ่าน​เท่า​เดิม​เพราะ​หนึ่ง​หน้า​ของ​มัน​ถือ​ทั้ง​แถว”

ค่า​ของ heap ต้องอ่าน เท่ากับ​ค่า​ของ​การ​สแกน​ทั้ง​ตาราง และ​นั่น​คือ​ทั้งหมด​ที่ heap ทำได้​ใน​ทรง​นี้

ฝั่ง heap ใน​ทรง​นี้​เดิน​ที​ละ​หน้า แล้ว​แกะ​ทุก​ช่อง​ใน​หน้า​นั้น​ออก​มา​เป็น​แถว ก่อน​จะ​ดู​ค่า​ของ column ที่​เงื่อนไข​ถาม

scripts/column-store/probe.py:243 — ฝั่ง heap ของ probe เดิน​ครบ​ทุก​หน้า
for pno in range(npages):
page = pg.read_page(pno)
for s in range(nslots(page)):
row = decode(read_slot(page, s))
if row[3] == TARGET:
target_rows += 1

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

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

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

projection pushdown คือ​สิทธิ์​ที่​จะ​ไม่​เปิด column ที่​ไม่​ได้​ถาม

หัวข้อ​ที่​มีชื่อ​ว่า “projection pushdown คือ​สิทธิ์​ที่​จะ​ไม่​เปิด column ที่​ไม่​ได้​ถาม”

กลไก​อยู่​ที่​คลัง​เก็บ​แต่ละ column ไว้​คนละ​ที่ คำถาม​จึง​เปิด​เฉพาะ​ตัว​ที่​มัน​ขอ

projection pushdownprojection pushdownการ​อ่าน​เฉพาะ column ที่​คำถาม​ขอ · คำถาม SELECT total WHERE restaurant = ครัว​คุณ​แม่ ที่​แตะ 2 จาก 6 column ใน​ทรง bitpack แตะ 304 หน้า ส่วน heap ต้อง​อ่าน 3153 หน้า · ทรง​เดียวกัน​ที่​แตะ​ครบ 6 จาก 6 แตะ 989 หน้า และ​ค่าที่​ถอดรหัส​ขึ้น​จาก 400,000 เป็น 1,200,000 ค่า คือ​การ​ส่ง​ราย​ชื่อ column ที่​คำถาม​ต้องการ​ลง​ไป​ถึง​ชั้น​ที่​อ่าน​คลัง แทนที่​จะ​อ่าน​ทุก​อย่าง​ขึ้น​มา​แล้ว​ค่อย​ตัด​ทิ้ง​ข้าง​บน

ใน​คลัง​ของ​คอร์ส​นี้ กลไก​นั้น​เป็น​เรื่อง​ของ​ที่​อยู่​ล้วนๆ

scripts/column-store/colstore.py:338 — หนึ่ง column หนึ่ง​ที่​เก็บ
def path(self, col):
return os.path.join(self.dir, col + ".col")
scripts/column-store/colstore.py:378 — คำถาม​เปิด Pager เฉพาะ column ที่​มัน​แตะ
def _pager(self, col):
p = self.pagers.get(col)
if p is None:
p = Pager(self.store.path(col), "rb")
self.pagers[col] = p
return p

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

นี่​คือ​สิ่ง​ที่​การ​วาง​แนว​ซื้อ​กลับ​มา ไม่ใช่​การ​อ่าน​ที่​เร็ว​กว่า แต่​เป็น​สิทธิ์​ที่​จะ​ไม่​อ่าน

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

ทรง bit packing ตอบ​ทั้ง​สอง​คำถาม​จาก​คลัง​ใบ​เดียวกัน row group ขนาด​เดียวกัน และ​การ​เข้า​รหัส​ชุด​เดียวกัน สิ่ง​เดียว​ที่​ขยับ​คือ​จำนวน column ที่​แตะ

ทรง​ที่​วัดคีย์​ใน​ผล​ของ probeหน้าที่​แตะ
แตะ 2 จาก 6 columncolumn 2 จาก 6 · bitpack304
แตะ 6 จาก 6 columncolumn 6 จาก 6 · bitpack989
คีย์​ใน​ผล​ของ probeค่า
heap หารด้วย 2 column bitpack10.37
heap หารด้วย 6 column bitpack3.19

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

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

ทำไม​ตาราง​จริง​ถึง​เข้า​ข้าง​ฝั่ง​นี้

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

ตาราง​ใบ​นี้​มี 6 column และ​นั่น​คือ​สิ่ง​ที่ probe วัด ส่วน​ตาราง​ที่​กว้าง​กว่า​คือ​ทรง​ที่ probe ไม่​ได้​วัด บท​นี้​จึง​ไม่​อ้าง​ตัวเลข​ของ​มัน

แม้แต่​ทรง​ที่​ยัง​ไม่​เข้า​รหัส​ก็​ชนะ ถ้า​คำถาม​แคบ​พอ

หัวข้อ​ที่​มีชื่อ​ว่า “แม้แต่​ทรง​ที่​ยัง​ไม่​เข้า​รหัส​ก็​ชนะ ถ้า​คำถาม​แคบ​พอ”

ค่า​เดียวกัน​สอง​ทรง แล้ว​ทรง​ที่​บท​ที่ 1 บอก​ว่า​แพ้ กลับ​มา​ชนะ​เมื่อ​คำถาม​แคบ​ลง

บท​ที่ 1 วัด​ทรง plain encoding ที่​แตะ​ครบ 6 จาก 6 column แล้ว​ได้ 3320 หน้า ซึ่ง​มากกว่า heap ที่ 3153 หน้า ข้อ​สรุป​ตอน​นั้น​คือ​การ​วาง​แนว​อย่าง​เดียว​แพ้

คำถาม​ที่​แคบ​ลง​เปลี่ยน​ผล​ของ​ทรง​เดิม​นั้น

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

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

ค่า 2247 น้อย​กว่า 3153 ส่วน​ค่า 3320 มากกว่า ทรง​เดียวกัน​จึง​อยู่​คนละ​ฝั่ง ของ heap ขึ้น​กับ​ความ​กว้าง​ของ​คำถาม

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

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

หัวข้อ​ที่​มีชื่อ​ว่า “หน่วย​ที่​สอง คือ​ค่าที่​ถอดรหัส และ​มัน​เดิน​ไป​ทาง​เดียวกัน​ใน​บท​นี้”

จำนวน​ค่าที่​คลัง​ต้อง​แกะ​ออก​มา​เป็น​ตัวเลข ขยับ​ตาม​จำนวน column ที่​แตะ​เหมือน​กัน

หน้าที่​แตะ​ไม่ใช่​หน่วย​เดียว​ที่ probe เก็บ อีก​หน่วย​หนึ่ง​คือ​จำนวน​ค่าที่​ถูก​ถอดรหัส​ออก​มา ซึ่ง​คือ​ค่า n ที่ scan นับ​ไว้

ทรง​ที่​วัดคีย์​ใน​ผล​ของ probeค่าที่​ถอดรหัส
แตะ 2 จาก 6 columnค่าที่ถอดรหัส 2 column400,000
แตะ 6 จาก 6 columnค่าที่ถอดรหัส 6 column1,200,000

ทั้ง​สอง​ค่า​คือ​จำนวน​แถว​ของ​ตาราง ซึ่ง​อยู่​ที่​คีย์ แถว = 200,000 คูณ​ด้วย​จำนวน column ที่​คำถาม​แตะ​พอดี เพราะ​การ​สแกน​เต็ม​แกะ​ทุก​ค่า​ใน​ทุก column ที่​มัน​เปิด

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

จำ​ไว้​ว่า​มัน​ไม่​ได้​เดิน​ด้วย​กัน​เสมอ บท​ที่ 8 วัด​กลไก​ที่​ลด​ค่าที่​ถอดรหัส​ได้​จริง แต่​ใน​ทรง​ที่​เงื่อนไข​ถาม​ร้าน ครัว​คุณ​แม่ มัน​ไม่​ลด​หน้าที่​แตะ สัก​หน้า​เดียว นั่น​คือ​เหตุผล​ที่​คอร์ส​นี้​รายงาน​สอง​หน่วย​แยก​กัน ไม่ใช่​รวม​เป็น​ตัวเลข​เดียว

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

flowchart TB
  Q["คำถาม · SELECT total WHERE restaurant = ครัวคุณแม่ · ขอค่าจาก column สองตัว"]
  Q --> H["heap · หนึ่งหน้าถือทั้งแถว · หยิบ restaurant ก็ลาก column ที่เหลือมาด้วย"]
  Q --> C["คลัง column · หนึ่ง column หนึ่งที่เก็บ · เปิด Pager เฉพาะตัวที่คำถามขอ"]
  H --> H1["ต้องอ่านทุกหน้า · 3153 หน้า"]
  C --> C1["แตะ 2 จาก 6 column · 304 หน้า"]
  C --> C2["แตะ 6 จาก 6 column · 989 หน้า"]

คำ​บรรยาย​ภาพ: กิ่ง​ซ้าย​ไม่มี​ทาง​แยก เพราะ​หนึ่ง​หน้า​ของ heap ถือ​ทุก column ของ​แถว​ที่​อยู่​ใน​นั้น กิ่ง​ขวา​แตก​เป็น​สอง​ใบ​ที่​ใช้​คลัง​ใบ​เดียวกัน ต่าง​กัน​ที่​จำนวน column ที่​คำถาม​ขอ ระยะ​จาก 304 ถึง 989 จึง​เป็น​ราคา​ของ​ความ​กว้าง​ของ​คำถาม ไม่ใช่​ราคา​ของ​การ​เข้า​รหัส

รู้​แล้ว​ว่าการ​วาง​แนว​ซื้อ​อะไร​กลับ​มา ยัง​ไม่รู้​ว่าการ​เข้า​รหัส​แต่ละ​แบบ​ทำ​อะไร​กับ column แต่ละ​ตัว

บท​ที่ 1 กับ​บท​นี้​ประกอบ​กัน​เป็น​ธี​สิ​ส​ครึ่ง​แรก​ของ​คอร์ส

  • การ​วาง​แนว ให้​สิทธิ์​ที่​จะ​ไม่​อ่าน column ที่​ไม่​ได้​ถาม · สิทธิ์​นั้น​จ่าย​คืน จาก​คีย์ heap หารด้วย 2 column bitpack ที่ 10.37 ลง​มา​เหลือ 3.19 เมื่อ​คำถาม​กว้าง​ขึ้น​จน​แตะ​ครบ
  • การ​เข้า​รหัส ตัดสิน​ว่า​ค่า​หนึ่ง​ค่า​กิน​ที่​เท่าไร · บท​ที่ 1 วัด​ว่า​มัน​คือ​ฝั่ง​ที่​พลิก​ตัวเลข ตอน​คำถาม​แตะ​ครบ 6 จาก 6 column

ทุก​ตัวเลข​ใน​บท​นี้​ใช้​ทรง bit packing กับ​ทรง plain encoding เป็น​ก้อน​ตัน​สอง​ก้อน โดย​ไม่​ถาม​ว่า ข้าง​ใน​ก้อน​นั้น​มี​อะไร

บท​ที่ 3 เปิด​ก้อน​นั้น​ออก​ที​ละ column แล้ว​เจอ​ว่าการ​เข้า​รหัส​ที่​ตำรา​เรียก​ว่า​ตัว​ย่อ​ขนาด ทำให้​สาม column ใน​หก​ตัว ใหญ่​ขึ้น


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Apache Parquet — File Format (เข้า​ถึง 2026-09-10) — ที่มา​ของ​ข้อ​ที่​ว่า​รูป​แบบ​แนว column เก็บ​ตำแหน่ง​ของ​ทุก column chunk ไว้​ใน metadata เพื่อ​ให้​คำถาม​กระโดด​ไป​อ่าน​เฉพาะ column ที่​มัน​ขอ​ได้ · คลัง​ของ​คอร์ส​นี้ ไม่ใช่ Parquet มัน​ใช้​ที่​เก็บ​แยก​ต่อ​หนึ่ง column แทน

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก python3 scripts/column-store/probe.py หมวด ### 1_ฉายภาพ column และ​หมวด ### heap · ค่าที่​ตรึง​ไว้​อยู่​ที่ scripts/column-store/expected/column-store.txt

  • fence ของ​ฝั่ง heap ยก​มา​จาก scripts/column-store/probe.py บรรทัด​ที่ 243 ซึ่ง​เรียก Pager กับ decode ของ​คอร์ส​ที่ 26 · คอร์ส​นี้​ไม่​แก้​อะไร​ใน scripts/query-engine/qe/ แม้แต่ byte เดียว

  • fence ของ​ฝั่ง​คลัง column ยก​มา​จาก scripts/column-store/colstore.py บรรทัด​ที่ 338 และ​บรรทัด​ที่ 378 · หน้าที่​แตะ​ทุก​ตัว​นับ​ด้วย Pager.reads ตัว​เดียวกัน​ทั้ง​สอง​ฝั่ง

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

ข้อ 1 / 3

คำถาม SELECT total WHERE restaurant = ครัวคุณแม่ บนคลัง column ทรง bit packing ที่แตะแค่ 2 จาก 6 column แตะกี่หน้า และ heap ต้องอ่านกี่หน้า