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

ขนาด transaction ที่​ถูก​ที่สุด​ไม่ใช่​ใหญ่​ที่สุด — วัด​สอง​เอน​จิน​ด้วย​งาน​เดียวกัน

คำ​แนะนำ​เรื่อง​นี้​มี​อยู่​ใน​ทุก​ที่ อย่า commit ที​ละ​แถว ให้​รวม​งาน​เข้า​เป็น transaction เดียว แล้ว I/O จะ​ลด​ลง​มหาศาล

หน้า​นี้​ไป​วัด​ว่า​มัน​จริง​แค่​ไหน โดย​รัน งาน​เดีย​วกันเป๊ะ บน PostgreSQL 18.6 กับ SQLite 3.53.4 ที่​ทุกข​นาด transaction แล้ว​วัด WAL ของ​แต่ละ​เอน​จิน​ด้วย​หน่วย​ของ​มัน​เอง

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

ชั้น​ที่​สอง​คือ​สิ่ง​ที่​ไม่มี​ใคร​เตือน​ไว้ บน PostgreSQL การ commit ครั้ง​เดียว​ทั้ง​ชุด​แพง​กว่า การ​แบ่ง commit และ​ตัวเลข​บอก​ได้​ว่า​เพราะ​อะไร

ฝั่ง SQLite ของ​เรื่อง​นี้ มี​คน​วัด​ไว้​ให้​แล้ว และ​หน้า​นี้​ยืน​บน​ไหล่​ของ​มัน

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

เครดิต​ต้อง​ให้​ก่อน เพราะ​สาม​ใน​สี่​หัวข้อ​ฝั่ง SQLite ของ​หน้า​นี้​เป็นการ​ยืนยัน​สิ่ง​ที่​คอร์ส​ที่ 28 บท​ที่ 7 วัด​ไว้​แล้ว

บท​ที่ 7 ของ​คอร์ส transaction engine วัด​ราคา​ของ commit บน SQLite ไว้​ครบ​แล้ว ทั้ง​การ​นับ​เป็น frame ทั้ง​การก​วาด​ขนาด transaction และ​ทั้ง​ราคา​ของ​ลำดับ​ที่​คีย์​มา​ถึง

หน้า​นี้​จึง​ไม่​ได้​ค้น​พบ​ฝั่ง SQLite ขึ้น​มา​ใหม่ มัน​วัด​ซ้ำ​ด้วย​เครื่อง​มือ​คนละ​ตัว​แล้ว​ได้​ทิศ​เดียวกัน ของ​ใหม่​ทั้งหมด​อยู่​ฝั่ง PostgreSQL และ​อยู่​ที่​การ​เอา​สอง​ฝั่ง​มา​วาง​ข้าง​กัน

และ​การ​วาง​ข้าง​กัน​นั้น หัก​ล้าง​ประโยค​หนึ่ง​ของ​บท​เรียน​นั้น​เอง

callout ปิด​ท้าย​ของ​บท​ที่ 7 เขียน​ไว้​ว่า “รูป​ของ​เส้น​ราคา​จึง​คล้าย​กัน แต่​ตัว​คูณ​ไม่ใช่​ตัว​เดียวกัน” เมื่อ​เทียบ SQLite กับ PostgreSQL

ตัว​คูณ​ต่าง​กัน​จริง​ตาม​ที่​บท​เรียน​บอก แต่​รูป​ไม่​คล้าย​กัน ของ SQLite ลง​อย่าง​เดียว ส่วน​ของ PostgreSQL เป็น​รูป​ตัว U ซึ่ง​กลับ​ทิศ​กลาง​ทาง · ตาราง​ใน​หัวข้อ กวาด​ทั้ง​ช่วง ข้าง​ล่าง​คือ​หลัก​ฐาน

บท​เรียน​นั้น​เขียน​ขึ้น​ตอน​ที่​ยัง​ไม่มี​ใคร​วัด​ฝั่ง PostgreSQL ด้วย​งาน​เดียวกัน ประโยค​นั้น​จึง​เป็น การ​ประมาณ​ที่​สม​เหตุ​สม​ผล​ใน​ตอน​นั้น ไม่ใช่​ความ​ผิด​พลาด · ตอน​นี้​วัด​แล้ว และ​บท​เรียน​ถูก​แก้​ตาม​แล้ว

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

หัวข้อ​ที่​มีชื่อ​ว่า “หน้า​นี้​ถอด​ข้อ​จำกัด​ที่​บทความ​ก่อน​หน้า​เขียน​กำกับ​ตัว​เอง​ไว้”

บทความ​ที่ 18 วัด​ฝั่ง PostgreSQL ไว้​แล้ว พร้อม​ประกาศ​เอง​ว่า​ห้าม​เอา​ตัวเลข​ข้าม​เอน​จิน หน้า​นี้​คือ​การ​ทำให้​ข้าม​ได้

บทความ​ที่ 18 กลไก transaction ของ PostgreSQL วัด​ไว้​ว่า commit ที​ละ​แถว เทียบ commit ครั้ง​เดียว​ต่าง​กัน 1.2 เท่า แล้ว​เขียน callout กำกับ​ตัว​เอง​ไว้​ว่า​สิ่ง​ที่​หน้า​นั้น​ยืนยัน​ได้​คือ ทิศ ไม่ใช่​ตัว​คูณ​ข้าม​เอน​จิน

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

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

byte ดิบ​ข้าม​เอน​จิ​น​ยัง​เทียบ​กัน​ไม่​ได้ และ​จะ​ไม่มี​วัน​ได้

WAL ของ​สอง​ตัว​เก็บ​คนละ​อย่าง PostgreSQL เก็บ ระเบียน​ของ​การ​เปลี่ยนแปลง ส่วน SQLite เก็บ หน้า​เต็ม ตัวเลข byte จึง​อยู่​คนละ​อันดับ​โดย​ธรรมชาติ ไม่ใช่​เพราะ​ตัว​หนึ่ง​ดี​กว่า​อีก​ตัว

สิ่ง​ที่​เทียบ​ได้​คือ​อัตราส่วน​ภายใน​เอน​จิ​น​เดียวกัน คือ การ​รวม transaction ให้​ใหญ่​ขึ้น ช่วย​เอน​จิ​น​นี้​กี่​เท่า แล้ว​เอา​สอง​อัตราส่วน​นั้น​มา​เทียบ​กัน​อีก​ที ทุก​ข้อ​สรุป​ใน​หน้า​นี้​อยู่​ใน​รูป​นั้น

ตาราง​เดียวกัน แถว​เท่า​กัน คำ​สั่ง​เดียวกัน ตัวแปร​เดียว​ที่​ขยับ​คือ commit ทุก​กี่​แถว

ตาราง​คือ t(id, v) ที่​มี 1,000 แถว งาน​คือ​สั่ง update t set v=v+1 where id=? ครบ​ทุก​แถว​หนึ่ง​รอบ ทุก​ช่อง​ใน​ทุก​ตาราง​ข้าง​ล่าง​ทำงาน​นี้​เหมือน​กัน​หมด

ฝั่ง PostgreSQL วัด​ด้วย pg_wal_lsn_diff ระหว่าง​สอง LSN คร่อม​งาน ส่วน​ฝั่ง SQLite วัด​จาก ขนาด​ของ file -wal ที่​โต​ขึ้น หลัง​ตัด WAL ให้​เป็น​ศูนย์​ด้วย wal_checkpoint(TRUNCATE)

ตัวแปร​ที่​คุม​ไว้​มี​สาม​ตัว

  • autovacuum ปิด ทั้ง​ที่​ระดับ server และ​ระดับ​ตาราง
  • checkpoint_timeout ตั้ง​ไว้​หนึ่ง​ชั่วโมง
  • wal_autocheckpoint ของ SQLite ปิด

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

สอง​ปลายข​อง​ช่วง คำ​แนะนำ​เดียวกัน​ให้​ผล​คนละ​อันดับ

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

คำ​แนะนำ “รวม transaction ให้​ใหญ่” จริง​มาก​บน SQLite และ​เกือบ​ไม่​จริง​เลย​บน PostgreSQL

เอน​จินcommit ที​ละ​แถวcommit ครั้ง​เดียวประหยัด​ได้
PostgreSQL 18.6328,096 B276,688 B1.2 เท่า
SQLite 3.53.44,120,032 B8,272 B498.1 เท่า

อัตราส่วน​ของ SQLite สูง​กว่า​ของ PostgreSQL 420 เท่า นี่​คือ​ตัวเลข​ที่​หน้า​ก่อน​หน้า​ประกาศ​ว่า ตัว​เอง​อ้าง​ไม่​ได้ และ​เป็น​ตัวเลข​เดียว​ใน​หน้า​นี้​ที่​ข้าม​เอน​จิน

ความ​ต่าง​ไม่​ได้​อยู่​ที่​ตัว​ไหน​เขียน​เก่ง​กว่า มัน​อยู่​ที่ หน่วย​ของ​สิ่ง​ที่ WAL เก็บ ซึ่ง​ย่อหน้า​ถัด​ไป คิด​ออก​มา​เป็น​เลข​ได้​ทั้ง​ตัว

ตัวเลข​ของ SQLite อธิบาย​ได้​ทั้ง​ตัว ไม่ใช่​แค่​วัด​ได้​เท่า​นี้

หัวข้อ​ที่​มีชื่อ​ว่า “ตัวเลข​ของ SQLite อธิบาย​ได้​ทั้ง​ตัว ไม่ใช่​แค่​วัด​ได้​เท่า​นี้”

สูตร​บน​กระดาษ​ให้​เลข​ตรง​กับ​ที่​วัด​ได้ที​ละ byte ทั้ง​สอง​ช่อง

SQLite เขียน WAL เป็น frame และ​หนึ่ง frame คือ หนึ่ง​หน้า​เต็ม บวก header 4,096 + 24 byte ไม่​ว่า​หน้า​นั้น​จะ​ถูก​แก้​ไป​กี่ byte ก็ตาม

commit ที​ละ​แถว​จึง​เขียน 1,000 หน้า และ​ได้ 1,000 × (4,096 + 24) + 32 = 4,120,032 B ตรง​กับ​ที่​วัด​ได้ที​ละ byte

commit ครั้ง​เดียว​แตะ​แค่ 2 หน้า จึง​ได้ 2 × (4,096 + 24) + 32 = 8,272 B ตรง​เช่น​กัน เลข 32 ท้าย​สูตร​คือ header ของ file WAL

ตัวเลข​ที่​อธิบาย​ได้​ทั้ง​ตัว​แบบ​นี้ ตรึง​เป็น byte ได้ ตัว​ตรวจ​ของ​หน้า​นี้​จึง​เทียบ​ฝั่ง SQLite ทีละ byte ทุก​ช่อง

ฝั่ง PostgreSQL ตรึง​แบบ​นั้น​ไม่​ได้ วัด​ซ้ำสาม​รอบ​บน​เครื่อง​เดียวกัน​แล้ว​ช่อง​เดียวกัน​แกว่ง​ได้ ถึง 56 B จาก 328,096 B ซึ่ง​คือ 0.02 % · ตัว​ตรวจ​จึง​ใช้​กรอบ 0.5 % กับ​ฝั่ง​นั้น กว้าง​กว่า​ที่​วัด​ได้​ยี่สิบ​เท่า แต่​ยัง​แคบ​กว่า​ข้อ​อ้าง​ของ​หน้า​นี้​หลาย​สิบ​เท่า

กวาด​ทั้ง​ช่วง แล้ว​เส้น​โค้ง​ของ PostgreSQL ไม่​ได้​ลง​อย่าง​เดียว

หัวข้อ​ที่​มีชื่อ​ว่า “กวาด​ทั้ง​ช่วง แล้ว​เส้น​โค้ง​ของ PostgreSQL ไม่​ได้​ลง​อย่าง​เดียว”

สอง​ปลายข​อง​ช่วง​บอก​ทิศ​ถูก แต่​บอก​รูป​ทรง​ผิด และ​รูป​ทรง​คือ​สิ่ง​ที่​เอา​ไป​ใช้ได้

commit ทุก​กี่​แถวPostgreSQL 18.6SQLite 3.53.4
1328,096 B4,120,032 B
10238,576 B416,152 B
100240,800 B45,352 B
1000276,688 B8,272 B

ฝั่ง SQLite ลง​อย่าง​เดียว​ทุก​ขั้น จาก 4,120,032 ไป​จนถึง 8,272 B ยิ่ง​รวม​ยิ่ง​ถูก ไม่มี​จุด​กลับ

ฝั่ง PostgreSQL ลง​ถึง​จุด​ต่ำ​สุด​ที่ commit ทุก 10 แถว แล้ว กลับ​ขึ้น การ commit ครั้ง​เดียว​ทั้ง​ชุด​แพง​กว่า​จุด​ต่ำ​สุด​นั้น 1.2 เท่า

พูด​อีก​อย่าง​คือ ถ้า​เชื่อ​คำ​แนะนำ​จน​สุด​ทาง​แล้ว​ยัด​ทุก​แถว​ไว้​ใน transaction เดียว บน PostgreSQL คุณ​เดิน​เลย​จุด​ที่​ถูก​ที่สุด​ไป​แล้ว

การ commit เพิ่ม​หนึ่ง​ครั้ง​บน PostgreSQL มี​ราคา​ประมาณ 99 B คิด​จาก​ส่วน​ต่าง​ของ​สอง​แถว​แรก หาร​ด้วย​จำนวน commit ที่​หาย​ไป นี่​คือ​ขา​ลง​ของ​เส้น​โค้ง ส่วน​ขา​ขึ้น​เป็น​คนละ​กลไก

หน้า​เต็ม​ไม่ใช่​คำ​อธิบาย และ​ตัวเลข​บอก​เอง​ว่า​ไม่ใช่

หัวข้อ​ที่​มีชื่อ​ว่า “หน้า​เต็ม​ไม่ใช่​คำ​อธิบาย และ​ตัวเลข​บอก​เอง​ว่า​ไม่ใช่”

ผู้​ต้อง​สงสัย​อันดับ​แรก​คือ full page write หน้า​นี้​ตัด​มัน​ออก​ด้วย​การ​นับ ไม่ใช่​ด้วย​การ​ให้​เหตุผล

PostgreSQL เขียน​หน้า​เต็ม​ลง WAL ครั้ง​แรก​ที่​แตะ​หน้า​นั้น​หลัง checkpoint กลไก​นี้​ชื่อ full_page_writes และ​ภาพ​หน้า​เต็ม​ที่​มัน​เขียน​เรียก​ว่า full page image (FPI) ซึ่ง​เป็น​ชื่อ​ของ column wal_fpi ใน pg_stat_wal ที่​ตาราง​ข้าง​ล่าง​นับ​มา

มัน​คือ​คำ​อธิบาย​ที่​คน​คิดถึง​ก่อน​เสมอ​เมื่อ WAL โต​ผิด​คาด

commit ทุก​กี่​แถวWALระเบียนหน้า​เต็มupdate ที่ HOTindex โต​ขึ้น
1328,096 B4,41493258,192 B
10238,576 B2,56593258,192 B
100240,824 B2,513926016,384 B
1000276,688 B3,0229024,576 B

จำนวน FPI เท่า​กัน​หมด​ทุก​ช่อง คือ 9 ภาพ ขนาด transaction ไม่​ได้​เปลี่ยน​มัน​เลย แม้แต่​ภาพ​เดียว full_page_writes จึง​ตก​เป็น​ผู้​ต้อง​สงสัย​ไม่​ได้

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

update ที่​อยู่​ใน​หน้า​เดิม​ได้ ไม่​ต้อง​แตะ index เลย และ transaction ที่​ใหญ่​เกิน​ไป​ทำให้​มัน​เป็น​ไป​ไม่​ได้

PostgreSQL ไม่​เคย​เขียน​ทับ​แถว​เก่า มัน​เขียน​แถว​ใหม่​แล้ว​ปล่อย​ของ​เก่า​ค้าง​ไว้ ถ้า​แถว​ใหม่​นั้น เป็น HOT update ได้ index จะ​ยัง​ชี้​ถูก​อยู่ และ​ไม่​ต้อง​เขียน index เลย​สัก​ระเบียน

เอกสาร​กำหนด​เงื่อนไข​ไว้ สอง​ข้อ ต้อง​ครบ​ทั้ง​คู่

  1. การ update นั้น​ไม่​แตะ column ที่ index ตัว​ไหน​อ้าง​ถึง
  2. หน้าที่​แถว​เก่า​อยู่ ยัง​มี​ที่​ว่าง​พอ​สำหรับ​แถว​ใหม่

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

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

flowchart LR
  subgraph HOT["HOT update · หน้าเดิมยังมีที่ว่าง"]
    direction TB
    H1["แถวเก่า · หน้า 7"] --> H2["แถวใหม่ · หน้า 7"]
    HI["index"] -. "ชี้ที่เดิม ไม่ต้องเขียน" .-> H1
  end
  subgraph COLD["ไม่ HOT · หน้าเดิมเต็มแล้ว"]
    direction TB
    C1["แถวเก่า · หน้า 7"] --> C2["แถวใหม่ · หน้า 9"]
    CI["index"] ==> C2
  end
  HOT -->|"transaction ใหญ่ขึ้น<br/>ไม่มีใครเก็บกวาดระหว่างทาง"| COLD

คำ​บรรยาย​ภาพ: เมื่อ​หน้า​เดิม​ยัง​มี​ที่​ว่าง update จบ​ใน​หน้า​เดียว​และ index ไม่​ต้อง​ถูก​เขียน เมื่อ​หน้า​เต็ม แถว​ย้าย​หน้า และ index ต้อง​เขียน​ตาม

ที่ commit ที​ละ​แถว มี HOT update 325 จาก 1,000 ครั้ง ที่ commit ครั้ง​เดียว เหลือ 0 ศูนย์ ไม่ใช่​ตัวเลข​ที่​น้อย​ลง

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

ผล​คือ index ของ primary key โต​ขึ้น 24,576 B แทนที่​จะ​เป็น 8,192 B และ WAL ที่​ประหยัด​ได้​จาก​การ​ลด​จำนวน commit ถูก​กิน​คืน​ไป​หมด​แล้ว​เกิน

ลำดับ​ที่​ยิง update มี​ราคา​บน SQLite แต่​แทบ​ไม่มี​บน PostgreSQL

หัวข้อ​ที่​มีชื่อ​ว่า “ลำดับ​ที่​ยิง update มี​ราคา​บน SQLite แต่​แทบ​ไม่มี​บน PostgreSQL”

งาน​เดียวกัน จำนวน commit เท่า​กัน ต่าง​แค่​ลำดับ​ของ id แล้ว​เอน​จิ​น​หนึ่ง​จ่าย​เพิ่ม​เท่า​ตัว

ทุก​ช่อง​ข้าง​บน​ยิง update ตาม​ลำดับ id ตาราง​นี้​ยิง​ชุด​เดิม​ด้วย​ลำดับ​สุ่ม​ที่​ปัก​หมุด​ไว้​ด้วย seed ตายตัว งาน​และ​จำนวน commit เท่า​กัน​ทุก​ประการ

commit ทุก​กี่​แถวSQLite 3.53.4 เรียงSQLite 3.53.4 สุ่มPostgreSQL 18.6 เรียงPostgreSQL 18.6 สุ่ม
14,120,032 B4,120,032 B328,096 B326,768 B
10416,152 B824,032 B238,576 B260,664 B
10045,352 B82,432 B240,800 B235,544 B
10008,272 B8,272 B276,688 B277,312 B

บน SQLite ที่ commit ทุก 10 แถว ลำดับ​สุ่ม​แพง​กว่า​ลำดับ​เรียง 1.98 เท่า เพราะ batch ที่ id เรียง​กัน​ตก​อยู่​ใน​หน้า​เดียว ส่วน batch ที่ id กระจาย​ไป​แตะ​หลาย​หน้า และ SQLite คิด​ราคา​เป็น​หน้า​เต็ม​ทุก​หน้า

ที่​ปลาย​ทั้ง​สอง​ข้าง​ลำดับ​ไม่มี​ผล​เลย ตัวเลข​เท่า​กัน​ที​ละ byte ที่ commit ที​ละ​แถว​ทุก commit แตะ​หน้า​เดียว​อยู่​แล้ว ส่วน​ที่ commit ครั้ง​เดียว​ทุก​หน้า​ถูก​ยุบ​รวม​เป็น​ครั้ง​เดียว​อยู่ดี

บน PostgreSQL ผล​ของ​ลำดับ​เล็ก​กว่า​มาก และ ไม่​ได้​ไป​ทาง​เดียวกัน​ทุก​ช่อง ที่ commit ทุก 10 แถว ลำดับ​สุ่ม​แพง​กว่า 9 % ส่วน​ที่ commit ทุก 100 แถว มัน ถูก​กว่า 2 %

เหตุผล​ที่​ผล​เล็ก​คือ WAL เก็บ​ระเบียน​ของ​การ​เปลี่ยนแปลง ไม่ใช่​หน้า การก​ระ​จาย​ของ​แถว​จึง​ไม่​ได้ คูณ​จำนวน byte ขึ้น​ทั้ง​หน้า​แบบ​ฝั่ง SQLite · ส่วน​ที่​ยัง​เหลือ​อยู่​มา​จาก​การ​ที่​ลำดับ​เปลี่ยน​ว่า หน้า​ไหน​เต็ม​ก่อน ซึ่ง​เปลี่ยน​จำนวน HOT update ตาม​ไป​ด้วย

นี่​คือ​ความ​ต่าง​ที่​เอา​ไป​ใช้ได้​ทันที ถ้า​เขียน​งาน bulk update บน SQLite การ​เรียง​ตาม primary key ก่อน​ยิง​เป็นการ​ประหยัด​จริง บน PostgreSQL มัน​ไม่ใช่

บน SQLite คำ​ตอบ​คือ​ใหญ่​เท่า​ที่​ความ​ถูก​ต้อง​ยอม​ให้ · บน PostgreSQL มี​จุด​ที่​เลย​ไป​แล้ว​แพง​ขึ้น

บน SQLite รวม​ให้​ใหญ่​เท่า​ที่​ขอบเขต​ความ​ถูก​ต้อง​ยอม​ให้ เส้น​โค้ง​ลง​อย่าง​เดียว​จริง และ​ราคา​ต่อ commit คือ​หน้า​เต็ม​ทุก​หน้าที่​แตะ ซึ่ง​แพง​กว่า​ราคา​ต่อ commit ของ PostgreSQL หลาย​อันดับ

บน PostgreSQL จุด​ต่ำ​สุด​ของ​งาน​ชุด​นี้​อยู่​ที่ commit ทุก 10 แถว ตัวเลข​นี้​ไม่ใช่ ค่า​คงที่​สากล มัน​คือ​จุด​ที่​ราคา​คงที่​ต่อ commit กับ​ราคา​ของ HOT update ที่​หาย​ไป หัก​ล้าง​กัน​พอดี

ตาราง​ลำดับ​สุ่ม​ใน​หัวข้อ​ที่​แล้ว​พิสูจน์​เรื่อง​นี้​ใน​หน้า​เดียวกัน​นี้​เอง พอ​เปลี่ยน​แค่​ลำดับ​ที่​ยิง update จุด​ต่ำ​สุด​ก็​ย้าย​ไป​อยู่​ที่ commit ทุก 100 แถว​แทน โดยที่​งาน​ยัง​เป็น​งาน​เดิม​ทุก​อย่าง

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

และ​มี​เงื่อนไข​หนึ่ง​ที่​ไม่​ได้​เลื่อน​จุด​ต่ำ​สุด แต่​ลบ​เส้น​โค้ง​ทิ้ง​ทั้ง​เส้น ถ้า update ของ​คุณ แตะ column ที่ index ตัว​ใด​ตัว​หนึ่ง​อ้าง​ถึง เงื่อนไข​ข้อ​แรก​ของ HOT ตก​ทันที และ​จะ​ไม่มี HOT update เลย​สัก​ตัว​ไม่​ว่า​จะ​ซอย transaction เล็ก​แค่​ไหน

งาน​ที่​หน้า​นี้​วัด​แก้​เฉพาะ column ที่​ไม่มี index อ้าง​ถึง ตัวเลข​ทุก​ช่อง​จึง​เป็น​ของ​กรณี​ที่ เงื่อนไข​ข้อ​แรก​ผ่าน​แล้ว · ถ้า​งาน​ของ​คุณ update column ที่​มี index ให้​เริ่ม​จาก​คำถาม​นั้น​ก่อน ไม่ใช่​จาก​ขนาด​ของ transaction

ถ้า​จะ​หา​จุด​ของ​งาน​จริง วัด​สาม​อย่าง​นี้​ระหว่าง​รัน

  • n_tup_hot_upd จาก pg_stat_user_tables
  • ขนาด​ของ index
  • WAL จาก pg_wal_lsn_diff

ตาราง​ข้าง​บน​แสดง​ว่า​สาม​ตัว​นี้​ขยับ ไป​ด้วย​กัน ไม่​ได้​ขยับ​ตาม​กัน​เป็น​ลำดับ ตัว​แรก​อ่าน​ง่าย​ที่สุด เพราะ​มัน​เป็น​จำนวนนับ​ตรง ๆ ไม่​ต้อง​คร่อม LSN สอง​ค่า

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

หัวข้อ​ที่​มีชื่อ​ว่า “หน้า​นี้​ไม่​ได้​ให้​ต้นทุน​มา​เลือก​ขอบเขต มัน​ตอบ​คำถาม​ที่​สอง”

ไซต์​นี้​แยก​สอง​คำถาม​นี้​ไว้​แล้ว หน้า​นี้​อยู่​ใน​คำถาม​ที่​สอง​ทั้ง​หน้า

คอร์ส EF Core วาง​ขอบเขต​ความ​ถูก​ต้อง​ไว้​ที่ หนึ่ง aggregate ต่อ​หนึ่ง transaction

ส่วน บท​ที่ 7 ของ​คอร์ส transaction engine สรุป​กฎ​ที่​ใช้ได้​จริง​ไว้​ว่า ให้​ความ​ถูก​ต้อง​เลือก​ขอบเขต แล้ว​ให้​ต้นทุน​เลือก​จำนวน commit ภายใน​ขอบเขต​นั้น

ตัวเลข​ทุก​ช่อง​ใน​หน้า​นี้​อยู่​ใน​ครึ่ง​หลัง​ของ​ประโยค​นั้น งาน 1,000 แถว​ที่​วัด ไม่ใช่ 1,000 aggregate มัน​คือ​งาน​นำ​เข้า​ข้อมูล​ก้อน​เดียว​ที่​หน่วย​ความ​ถูก​ต้อง ของ​มัน​คือ​ทั้ง​ก้อน คำถาม​ที่​เหลือ​คือ​จะ​ซอย​เป็น​กี่ commit

สิ่ง​ที่​หน้า​นี้​เพิ่ม​เข้าไป​คือ​ครึ่ง​หลัง​ของ​เส้น​โค้ง บน​ฝั่ง SQLite ที่​บท​เรียน​นั้น​วัด คำ​ตอบ​คือ ยิ่ง​รวม​ยิ่ง​ถูก ไม่มี​จุด​กลับ · บน PostgreSQL มี​จุด​กลับ และ​การ​ซอย​เป็น​ก้อน​พอ​ประมาณ ถูก​กว่า การ​ยัด​ทั้ง​ก้อน​ไว้​ใน commit เดียว

ผลพลอยได้​คือ​กฎ ห้าม​ขยาย transaction เพื่อ​ประหยัด​การ​เขียน ของ​บท​เรียน​นั้น​แข็ง​แรง​ขึ้น​อีก บน PostgreSQL การ​ขยาย​ขอบเขต​เพื่อ I/O ไม่​ได้​แค่​แลก​ด้วย lock ที่​ถือ​นาน​ขึ้น มัน​ไม่​ได้ ส่วนลด​ที่​หวัง​ไว้​ด้วย​ซ้ำ

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

  • byte ดิบ​ข้าม​เอน​จิ​น​ยัง​เทียบ​ไม่​ได้ ทุก​ข้อ​สรุป​ใน​หน้า​นี้​เป็น​อัตราส่วน​ภายใน​เอน​จิ​น​เดียวกัน ยกเว้น​ตัวเลข 420 เท่า ซึ่ง​เป็น​อัตราส่วน​ของ​อัตราส่วน
  • หน้า​นี้​ไม่​ได้​วัด​เวลา ไม่มี​วินาที ไม่มี throughput WAL ที่​น้อย​กว่า​ไม่​ได้​แปล​ว่า​เร็ว​กว่า​เสมอ​ไป เพราะ​จำนวน fsync กับ latency ของ commit เป็น​คนละ​แกน
  • ตาราง​มี index ตัว​เดียว คือ primary key งาน​จริง​ที่​มี​หลาย index จะ​เห็น​ขา​ขึ้น​ของ​เส้น​โค้ง ชัน​กว่า​นี้ เพราะ​ทุก index ต้อง​เขียน​ตาม​เมื่อ update ไม่​เป็น HOT
  • autovacuum ปิด​อยู่ ทั้ง​ระดับ server และ​ระดับ​ตาราง เพื่อ​ไม่​ให้​มัน​แทรก​กลาง​การ​วัด งาน​จริง​ที่​เปิด​ไว้​จะ​ได้​ตัวเลข​ไม่​เท่า​นี้ · การ​เก็บ​กวาด​ที่​ทำให้ HOT เกิด​ได้​ใน​หน้า​นี้​คือ การ prune ระดับ​หน้าที่​เกิด​ตอน​มี​คน​มา​แตะ​หน้า​นั้น ซึ่ง​ไม่ใช่ autovacuum · แต่​ที่​ว่าง​ของ​แถว ที่​ย้าย​หน้า​ไป​แล้ว​ต้อง​รอ VACUUM ถึง​จะ​กลับ​มา​ใช้ได้ ตรง​นั้น autovacuum เกี่ยว​เต็ม ๆ
  • จุด​ต่ำ​สุด​ที่ 10 แถว​ผูก​กับ​งาน​ชุด​นี้ ขนาด​แถว จำนวน index และ fillfactor ย้าย​จุด​นั้น​ได้​หมด สิ่ง​ที่​ยืนยัน​คือ​รูป​ทรง​ของ​เส้น​โค้ง
  • ตัวเลข​ทั้งหมด​มา​จาก​เครื่อง​เดียว PostgreSQL 18.6 ใน container และ SQLite 3.53.4 บน arm64 เอน​จิ​น​คนละ​รุ่น​ให้ byte ไม่​เท่า​กัน​ได้

ตัวเลข​ทุก​ตัว​ใน​หน้า​นี้​มา​จาก​สอง​คำ​สั่ง​ใน repo ของ​ไซต์​นี้

Terminal window
scripts/pg-transaction-spike/up.sh # postgres ที่คุมตัวแปรแล้ว
npm run verify:engine-compare # วัดซ้ำแล้วเทียบกับ file ข้อมูล

ตัว​วัด​เขียน src/data/perf/engine-compare.ts ให้​เอง หน้า​นี้​อ่าน​ตัวเลข​จาก file นั้น​ทุก​ตัว แก้ตัว​เลข​ด้วย​มือ​ไม่​ได้ เพราะ​ตัว​ตรวจ​จะ​วัด​ใหม่​แล้ว​เทียบ​ที​ละ​ช่อง

ตัว​ตรวจ​ไม่​ได้​ตรวจ​แค่ byte มัน​ตรวจ ข้อ​อ้าง ด้วย สาม​ข้อ

  • เส้น​โค้ง​ของ PostgreSQL ต้อง​ยัง​เป็น​รูป​ตัว U
  • เส้น​โค้ง​ของ SQLite ต้อง​ยัง​ลง​อย่าง​เดียว
  • HOT update ที่ commit ครั้ง​เดียว​ต้อง​ยัง​เป็น​ศูนย์

ฝั่ง PostgreSQL ของ​ตัว​วัด​นี้ import code วัด​ตัว​เดียว​กับ​บทความ​ที่ 18 ไม่​ได้​คัด​ลอก​มา วิธี​วัด​ของ​สอง​หน้า​จึง​แก้​ที่​เดียว​เสมอ

แต่ ตัวเลข ของ​สอง​หน้า​เป็น​คนละ snapshot บทความ​ที่ 18 ตรึง​ค่า​ไว้​ตอน​ที่​มัน​วัด หน้า​นี้​ตรึง ตอน​ที่​หน้า​นี้​วัด สอง​ค่า​นั้น​ห่าง​กัน​ได้​ใน​ระดับ​ที่ 0.5 % ครอบ​ไว้ และ​ของ​จริง​ตอน​นี้​ห่าง​กัน 40 B · ถ้า​อยาก​ได้​เลข​เดีย​วกันเป๊ะ ต้อง​วัด​ใหม่​ทั้ง​สอง​หน้า​ใน​รอบ​เดียวกัน

🔗 อ้างอิง​ต้นทาง
  • PostgreSQL 18 Documentation — 66.7 Heap-Only Tuples (HOT) (ตรวจ​แล้ว 2026-08-21) — ที่มา​ของ​เงื่อนไข​สอง​ข้อ​ของ HOT update และ​ของ​ข้อ​ที่​ว่า HOT update ไม่​สร้าง​ระเบียน index ใหม่
  • PostgreSQL 18 Documentation — 28.5 WAL Configuration (ตรวจ​แล้ว 2026-08-21) — ที่มา​ของ​กลไก full_page_writes ที่​ตาราง​ใน​หัวข้อ หน้า​เต็ม​ไม่ใช่​คำ​อธิบาย ตัด​ออก​ไป
  • PostgreSQL 18 Documentation — 27.2 The Cumulative Statistics System (ตรวจ​แล้ว 2026-08-21) — ที่มา​ของ n_tup_hot_upd และ pg_stat_wal ที่​ใช้​แยก​ก้อน WAL
  • SQLite — Write-Ahead Logging, §Format (ตรวจ​แล้ว 2026-08-21) — ที่มา​ของ header 32 byte ต่อ file และ 24 byte ต่อ frame ใน​สูตร​ข้าง​บน