Các câu lệnh MySQL nâng cao
Trần Thị Vân
- 112
- 24/07/2026
Khám phá các câu lệnh MySQL nâng cao như JOIN, CTE, Window Function, Transaction, Stored Procedure, Trigger, View, Index, EXPLAIN, JSON và nhiều kỹ thuật tối ưu truy vấn giúp xây dựng hệ thống nhanh, ổn định và dễ mở rộng, chi tiết như nào hãy cùng chúng tôi phân tích:
MySQL không chỉ dừng lại ở những câu lệnh SELECT, INSERT, UPDATE hay DELETE quen thuộc. Khi dữ liệu ngày càng lớn, hệ thống phát triển phức tạp và yêu cầu tối ưu hiệu suất cao hơn, lập trình viên cần khai thác các tính năng nâng cao của MySQL để xử lý dữ liệu nhanh, chính xác và dễ bảo trì hơn.
Bài viết này tập trung vào các câu lệnh MySQL nâng cao được sử dụng phổ biến trong thực tế. Nội dung được trình bày theo hướng từ nền tảng đến chuyên sâu, giúp bạn hiểu bản chất từng tính năng thay vì chỉ ghi nhớ cú pháp.

Vì sao nên học các tính năng nâng cao của MySQL?
Khi làm việc với những cơ sở dữ liệu nhỏ, các câu lệnh cơ bản thường đủ để giải quyết yêu cầu. Tuy nhiên, trong các hệ thống quản lý doanh nghiệp, website thương mại điện tử, ERP, CRM hoặc các ứng dụng có hàng triệu bản ghi, việc chỉ sử dụng truy vấn đơn giản sẽ khiến tốc độ xử lý giảm đáng kể.
Các câu lệnh nâng cao giúp:
- Tăng hiệu suất truy vấn.
- Giảm số lượng câu lệnh phải thực hiện.
- Đảm bảo tính toàn vẹn dữ liệu.
- Tối ưu khả năng mở rộng của hệ thống.
- Dễ dàng xây dựng các báo cáo thống kê.
- Đơn giản hóa logic xử lý ở phía ứng dụng.
Truy vấn nhiều bảng bằng JOIN
JOIN là kỹ thuật quan trọng nhất trong MySQL nâng cao. Thay vì lưu toàn bộ dữ liệu trong một bảng lớn, cơ sở dữ liệu thường được chuẩn hóa thành nhiều bảng khác nhau và liên kết thông qua khóa.
INNER JOIN
INNER JOIN chỉ trả về các bản ghi tồn tại ở cả hai bảng.
SELECT users.name, orders.total
FROM users
INNER JOIN orders
ON users.id = orders.user_id;
Kết quả chỉ chứa các khách hàng đã có đơn hàng.
LEFT JOIN
LEFT JOIN luôn lấy toàn bộ dữ liệu từ bảng bên trái, kể cả khi bảng bên phải không có dữ liệu tương ứng.
SELECT users.name, orders.total
FROM users
LEFT JOIN orders
ON users.id = orders.user_id;
Đây là kiểu JOIN thường dùng để thống kê khách hàng chưa từng mua hàng.
RIGHT JOIN
Ngược lại với LEFT JOIN, RIGHT JOIN ưu tiên toàn bộ dữ liệu của bảng bên phải.
SELECT users.name, orders.total
FROM users
RIGHT JOIN orders
ON users.id = orders.user_id;
CROSS JOIN
CROSS JOIN tạo ra tích Descartes giữa hai bảng. Mỗi dòng của bảng thứ nhất sẽ ghép với mọi dòng của bảng thứ hai.
SELECT *
FROM products
CROSS JOIN colors;
Loại JOIN này chỉ nên sử dụng khi thực sự cần vì số lượng bản ghi có thể tăng rất lớn.
Truy vấn lồng bằng Subquery
Subquery là câu lệnh SELECT nằm bên trong một câu lệnh SQL khác.
Subquery trong WHERE
SELECT *
FROM products
WHERE category_id IN
(
SELECT id
FROM categories
WHERE active = 1
);
Truy vấn trên chỉ lấy sản phẩm thuộc các danh mục đang hoạt động.
Subquery trả về một giá trị
SELECT *
FROM employees
WHERE salary >
(
SELECT AVG(salary)
FROM employees
);
Kết quả là danh sách nhân viên có mức lương cao hơn mức trung bình.
Common Table Expression (CTE)
Từ MySQL 8 trở lên, CTE giúp viết truy vấn rõ ràng và dễ bảo trì hơn.
WITH HighSalary AS
(
SELECT *
FROM employees
WHERE salary > 2000
)
SELECT *
FROM HighSalary;
So với Subquery, CTE giúp chia nhỏ truy vấn phức tạp thành nhiều bước logic.
CTE đệ quy
Recursive CTE thường dùng để xử lý dữ liệu dạng cây như danh mục sản phẩm hoặc sơ đồ tổ chức.
WITH RECURSIVE numbers AS
(
SELECT 1 AS n
UNION ALL
SELECT n + 1
FROM numbers
WHERE n < 10
)
SELECT *
FROM numbers;
Ví dụ trên tạo ra dãy số từ 1 đến 10 mà không cần tạo bảng tạm.
Sử dụng GROUP BY nâng cao
GROUP BY giúp gom dữ liệu thành từng nhóm để tính toán.
SELECT category_id,
COUNT(*) AS total_products,
AVG(price) AS average_price,
MAX(price) AS highest_price,
MIN(price) AS lowest_price
FROM products
GROUP BY category_id;
Đây là nền tảng để xây dựng các báo cáo thống kê trong doanh nghiệp.
Kết hợp HAVING
HAVING dùng để lọc dữ liệu sau khi đã GROUP BY.
SELECT category_id,
COUNT(*) AS total
FROM products
GROUP BY category_id
HAVING COUNT(*) > 10;
Khác với WHERE, HAVING hoạt động trên kết quả đã được nhóm.
Window Function trong MySQL
Window Function là một trong những tính năng mạnh nhất của MySQL hiện đại. Thay vì gộp dữ liệu như GROUP BY, Window Function vẫn giữ nguyên từng dòng dữ liệu và bổ sung các giá trị tính toán.
ROW_NUMBER()
SELECT
ROW_NUMBER() OVER(ORDER BY salary DESC) AS ranking,
name,
salary
FROM employees;
Hàm này đánh số thứ tự theo điều kiện sắp xếp.
RANK()
SELECT
name,
salary,
RANK() OVER(ORDER BY salary DESC) AS ranking
FROM employees;
Nếu hai nhân viên có cùng lương thì cùng hạng và sẽ có khoảng trống ở thứ hạng tiếp theo.
DENSE_RANK()
DENSE_RANK hoạt động tương tự RANK nhưng không bỏ qua số thứ tự.
LAG()
SELECT
order_date,
revenue,
LAG(revenue) OVER(ORDER BY order_date) AS previous_day
FROM revenues;
Hàm này thường được sử dụng khi so sánh doanh thu giữa các ngày.
LEAD()
LEAD cho phép lấy giá trị của dòng kế tiếp mà không cần JOIN.
CASE WHEN trong truy vấn
CASE giúp tạo điều kiện trực tiếp trong SQL.
SELECT
name,
salary,
CASE
WHEN salary >= 3000 THEN 'Senior'
WHEN salary >= 1500 THEN 'Middle'
ELSE 'Junior'
END AS level
FROM employees;
Việc phân loại dữ liệu ngay trong truy vấn giúp giảm xử lý ở phía ứng dụng.
UNION và UNION ALL
UNION kết hợp kết quả từ nhiều truy vấn.
SELECT name
FROM customers
UNION
SELECT name
FROM suppliers;
UNION tự loại bỏ dữ liệu trùng lặp.
Nếu muốn giữ nguyên toàn bộ dữ liệu, hãy dùng UNION ALL.
SELECT name
FROM customers
UNION ALL
SELECT name
FROM suppliers;
Quản lý giao dịch bằng Transaction
Trong các hệ thống có nhiều thao tác cập nhật dữ liệu liên tiếp, Transaction đóng vai trò đảm bảo tất cả các thao tác hoặc được thực hiện thành công hoàn toàn, hoặc bị hủy hoàn toàn. Điều này giúp tránh tình trạng dữ liệu bị sai lệch nếu xảy ra lỗi giữa chừng.
Ví dụ phổ biến là chuyển tiền giữa hai tài khoản. Nếu chỉ trừ tiền ở tài khoản gửi nhưng chưa cộng vào tài khoản nhận thì dữ liệu sẽ không còn chính xác.
Bắt đầu một Transaction
START TRANSACTION;
Sau khi bắt đầu, mọi thay đổi sẽ chưa được lưu vĩnh viễn cho đến khi thực hiện COMMIT.
Lưu thay đổi bằng COMMIT
START TRANSACTION;
UPDATE accounts
SET balance = balance - 500
WHERE id = 1;
UPDATE accounts
SET balance = balance + 500
WHERE id = 2;
COMMIT;
Khi COMMIT được thực hiện, toàn bộ thay đổi sẽ được ghi xuống cơ sở dữ liệu.
Hủy Transaction bằng ROLLBACK
Nếu phát hiện lỗi trong quá trình xử lý, có thể đưa cơ sở dữ liệu trở về trạng thái trước khi Transaction bắt đầu.
START TRANSACTION;
UPDATE products
SET quantity = quantity - 5
WHERE id = 10;
ROLLBACK;
Sau khi ROLLBACK, mọi thay đổi trước đó trong Transaction sẽ bị hủy.
Sử dụng SAVEPOINT
SAVEPOINT cho phép tạo các điểm đánh dấu trong Transaction để có thể quay lại từng bước thay vì hủy toàn bộ.
START TRANSACTION;
UPDATE products
SET quantity = quantity - 5
WHERE id = 10;
SAVEPOINT step1;
UPDATE orders
SET total = total + 100
WHERE id = 5;
ROLLBACK TO step1;
COMMIT;
Kỹ thuật này hữu ích trong các quy trình xử lý nhiều bước.
Tạo View để đơn giản hóa truy vấn
View là bảng ảo được tạo từ một hoặc nhiều câu lệnh SELECT. Dữ liệu không được lưu riêng mà được lấy trực tiếp từ các bảng gốc.
Tạo View
CREATE VIEW active_users AS
SELECT
id,
name,
email
FROM users
WHERE status = 'active';
Sau khi tạo, có thể truy vấn giống như một bảng thông thường.
SELECT *
FROM active_users;
Cập nhật View
Một số View đơn giản cho phép UPDATE hoặc DELETE trực tiếp.
UPDATE active_users
SET name = 'Nguyen Van A'
WHERE id = 3;
Thực tế dữ liệu sẽ được cập nhật vào bảng gốc.
Xóa View
DROP VIEW active_users;
Stored Procedure giúp tái sử dụng nghiệp vụ
Stored Procedure là tập hợp nhiều câu lệnh SQL được lưu trong máy chủ MySQL. Thay vì gửi nhiều truy vấn từ ứng dụng, lập trình viên chỉ cần gọi Procedure.
Tạo Stored Procedure
DELIMITER //
CREATE PROCEDURE GetAllProducts()
BEGIN
SELECT *
FROM products;
END //
DELIMITER ;
Thực thi Procedure
CALL GetAllProducts();
Việc đóng gói nghiệp vụ giúp giảm lượng dữ liệu truyền qua mạng và tăng khả năng tái sử dụng.
Procedure có tham số
DELIMITER //
CREATE PROCEDURE GetProductByCategory
(
IN categoryId INT
)
BEGIN
SELECT *
FROM products
WHERE category_id = categoryId;
END //
DELIMITER ;
Gọi Procedure:
CALL GetProductByCategory(5);
Sử dụng Function trong MySQL
Khác với Procedure, Function luôn trả về một giá trị và có thể sử dụng trực tiếp trong câu lệnh SELECT.
Tạo Function
DELIMITER //
CREATE FUNCTION TotalPrice
(
price DECIMAL(10,2),
quantity INT
)
RETURNS DECIMAL(10,2)
BEGIN
RETURN price * quantity;
END //
DELIMITER ;
Gọi Function
SELECT
name,
TotalPrice(price, quantity)
FROM products;
Tự động hóa bằng Trigger
Trigger là đoạn mã SQL tự động chạy khi có thao tác INSERT, UPDATE hoặc DELETE trên bảng.
Trigger BEFORE INSERT
DELIMITER //
CREATE TRIGGER before_insert_product
BEFORE INSERT
ON products
FOR EACH ROW
BEGIN
SET NEW.created_at = NOW();
END //
DELIMITER ;
Khi thêm sản phẩm mới, trường created_at sẽ tự động nhận thời gian hiện tại.
Trigger AFTER UPDATE
DELIMITER //
CREATE TRIGGER log_salary
AFTER UPDATE
ON employees
FOR EACH ROW
BEGIN
INSERT INTO employee_logs
(
employee_id,
old_salary,
new_salary
)
VALUES
(
OLD.id,
OLD.salary,
NEW.salary
);
END //
DELIMITER ;
Trigger này giúp lưu lại lịch sử thay đổi lương.
Khóa dữ liệu bằng Lock
Trong môi trường nhiều người dùng cùng thao tác, Lock giúp tránh xung đột dữ liệu.
Khóa để đọc
SELECT *
FROM products
LOCK IN SHARE MODE;
Khóa để cập nhật
SELECT *
FROM products
WHERE id = 5
FOR UPDATE;
Các câu lệnh này thường được sử dụng bên trong Transaction.
Sử dụng EXISTS để tối ưu truy vấn
Trong nhiều trường hợp, EXISTS nhanh hơn IN khi dữ liệu lớn vì chỉ cần tìm thấy một bản ghi phù hợp là dừng.
SELECT *
FROM customers c
WHERE EXISTS
(
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
Truy vấn trên lấy danh sách khách hàng đã từng phát sinh đơn hàng.
ANY và ALL trong điều kiện so sánh
ANY và ALL cho phép so sánh với tập kết quả từ Subquery.
ANY
SELECT *
FROM products
WHERE price > ANY
(
SELECT price
FROM products
WHERE category_id = 2
);
Điều kiện đúng nếu giá lớn hơn ít nhất một sản phẩm trong danh mục.
ALL
SELECT *
FROM products
WHERE price > ALL
(
SELECT price
FROM products
WHERE category_id = 2
);
Điều kiện chỉ đúng khi giá lớn hơn toàn bộ tập kết quả.
Sử dụng COALESCE để xử lý giá trị NULL
NULL thường gây ra nhiều vấn đề trong tính toán. COALESCE giúp trả về giá trị đầu tiên khác NULL.
SELECT
name,
COALESCE(phone, 'Chưa cập nhật')
FROM users;
Người dùng chưa có số điện thoại sẽ hiển thị dòng chữ "Chưa cập nhật".
IFNULL và NULLIF
IFNULL
SELECT
IFNULL(discount, 0)
FROM products;
Nếu discount bằng NULL thì trả về 0.
NULLIF
SELECT
NULLIF(quantity, 0)
FROM products;
Nếu quantity bằng 0 thì kết quả sẽ là NULL.
Tăng tốc truy vấn bằng Index
Index là một trong những kỹ thuật tối ưu quan trọng nhất trong MySQL. Có thể hình dung Index giống như mục lục của một cuốn sách. Thay vì phải đọc toàn bộ nội dung để tìm một thông tin, MySQL chỉ cần tra cứu trong Index để xác định vị trí dữ liệu.
Tuy nhiên, Index không phải càng nhiều càng tốt. Mỗi Index đều tiêu tốn thêm dung lượng lưu trữ và làm chậm các thao tác INSERT, UPDATE hoặc DELETE vì hệ thống phải cập nhật lại Index sau mỗi lần dữ liệu thay đổi.
Tạo Index
CREATE INDEX idx_email
ON users(email);
Index này giúp tăng tốc các truy vấn tìm kiếm theo trường email.
Xóa Index
DROP INDEX idx_email
ON users;
Khi nào nên tạo Index?
- Các cột thường xuyên xuất hiện trong điều kiện WHERE.
- Các cột dùng để JOIN giữa nhiều bảng.
- Các trường ORDER BY hoặc GROUP BY được sử dụng thường xuyên.
- Các cột có số lượng giá trị khác nhau lớn.
Không nên tạo Index cho những cột có rất ít giá trị khác nhau, ví dụ giới tính hoặc trạng thái chỉ có hai hoặc ba giá trị.
Composite Index (Index nhiều cột)
Thay vì tạo nhiều Index đơn lẻ, MySQL cho phép tạo Index trên nhiều cột cùng lúc.
CREATE INDEX idx_name_city
ON customers(name, city);
Index trên sẽ phát huy hiệu quả với các truy vấn:
SELECT *
FROM customers
WHERE name = 'An';
Hoặc:
SELECT *
FROM customers
WHERE name = 'An'
AND city = 'Ha Noi';
Trong khi đó, nếu chỉ tìm theo city thì Index này có thể không được sử dụng do nguyên tắc tiền tố bên trái (Leftmost Prefix).
Unique Index đảm bảo dữ liệu không bị trùng
Unique Index không chỉ tăng tốc truy vấn mà còn ngăn việc lưu các giá trị trùng lặp.
CREATE UNIQUE INDEX idx_username
ON users(username);
Nếu đã tồn tại username giống nhau, MySQL sẽ từ chối thao tác INSERT hoặc UPDATE.
Full Text Index phục vụ tìm kiếm văn bản
Khi cần tìm kiếm nội dung bài viết, tiêu đề hoặc mô tả dài, Full Text Index mang lại hiệu quả cao hơn nhiều so với toán tử LIKE.
Tạo Full Text Index
CREATE FULLTEXT INDEX idx_article
ON articles(title, content);
Truy vấn Full Text
SELECT *
FROM articles
WHERE MATCH(title, content)
AGAINST('mysql optimization');
Đây là kỹ thuật được nhiều hệ thống quản lý nội dung sử dụng để xây dựng chức năng tìm kiếm.
Phân tích kế hoạch thực thi bằng EXPLAIN
EXPLAIN giúp lập trình viên biết MySQL đang thực hiện truy vấn theo cách nào, có sử dụng Index hay không và ước lượng số lượng bản ghi cần đọc.
Sử dụng EXPLAIN
EXPLAIN
SELECT *
FROM products
WHERE category_id = 5;
Kết quả trả về nhiều thông tin như:
- Loại truy vấn.
- Index đang được sử dụng.
- Số lượng bản ghi dự kiến phải đọc.
- Phương thức truy cập dữ liệu.
Đây là công cụ gần như bắt buộc khi tối ưu hệ thống lớn.
EXPLAIN ANALYZE
Trong MySQL 8, EXPLAIN ANALYZE còn cung cấp thời gian thực thi thực tế.
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE customer_id = 20;
Thông tin này giúp xác định chính xác vị trí gây chậm trong câu lệnh SQL.
Phân vùng dữ liệu bằng Partition
Khi bảng chứa hàng chục triệu hoặc hàng trăm triệu bản ghi, việc chia dữ liệu thành nhiều Partition sẽ giúp tăng hiệu suất truy vấn.
Partition theo phạm vi
CREATE TABLE sales
(
id INT,
sale_date DATE
)
PARTITION BY RANGE(YEAR(sale_date))
(
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
MySQL chỉ cần đọc đúng Partition liên quan thay vì quét toàn bộ bảng.
Làm việc với dữ liệu JSON
Từ MySQL 5.7 trở đi, kiểu dữ liệu JSON được hỗ trợ chính thức. Điều này rất hữu ích khi dữ liệu có cấu trúc linh hoạt.
Tạo bảng chứa JSON
CREATE TABLE customers
(
id INT PRIMARY KEY,
profile JSON
);
Đọc dữ liệu JSON
SELECT
JSON_EXTRACT(profile, '$.phone')
FROM customers;
Truy cập bằng toán tử
SELECT
profile -> '$.email'
FROM customers;
Kiểu dữ liệu JSON giúp giảm số lượng cột trong các trường hợp dữ liệu thay đổi thường xuyên.
Quản lý quan hệ bằng Foreign Key
Foreign Key giúp đảm bảo dữ liệu giữa các bảng luôn nhất quán.
Tạo khóa ngoại
CREATE TABLE orders
(
id INT PRIMARY KEY,
customer_id INT,
FOREIGN KEY(customer_id)
REFERENCES customers(id)
);
MySQL sẽ không cho phép tạo đơn hàng nếu khách hàng không tồn tại.
Xóa theo dây chuyền
FOREIGN KEY(customer_id)
REFERENCES customers(id)
ON DELETE CASCADE;
Khi khách hàng bị xóa, toàn bộ đơn hàng liên quan cũng sẽ bị xóa.
Thêm dữ liệu khi chưa tồn tại
Đây là tình huống rất phổ biến trong các hệ thống đồng bộ dữ liệu.
INSERT IGNORE
INSERT IGNORE INTO users
(
id,
name
)
VALUES
(
1,
'Nguyen Van A'
);
Nếu khóa chính đã tồn tại, MySQL sẽ bỏ qua thay vì phát sinh lỗi.
INSERT ... ON DUPLICATE KEY UPDATE
INSERT INTO users
(
id,
name
)
VALUES
(
1,
'Nguyen Van A'
)
ON DUPLICATE KEY UPDATE
name = VALUES(name);
Đây là kỹ thuật thường dùng để đồng bộ dữ liệu hoặc cập nhật thông tin nếu bản ghi đã tồn tại.
Thay thế dữ liệu bằng REPLACE
REPLACE sẽ xóa bản ghi cũ rồi thêm bản ghi mới nếu khóa chính hoặc Unique Key bị trùng.
REPLACE INTO products
(
id,
name,
price
)
VALUES
(
5,
'Laptop',
1500
);
Cần lưu ý rằng REPLACE thực chất thực hiện DELETE rồi INSERT nên có thể kích hoạt Trigger hoặc làm thay đổi khóa ngoại.
Sử dụng Event Scheduler để tự động hóa công việc
Event Scheduler cho phép MySQL thực hiện các tác vụ định kỳ mà không cần ứng dụng bên ngoài.
Tạo Event
CREATE EVENT delete_old_logs
ON SCHEDULE
EVERY 1 DAY
DO
DELETE
FROM logs
WHERE created_at <
NOW() - INTERVAL 30 DAY;
Ví dụ trên sẽ tự động xóa các bản ghi log quá 30 ngày mỗi ngày một lần.
Những sai lầm thường gặp khi sử dụng MySQL nâng cao
- Tạo quá nhiều Index dẫn đến giảm tốc độ ghi dữ liệu.
- Sử dụng SELECT * trong các bảng có nhiều cột.
- Không kiểm tra EXPLAIN trước khi đưa truy vấn vào hệ thống thực tế.
- Lạm dụng Subquery khi JOIN hoặc CTE sẽ phù hợp hơn.
- Thiếu Transaction trong các nghiệp vụ cập nhật nhiều bảng.
- Không xây dựng Foreign Key khiến dữ liệu mất tính nhất quán.
- Sử dụng LIKE với ký tự % ở đầu chuỗi làm mất khả năng sử dụng Index.
- Không phân trang dữ liệu khi truy vấn số lượng bản ghi lớn.
Kết luận
Việc thành thạo các câu lệnh MySQL nâng cao không chỉ giúp viết truy vấn ngắn gọn hơn mà còn nâng cao đáng kể hiệu suất và độ ổn định của hệ thống. Các kỹ thuật như JOIN, CTE, Window Function, Transaction, Stored Procedure, Trigger, View, Index, EXPLAIN, JSON, Partition hay Event Scheduler đều giải quyết những bài toán thực tế mà các câu lệnh SQL cơ bản không đáp ứng được.
Thay vì học thuộc cú pháp, bạn nên thực hành trên các bộ dữ liệu lớn và phân tích kế hoạch thực thi của từng truy vấn. Khi hiểu cách MySQL xử lý dữ liệu phía sau, bạn sẽ biết lựa chọn câu lệnh phù hợp cho từng tình huống, từ đó xây dựng những hệ thống có khả năng mở rộng tốt, tốc độ cao và dễ bảo trì trong thời gian dài.
- 0 Bình luận
Email, Điện thoại của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *