Khi một câu lệnh SQL SELECT được gửi đến máy chủ MySQL, nó sẽ trải qua một loạt các giai đoạn xử lý phức tạp trước khi trả về kết quả cho người dùng. Việc hiểu rõ quy trình này giúp tối ưu hóa hiệu suất truy vấn và khắc phục sự cố.
1. Tiếp nhận truy vấn và theo dõi trạng thái
Khi một máy khách gửi một truy vấn SELECT, MySQL sẽ giao nhiệm vụ xử lý truy vấn đó cho một luồng (thread) riêng biệt trên máy chủ. Trạng thái của các luồng này có thể được giám sát bằng lệnh:
SHOW [FULL] PROCESSLIST;
Kết quả từ lệnh này bao gồm cột Command, cho biết trạng thái hiện tại của luồng. Ví dụ, trạng thái Sleep cho thấy luồng đang chờ yêu cầu mới từ máy khách, trong khi Query có nghĩa là luồng đang thực thi truy vấn hoặc gửi kết quả về máy khách.
2. Bộ nhớ đệm truy vấn (Query Cache)
Sau khi tiếp nhận, truy vấn sẽ được kiểm tra xem có thể sử dụng bộ nhớ đệm truy vấn (Query Cache) hay không. Đây là một cơ chế lưu trữ các câu lệnh SQL đã được thực thi và kết quả tương ứng dưới dạng cặp khóa-giá trị (key-value) trong bộ nhớ.
- Khóa (Key): Là một giá trị băm được tạo từ chính câu lệnh SQL, tên cơ sở dữ liệu hiện tại, phiên bản giao thức máy khách, và các yếu tố khác.
- Giá trị (Value): Chính là tập hợp kết quả của truy vấn đó.
Nếu truy vấn hiện tại trùng khớp hoàn toàn với một khóa trong Query Cache và không chứa từ khóa SQL_NO_CACHE, MySQL sẽ trả về kết quả đã được lưu trong bộ đệm mà không cần thực thi lại. Để bỏ qua Query Cache, bạn có thể sử dụng cú pháp sau:
SELECT SQL_NO_CACHE * FROM ten_bang;
Hoặc cấu hình tham số query_cache_type thành DEMAND.
Tuy nhiên, Query Cache có những hạn chế đáng kể:
- Bất kỳ thao tác cập nhật (
UPDATE,INSERT,DELETE) nào trên một bảng cũng sẽ làm mất hiệu lực toàn bộ các kết quả truy vấn liên quan đến bảng đó trong bộ đệm. - Sự khác biệt nhỏ nhất trong câu lệnh SQL, như khoảng trắng hay chú thích, cũng khiến truy vấn không khớp với bộ đệm.
Chính vì những hạn chế này, Query Cache đã bị loại bỏ hoàn toàn từ MySQL 8.0, do giá trị sử dụng thực tế của nó rất hạn chế, chủ yếu phù hợp với các bảng cấu hình ít thay đổi.
3. Bộ phân tích cú pháp (Parser) và Tiền xử lý (Preprocessor)
Nếu truy vấn không thể sử dụng Query Cache, nó sẽ chuyển sang giai đoạn phân tích:
3.1. Phân tích từ vựng (Lexical Analysis)
Giai đoạn này sẽ chia câu lệnh SQL thành các "token" (mã thông báo) dựa trên các quy tắc từ vựng. Ví dụ, câu lệnh SELECT username FROM userinfo; sẽ được phân tách thành các token như SELECT, username, FROM, userinfo.
3.2. Phân tích cú pháp (Syntax Analysis)
Tiếp theo, MySQL sẽ kiểm tra xem chuỗi token này có tuân thủ các quy tắc ngữ pháp của SQL hay không. Nếu hợp lệ, nó sẽ tạo ra một cây cú pháp trừu tượng (Abstract Syntax Tree - AST) để biểu diễn cấu trúc của truy vấn. Nếu có lỗi cú pháp, người dùng sẽ nhận được thông báo lỗi như You have an error in your SQL syntax.
3.3. Tiền xử lý (Preprocessor)
Sau khi cây cú pháp được tạo, giai đoạn tiền xử lý sẽ thực hiện các kiểm tra ngữ nghĩa và xác thực quan trọng:
- Kiểm tra tính hợp lệ: Xác minh xem các tên bảng và cột được sử dụng trong truy vấn có tồn tại trong cơ sở dữ liệu hay không. Nếu không, lỗi như
Unknown column 'xxx' in 'where clause'sẽ được trả về. - Kiểm tra quyền truy cập (Privilege Check): Đây là bước quan trọng để đảm bảo người dùng thực thi truy vấn có đủ quyền hạn trên các bảng và hoạt động được yêu cầu. Nếu không, truy vấn sẽ bị từ chối với thông báo lỗi tương tự
ERROR 1142 (42000): SELECT command denied to user 'root'@'localhost' for table 'xxx'. Việc thực hiện kiểm tra quyền ở giai đoạn này giúp MySQL tránh lãng phí tài nguyên xử lý nếu người dùng không có quyền truy cập ngay từ đầu.
4. Bộ tối ưu hóa (Optimizer)
Cây cú pháp đã được xác thực sẽ được chuyển đến Bộ tối ưu hóa. Nhiệm vụ của nó là đánh giá các cách khác nhau để thực thi truy vấn và chọn ra kế hoạch hiệu quả nhất.
Ví dụ, với một truy vấn JOIN:
SELECT t1.*
FROM Table1 t1
INNER JOIN Table2 t2 ON t1.CommonID = t2.CommonID;
Bộ tối ưu hóa sẽ quyết định nên truy vấn Table1 trước rồi kết nối với Table2, hay ngược lại. Các yếu tố như chỉ mục, thống kê dữ liệu, và kích thước bảng sẽ được xem xét để đưa ra một *kế hoạch thực thi* tối ưu.
Trong một số trường hợp, người dùng có thể can thiệp vào quyết định của bộ tối ưu hóa bằng các gợi ý (hint). Ví dụ, từ khóa STRAIGHT_JOIN có thể được sử dụng để ép buộc thứ tự kết nối bảng:
SELECT t1.*
FROM Table1 t1
STRAIGHT_JOIN Table2 t2 ON t1.CommonID = t2.CommonID;
Điều này báo cho MySQL biết rằng Table1 nên được xử lý trước rồi mới liên kết với Table2. Kết quả của giai đoạn này là một kế hoạch thực thi chi tiết, sẵn sàng cho bước tiếp theo.
5. Bộ thực thi (Executor)
Bộ thực thi chịu trách nhiệm thực hiện kế hoạch đã được tối ưu hóa. Nó sẽ gọi các API của công cụ lưu trữ (Storage Engine) dưới dạng từng bước một. MySQL sử dụng một kiến trúc công cụ lưu trữ dạng plugin, với một lớp giao diện trừu tượng gọi là handler (được định nghĩa trong sql/handler.h).
Các công cụ lưu trữ như InnoDB hoặc MyISAM phải triển khai các hàm giao diện này (ví dụ: ha_open để mở bảng, ha_read_row để đọc hàng, ha_index_end để kết thúc quét chỉ mục) để MySQL có thể tương tác và lấy dữ liệu theo yêu cầu của kế hoạch thực thi.
6. Trả về kết quả
Cuối cùng, sau khi Bộ thực thi hoàn thành công việc, MySQL sẽ thu thập các hàng dữ liệu và gửi kết quả truy vấn trở lại máy khách đã gửi yêu cầu ban đầu.