9.10 · Indexing, transactions, performance
บทนี้รวมเรื่อง performance ของ data layer — index coverage, transaction bug ที่ลามทั้งระบบ, N+1 query patterns, และ type choices ที่กระทบ perf
Index coverage — sparse
จาก 95 model definitions ใน vtrc-api มีแค่ 36 ไฟล์ (~38%) ที่ประกาศ indexes: [...] ผ่าน Sequelize
ที่เหลือ ~62% ไม่มี index declaration — ทำให้ query ที่ filter ด้วย column ที่ไม่ใช่ PK ต้อง full table scan
ตัวอย่าง index ที่ดี
indexes: [{ fields: ['refreshToken', 'expireDate'] }]Composite index บน refreshToken + expireDate — ตรงกับ query WHERE refreshToken = ? AND expireDate > NOW() ที่ใช้ตอน validate session
indexes: [{ fields: ['wdHospitalId'] }, { fields: ['wdLeaveId'] }]สอง index แยก — ตรงกับที่ Approver เป็น polymorphic (query ด้วย wdHospitalId หรือ wdLeaveId แยกกัน)
ตัวอย่างที่ขาด
| Model | Column ที่ query บ่อย | มี index ไหม |
|---|---|---|
User | param (EmpCode cache), refId | ไม่มี |
UserDivision | userId + level + idLevel + profileKey | มี (composite) |
Notification | userId, notificationGroup | ไม่มี |
LeaveDocument | empCode | ไม่มี (มีแค่ status) |
WDHospital | empCode, orgCode, status | มีแค่ status |
FundRequest | userId, empCode, requestType | ไม่มี |
ตารางที่ใหญ่ขึ้น (เช่น LeaveDocument ที่สะสมปีละหมื่น record) → query โดยไม่มี index จะช้ามาก
Unique constraint
indexes: [{ unique: true, fields: ['level', 'postId', 'idLevel'] }]บางที่ประกาศ unique: true เพื่อ enforce business constraint — เช่น post ต่อ division ต่อ level ต้องไม่ซ้ำกัน
Transaction bug — ลามทั้งระบบ
รูปแบบ bug เดียวกันพบในหลายไฟล์ (withdraw, leave, fund, ...)
Bug pattern
const result = await Models.WDHospital.update(
args,
{ where: { wdHospitalId: wdHospitalId } }, // ← options ที่ถูกต้อง
{ transaction: t } // ← argument ที่ 3 (ไม่มีใน Sequelize API)
).catch(...)Sequelize.Model.update(values, options) รับแค่ 2 arguments — argument ตัวที่ 3 ({ transaction: t }) ถูก discard อย่างเงียบ
ผลกระทบ:
- transaction ไม่ถูกใช้จริง → update commit ทันที ไม่รอ rollback
- ถ้ามีหลาย write ใน flow เดียวกัน → ถ้าตัวที่ 2 fail ตัวแรก commit ไปแล้ว (data inconsistent)
- bug ไม่มี error ไม่มี warning → ไม่มีใครรู้
ตัวอย่างใน repositories/withdraw/hospital.js
const result = await Models.WDHospital.update(args, { where: { wdHospitalId: wdHospitalId } }, { transaction: t }).catch(e => {update(values, options, thirdArg) — thirdArg หายไป
บรรทัด 156-157 อาจจะถูกต้อง:
return Models.db.transaction().then(async t => {
const result = await Models.WDHospital.update(args, conditions, { transaction: t }).catch(e => {อันนี้ใช้ transaction().then(...) pattern — conditions รวม where กับ transaction ใน object เดียวกัน → ถูกต้อง
วิธีแก้
// ผิด:
Model.update(args, { where: {...} }, { transaction: t })
// ถูก:
Model.update(args, { where: {...}, transaction: t })ขอบเขตของ bug
พบในไฟล์เหล่านี้ (non-exhaustive):
lib/repositories/withdraw/slip.jslib/repositories/withdraw/hospital.jslib/repositories/withdraw/historyAction.jslib/repositories/withdraw/export.jslib/repositories/withdraw/document.jslib/repositories/withdraw/delegate.jslib/repositories/withdraw/approver.jslib/repositories/version/version.jslib/repositories/user/userDivision.jslib/repositories/user/user.js
transaction ใน vtrc-api "ใช้ไม่ได้จริง" ในกรณีส่วนใหญ่ — rollback ไม่ทำงาน, commit ไม่รอกัน
N+1 query patterns
Resolver หลายตัวมี pattern ที่ trigger N+1:
// ตัวอย่าง (pseudo):
const hospitals = await Models.WDHospital.findAll({ where: {...} })
for (const h of hospitals) {
const approvers = await Models.Approver.findAll({ where: { wdHospitalId: h.wdHospitalId } })
h.approvers = approvers
}ถ้า hospitals มี 50 row → 50 query ถัดไป
วิธีที่ควรจะเป็น:
const hospitals = await Models.WDHospital.findAll({
where: {...},
include: [{ model: Models.Approver, as: 'approver' }]
})หรือใช้ DataLoader (แต่ vtrc-api ไม่ได้ใช้)
Pagination — limit cap ที่ไม่ทำงาน
ใน lib/clouse.js (filter builder):
if (args.limit) {
let limit
if (limit > 50) {
limit = 50
} else {
limit = args.limit
}let limit declare ไม่มีค่า → undefined > 50 → false เสมอ → limit = args.limit ใช้ค่าที่ส่งมา
ผลกระทบ — client ขอ limit 10000 ได้ → query หนัก → potential DoS
วิธีแก้ — เปลี่ยน let limit เป็น let limit = args.limit (หรือใช้ Math.min(args.limit, 50))
DECIMAL vs FLOAT
| Field | Type | เหมาะไหม |
|---|---|---|
WDHospital.totalWithdraw | DECIMAL(12,2) | ใช่ — เงิน |
WDSlip.totalPrice | DECIMAL(12,2) | ใช่ — เงิน |
LeaveDocument.leaveDay | FLOAT | ไม่ดีนัก — ควรเป็น DECIMAL(4,1) หรือ INTEGER ถ้าเป็น half-day |
LeaveAttachFile.fileSize | FLOAT | ไม่ดี — ควรเป็น BIGINT (bytes เป็นจำนวนเต็ม) |
FLOAT มี precision error → leaveDay === 0.5 อาจจะไม่ตรงเพราะ 0.5 ใน IEEE 754 อาจจะเป็น 0.49999... หรือ 0.50001...
Pool sizing — สูงเกินไป
pool: {
max: 100,
min: 0,
acquire: 60000,
idle: 10000
}max: 100 × 2 connection (db + db2) × 4 replica (APP_NO 1, 2, 3, 5) = 800 connection สูงสุด
MariaDB default max_connections = 151 → ถ้าทุก replica เต็มพร้อมกัน → ER_CON_COUNT_ERROR
ปกติไม่เจอเพราะ traffic ไม่ได้พุ่งพร้อมกัน แต่ตอน batch job หรือ traffic spike → risk สูง
Sync I/O ใน event loop
ไฟล์ utility (lib/file.js, lib/wlog.js) ใช้ fs.writeFileSync / fs.appendFileSync — block event loop ทุกครั้ง
ถ้ามี request 100 ตัวที่ต้อง log → event loop ค้างรอ I/O → response time พุ่ง
ควรจะใช้ fs.promises.writeFile หรือ logger async
Cross-schema join ที่ไม่ได้
Sequelize relation ไม่ cross-database → ทุก join ข้าม vtrc ↔ vtrc-centralize ต้อง:
- query ฝั่งเดียวก่อน
- extract ID list
- query อีกฝั่งด้วย
WHERE id IN (...) - merge ใน memory
ทำให้ latency สะสม + memory consumption สูง
เคล็ดลับตอนทำงานจริง
ถ้า query ช้า
- เช็ก
EXPLAINใน DB — ดูว่าใช้ index ไหม - เช็กว่ามี index declaration ใน model ไหม (
grep "indexes:" models/...) - ถ้าไม่มี → เพิ่มใน model + manual
CREATE INDEXใน DB
ถ้าเจอ data inconsistent
transaction bug → เช็กว่า controller ที่ใช้ transaction มี pattern update(args, { where }, { transaction }) ไหม → แก้เป็น update(args, { ...where, transaction })
อย่า trust Models.db.transaction()
ถ้าเห็น .transaction() ใน code เก่า → สมมติฐานคือ transaction อาจจะไม่ทำงาน → เช็ก pattern ก่อน
ถ้าจะเพิ่ม index ในตารางเดิม
- เพิ่มใน model definition (
indexes: [{ fields: [...] }]) - deploy →
syncModelToTable()จะเช็กว่า index มีอยู่ไหม ถ้าไม่มีจะCREATE INDEX - verify ใน DB:
SHOW INDEXES FROM TableName