Skip to content

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 ที่ดี

63:63:vtrc-api/api/src/models/core/session/session.js
    indexes: [{ fields: ['refreshToken', 'expireDate'] }]

Composite index บน refreshToken + expireDate — ตรงกับ query WHERE refreshToken = ? AND expireDate > NOW() ที่ใช้ตอน validate session

45:45:vtrc-api/api/src/models/core/withdraw/approver.js
    indexes: [{ fields: ['wdHospitalId'] }, { fields: ['wdLeaveId'] }]

สอง index แยก — ตรงกับที่ Approver เป็น polymorphic (query ด้วย wdHospitalId หรือ wdLeaveId แยกกัน)

ตัวอย่างที่ขาด

ModelColumn ที่ query บ่อยมี index ไหม
Userparam (EmpCode cache), refIdไม่มี
UserDivisionuserId + level + idLevel + profileKeyมี (composite)
NotificationuserId, notificationGroupไม่มี
LeaveDocumentempCodeไม่มี (มีแค่ status)
WDHospitalempCode, orgCode, statusมีแค่ status
FundRequestuserId, empCode, requestTypeไม่มี

ตารางที่ใหญ่ขึ้น (เช่น LeaveDocument ที่สะสมปีละหมื่น record) → query โดยไม่มี index จะช้ามาก

Unique constraint

28:28:vtrc-api/api/src/models/core/post/postDivision.js
        indexes: [{ unique: true, fields: ['level', 'postId', 'idLevel'] }]

บางที่ประกาศ unique: true เพื่อ enforce business constraint — เช่น post ต่อ division ต่อ level ต้องไม่ซ้ำกัน


Transaction bug — ลามทั้งระบบ

รูปแบบ bug เดียวกันพบในหลายไฟล์ (withdraw, leave, fund, ...)

Bug pattern

javascript
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

124:124:vtrc-api/api/src/lib/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 อาจจะถูกต้อง:

156:157:vtrc-api/api/src/lib/repositories/withdraw/hospital.js
        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 เดียวกัน → ถูกต้อง

วิธีแก้

javascript
// ผิด:
Model.update(args, { where: {...} }, { transaction: t })

// ถูก:
Model.update(args, { where: {...}, transaction: t })

ขอบเขตของ bug

พบในไฟล์เหล่านี้ (non-exhaustive):

  • lib/repositories/withdraw/slip.js
  • lib/repositories/withdraw/hospital.js
  • lib/repositories/withdraw/historyAction.js
  • lib/repositories/withdraw/export.js
  • lib/repositories/withdraw/document.js
  • lib/repositories/withdraw/delegate.js
  • lib/repositories/withdraw/approver.js
  • lib/repositories/version/version.js
  • lib/repositories/user/userDivision.js
  • lib/repositories/user/user.js

transaction ใน vtrc-api "ใช้ไม่ได้จริง" ในกรณีส่วนใหญ่ — rollback ไม่ทำงาน, commit ไม่รอกัน


N+1 query patterns

Resolver หลายตัวมี pattern ที่ trigger N+1:

javascript
// ตัวอย่าง (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 ถัดไป

วิธีที่ควรจะเป็น:

javascript
const hospitals = await Models.WDHospital.findAll({
    where: {...},
    include: [{ model: Models.Approver, as: 'approver' }]
})

หรือใช้ DataLoader (แต่ vtrc-api ไม่ได้ใช้)


Pagination — limit cap ที่ไม่ทำงาน

ใน lib/clouse.js (filter builder):

21:36:vtrc-api/api/src/lib/clouse.js
  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

FieldTypeเหมาะไหม
WDHospital.totalWithdrawDECIMAL(12,2)ใช่ — เงิน
WDSlip.totalPriceDECIMAL(12,2)ใช่ — เงิน
LeaveDocument.leaveDayFLOATไม่ดีนัก — ควรเป็น DECIMAL(4,1) หรือ INTEGER ถ้าเป็น half-day
LeaveAttachFile.fileSizeFLOATไม่ดี — ควรเป็น BIGINT (bytes เป็นจำนวนเต็ม)

FLOAT มี precision error → leaveDay === 0.5 อาจจะไม่ตรงเพราะ 0.5 ใน IEEE 754 อาจจะเป็น 0.49999... หรือ 0.50001...


Pool sizing — สูงเกินไป

21:26:vtrc-api/api/src/connector.js
  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 ข้าม vtrcvtrc-centralize ต้อง:

  1. query ฝั่งเดียวก่อน
  2. extract ID list
  3. query อีกฝั่งด้วย WHERE id IN (...)
  4. merge ใน memory

ทำให้ latency สะสม + memory consumption สูง


เคล็ดลับตอนทำงานจริง

ถ้า query ช้า

  1. เช็ก EXPLAIN ใน DB — ดูว่าใช้ index ไหม
  2. เช็กว่ามี index declaration ใน model ไหม (grep "indexes:" models/...)
  3. ถ้าไม่มี → เพิ่มใน 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 ในตารางเดิม

  1. เพิ่มใน model definition (indexes: [{ fields: [...] }])
  2. deploy → syncModelToTable() จะเช็กว่า index มีอยู่ไหม ถ้าไม่มีจะ CREATE INDEX
  3. verify ใน DB: SHOW INDEXES FROM TableName

ไป 8.11 Data integrity risks