Một cột thiếu index đã khiến cơ sở dữ liệu của chúng tôi "gục ngã" như thế nào

Chỉ một index bị thiếu đã biến một API 8ms thành một cơn ác mộng kéo dài 8 giây.

Chúng tôi đã thực hiện kiểm tra tải (load test) trước một đợt phát hành lớn. Môi trường phát triển cục bộ (local) xử lý 100 dòng dữ liệu rất ổn. Nhưng ở môi trường staging, chúng tôi mô phỏng 500 người dùng trên 1,5 triệu dòng.

Mọi thứ đều sụp đổ.

  • Thời gian phản hồi API tăng vọt từ 45 ms lên 8.000 ms.
  • CPU của cơ sở dữ liệu chạm mức 100%.
  • Các connection pool bị cạn kiệt.
  • Các yêu cầu bị quá hạn thời gian (timeout).

Thủ phạm là một câu truy vấn đơn giản dùng để lấy lịch sử đơn hàng theo user_idstatus.

Khi chạy EXPLAIN ANALYZE trên PostgreSQL, kết quả cho thấy một đợt quét tuần tự (Sequential Scan). Do không có index trên user_id, công cụ cơ sở dữ liệu phải đọc toàn bộ 1,5 triệu dòng cho mỗi yêu cầu.

Với 100 yêu cầu đồng thời, cơ sở dữ liệu đã phải quét 150 triệu dòng cùng một lúc.

Việc khắc phục chỉ mất năm phút. Chúng tôi đã tạo một composite index trên (user_id, status, created_at DESC) bằng lệnh CREATE INDEX CONCURRENTLY để bảng vẫn duy trì trạng thái hoạt động.

Kết quả:

  • Thời gian truy vấn giảm từ 8.150 ms xuống còn 0,14 ms.
  • Độ trễ API giảm từ 8 giây xuống còn 12 ms.
  • Mức sử dụng CPU giảm từ 100% xuống dưới 8%.

Bài học rút ra:

  • Kiểm thử ở local dễ gây nhầm lẫn; 100 dòng không thể đại diện cho một triệu dòng.
  • Hãy đánh index cho các khóa ngoại (foreign keys) — hầu hết các ORM đều bỏ qua bước này.
  • Hãy chạy EXPLAIN ANALYZE; cơ sở dữ liệu sẽ chỉ ra vấn đề cho bạn.
  • Tối ưu hóa các câu truy vấn trước khi quyết định mua thêm phần cứng.

Đừng nâng cấp máy chủ trước. Hãy tối ưu hóa truy vấn.

Nguồn: https://dev.to/mia_keller_ffd2584c046ecb/how-an-unindexed-column-silently-killed-our-database-under-load-and-the-5-minute-fix-m32