Quy tắc: cột so sánh bằng (=) đứng trước, cột khoảng (range) đứng sau cùng.
Lý do nằm ở cách B-tree lưu trữ — index sắp xếp theo bộ khoá, nên chỉ cột đầu tiên gặp điều kiện range còn thu hẹp được vùng đọc; mọi cột sau nó chỉ dùng làm filter trên các entry đã đọc, không giúp giảm phạm vi quét.
-- wrong: created_at is a range, everything after it degrades to a filter
CREATE INDEX bad ON events (tenant_id, created_at, status);
-- right: equality columns first, range last
CREATE INDEX good ON events (tenant_id, status, created_at);Với good, database nhảy thẳng tới đoạn (tenant_id=?, status=?) rồi đọc tiếp theo created_at — mọi entry đọc ra đều là kết quả. Với bad, nó phải đọc toàn bộ khoảng thời gian của tenant rồi mới lọc status.
Xếp thứ tự trong nhóm equality: cột chọn lọc hơn nên đứng trước nếu có truy vấn chỉ dùng phần prefix — vì (a, b) phục vụ được WHERE a = ? nhưng không phục vụ WHERE b = ?. Nếu mọi truy vấn đều dùng đủ cả hai thì thứ tự trong nhóm equality gần như không ảnh hưởng tốc độ.
Kết hợp với ORDER BY: nếu truy vấn còn ORDER BY created_at DESC LIMIT 20, index good phục vụ luôn cả sắp xếp — cột range nằm cuối cũng chính là cột sắp xếp. Đó là lý do dạng (equality..., sort/range) là layout composite index hay gặp nhất trong hệ multi-tenant.
Cách kiểm chứng: xem EXPLAIN — điều kiện nằm ở Index Cond là đang thu hẹp phạm vi; nằm ở Filter là đang bị loại sau khi đọc.