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ểu TEXT, 10KB/row).

    • image (kiểu BLOB, 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ột password_hash, api_key.

    • Ứng dụng chỉ cần nameemail, nhưng SELECT * 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ến phone 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ểu TEXT, 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 :

  1. Truy vấn gốc:

     SELECT * FROM customers  
     WHERE registration_date > '2023-01-01';
    

    (Ứng dụng chỉ cần customer_id, name, email).

  2. 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.