Case study · ระบบ NPA · 2026
ทำหน้ารายการหลักประกันให้เร็วขึ้น 89 เท่า
หน้ารายการหลักประกันเป็นหน้าที่ทีม NPA เปิดทั้งวัน แต่ทุกครั้งที่กดเปลี่ยนหน้าต้องรอเกือบ 20 วินาที นี่คือวิธีที่ผมหาสาเหตุ แก้ และพิสูจน์ว่าข้อมูลที่แสดงยังถูกต้องเหมือนเดิมทุกแถว
- เปิดหน้ารายการ
18 วิ0.2 วิ- Export ทั้งหมด
35 วิ4 วิ- ข้อมูลที่ต้องอ่าน (logical reads)
257,4249,510- เวลา CPU
2,188 ms78 ms
01 ปัญหา
หน้ารายการแสดงทีละ 20 แถวจากทรัพย์ราว 3,500 รายการ แต่ใช้เวลา 18–20 วินาทีทุกหน้า หน้าลึกๆ ช้าพอกัน และปุ่ม Export ใช้ 35 วินาที ผู้ใช้จึงต้องรอแทบทุกครั้งที่ค้นหาหรือเปลี่ยนหน้า
02 หาสาเหตุ
- query นับเลขแถวและยอดรวมด้วย ROW_NUMBER() / COUNT() OVER() ทำให้ฐานข้อมูลต้องประมวลผลทุกแถวที่ผ่านตัวกรอง (3,500+ แถว) ให้ครบก่อน แล้วค่อยตัดเหลือ 20 แถว
- ตัวค้นหา "สถานะช่างล่าสุด" (OUTER APPLY … TOP 1) จึงถูกรัน 3,500 ครั้ง แทนที่จะเป็น 20 ครั้ง และตารางที่มันค้นไม่มี index ที่ใช้ได้
- ส่วนแบ่งหน้า join ครบ 14 ตาราง ทั้งที่เงื่อนไขค้นหาใช้แค่ไม่กี่ตาราง หนึ่งในนั้นเป็นการ join ข้ามฐานข้อมูลที่ต้องแปลง collation จึงใช้ index ไม่ได้ พอเป็นหน้าที่ 2 ขึ้นไป optimizer เลือก nested loop ที่อ่านตารางนั้นซ้ำทุกแถว
03 วิธีแก้
- แบ่งหน้าก่อนบนคอลัมน์แคบๆ (id + เลขแถว + ยอดรวม) ด้วย OFFSET / FETCH แล้วค่อย join กลับไปดึงข้อมูลเต็มและสถานะช่างเฉพาะ 20 แถวของหน้านั้น
- ในขั้นแบ่งหน้า join เฉพาะตารางที่มีผลต่อจำนวนแถว และตารางที่เงื่อนไขค้นหาอ้างถึงจริง
- บังคับ hash join กับข้อมูลข้ามฐานข้อมูล ให้อ่านครั้งเดียวแทนการอ่านซ้ำทุกแถว
- เปลี่ยนตัวหาสถานะช่างล่าสุดเป็น derived table + ROW_NUMBER ให้อ่านตารางครั้งเดียว ทั้งตอนแสดงหน้าและตอน Export
04 พิสูจน์ว่าไม่พัง
- ตรวจก่อนแก้ว่า join ไม่ทำให้แถวซ้ำ: COUNT(*) = COUNT(DISTINCT id) = 3,503
- เทียบผลลัพธ์กับ query เดิมทีละแถว ใน 9 ชุดตัวกรอง ตรงกันทั้งหมด ไม่มีแถวไหนต่าง
- เคสขอบ (ช่วงหน้าที่ผิด) ยังคืนผลแบบเดิม คือ 0 แถว ไม่ใช่ทุกแถว
- จดเหตุผลของ collation และ hash hint ไว้ในเอกสาร GOTCHAS คนที่มาแก้ต่อจะได้ไม่ลบทิ้งโดยไม่รู้
05 ผลที่วัดได้
| วัดที่ | ก่อน | หลัง | เร็วขึ้น |
|---|---|---|---|
| หน้า 1 (ไม่กรอง) | 17,962 ms | 203 ms | 88.5× |
| หน้า 3 (ไม่กรอง) | 18,352 ms | 205 ms | 89.5× |
| หน้าลึก | 18,566 ms | 140 ms | 133× |
| Export | 35 วิ | 4 วิ | 8.8× |
| Logical reads | 257,424 | 9,510 | 27× |
| CPU | 2,188 ms | 78 ms | 28× |
06 บั๊กที่เจอระหว่างทาง
คอลัมน์ "ช่างปรับสถานะ" ขึ้นเป็น "-" ทั้งที่ช่างกดอัปเดตแล้ว เพราะแอปมือถือกับหน้าเว็บเขียนสถานะลงคนละที่ (วัดจริง: 15 ทรัพย์มีสถานะ แต่มีประวัติแค่ 3) แก้ให้ทั้งสองทางเขียนครบทั้งสองที่ และกันแถวซ้ำเวลาช่างกดส่งฟอร์มหลายรอบ
07 อีกหน้าที่ช้า แต่คนละสาเหตุ
หน้ารับเรื่องคดีขับไล่ช้าด้วยอีกเหตุผล: query ที่ทำงานแบบ parallel รอกันเองตอนเครื่องฐานข้อมูลมีงานเยอะ พอจำกัดให้ขั้นนั้นทำงานแบบเดียว (MAXDOP 1) แท็บแรกเร็วขึ้นจาก 33.8 วิ เหลือ 1.9–2.4 วิ และ Excel จาก 13.5 วิ เหลือ 2–3 วิ ผลลัพธ์เท่าเดิม
08 AI อยู่ตรงไหนในงานนี้
ทุก commit ของงานนี้ทำร่วมกับ Claude Code ส่วนที่ผมรับผิดชอบเองคือวัดเวลาบนข้อมูลชุดเดียวกัน ตรวจผลลัพธ์ทีละแถวว่าไม่เปลี่ยน แล้วจึงขึ้นระบบพร้อม rollback