All posts
Select thông minh: Tại sao select(*) là một thói quen xấu trong các dự án lớn?
laravelmysql

Select thông minh: Tại sao select(*) là một thói quen xấu trong các dự án lớn?

Hao Nguyen K.'s avatarHao Nguyen K.
Table of Contents10 sections

Khi mới làm quen với Laravel, hầu hết chúng ta đều yêu thích sự nhanh gọn của Eloquent:

PHP

$users = User::all(); 
// Tương đương với: SELECT * FROM users

Chỉ với một dòng code ngắn, bạn đã lấy ra toàn bộ dữ liệu. Ở giai đoạn đầu của dự án (môi trường Local hoặc Staging với vài trăm bản ghi), mọi thứ chạy mượt mà như một giấc mơ.

Thế nhưng, khi dự án lớn dần lên, lượng truy cập tăng vọt và bảng dữ liệu chạm ngưỡng hàng triệu dòng, thói quen lạm dụng select(*) – hay lấy toàn bộ các cột trong Database – sẽ âm thầm trở thành "sát thủ" bòn rút tài nguyên hệ thống.

Bài viết này sẽ bóc trần lý do tại sao select(*) lại là một "anti-pattern" (thói quen xấu) và hướng dẫn bạn cách áp dụng tư duy Select thông minh để giải cứu hiệu năng hệ thống.

1. 3 tác hại khôn lường của select(*) trong hệ thống Enterprise

Tác hại 1: Nghẽn băng thông mạng giữa Web Server và Database Server

Nhiều nhà phát triển nghĩ rằng câu lệnh SQL chạy trên máy chủ nên không tốn băng thông Internet của người dùng. Đúng, nhưng nó lại ngốn băng thông nội bộ (Internal Bandwidth) giữa Web Server (chạy PHP) và Database Server (chạy MySQL/Postgres).

Nếu bảng products của bạn có 30 cột (bao gồm các cột nặng như description dạng TEXT, metadata dạng JSON) và bạn cần lấy ra 1.000 sản phẩm để làm danh sách hiển thị, việc dùng select(*) sẽ ép Database phải đóng gói hàng chục Megabyte dữ liệu qua đường truyền mạng nội bộ. Khi có 10.000 người truy cập cùng lúc, băng thông mạng sẽ bị thắt nút cổ chai (Network Bottleneck).

Tác hại 2: Tràn bộ nhớ RAM (Out of Memory) trên Web Server

Sau khi nhận cục dữ liệu khổng lồ từ Database, Laravel phải làm một nhiệm vụ gọi là Hydration — biến các dòng dữ liệu thô (raw array) thành các Eloquent Model Instance (đối tượng PHP).

Một Model chứa càng nhiều thuộc tính thì dung lượng RAM nó chiếm dụng càng lớn. Việc gánh hàng ngàn Model "béo phì" (chứa đầy đủ các cột TEXT/JSON không cần thiết) sẽ nhanh chóng vắt kiệt bộ nhớ RAM của PHP-FPM, dẫn đến lỗi kinh điển Fatal Error: Allowed Memory Size Exhausted.

Tác hại 3: Phá vỡ hiệu năng của Database Index

Database Engine rất thông minh. Nếu bạn tạo một Index cho cột statuscreated_at, và bạn thực hiện câu lệnh:

SQL

SELECT status, created_at FROM orders WHERE status = 'completed';

Database sẽ lấy ngay dữ liệu từ Index mà không cần lật bảng dữ liệu gốc (Covering Index).

Nhưng nếu bạn dùng SELECT *, Database bắt buộc phải tốn thêm một bước lật lại bảng gốc (gọi là Bookmark Lookup) để lấy ra các cột còn lại. Hành động này làm tăng số lần đọc đĩa cứng (Disk I/O) và kéo tụt tốc độ truy vấn.

2. Giải pháp: Tư duy "Chỉ lấy đủ, không lấy thừa" trong Laravel

Laravel cung cấp rất nhiều cơ chế mượt mà giúp bạn chỉ định chính xác những gì mình cần.

Kỹ thuật 1: Khai báo cột cụ thể ngay trong Eloquent

Thay vì gọi all() hoặc get(), hãy luôn tập thói quen truyền mảng các cột cần thiết vào hàm get() hoặc sử dụng hàm select():

PHP

// Cách 1: Truyền trực tiếp vào get()
$users = User::where('status', 'active')->get(['id', 'name', 'email']);

// Cách 2: Sử dụng phương thức select() rõ ràng
$products = Product::select('id', 'title', 'price', 'thumbnail')
    ->where('is_available', true)
    ->get();

Kỹ thuật 2: Loại bỏ các cột nặng ký mặc định (Global/Local Scopes)

Nếu một bảng có cột content hoặc json_payload quá nặng mà 90% các màn hình không dùng tới, hãy tạo một Scope để loại bỏ nó mặc định, hoặc chỉ gọi khi cần:

PHP

// Trong Model Article
public function scopeWithoutContent($query)
{
    // Lấy danh sách tất cả các cột trừ cột 'content'
    return $query->select(['id', 'title', 'slug', 'author_id', 'created_at']);
}

// Khi dùng ở trang danh sách: Code cực nhẹ
$articles = Article::withoutContent()->latest()->paginate(10);

// Khi vào trang chi tiết: Mới lấy cột nặng
$article = Article::findOrFail($id); // Lấy tất cả cột

Kỹ thuật 3: Tối ưu hóa khi gọi Quan hệ (Eager Loading Columns)

Lỗi phổ biến nhất là tối ưu hóa câu lệnh cha nhưng lại quên mất câu lệnh con khi gọi Relationship. Hãy chỉ định cột cần lấy của bảng con theo cú pháp Mối_quan_hệ:col1,col2.

PHP

// Sai lầm: Lấy toàn bộ cột của bảng posts và bảng users
$posts = Post::with('user')->get();

// Tối ưu: Chỉ lấy id, title của bài viết VÀ id, name của user
// (Lưu ý: Luôn phải giữ lại khóa ngoại 'user_id' và khóa chính 'id' để Laravel kết nối dữ liệu)
$posts = Post::select('id', 'title', 'user_id')
    ->with('user:id,name')
    ->get();

3. Ngoại lệ: Khi nào select(*) không xấu?

Nói đi thì cũng phải nói lại, không phải lúc nào select(*) cũng là tội đồ. Bạn hoàn toàn có thể dùng nó trong các trường hợp:

  1. Trang chi tiết (Detail Page): Khi người dùng bấm vào xem chi tiết 1 bản ghi duy nhất (ví dụ: User::find($id)), việc lấy toàn bộ cột là hợp lý vì giao diện lúc này cần hiển thị toàn bộ thông tin.

  2. Bảng dữ liệu chỉ toàn các cột nhẹ: Nếu bảng của bạn chỉ có 3-4 cột và đều là kiểu dữ liệu nhỏ (integer, datetime, string ngắn), sự chênh lệch hiệu năng giữa select(*) và select cụ thể là không đáng kể.

Lời kết

Trong các dự án Enterprise lớn, tối ưu hóa hiệu năng không phải là làm một cái gì đó quá vĩ đại, mà là sự tích lũy từ những thói quen nhỏ nhất của từng lập trình viên. Thay đổi thói quen từ select(*) sang Select thông minh sẽ giúp hệ thống của bạn tiết kiệm hàng Gigabyte RAM, giảm tải băng thông và mang lại trải nghiệm mượt mà cho hàng triệu người dùng.