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

NULL แถว​เดียว​ทำให้ NOT IN คืน​ศูนย์​แถว และ​ท่า​ที่​ตอบ​ผิด​คือ​ท่า​ที่​จ่าย​น้อย​ที่สุด

ทีม​การ​เงิน​ถาม​คำถาม​เดียว ออ​เด​อร์​ที่​ยกเลิก​ไป​แล้ว​เหลือ​อีก​กี่​รายการ​ที่​ยัง​ไม่​ได้​คืน​เงิน​ให้​ลูกค้า คุณ​เขียน query ที่​อ่าน​แล้ว​ตรง​กับ​คำถาม​คำ​ต่อ​คำ

SELECT id
FROM orders
WHERE status = 'Cancelled'
AND id NOT IN (SELECT order_id FROM refunds)

มัน​ตอบ​ว่า 0

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

คำ​ตอบ​ที่​ถูก​คือ 3,003 รายการ ข้อมูล​ชุด​เดิม ตาราง​ใบ​เดิม สิ่ง​เดียว​ที่​ทำให้​ผล​ต่าง​กัน​คือ​ใน​ตาราง refunds มี​แถว​ที่ order_id เป็น NULL อยู่ หนึ่ง​แถว

สิ่ง​ที่​หน้า​นี้​จะ​พิสูจน์​ด้วย​การ​วัด

NOT IN (SELECT …) ที่​เจอ NULL ไม่​ได้​คืนคำ​ตอบ​ที่​ขาด​ไป​บาง​ส่วน มัน​คืน​ศูนย์​แถว​เสมอ · และ​ท่า​ที่​ตอบ​ผิด​นั้น จ่าย​น้อย​กว่า ท่า​ที่​ตอบ​ถูก​ทั้ง​บน SQLite และ​บน PostgreSQL ใคร​เลือก​ท่า​ด้วย​การ​จับ​เวลา​จะ​เลือก​ท่า​ที่​ตอบ​ผิด แล้ว benchmark จะ​ยืนยัน​ให้​ด้วย​ว่า​เลือก​ถูก

เพิ่ม​แถว NULL เข้าไป​ที​ละ​แถว​แล้ว​นับ​คำ​ตอบ ท่า​อื่น​ไม่​ขยับ​เลย มี​ท่า​เดียว​ที่​ตก​หน้าผา

ตาราง orders มี 200,000 แถว ใน​นั้น​เป็น​ออ​เด​อร์​ที่​ยกเลิก​แล้ว 5,931 แถว ตาราง refunds มี 2,928 แถว หนึ่ง​แถว​ต่อ​ออ​เด​อร์​ที่​คืน​เงิน​ไป​แล้ว

refunds.order_id รับ NULL ได้ เพราะ​แถว​คืน​เงิน​ถูก​สร้าง​จาก webhook ของ payment gateway ก่อน แล้ว​งาน reconcile ค่อย​เติม​เลข​ออ​เด​อร์​ทีหลัง ระหว่าง​นั้น​แถว​นั้น​มี order_id เป็น NULL

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

จำนวน​แถว​ที่ order_id เป็น NULLNOT INNOT EXISTSLEFT JOINEXCEPTNOT IN + IS NOT NULL
03,0033,0033,0033,0033,003
103,0033,0033,0033,003
203,0033,0033,0033,003
1003,0033,0033,0033,003

อ่าน​แถว​แรก​ก่อน ตอน​ที่​ยัง​ไม่มี NULL ทั้ง​ห้า​ท่า​คืน 3,003 เท่า​กัน​หมด นั่น​คือ​สิ่ง​ที่​ทำให้​เทียบ​กัน​ได้​ต่อ​จาก​นี้ ทุก​ท่า​ทำงาน​เดียวกัน​อยู่

จาก​แถว​ที่​สอง​ลง​ไป NOT IN เป็น 0 และ​อยู่​ที่ 0 ตลอด ไม่​ว่า​จะ​มี​แถว NULL หนึ่ง​แถว​หรือ​สิบ​แถว​ก็​ให้​ผล​เท่า​กัน

นี่​ไม่ใช่​เส้น​ที่​ลาด​ลง มัน​คือ​หน้าผา​ที่​แถว​แรก ถ้า​คำ​ตอบ​ค่อย ๆ หาย​ไป​ตาม​จำนวน​แถว NULL กลไก​จะ​เป็น​อย่าง​อื่น การ​ที่​มัน​ตก​ถึง​ศูนย์​ทันที​ตั้งแต่​แถว​แรก บอก​ว่า​กลไก​คือ​ตรรกะ​สาม​ค่า

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

SQL ไม่​ได้​มี​สอง​ค่า​ความ​จริง มัน​มี​สาม​ค่า จริง เท็จ และ NULL ที่​แปล​ว่า ไม่รู้

expressionผลลัพธ์
1 IN (1, 2, NULL)จริง
5 IN (1, 2, NULL)NULL
1 NOT IN (1, 2, NULL)เท็จ
5 NOT IN (1, 2, NULL)NULL
5 IN (1, 2)เท็จ
5 NOT IN (1, 2)จริง

สอง​บรรทัด​ล่าง​คือ​สิ่ง​ที่​ทุก​คน​คาด​ไว้ ไม่มี NULL เมื่อไร IN กับ NOT IN ก็​เป็น​นิเสธ​ของ​กัน

บรรทัด​ที่​สี่​คือ​เหตุผล​ที่​หน้า​นี้​มี​อยู่ เอน​จินต​อบ​ว่า ไม่รู้ ไม่​ได้​ตอบ​ว่า​จริง เพราะ​มัน​เทียบ 5 กับ NULL แล้ว​ได้ NULL ซึ่ง​แปล​ว่า “ค่าที่​ซ่อน​อยู่​ตัว​นั้น​อาจ​เป็น 5 ก็ได้”

ความ​ไม่​สมมาตร​อยู่​ตรง​นี้ IN ยัง​ตอบ​ว่า จริง ได้ ถ้า​มัน​เจอ​ตัว​ที่​ตรง​จริง ๆ ก่อน แต่ NOT IN ไม่มี​ทาง​ตอบ​ว่า​จริง​ได้​เลย เมื่อ​ใน​รายการ​มี NULL อยู่​แม้แต่​ตัว​เดียว เพราะ​มัน​ต้อง​พิสูจน์​ว่า ไม่​ตรง​กับ​ทุก​ตัว และ​ตัว​ที่​เป็น NULL พิสูจน์​ไม่​ได้

WHERE เก็บ​เฉพาะ​แถว​ที่​เงื่อนไข​เป็น​จริง มัน​ทิ้ง​ทั้ง​เท็จ​และ NULL เมื่อ​ไม่มี​แถว​ไหน​เป็น​จริง​ได้​เลย ผลลัพธ์​จึง​ว่าง​เปล่า

ประโยค​เดียว​ที่​ต้อง​จำ​จาก​หน้า​นี้

NOT IN ที่​รายการ​มี NULL ไม่​ได้​กรอง​ผิด​บาง​แถว มัน​กรอง​ทิ้ง​ทุก​แถว และ​มัน​ทำ​แบบ​นั้น​เงียบ ๆ ไม่มี error ไม่มี warning ไม่มี​อะไร​ใน​ผลลัพธ์​ที่​บอก​ว่า​เกิด​อะไร​ขึ้น

นับ opcode ที่ SQLite เดิน​จริง ท่า​ที่​คืน​ศูนย์​แถว​จ่าย​น้อย​กว่า​ทุก​ท่า​ที่​คืนคำ​ตอบ​ถูก

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

ท่าคืน​กี่​แถวopcode
NOT IN067,557
NOT EXISTS3,003100,213
LEFT JOIN3,003106,290
NOT IN + IS NOT NULL3,003120,866
EXCEPT3,003132,720

ท่า​ที่​ตอบ​ผิด​จ่าย​น้อย​กว่า​ท่า​ที่​ตอบ​ถูก​ที่สุด​อยู่ 33%

ผล​ข้าง​เคียง​ของ​เรื่อง​นี้​ร้ายก​ว่า​ตัวเลข สมมติ​มี​คน​เทียบ​สอง​ท่า​นี้​ด้วย​การ​จับ​เวลา เขา​จะ​เห็น​ว่า NOT IN ชนะ แล้ว​เขา​จะ​เลือก​มัน และ​เขา​จะ​มี​ตัวเลข​ยืนยัน​ว่า​เขา​เลือก​ถูก

benchmark ที่​ไม่​ได้​ตรวจ​ว่า​คำ​ตอบ​ตรง​กัน ไม่​ได้​วัด​ว่า​ท่า​ไหน​ดี​กว่า มัน​วัด​ว่า​ท่า​ไหน​เลิก​ทำงาน​เร็ว​กว่า

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

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

คำ​ตอบ​เป็น​ศูนย์​แถว แต่​เอน​จิน​ไม่​ได้​หยุด​ตั้งแต่​เห็น NULL มัน​เดิน​ครบ​แล้ว​ทิ้ง​ผล​ทั้งหมด

คน​อ่าน​จะ​เดา​ว่า​คืน 0 แถว​ก็​ต้อง​เร็ว​สิ ไม่มี​อะไร​ให้​ส่ง​กลับ และ​ถ้า​เอน​จิน​ฉลาด​พอ มัน​ควร​รู้​ตั้งแต่​เห็น​แถว NULL ว่า query นี้​ไม่มี​ทาง​คืน​อะไร​ได้​เลย

มัน​ไม่รู้

ตอน​ที่​ยัง​ไม่มี NULL ท่า​นี้​จ่าย 76,566 opcode เพื่อ​คืน 3,003 แถว ตอน​ที่​มี NULL แล้ว มัน​จ่าย 67,557 opcode เพื่อ​คืน 0 แถว คิด​เป็น 88% ของ​งาน​เดิม

ถ้า​มัน​ลัด​ได้ ราคา​จะ​เหลือ​หลัก​ร้อย ไม่ใช่​หลัก​หมื่น สิ่ง​ที่​มัน​ทำ​จริง​คือ​เดิน​ครบ​ทั้ง 5,931 แถว เทียบ​ที​ละ​แถว​กับ​รายการ​ที่​มี NULL อยู่ ได้​ผล​เป็น ไม่รู้ ทุก​แถว แล้ว​ทิ้ง​ทั้งหมด

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

ลบ​สถิติ​ทิ้ง​ทั้งหมด ราคา​ขึ้น​สิบ​เท่า แผน​เปลี่ยน​จาก SEARCH เป็น SCAN แต่​คำ​ตอบ​ไม่​ขยับ​เลย

ข้อ​โต้​แย้ง​แรก​ที่​ทุก​คน​ยก​คือ​เดี๋ยว optimiser ก็​จัดการ​ให้​เอง หน้า​นี้​จึง​วัด​ช่อง​เดิม​ซ้ำ​บน​ฐาน​ที่​ลบ sqlite_stat1 และ sqlite_stat4 ทิ้ง​หมด บน connection ใหม่

ท่าopcode เมื่อ​มี​สถิติopcode เมื่อ​ลบ​สถิติคืน​กี่​แถว เมื่อ​มี​สถิติคืน​กี่​แถว เมื่อ​ลบ​สถิติ
NOT IN67,557641,69100
NOT EXISTS100,213659,3323,0033,003
LEFT JOIN106,290665,4103,0033,003
EXCEPT132,720677,2003,0033,003

ราคา​ขึ้น​ราว 9 เท่า​ทุก​ท่า แผน​ของ NOT IN เปลี่ยน​จาก SEARCH เป็น SCAN จริง แต่​ช่อง​ขวา​สอง​ช่อง​ไม่​ขยับ​แม้แต่​ช่อง​เดียว

คำ​ตอบ​ที่​ผิด​ไม่​ได้​มา​จาก​แผนที่​แย่ มัน​มา​จาก​ความ​หมาย​ของ operator optimiser เปลี่ยน​ได้​แค่ วิธี ที่​เอน​จิ​น​เดิน​ไป​หา​คำ​ตอบ มัน​เปลี่ยน คำ​ตอบ ไม่​ได้ และ​ไม่​ควร​เปลี่ยน​ด้วย

หลัก​ฐาน​ที่​ตรง​ที่สุด​ของ​ข้อ​นี้​อยู่​ใน​แผน​เอง แผน​ของ NOT IN เหมือน​กัน​ทุก​ตัว​อักษร ทั้ง​ตอน​ที่​ตาราง refunds มี​แถว NULL และ​ตอน​ที่​ไม่มี optimiser มอง​ไม่​เห็น​ความ​ต่าง​ตรง​นั้น​เลย

และ​ท่า​นี้​ก็​ไม่​ได้​ถูก​บังคับ​ให้ materialise รายการ​ทั้ง​ชุด​ขึ้น​มา​ด้วย แผน​บอก​ว่า​มัน​ใช้ index ix_refunds_order อยู่​แล้ว เรื่อง​นี้​จึง​ไม่ใช่​เรื่อง​แผนที่​ยัง​ปรับ​ไม่​ดี​พอ

ยก​ข้อมูล​ชุด​เดิม​ทั้ง​ชุด​เข้า PostgreSQL แล้ว​ถาม​คำถาม​เดิม หน้าผา​เดียวกัน​เกิด​ขึ้น​อีก

ข้อ​โต้​แย้ง​ที่​สอง​คือ SQLite เป็น​ฐาน​ข้อมูล​ตัว​เล็ก เอา​ไป​เทียบ​กับ​ตัว​จริง​ไม่​ได้

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

ท่าคืน​กี่​แถว เมื่อ​ไม่มี NULLคืน​กี่​แถว เมื่อ​มี NULL หนึ่ง​แถวbuffers
NOT IN3,00302,748
NOT EXISTS3,0033,0032,939
LEFT JOIN3,0033,0032,975
EXCEPT3,0033,0032,936

ผล​เหมือน​กัน​ทุก​ช่อง และ NOT IN ยัง​เป็น​ท่า​ที่​แตะ buffer น้อย​ที่สุด​เหมือน​ฝั่ง SQLite ท่า​ที่​ตอบ​ผิด​จ่าย​น้อย​ที่สุด​บน​ทั้ง​สอง​เอน​จิน

ความ​ต่าง​อยู่​ที่​แผน NOT EXISTS ได้ Hash Anti Join ซึ่ง​เป็น​วิธี​ที่ planner เตรียม​ไว้ สำหรับ​คำถาม​รูป​นี้​โดย​เฉพาะ ส่วน NOT IN ได้ hashed SubPlan ทุก​ครั้ง ไม่​เคย​ได้ anti-join เลย

คน​ที่​รู้​เรื่อง​นี้​อยู่​แล้ว​จะ​เถียง​ทันที​ว่า​ถ้า​รู้​ว่า column ไม่มี NULL ก็​ประกาศ NOT NULL สิ แล้ว planner จะ​รู้​ว่า​มัน​แปลง​เป็น anti-join ได้​อย่าง​ปลอดภัย

หน้า​นี้​จึง​วัด​ข้อ​นั้น​แทนที่​จะ​เดา ข้อมูล​ชุด​เดิม​ทุก​แถว query เดิม​ทุก​ตัว​อักษร ต่าง​กัน​แค่​คำ​ประกาศ​บน column เดียว

คำ​ประกาศ​บน refunds.order_idbuffersแผน
รับ NULL ได้2,748hashed SubPlan
NOT NULL2,748hashed SubPlan

ไม่​ขยับ​เลย​แม้แต่ buffer เดียว planner ยัง​ไม่​ยอม​แปลง NOT IN เป็น anti-join ต่อ​ให้​ประกาศ​ไว้​ชัดเจน​แล้ว​ว่า column นั้น​ไม่มี NULL

ทำไม​ถึง​เชื่อ​ผล​ของ​การ​ทดลอง​นี้​ได้

ผล​ที่​ออก​มา “เห​มือ​นกันเป๊ะ​ทั้ง​สอง​ฝั่ง” เป็น​ผล​ที่​เกิด​ได้​จาก​อีก​สาเหตุ​หนึ่ง​ด้วย คือ​คำ​สั่ง DDL ไม่​ได้​ลง แล้ว​เรา​วัด​ของ​อย่าง​เดียวกัน​สอง​ครั้ง​โดย​ไม่รู้​ตัว

ตัว​วัด​จึง​ถาม information_schema.columns ก่อน​สรุป​ทุก​ครั้ง ว่า column นั้น​ประกาศ​ไว้​อย่างไร​จริง ๆ ได้ YES ใน​รอบ​แรก​และ NO ใน​รอบ​หลัง ถ้า​ไม่​ได้​สอง​ค่า​นี้ probe จะ​หยุด​และ​ไม่​รายงาน​อะไร​เลย

การ​ประกาศ NOT NULL ยัง​คุ้ม​ที่​จะ​ทำ แต่​คุ้ม​ด้วย​เหตุผล​อื่น มัน​ทำให้​แถว​ที่​เป็นต้น​เหตุ ใส่​ลง​ตาราง​ไม่​ได้​ตั้งแต่​แรก ไม่ใช่​ทำให้ query ทน​ต่อ​แถว​นั้น

ถาม EF Core ด้วย ToQueryString แล้ว​รัน​จริง​บน​ข้อมูล​ชุด​เดิม LINQ ทุก​ท่า​ตอบ​ถูก​หมด

ถึง​ตรง​นี้​คน​เขียน C# ควร​จะ​กลัว​แล้ว เพราะ !list.Contains(x) คือ LINQ ที่​เขียน​กัน​ทุก​วัน

หน้า​นี้​จึง​ไม่​ได้​อ่าน​เอกสาร​แล้ว​รายงาน มัน​ถาม EF Core 10.0.10.0 ตรง ๆ ว่า​แปล​ออก​มา​เป็น​อะไร แล้ว​รัน query นั้น​บน​ฐาน​ข้อมูล​ที่ probe สร้าง​ให้ ซึ่ง​เป็น​ข้อมูล​ชุด​เดียว​กับ​ตาราง​ทุก​ตาราง​ข้าง​บน

LINQ ที่​เขียนคืน​กี่​แถว
!db.Refunds.Select(r => r.OrderId).Contains(o.Id)3,003
!refundedIds.Contains(o.Id)3,003
!db.Refunds.Any(r => r.OrderId == o.Id)3,003
.Except(db.Refunds.Select(r => r.OrderId))3,003

ทุก​ท่า​ตอบ 3,003 ไม่มี​ท่า​ไหน​ตกหลุม​เลย​สัก​ท่า

EF Core กัน​ไว้ สอง​ชั้น และ​สอง​ชั้น​นี้​เป็น​คนละ​กลไก​กัน

ชั้น​แรก​อยู่​ใน​ตัว​แปล ท่า​ที่​เป็น subquery ไม่​ได้​ออก​มา​เป็น NOT IN ดิบ ๆ มัน​ออก​มา​แบบ​นี้

NOT (COALESCE("o"."id" IN (
SELECT "r"."order_id"
FROM "refunds" AS "r"
), 0))

COALESCE(…, 0) แปลง NULL ของ​ตรรกะ​สาม​ค่า​ให้​เป็น​เท็จ​ก่อน แล้ว NOT จึง​ได้​จริง​ตาม​ที่​ควร​เป็น นั่น​คือ​การ​แก้​กับ​ดัก​ที่​หน้า​นี้​ทั้ง​หน้า​พูด​ถึง เขียน​ไว้​ใน​ตัว​แปล​ของ ORM

ชั้น​ที่​สอง​อยู่​ฝั่ง client ท่า​ที่​โหลด id ขึ้น​มา​ก่อน​แล้ว​ค่อย​กรอง รายการ​ที่​โหลด​มา​มี​สมาชิก 2,929 ตัว และ​ใน​นั้น​เป็น null อยู่ 1 ตัว

EF ทิ้ง null ตัว​นั้น​ออก​จาก​รายการ​ก่อน​ปล่อย SQL ออก​ไป SQL ที่​ได้​เป็น NOT IN ธรรมดา มี parameter 2,999 ตัว และ​ไม่มี parameter ตัว​ไหน​เป็น NULL เลย​แม้แต่​ตัว​เดียว

สิ่ง​ที่​การ​กัน​สอง​ชั้น​บอก​เรา

ทีม​หนึ่ง​ไม่​เขียน code กัน​สอง​ชั้น​ให้​กับ​ปัญหา​ที่​ไม่มี​จริง การ​ที่​ตัว​แปล​ไล่​แปะ COALESCE รอบ IN ทุก​ตัว และ ยัง​กรอง null ทิ้ง​จาก​ฝั่ง client อีก​ชั้น คือ​หลัก​ฐาน​ว่า​กับ​ดัก​นี้​เคย​กัด​คน​มา​แล้ว​มาก​พอที่​จะ​คุ้ม​กับ​การ​เขียน code กันเอา​ไว้

ORM กัน​ให้​เฉพาะ​ตอน​ที่​คุณ​เขียน LINQ พอ​คุณ​เปิด​หน้าต่าง SQL มัน​ตาม​ไป​ด้วย​ไม่​ได้

ข่าวดี​ของ​หัวข้อ​ที่​แล้ว​คือ​ข่าว​ร้าย​ของ​หัวข้อ​นี้

คน​เขียน C# ที่​ใช้ EF Core มา​ห้า​ปี​จะ​ไม่​เคย​เจอ​กับ​ดัก​นี้​เลย​สัก​ครั้ง เขา​จึง​สร้าง​สัญชาตญาณ​ขึ้น​มา​ว่า NOT IN ปลอดภัย เพราะ​ใน​ประสบการณ์​ของ​เขา​มัน​ปลอดภัย​จริง

แล้ว​วัน​หนึ่ง​เขา​ต้อง​เขียน query รายงาน​ให้​ฝ่าย​การ​เงิน งาน​นั้น​ไม่​ผ่าน ORM มัน​เป็น SQL ที่​พิมพ์​ลง​ไป​ใน​หน้าต่าง query ตรง ๆ

FromSql ก็​เป็น​อีก​ช่อง​หนึ่ง stored procedure ก็​อีก​ช่อง เครื่อง​มือ​ทำ dashboard ที่​ต่อ​ตรง​เข้า​ฐาน​ข้อมูล​ก็​อีก​ช่อง งาน ETL ที่​เขียน​เป็น SQL ล้วน​ก็​อีก​ช่อง ทุก​ช่อง​เหล่า​นี้​ไม่มี​ตัว​แปล​ของ ORM คอย​แปะ COALESCE ให้

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

เขียน NOT EXISTS และ​ปิด​ต้น​เหตุ​ที่ schema ทั้ง​สอง​อย่าง ไม่ใช่​เลือก​อย่าง​ใด​อย่าง​หนึ่ง

เขียน NOT EXISTS แทน มัน​ตอบ​ถูก​ไม่​ว่า column จะมี NULL หรือ​ไม่ และ​บน PostgreSQL มัน​เป็น​ท่า​เดียว​ใน​สี่​ท่า​ที่​ได้ Hash Anti Join

SELECT o.id
FROM orders o
WHERE o.status = 'Cancelled'
AND NOT EXISTS (SELECT 1 FROM refunds r WHERE r.order_id = o.id)

เหตุผล​ที่​มัน​รอด​ไม่ใช่​เพราะ​มัน​ฉลาด​กว่า แต่​เพราะ​มัน​ถาม​คนละ​คำถาม EXISTS ถาม​ว่า​มี​แถว​ที่​เข้า​เงื่อนไข​ไหม ซึ่ง​ตอบ​ได้​แค่​มี​หรือ​ไม่มี ไม่มี​ช่อง​ให้ ไม่รู้ โผล่​มา

ถ้า​ต้อง​ใช้ NOT IN จริง ๆ ให้​กรอง NULL ทิ้ง​ใน​ตัว subquery อย่า​กรอง​ที่​อื่น

AND id NOT IN (SELECT order_id FROM refunds WHERE order_id IS NOT NULL)

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

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

ถ้า​โดเมน​บอก​ว่า column นั้น​ว่าง​ได้​จริง อย่า​ฝืน​ประกาศ NOT NULL ให้​ยอมรับ​ว่า​มี NULL อยู่ แล้ว​เลิก​ใช้ NOT IN กับ column นั้น​แทน

ข้อ​ที่​ตรวจ​ได้​เร็ว​ที่สุด​ใน code ที่​มี​อยู่​แล้ว

หา NOT IN (SELECT ใน code ทั้งหมด แล้ว​ดู​ที​ละ​ที่​ว่า column ใน subquery ประกาศ NOT NULL ไว้​ไหม ที่ไหน​ไม่​ได้​ประกาศ ที่​นั่น​คือ query ที่​คืน​ศูนย์​แถว​ได้​ทุก​เมื่อ โดย​ไม่มี​อะไร​ฟ้อง

ตรวจ view และ CTE ด้วย column ที่มา​จาก LEFT JOIN เป็น NULL ได้​เสมอ ต่อ​ให้ column ต้นทาง​ประกาศ NOT NULL ไว้​ก็ตาม

ทุก​ตัวเลข​ใน​หน้า​นี้​มา​จาก script ที่​รัน​ซ้ำ​ได้ และ​มี​ด่าน​ที่​พิสูจน์​แล้ว​ว่า​มัน​แดง​ได้

ฝั่ง SQLite ใช้ Python stdlib ล้วน ไม่​ต้อง​ติด​ตั้ง​อะไร ไม่​ต้อง​มี​เซิร์ฟเวอร์ มัน​อยู่​ใน​เกต unit ของ repo นี้ จึง​ถูก​วัด​ซ้ำ​ทุก​ครั้ง​ที่​มี​คน push

Terminal window
python3 scripts/not-in-null/probe.py --check # วัดซ้ำแล้วตรวจข้ออ้าง
python3 scripts/not-in-null/probe.py --selftest # พิสูจน์ว่าด่านแดงได้

--check ไม่​ได้​ตรึง​จำนวน opcode ที​ละ​ตัว เพราะ​จำนวน opcode ขึ้น​กับ​บิ​ลด์​ของ SQLite ที่​รัน สิ่ง​ที่​มัน​ตรวจ​คือ​รูป​ของ​ผลลัพธ์ ซึ่ง​เป็น​สิ่ง​ที่​หน้า​นี้​อ้าง​จริง ๆ

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

ฝั่ง PostgreSQL และ​ฝั่ง EF Core ต้อง​มี Docker และ dotnet จึง​อยู่​นอก​เก​ตนั้น ผล​ของ​ทั้ง​สอง​ฝั่ง​ถูก commit ไว้​เป็น JSON และ​เกต​เทียบ​ตัวเลข​ใน​หน้า​นี้​กับ JSON นั้น​ทุก​ครั้ง

ตาราง orders เป็น​ตาราง​ใบ​เดียว​กับ​ที่​บทความ​เรื่อง index และ​เรื่อง keyset pagination ใช้ ตัว​วัด​ของ​หน้า​นี้ import ตัว​สร้าง​ของ​มัน​มา​ใช้​ซ้ำ ไม่​ได้​คัด​ลอก code มา ข้อ​อ้าง​ว่า​เป็น​ตาราง​ใบ​เดียวกัน​จึง​ผิด​เงียบ ๆ ไม่​ได้

เอน​จิ​นที่​ผลิต​ตัวเลข​ชุด​นี้​คือ SQLite 3.53.4 และ PostgreSQL 18.6