Giới thiệu

Trong các dự án PHP thực tế, việc truy vấn cơ sở dữ liệu thường là điểm nghẽn hiệu năng. Khi dữ liệu tăng lên, những truy vấn chưa được tối ưu có thể kéo dài thời gian phản hồi, gây ảnh hưởng tới trải nghiệm người dùng. Bài viết này sẽ chia sẻ các kỹ thuật tối ưu truy vấn PDO từ mức độ câu lệnh SQL, cấu trúc chỉ mục cho tới cấu hình kết nối, giúp bạn giảm đáng kể thời gian thực thi và tài nguyên tiêu thụ.

Tối ưu cấu trúc truy vấn

Sử dụng Prepared Statements

Prepared Statements không chỉ bảo vệ chống SQL Injection mà còn cho phép MySQL tái sử dụng kế hoạch thực thi (execution plan). Khi cùng một câu lệnh được gọi nhiều lần với các tham số khác nhau, MySQL sẽ không cần phân tích lại toàn bộ câu lệnh.

<?php
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false,
]);

$stmt = $pdo->prepare('SELECT id, name FROM users WHERE status = :status LIMIT :limit');
$stmt->bindValue(':status', $status, PDO::PARAM_STR);
$stmt->bindValue(':limit', (int)$limit, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>

Tránh SELECT *

Việc lấy toàn bộ cột bằng SELECT * khiến MySQL phải đọc và truyền dữ liệu không cần thiết, đặc biệt khi bảng có nhiều cột lớn. Hãy liệt kê rõ ràng các cột cần thiết, ví dụ SELECT id, name, email. Điều này giảm lượng dữ liệu truyền qua mạng và giảm thời gian I/O.

Sử dụng LIMIT và OFFSET hợp lý

Đối với danh sách dài, luôn kết hợp LIMIT với OFFSET hoặc WHERE dựa trên khóa tăng dần (cursor pagination). Tránh việc tải toàn bộ bảng vào bộ nhớ PHP.

Index và EXPLAIN

Kiểm tra kế hoạch thực thi

MySQL cung cấp lệnh EXPLAIN để hiển thị cách tối ưu hoá truy vấn. Khi một câu lệnh chưa sử dụng index, type sẽ hiển thị ALL (full table scan). Hãy chạy EXPLAIN và xem cột key để xác định index đang được dùng.

<?php
$sql = 'SELECT * FROM orders WHERE user_id = :uid AND created_at > :date';
$stmt = $pdo->prepare('EXPLAIN ' . $sql);
$stmt->execute([':uid' => $uid, ':date' => $date]);
$plan = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($plan);
?>

Tạo index phù hợp

Đối với truy vấn có điều kiện WHERE user_id = ? AND created_at > ?, một index kết hợp (user_id, created_at) sẽ tối ưu hơn so với index riêng lẻ. Sử dụng lệnh:

<?php
$pdo->exec('CREATE INDEX idx_user_date ON orders (user_id, created_at)');
?>

Nhớ kiểm tra lại EXPLAIN sau khi tạo index để xác nhận MySQL đã chuyển sang ref hoặc range.

Kết nối và cấu hình PDO

Vô hiệu hoá emulate prepares

Thiết lập PDO::ATTR_EMULATE_PREPARES => false để MySQL thực hiện chuẩn bị câu lệnh thực tế, giúp tận dụng tối đa khả năng cache kế hoạch.

Giảm thời gian kết nối

Đối với các ứng dụng có lưu lượng cao, hãy sử dụng connection pooling (ví dụ persistence trong PDO) hoặc giữ kết nối mở trong vòng đời request. Đối với môi trường FPM, cấu hình pm.max_children hợp lý cũng giảm thời gian tạo kết nối mới.

Kết luận

Áp dụng các biện pháp trên – từ viết câu lệnh SQL ngắn gọn, sử dụng index hợp lý, tới cấu hình PDO tối ưu – sẽ giúp giảm thời gian thực thi truy vấn từ vài trăm mili giây xuống còn vài chục mili giây, nâng cao khả năng chịu tải của ứng dụng PHP.

Để nắm vững toàn bộ quy trình và mở rộng kiến thức lên các framework PHP hiện đại, Tham khảo khóa học "Lập trình PHP nâng cao - chuyên sâu để đi làm" tại đây.