Bài 6: Tránh SELECT * – Tác hại không ngờ và cách viết query "sạch"
Dưới đây là nội dung chi tiết cho Bài 6: Tránh SELECT * – Phân tích sâu tác hại và cách viết query "sạch", tập trung vào nguyên nhân gốc rễ và giải pháp triệt để:
Hiểu sâu ảnh hưởng của SELECT * đến hiệu năng, bảo mật và khả năng mở rộng
1. Vấn đề: SELECT * có thực sự "xấu"?
1.1. Ví dụ điển hình
-- Truy vấn "tiện lợi"
SELECT * FROM users;
-- Truy vấn tối ưu
SELECT id, name, email FROM users;
➜ Câu hỏi: Tại sao chỉ chọn một số cột lại tốt hơn?
2. Phân tích sâu tác hại của SELECT *
2.1. Tốn tài nguyên mạng và bộ nhớ
Cơ chế:
Database trả về tất cả cột, kể cả cột
TEXT
,BLOB
, hoặc cột không dùng đến.Ứng dụng phải xử lý lượng dữ liệu lớn hơn cần thiết.
Case study:
Bảng
products
có 50 cột, trong đó:description
(kiểuTEXT
, 10KB/row).image
(kiểuBLOB
, 1MB/row).
Truy vấn
SELECT *
:Mỗi row trả về ~1.01MB.
1000 rows → ~1.01GB dữ liệu.
Truy vấn
SELECT id, name, price
:Mỗi row trả về ~50 bytes.
1000 rows → ~50KB dữ liệu.
→ Chênh lệch: 20,000 lần về dung lượng!
2.2. Phá vỡ khả năng sử dụng Index (Index Scan vs Full Scan)
Covering Index: Khi tất cả cột cần thiết đều nằm trong index, database chỉ cần đọc index (Index Only Scan).
SELECT * phá hỏng cơ chế này:
- Database buộc phải đọc cả bảng (Heap) để lấy các cột không có trong index.
Ví dụ:
-- Tạo index trên cột 'email'
CREATE INDEX idx_users_email ON users(email);
-- Truy vấn 1 (Tốt)
SELECT email FROM users WHERE email LIKE '%@gmail.com'; -- Sử dụng Index Only Scan
-- Truy vấn 2 (Xấu)
SELECT * FROM users WHERE email LIKE '%@gmail.com'; -- Sử dụng Index Scan + Heap Fetch
➜ Execution Plan:
Truy vấn 1:
Operation: Index Only Scan (nhanh).
Cost: Thấp.
Truy vấn 2:
Operation: Index Scan → Fetch từ Heap (chậm).
Cost: Cao hơn 3-10 lần tùy kích thước bảng.
2.3. Rủi ro bảo mật
Lộ cột nhạy cảm:
Ví dụ: Bảng
users
có cộtpassword_hash
,api_key
.Ứng dụng chỉ cần
name
vàemail
, nhưngSELECT *
trả về tất cả.
Vi phạm GDPR/CCPA:
- Trả về dữ liệu cá nhân không cần thiết → Rủi ro pháp lý.
2.4. Phụ thuộc schema
Khi schema thay đổi:
Thêm/xóa cột → Ứng dụng dùng
SELECT *
có thể bị lỗi hoặc xử lý sai.Ví dụ: Ứng dụng đọc cột
phone
ở vị trí thứ 5, nhưng schema thay đổi khiếnphone
thành cột thứ 6 → Lỗi runtime.
3. Giải pháp triệt để
3.1. Chỉ chọn cột cần thiết
-- Thay vì
SELECT * FROM orders;
-- Hãy viết
SELECT order_id, customer_id, total_amount, status FROM orders;
3.2. Sử dụng View hoặc Materialized View
View: Định nghĩa sẵn tập cột cần thiết.
CREATE VIEW user_public_info AS SELECT id, name, email FROM users; SELECT * FROM user_public_info; -- "SELECT *" ở đây an toàn
3.3. Tối ưu ORM
Cấu hình ORM (Hibernate, Sequelize): Chỉ fetch các cột cần thiết, không fetch toàn bộ entity.
# Django ORM User.objects.only('id', 'name', 'email')
4. Case Study: Tối ưu hóa ứng dụng thực tế
4.1. Bối cảnh
Ứng dụng đọc danh sách bài viết từ bảng
posts
(10 triệu rows).Truy vấn ban đầu:
SELECT * FROM posts WHERE category = 'tech' ORDER BY created_at DESC LIMIT 100;
Cột
content
(kiểuTEXT
, trung bình 10KB/row).Thời gian trả về: 2.5 giây.
4.2. Tối ưu
Bước 1: Chỉ chọn cột cần thiết:
SELECT post_id, title, author, created_at FROM posts WHERE category = 'tech' ORDER BY created_at DESC LIMIT 100;
- Thời gian trả về: 0.3 giây.
Bước 2: Thêm covering index:
CREATE INDEX idx_posts_category_created_at ON posts(category, created_at DESC) INCLUDE (title, author);
- Thời gian trả về: 0.05 giây.
→ Tổng cải thiện: 50 lần!
*5. Ngoại lệ: Khi nào dùng SELECT ?
Audit toàn bộ dữ liệu:
-- Export toàn bộ bảng SELECT * FROM products_history;
Dynamic query (không biết trước cột):
Tool quản lý database (pgAdmin, MySQL Workbench).
Reporting tool tự động.
Khi đã có covering index:
-- Index chứa tất cả cột cần thiết CREATE INDEX idx_covering ON table (col1) INCLUDE (col2, col3); SELECT * FROM table WHERE col1 = 'value'; -- Sử dụng Index Only Scan
6. Bài tập thực hành
*Viết lại các truy vấn sau để loại bỏ SELECT :
Truy vấn gốc:
SELECT * FROM customers WHERE registration_date > '2023-01-01';
(Ứng dụng chỉ cần
customer_id
,name
,email
).Truy vấn gốc:
SELECT * FROM transactions WHERE status = 'PENDING';
(Ứng dụng chỉ cần
transaction_id
,amount
,created_at
).
Gợi ý đáp án:
1.
SELECT customer_id, name, email
FROM customers
WHERE registration_date > '2023-01-01';
SELECT transaction_id, amount, created_at
FROM transactions
WHERE status = 'PENDING';
7. Tổng kết
*SELECT :
Ưu điểm: Tiện lợi, phù hợp cho ad-hoc query.
Nhược điểm: Tốn tài nguyên, rủi ro bảo mật, khó bảo trì.
Best Practice:
Luôn liệt kê cột cần thiết.
Sử dụng View/ORM để quản lý tập cột.
Tạo covering index nếu cần hiệu năng cao.
Thay đổi thói quen nhỏ → Cải thiện lớn về hiệu năng và độ ổn định!
Preview bài tiếp theo:
Bài 7: Xử lý NULL đúng cách – Tránh "sập bẫy" logic và hiệu năng.