DHDataHọc
Lý thuyếtBài 21/235 phút đọc+10 XP

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= trong EXPLAIN đo chi phí từng dòng.
  • SELECT * chặn Index Only Scan và 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:

code
EXPLAIN SELECT * FROM orders WHERE customer_id = 2;
code
Seq Scan on orders  (cost=0.00..1.20 rows=1 width=44)
code
EXPLAIN SELECT id FROM orders WHERE customer_id = 2;
code
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:

code
SELECT id, order_date FROM orders ORDER BY id LIMIT 3;
code
 id | order_date
----+------------
  1 | 2025-01-05
  2 | 2025-01-18
  3 | 2025-02-02

Cù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):

code
-- 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 psql hoặ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ết SELECT 1 vẫ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:

code
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:

code
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:

  1. Chọn cột ở tầng query, không lọc ở tầng ứng dụng sau khi đã lấy hết.
  2. Đo width trong EXPLAIN trước và sau khi thu hẹp cột. Nếu width giả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

  1. Chạy hai câu EXPLAIN ở trên và ghi lại width của mỗi câu.
  2. 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ể.
  3. Tạo index INCLUDE như trên và kiểm tra kế hoạch có Index Only Scan không (bảng nhỏ có thể vẫn Seq Scan — đọc width và ghi chú).
  4. 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.
  5. Ướ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ứngNguyên nhânCách sửa
API trả về cột mới ngoài ý muốnSELECT *Liệt kê cột tường minh
Index Only Scan không xuất hiệnTruy vấn cần cột ngoài indexChọn ít cột hoặc thêm INCLUDE
Response lớn bất thườngKéo theo cột text/jsonbChỉ chọn cột cần
Client vỡ sau khi đổi tên cộtPhụ thuộc thứ tự và tên cột từ *Hợp đồng cột rõ ràng
Chậm dù đã có indexSELECT * buộc đọc heapThu hẹp cột
Băng thông tăng theo thời gianBảng thêm cột, * tự mở rộngChọ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ặn Index Only Scan.
  • width trong EXPLAIN là 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 INCLUDE cho 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