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

ราคา​ของ transaction — ขนาด​ของ​มัน​คุม​จำนวน​หน้าที่​เขียน

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

บท​นี้​ถาม​คำถาม​ที่​เหลือ​อยู่​ข้อ​เดียว คือ การ commit หนึ่ง​ครั้ง​ต้อง​เขียน​อะไร​ลง disk บ้าง

คำ​ตอบ​ขึ้น​กับ​ตัวแปร​ที่​คน​ส่วน​ใหญ่​ไม่​นับ​ว่า​เป็น​ตัวแปร คือ ขนาด ของ transaction และ​มัน​คุม​แรงกว่าที่​คาด​ไว้​มาก

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

ตัวเลข​ของ​เอนจิน​ใน​บท​นี้​มา​จาก แบบ​จำลอง ที่​เรา​เขียน​เอง​ใน scripts/transaction-engine/src/wal.rs มัน​นับ​ตาม​กฎ​ที่​เรา​ประกาศ​ไว้​เอง ไม่​ได้​ไป​วัด​ฐาน​ข้อมูล​จริง​สัก​ตัว

ตัวเลข​ของ SQLite มา​จาก เครื่องมือ​คนละ​ตัว คือ scripts/write-amp/probe.py ซึ่ง​เขียน​ด้วย Python เพราะ crate ของ​คอร์ส​นี้​เป็น standard library ล้วน มัน​จึง​คุย​กับ SQLite ไม่​ได้

สอง​ชุด​นี้​จะ​ไม่​ถูก​จับ​ใส่​ตาราง​เดียวกัน​ใน​บท​นี้ วิธี​วัด​คนละ​วิธี ฐาน​คนละ​ฐาน

log ที่​ต้อง​ไป​ถึง​ก่อน การ​แก้​ถึง​จะ​นับ​ว่า​เกิด​ขึ้น

หัวข้อ​ที่​มีชื่อ​ว่า “log ที่​ต้อง​ไป​ถึง​ก่อน การ​แก้​ถึง​จะ​นับ​ว่า​เกิด​ขึ้น”

หัวข้อ​นี้​บอกว่า write-ahead log บังคับ​ลำดับ​อะไร และ checkpoint คือ​ขั้นตอน​ไหน​ของ​เรื่อง

ถ้า​แก้ file ฐาน​ข้อมูลหลักตรงๆ แล้ว​เครื่อง​ดับ​กลาง​การ​เขียน​หนึ่ง​หน้า ของ​ที่​เหลือ​อยู่​บน disk คือ​หน้าที่​เขียน​ไป​ได้​ครึ่ง​เดียว ไม่ใช่​ของ​เก่า​และ​ไม่ใช่​ของ​ใหม่

Write-Ahead LogWrite-Ahead Log (WAL)log ที่​ต้อง​เขียน​ลง disk ให้​เสร็จ​ก่อน​ที่​การ​แก้​ฐาน​ข้อมูล​จริง​จะ​ถูก​ยอมรับ ทำให้​กู้​สภาพ​กลับ​มา​ได้​หลัง​เครื่อง​ดับ​กลางคัน แก้​ปัญหา​นี้​ด้วย​กฎ​เรื่อง ลำดับ ข้อ​เดียว คือ​เขียน​คำ​อธิบาย​การ​แก้​ลง log ให้​เสร็จ​ก่อน แล้ว​การ​แก้​นั้น​ถึง​จะ​นับ​ว่า commit แล้ว

ชื่อ​ของ​มัน​คือ​กฎ​ของ​มัน​เอง log ไป​ก่อน (write-ahead) ส่วน file ฐาน​ข้อมูล​หลัก​ตาม​ไป​ทีหลัง เมื่อไร​ก็ได้ เพราะ​ทุก​อย่าง​ที่​ต้อง​ใช้​ประกอบ​สภาพ​กลับ​คืน​อยู่​ใน log ครบ​แล้ว

แต่ log โต​ขึ้น​เรื่อยๆ จึง​ต้อง​มี​จังหวะ​ย้าย​ของ​กลับ CheckpointCheckpointจังหวะ​ที่​ระบบ​ย้าย​สิ่ง​ที่​ค้าง​อยู่​ใน log กลับ​เข้า file ฐาน​ข้อมูล​หลัก แล้ว​ตัด​ส่วน​ของ log ที่​ไม่​ต้อง​ใช้​แล้ว​ทิ้ง คือ​จังหวะ​นั้น มัน​ย้าย​สิ่ง​ที่​จอด​อยู่​ใน log กลับ​เข้า file ฐาน​ข้อมูล​หลัก แล้ว​ส่วน​ของ log ที่​ไม่​ต้อง​ใช้​แล้ว ก็​ตัด​ทิ้ง​ได้

⚙️ log ถึง disk จริง กับ log ถึง​แค่ OS เป็น​ค่าที่​ตั้ง​ได้

เอกสาร​ของ SQLite เขียน​ไว้​ว่า​ผู้​เขียน sync WAL ทุก​ครั้ง​ที่ commit เมื่อ PRAGMA synchronous เป็น FULL แต่​ข้าม sync นั้น​เมื่อ​ตั้ง​เป็น NORMAL คำ​ว่า “ทน​ต่อ​ไฟ​ดับ” จึง​มี​ปุ่ม​ปรับ​อยู่

เครื่องมือ​วัด​ของ​บท​นี้​ตรึง synchronous=OFF ไว้​โดย​ตั้งใจ เพราะ​มัน​นับ หน้าที่​ถูก​เขียน ไม่​ได้​นับ​จำนวน​ครั้ง​ที่​เรียก sync

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

หัวข้อ​นี้​นิยาม frame แล้ว​แสดง​ค่า​คงที่​สอง​ตัว​ที่​แบบ​จำลอง​ของ​เรา​ใช้​นับ

หนึ่ง frame คือ​หนึ่ง​หน้าที่​ถูก​เขียน​ลง log หนึ่ง​ครั้ง หน้า​เดิม​ที่​ถูก​เขียน​ซ้ำ​นับ​ใหม่​ทุก​ครั้ง

ตรง​คำ​ว่า “นับ​ใหม่​ทุก​ครั้ง” นี่เอง​คือ Write AmplificationWrite Amplificationอัตราส่วน​ระหว่าง​จำนวน​ที่​ระบบ​เขียน​ลง disk จริง กับ​จำนวน​ที่​โปรแกรม​สั่ง​เขียน ตัวเลข​นี้​แกว่ง​ตาม​ขนาด​ของ transaction ได้​หลาย​สิบ​เท่า คือ​อัตราส่วน​ระหว่าง​จำนวน​ที่​ระบบ เขียน​จริง กับ​จำนวน​ที่​โปรแกรม​สั่ง​เขียน

แบบ​จำลอง​ของ​บท​นี้​มี​ค่า​คงที่​แค่​สอง​ตัว และ​ทั้ง​สอง​ตัว​อ่าน​ออก​ได้​จาก code ตรงๆ

src/wal.rs · ค่า​คงที่​สอง​ตัว​ที่​แบบ​จำลอง​นี้​ใช้
/// จำนวนแถวที่ใส่ได้ในหนึ่งหน้า
pub const ROWS_PER_PAGE: usize = 4;
/// ค่าคงที่ต่อหนึ่ง commit — หนึ่ง frame สำหรับระเบียนที่บอกว่า transaction นี้จบแล้ว
pub const COMMIT_FRAMES: usize = 1;

ROWS_PER_PAGE เล็ก​เกิน​จริง​โดย​ตั้งใจ หนึ่ง​หน้า​ของ​จริง​ใส่​ได้​เป็น​ร้อย​แถว แต่​เลข 4 ทำให้​ผล​ของ​การ​ซอย transaction มอง​เห็น​ได้​ด้วย​ตา​เปล่า

COMMIT_FRAMES คือ​ค่า​คงที่​ที่​ทั้ง​บท​หมุน​รอบ​มัน ทุก commit ต้อง​เขียน​ระเบียน​ที่​บอกว่า transaction นี้​จบ​แล้ว เพิ่ม​อีก​หนึ่ง frame เสมอ ไม่​ว่า​จะ​แก้​ไป​กี่​แถว​ก็ตาม

สูตร​นับ​ทั้งหมด​อยู่​ใน function เดียว

src/wal.rs · นับ frame เมื่อ​ซอย​งาน​ก้อน​เดิม​เป็น transaction ขนาด​ต่าง​กัน
/// นับ frame ที่ต้องเขียน เมื่อใส่ `rows` แถว โดย commit ทุกๆ `commit_every` แถว
///
/// แถวถูกใส่เรียงกันไป แถวที่ `i` ตกอยู่บนหน้าที่ `i / ROWS_PER_PAGE` เสมอ
/// หนึ่ง batch จึงแตะหน้าตั้งแต่หน้าของแถวแรกไปจนถึงหน้าของแถวสุดท้ายของ batch นั้น
/// **หน้าที่คาบเกี่ยวสอง batch ถูกเขียนสองครั้ง และนับสองครั้ง** ซึ่งตรงกับที่เกิดจริง
pub fn cost(rows: usize, commit_every: usize) -> Cost {
let mut frames = 0;
let mut commits = 0;
let mut start = 0;
while start < rows {
let end = (start + commit_every).min(rows);
let first_page = start / ROWS_PER_PAGE;
let last_page = (end - 1) / ROWS_PER_PAGE;
frames += (last_page - first_page + 1) + COMMIT_FRAMES;
commits += 1;
start = end;
}
Cost { commit_every, commits, frames }
}

บรรทัด​ที่​สำคัญ​ที่สุด​คือ (last_page - first_page + 1) มัน​นับ หน้าที่ batch นี้​แตะ ไม่ใช่​จำนวน​แถว หนึ่ง batch ที่​ยาว​สี่​แถว​พอดี​จึง​เสีย 1 frame ของ​หน้า ไม่ใช่​สี่

และ​หน้าที่​คาบเกี่ยว​สอง batch ถูก​นับ​สอง​ครั้ง เพราะ​มัน​ถูก​เขียน​ลง log สองครั้งจริงๆ ครั้ง​หนึ่ง​ตอน commit แรก อีก​ครั้ง​ตอน commit ถัด​มา

รัน​ดู​เอง แถว​เท่า​กัน​ทุก​บรรทัด ต่าง​กัน​แค่​ขนาด​ของ transaction

หัวข้อ​ที่​มีชื่อ​ว่า “รัน​ดู​เอง แถว​เท่า​กัน​ทุก​บรรทัด ต่าง​กัน​แค่​ขนาด​ของ transaction”

หัวข้อ​นี้​แสดง​ผล​รัน​ของ​แบบ​จำลอง แล้ว​ชี้​ว่า​เส้น​ราคา​ชัน​ตรง​ไหน​และ​แบน​ตรง​ไหน

cargo run --quiet -- 07
== บทที่ 7 ราคาของ transaction ==
ใส่ 1000 แถว · 4 แถวต่อหน้า · commit เพิ่มอีก 1 frame ต่อครั้ง
commit ทุกๆ ครั้งที่ commit frame เทียบกับ commit ทีเดียว
1 1000 2000 7.97x
10 100 400 1.59x
100 10 260 1.04x
1000 1 251 1.00x
แถวเท่ากันทุกบรรทัด ต่างกันแค่ *ขนาดของ transaction* อย่างเดียว

ทั้ง​สี่​บรรทัด​ใส่ 1000 แถว​เท่า​กัน ต่าง​กัน​อย่าง​เดียว​คือ​ซอย​เป็น transaction ขนาด​เท่าไร

บรรทัด​บน​สุด​คือ commit ทุก​แถว มัน​จ่าย​ไป 2000 frame ส่วน​บรรทัด​ล่าง​สุด​คือ commit ที​เดียว จ่าย​ไป 251 frame ต่าง​กัน 7.97 เท่า จาก​งาน​ที่​เท่า​กัน​ทุก​ประการ

แต่​สอง​บรรทัด​กลาง​สำคัญ​กว่า​นั้น commit ทุก 10 แถว​ลง​มา​เหลือ 1.59x แล้ว และ commit ทุก 100 แถว​ลง​มา​เหลือ 1.04x

เส้น​ราคา​นี้ ชัน​มาก​ตอน​ต้น แล้ว​แบน​เกือบ​สนิท​ตอน​ท้าย ก้าว​แรก​ที่​ออก​จาก “commit ทุก​แถว” ซื้อ​ส่วนลด​ไป​เกือบ​ทั้งหมด​แล้ว การ​ขยาย transaction ให้​ใหญ่​กว่า​นั้น​ซื้อ​เพิ่ม​ได้​อีก​น้อย​มาก

จำ​รูป​ของ​เส้น​นี้​ไว้ หัวข้อ​สุดท้าย​จะ​กลับ​มา​ใช้​มัน เพราะ​มัน​คือ​เหตุผล​ที่​บท​นี้​ไม่​ได้​ขัด​กับ​กฎ “transaction ควร​เล็ก” ที่​ไซต์​นี้​สอน​ไว้​แล้ว

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

แยก 2000 frame ของ​บรรทัด​บน​สุด​ออก​มา​ดู มัน​เป็น​สอง​ก้อน​เท่าๆ กัน​พอดี

  • 1000 frame ของ​หน้า commit ทุก​แถว​แปล​ว่า​หนึ่ง commit แตะ​หน้า​เดียว และ​สี่​แถว​ที่​อยู่ หน้า​เดียวกัน​ตก​ไป​อยู่​คนละ commit หน้า​เดิม​จึง​ถูก​เขียน​ลง log สี่​รอบ
  • 1000 frame ของ commit คือ​ค่า​คงที่​ตัว​ละ​หนึ่ง frame คูณ​ด้วย​จำนวน​ครั้ง​ที่​หยุด commit

ฝั่ง commit ที​เดียว​แยก​ได้​เหมือน​กัน คือ 250 หน้าที่​แตะ​จริง บวก​ค่า​คงที่​อีก​หนึ่ง frame รวม​เป็น 251 frame

ก้อน​แรก​คือ write amplification ล้วนๆ หน้า 250 หน้า​กลาย​เป็นการ​เขียน 1000 ครั้ง เพราะ​เรา​ซอย transaction ถี่​กว่า​จังหวะ​ที่​หน้า​จะ​เต็ม

ก้อน​ที่​สอง​คือ​ค่า​คงที่​ที่​ไม่มี​ทาง​หาย​ไป มัน​ไม่​ได้​แพง​ขึ้น​ตาม​ขนาด​ของ​งาน มัน​แพง​ขึ้น​ตาม จำนวน​ครั้ง​ที่​เรา​หยุด​เพื่อ commit เท่านั้น

flowchart TD
  R["ใส่ 1000 แถว · 4 แถวต่อหน้า<br/>งานเท่ากันทั้งสองทาง"]
  R --> A["commit ทุกแถว<br/>commit 1000 ครั้ง"]
  R --> B["commit ทีเดียวตอนจบ<br/>commit 1 ครั้ง"]
  A --> AP["หน้าที่เขียนลง log<br/>1000 frame · หน้าเดิมซ้ำสี่รอบ"]
  A --> AC["ค่าคงที่ของ commit<br/>1000 frame"]
  B --> BP["หน้าที่เขียนลง log<br/>250 frame · หน้าละครั้งเดียว"]
  B --> BC["ค่าคงที่ของ commit<br/>1 frame"]
  AP --> AT["รวม 2000 frame<br/>7.97x ของฐาน"]
  AC --> AT
  BP --> BT["รวม 251 frame<br/>ฐานเทียบ 1.00x"]
  BC --> BT

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

หัวข้อ​นี้​แสดง​ว่า​เมื่อ​วัด​กับ​ฐาน​ข้อมูล​จริง ราคา​ของ​ลำดับ​ที่​คีย์​มา​ถึง​แกว่ง​จาก +0.5 % ถึง +2,948.0 % ตาม​ขนาด​ของ commit

ทุก​ตัวเลข​ข้าง​บน​มา​จาก​แบบ​จำลอง​ที่​เรา​เขียน​เอง มัน​นับ​ตาม​กฎ​ที่​เรา​ประกาศ​ไว้​เอง จึง​พิสูจน์​ได้ แค่​ว่า​สูตร​ของ​เรา​ให้​ผลอย่างไร ไม่​ได้​พิสูจน์​ว่า​ฐาน​ข้อมูล​จริง​ทำ​แบบ​นั้น

ตัวเลข​ข้าง​ล่าง​มา​จาก​คนละ​ที่ คือ scripts/write-amp/probe.py ซึ่ง​เปิด SQLite ของ​จริง ใส่ 200,000 แถว แล้ว​อ่าน​ขนาด file -wal ตอน​ที่ connection ยัง​เปิด​อยู่

มัน​หาร​ด้วย​ขนาด​ที่​เอกสาร​รูปแบบ file ของ SQLite ระบุ​ไว้ คือ header ของ file WAL ขนาด 32 B และ header ของ​แต่ละ frame ขนาด 24 B ตาม​ด้วย​หน้า​ข้อมูล​ขนาด 4096 B

python3 scripts/write-amp/probe.py · ตัวเลข​ของ SQLite ไม่ใช่​ของ crate
clustered (WITHOUT ROWID)
commit ทุก ๆ เรียง uuid4 uuid7 uuid4 vs uuid7 vs
1 272,944 312,927 272,822 +14.6 % -0.0 %
100 16,092 283,526 16,058 +1,661.9 % -0.2 %
1,000 6,283 191,503 6,275 +2,948.0 % -0.1 %
ทั้งชุด 4,703 4,728 4,702 +0.5 % -0.0 %

ทั้ง​สาม​ช่อง​ซ้าย​เก็บ payload ชุด​เดียวกันเป๊ะ และ​คีย์​ยาว 16 B เท่า​กัน​ทุก​ช่อง ต่าง​กัน​แค่ ลำดับ​ที่​คีย์​มา​ถึง คือ เรียง มา​ถึง​เรียง​ลำดับ uuid4 มา​ถึง​แบบ​สุ่ม ส่วน uuid7 มา​ถึง​เรียง​ตาม​เวลา

สอง​ช่อง​ขวา​เป็น​เปอร์เซ็นต์​เทียบ​กับ​ช่อง เรียง ของ​แถว​เดียวกัน เท่านั้น เทียบ​ข้าม​แถว​ไม่​ได้ เพราะ​ฐาน​คนละ​ตัว

บรรทัด​ล่าง​สุด​คือ commit ที​เดียว​ทั้ง​ชุด คีย์​แบบ​สุ่ม​แพง​กว่า​คีย์​เรียง​แค่ +0.5 %

บรรทัด​เหนือ​มัน​คือ commit ทุก 1,000 แถว คีย์​แบบ​สุ่ม​ตัว​เดียวกัน​แพง​กว่า +2,948.0 %

payload เท่า​กัน คีย์​เท่า​กัน ต่าง​กัน​แค่​ขนาด​ของ commit แล้ว​ราคา​ของ​คำ​ว่า “คีย์​สุ่ม” เปลี่ยน​จาก​แทบ​ไม่มี​อยู่ ไป​เป็น​เกือบ​สามสิบ​เท่า

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

คีย์​แบบ​สุ่ม​กระจาย​แถว​ไป​ทั่ว​ทั้ง b-tree แต่ละ batch ขนาด 1,000 แถว​จึง​แตะ​หน้า​คนละ​กลุ่ม​กัน แล้ว​เขียน​หน้า​ทั้ง​กลุ่ม​นั้น​ลง log ทุก​ครั้ง​ที่ commit

ส่วน​คีย์​เรียง​เติม​ต่อ​ท้าย​ของ​เดิม​ไป​เรื่อยๆ หนึ่ง batch จึง​แตะ​หน้า​ไม่​กี่​หน้า ซึ่ง​เป็น​กลไก เดียว​กับ​ที่​แบบ​จำลอง​ของ​เรา​เขียน​ไว้​ตรง​บรรทัด (last_page - first_page + 1)

⚠ ตัวเลข​แผ่น​นี้​เป็น​ของ SQLite เท่านั้น

SQLite เขียน WAL เป็น​หน้า​เต็ม​ทุก​ครั้ง​ที่ commit ส่วน PostgreSQL กับ SQL Server เขียน log ระดับ record แล้ว​ให้ checkpointer ยุบ​ให้​ทีหลัง

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

หัวข้อ​นี้​ตอบ​ว่า​ไม่​ขัด เพราะ​กฎ​หนึ่ง​เลือก​ขอบเขต​ความ​ถูกต้อง อีก​กฎ​วัด​ต้นทุน I/O ภายใน​ขอบเขต​นั้น

ไซต์​นี้​สอน​ไว้​แล้ว​ใน​คอร์ส EF Core บท​ที่ 6 ว่า SaveChanges หนึ่ง​ครั้ง​ควร​ครอบ aggregate เดียว​เท่านั้น ส่วน​บท​นี้​เพิ่ง​แสดง​ว่า transaction ใหญ่​เขียน​น้อย​กว่า​มาก

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

กฎ 1 aggregate ต่อ 1 transaction ตอบ​ว่า อะไร​ต้อง​สำเร็จ​หรือ​ล้ม​พร้อม​กัน คำ​ตอบ​มา​จาก invariant ของ​โดเมน ไม่​ได้​มา​จาก​ราคา​ของ I/O เลย​สัก​นิดเดียว

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

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

กฎ​สอง​ข้อ​ที่​ตาม​มา​จึง​ไม่มี​วัน​ชน​กัน

  • ห้าม​ขยาย transaction เพื่อ​ประหยัด​การ​เขียน การ​มัด​สอง aggregate ไว้​ด้วย​กัน​ซื้อ frame มา​ได้​จริง แต่​จ่าย​ด้วย lock ที่​ถือ​นาน​ขึ้น​ตาม​บท​ที่ 3 version ที่​ค้าง​มาก​ขึ้น และ​งาน​ที่​ถูก​ทิ้ง มาก​ขึ้น​เมื่อ​โดน abort ตาม​บท​ที่ 6
  • ห้าม​ซอย transaction ให้​เล็ก​กว่า​ขอบเขต​ความ​ถูกต้อง การ​ผ่า​งาน​ก้อน​เดียว​เป็น​หลาย commit ไม่​ได้​ปลอดภัย​ขึ้น มัน​แค่​แพง​ขึ้น และ​เปิด​สภาพครึ่งๆ กลางๆ ที่​ขอบเขต​นั้น​สัญญา​ว่า​จะ​ไม่มี​ใคร​เห็น

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

และ​บท​ที่ 6 ของ​คอร์ส EF Core เอง​ก็​ชี้​มา​ที่​จุด​เดียวกัน มัน​บอกว่า​จังหวะ​ที่​ต้อง​เปิด BeginTransaction เอง​คือ​งาน​บำรุง​รักษา​หรือ​ย้าย​ข้อมูล ที่​ต้อง​รัน SaveChanges หลาย​รอบ ให้ atomic กัน ซึ่ง​เป็น​ทรง​เดียว​กับ​งาน​นำ​เข้า​ข้อมูล​ใน​บท​นี้​พอดี

🔁 อ่าน​อีก​ด้าน​ของ trade-off เดียวกัน

Transactions & Unit of Work ของ​คอร์ส EF Core วาง​ขอบเขต​ความ​ถูกต้อง​ไว้​ที่ 1 aggregate ต่อ 1 transaction ส่วน​บท​นี้​วัด​ต้นทุน I/O ที่​เกิด​ขึ้น ภายใน ขอบเขต​นั้น

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

บท​ที่ 8 จะ​เอา​ทุก​อย่าง​ที่​สร้าง​มา​ไป​เทียบ​กับ​เอนจิน​จริง และ​จะ​พบ​ว่า SQLite เลือก​จ่าย​ราคา ด้วย​วิธี​ที่​คอร์ส​นี้​ยัง​ไม่​ได้​พูด​ถึง​เลย​สัก​ครั้ง


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • SQLite — Write-Ahead Logging (ตรวจ​แล้ว 2026-08-13) ต้นทาง​ของ​นิยาม checkpoint ตรงๆ “Moving the WAL file transactions back into the database is called a ‘checkpoint’” และ​เป็น​ที่มา​ของ​ข้อ​ที่ว่าการ sync ทุก commit เป็น​ค่าที่​ตั้ง​ได้ ผ่าน PRAGMA synchronous
  • SQLite — Database File Format (ตรวจ​แล้ว 2026-08-13) หัวข้อ WAL ระบุ​ขนาด​ที่​สูตร​นับ frame ของ scripts/write-amp/probe.py ใช้ตรงๆ คือ “The WAL header is 32 bytes in size” และ “Each frame consists of a 24-byte frame-header followed by a page-size bytes of page data”

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

ข้อ 1 / 3

ผลรันของแบบจำลองบอกว่า commit ทุกแถวจ่ายไป 2000 frame ส่วน commit ทีเดียวจ่ายไป 251 frame จากแถวจำนวนเท่ากัน ข้อใดอธิบายส่วนต่างนี้ได้ถูกต้อง