Tránh SELECT *
Bạn sẽ học được
- Vì sao
SELECT *tốn I/O, băng thông và bộ nhớ hơn mức bạn tưởng. - Cách
width=trongEXPLAINđo chi phí từng dòng. SELECT *chặnIndex Only Scanvà làm index tổ hợp mất tác dụng.- Rủi ro khi cấu trúc bảng thay đổi mà client vẫn phụ thuộc
*. - Ba tình huống
SELECT *vẫn hợp lý.
Khái niệm cốt lõi
SELECT * lấy mọi cột hiện có. Câu lệnh ngắn, tiện khi khám phá dữ liệu, nhưng trong mã production nó là một trong những nguyên nhân âm thầm nhất của truy vấn chậm.
So sánh trên orders:
EXPLAIN SELECT * FROM orders WHERE customer_id = 2;Seq Scan on orders (cost=0.00..1.20 rows=1 width=44)EXPLAIN SELECT id FROM orders WHERE customer_id = 2;Seq Scan on orders (cost=0.00..1.20 rows=1 width=4)Chỉ số width khác nhau: 44 byte so với 4 byte. Đó là chi phí trung bình mỗi dòng mà PostgreSQL phải dựng và truyền đi. Với 3 dòng, khác biệt không đáng kể. Với 10 triệu dòng nhân 44 byte, bạn đang chuyển hơn 400 MB thay vì 40 MB.
Bảng orders có bốn cột: id, customer_id, order_date, status. Nếu ứng dụng chỉ cần id và order_date:
SELECT id, order_date FROM orders ORDER BY id LIMIT 3; id | order_date
----+------------
1 | 2025-01-05
2 | 2025-01-18
3 | 2025-02-02Cùng kết quả cần dùng, ít dữ liệu hơn một nửa.
Ba chi phí thật của dấu hoa thị
Một, băng thông và bộ nhớ. Cột thừa đi qua đĩa, bộ đệm, mạng, driver, ORM, rồi bị bỏ đi ở tầng ứng dụng. Mỗi bước đều có giá. Với API JSON, SELECT * còn kéo theo các cột text dài hoặc jsonb lớn mà client không dùng.
Hai, chặn Index Only Scan. Index chỉ chứa các cột được đánh. Nếu truy vấn chỉ cần những cột đó, PostgreSQL đọc index là đủ — không chạm bảng. SELECT * luôn đòi thêm cột ngoài index, buộc phải quay lại bảng (heap), mất toàn bộ lợi ích.
Ví dụ với index orders(customer_id):
-- Có thể dùng index thuần nếu index phủ đủ cột
SELECT customer_id FROM orders WHERE customer_id = 2;
-- Không bao giờ index-only vì cần mọi cột
SELECT * FROM orders WHERE customer_id = 2;Ba, phụ thuộc cấu trúc. SELECT * trả về tập cột theo thời điểm chạy. Khi ai đó thêm cột, response API đột nhiên có thêm trường; khi ai đó đổi tên cột, client vỡ. Tệ hơn, một cột nhạy cảm vừa thêm (như internal_note) tự động lọt ra API công khai mà không ai review. Chọn cột tường minh biến việc thêm cột thành thay đổi không phá vỡ hợp đồng.
Khi nào SELECT * vẫn ổn
Không cần cực đoan. Ba trường hợp hợp lý:
- Khám phá dữ liệu trong
psqlhoặc notebook — không phải mã chạy production. - Bảng rất hẹp và ổn định, ví dụ bảng nối hai khóa mà ứng dụng thực sự cần cả hai cột.
- Câu
EXISTS— cột không quan trọng vì PostgreSQL không đọc giá trị, chỉ kiểm tra có dòng. Tuy vậy viếtSELECT 1vẫn rõ ý hơn.
Ngoài ba trường hợp đó, liệt kê cột.
Chọn cột và index phủ
Khi đã chọn cột tường minh, có thể thiết kế index phủ đúng nhu cầu:
CREATE INDEX idx_orders_customer_covering
ON orders (customer_id) INCLUDE (order_date, status);INCLUDE thêm cột vào phần lá của index mà không dùng để sắp xếp. Truy vấn:
SELECT order_date, status FROM orders WHERE customer_id = 2;giờ có thể chạy bằng Index Only Scan: index chứa cả điều kiện lẫn cột trả về. Không thể làm được điều này với SELECT * vì index không phủ hết bốn cột.
Đánh đổi: index phủ to hơn, tốn dung lượng và làm ghi chậm hơn. Chỉ dùng khi truy vấn đủ nóng và số cột trả về đủ ít.
Trong thực tế
Với ORM, SELECT * thường ẩn sau lời gọi "lấy entity". Hai thói quen giúp kiểm soát:
- Chọn cột ở tầng query, không lọc ở tầng ứng dụng sau khi đã lấy hết.
- Đo
widthtrongEXPLAINtrước và sau khi thu hẹp cột. Nếuwidthgiảm mạnh, băng thông cũng giảm tương ứng.
Một phép thử đơn giản: với mỗi endpoint, liệt kê cột mà client thực sự dùng. Nếu danh sách ngắn hơn nhiều so với số cột bảng, SELECT * đang lãng phí.
Thực hành
- Chạy hai câu
EXPLAINở trên và ghi lạiwidthcủa mỗi câu. - Viết lại một câu
SELECT *thành danh sách cột chỉ gồm những gì cần cho một màn hình cụ thể. - Tạo index
INCLUDEnhư trên và kiểm tra kế hoạch cóIndex Only Scankhông (bảng nhỏ có thể vẫnSeq Scan— đọcwidthvà ghi chú). - Liệt kê ba rủi ro khi một API công khai dùng
SELECT *trên bảng người dùng. - Ước lượng: bảng 1 triệu dòng, mỗi dòng 44 byte so với 12 byte — tiết kiệm bao nhiêu MB mỗi lần gọi?
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| API trả về cột mới ngoài ý muốn | SELECT * | Liệt kê cột tường minh |
Index Only Scan không xuất hiện | Truy vấn cần cột ngoài index | Chọn ít cột hoặc thêm INCLUDE |
| Response lớn bất thường | Kéo theo cột text/jsonb | Chỉ chọn cột cần |
| Client vỡ sau khi đổi tên cột | Phụ thuộc thứ tự và tên cột từ * | Hợp đồng cột rõ ràng |
| Chậm dù đã có index | SELECT * buộc đọc heap | Thu hẹp cột |
| Băng thông tăng theo thời gian | Bảng thêm cột, * tự mở rộng | Chọn cột cố định |
Tóm tắt
SELECT *lấy mọi cột: tốn I/O, bộ nhớ, băng thông và chặnIndex Only Scan.widthtrongEXPLAINlà thước đo trực tiếp chi phí mỗi dòng.SELECT *làm API phụ thuộc cấu trúc bảng và có thể rò rỉ cột mới.- Chọn cột tường minh, cân nhắc index
INCLUDEcho truy vấn nóng. - Chỉ dùng
SELECT *khi khám phá dữ liệu hoặc bảng thật sự hẹp và ổn định.
Dùng ← → để chuyển bài