1. Các Loại Cơ sở dữ liệu
Có hai nhóm chính là cơ sở dữ liệu quan hệ và phi quan hệ, mỗi loại phù hợp với những trường hợp sử dụng khác nhau.
1.1. Cơ sở dữ liệu quan hệ (RDBMS)
Các hệ quản trị cơ sở dữ liệu quan hệ phổ biến bao gồm MySQL, PostgreSQL, Oracle, SQL Server, và SQLite. Đặc điểm chính của chúng là:
- Dữ liệu được tổ chức dưới dạng các bảng, với các hàng (bản ghi) và cột (trường).
- Mối quan hệ giữa các bảng được thiết lập thông qua các khóa (khóa chính, khóa ngoại), đảm bảo tính toàn vẹn và nhất quán của dữ liệu.
- Yêu cầu định nghĩa cấu trúc bảng rõ ràng trước khi lưu trữ dữ liệu.
1.2. Cơ sở dữ liệu phi quan hệ (NoSQL)
Các hệ quản trị cơ sở dữ liệu NoSQL như MongoDB (document-oriented), Redis (key-value), và Memcached (key-value cache) mang lại sự linh hoạt cao hơn:
- Không yêu cầu cấu trúc bảng cố định, cho phép lưu trữ dữ liệu dưới nhiều định dạng khác nhau (ví dụ: cặp khóa-giá trị, tài liệu JSON, đồ thị).
- Thường được sử dụng cho các ứng dụng yêu cầu khả năng mở rộng ngang (horizontal scaling) và hiệu suất cao với lượng dữ liệu lớn.
1.3. Mã hóa cơ sở dữ liệu
Mã hóa ký tự là yếu tố quan trọng để đảm bảo dữ liệu được hiển thị chính xác. Thông thường, UTF-8 được sử dụng rộng rãi. Đối với việc hỗ trợ các ký tự đặc biệt hơn, bao gồm cả biểu tượng cảm xúc, UTF8mb4 là lựa chọn ưu việt hơn.
2. Khởi động và Cấu hình MySQL
Trước khi bắt đầu, đảm bảo thư mục bin của MySQL đã được thêm vào biến môi trường PATH để có thể thực thi các lệnh từ bất kỳ vị trí nào trong terminal.
2.1. Khởi động và Đăng nhập
Để chạy MySQL Server (dịch vụ), có thể thực hiện lệnh mysqld --install (chạy với quyền quản trị viên) để cài đặt MySQL như một dịch vụ hệ thống, giúp nó tự động khởi động. Sau đó, có thể khởi động dịch vụ.
Để kết nối với client MySQL:
# Đăng nhập với quyền root
mysql -h 127.0.0.1 -P 3306 -u root -p
# Có thể viết gọn hơn nếu kết nối local mặc định
mysql -u root -p
# Thoát client
exit;
quit;
2.2. Thay đổi Mật khẩu
Để thay đổi mật khẩu tài khoản root:
-- Khi chưa có mật khẩu
SET PASSWORD FOR root@localhost = PASSWORD('matkhau_moi');
-- Khi đã có mật khẩu (sử dụng mysqladmin)
mysqladmin -u root -p[matkhau_cu] password [matkhau_moi];
Trong trường hợp quên mật khẩu, có thể khôi phục bằng cách khởi động MySQL Server với tùy chọn bỏ qua kiểm tra quyền và sau đó cập nhật mật khẩu:
- Dừng MySQL Server hiện tại.
- Khởi động MySQL Server với lệnh
mysqld --skip-grant-tables. - Đăng nhập client mà không cần mật khẩu và cập nhật mật khẩu mới:
UPDATE mysql.user SET authentication_string = PASSWORD('matkhau_moi') WHERE User = 'root' AND Host = 'localhost'; FLUSH PRIVILEGES; -- Rất quan trọng để tải lại bảng quyền - Dừng server và khởi động lại bình thường.
2.3. Tệp cấu hình
MySQL sử dụng các tệp .ini (trên Windows) hoặc .cnf (trên Linux) để cấu hình. Bạn có thể tạo tệp my.ini/my.cnf tùy chỉnh để ghi đè các cài đặt mặc định.
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
skip-character-set-client-handshake
[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
Sau khi sửa đổi tệp cấu hình, cần khởi động lại MySQL Server để các thay đổi có hiệu lực.
2.4. Thiết lập Chế độ Nghiêm ngặt (Strict Mode)
Chế độ nghiêm ngặt giúp MySQL tuân thủ chuẩn SQL hơn, ngăn chặn việc lưu trữ dữ liệu không hợp lệ. Ví dụ, nếu một cột CHAR(4) được gán giá trị dài hơn 4 ký tự, MySQL sẽ báo lỗi thay vì tự động cắt bớt.
-- Xem các chế độ SQL hiện tại
SHOW VARIABLES LIKE "%sql_mode%";
-- Đặt chế độ nghiêm ngặt (tạm thời cho phiên hiện tại)
SET SESSION sql_mode = 'STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY';
-- Đặt chế độ nghiêm ngặt (toàn cục, có hiệu lực sau khi khởi động lại server hoặc kết nối lại client)
SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY';
3. Các Thao tác CRUD Cơ bản
Các thao tác CRUD (Create, Read, Update, Delete) là nền tảng của mọi tương tác với cơ sở dữ liệu.
3.1. Chèn dữ liệu hàng loạt (Bulk Insert)
Khi cần chèn nhiều bản ghi, việc sử dụng các phương pháp chèn hàng loạt sẽ hiệu quả hơn việc chèn từng bản ghi một.
import pymysql
# Ví dụ sử dụng executemany với PyMySQL
try:
conn = pymysql.connect(
host='127.0.0.1',
port=3306,
user='admin_user',
password='admin_password',
database='web_app_db',
charset='utf8mb4',
autocommit=True
)
cursor = conn.cursor(pymysql.cursors.DictCursor)
data_records = []
for i in range(10000):
data_records.append((f'record_{i:05d}', i * 10)) # Tuple (record_name, value)
sql_insert = 'INSERT INTO my_data_table (record_name, record_value) VALUES (%s, %s)'
cursor.executemany(sql_insert, data_records)
print(f"Đã chèn {cursor.rowcount} bản ghi.")
except Exception as e:
print(f"Lỗi khi chèn dữ liệu: {e}")
finally:
if 'cursor' in locals() and cursor:
cursor.close()
if 'conn' in locals() and conn:
conn.close()
3.2. Thao tác với Cơ sở dữ liệu (Database)
-- Tạo một cơ sở dữ liệu mới
CREATE DATABASE app_db;
CREATE DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Liệt kê tất cả các cơ sở dữ liệu
SHOW DATABASES;
-- Xem thông tin tạo của một cơ sở dữ liệu
SHOW CREATE DATABASE app_db;
-- Thay đổi mã hóa của cơ sở dữ liệu
ALTER DATABASE app_db CHARACTER SET = 'utf8mb4' COLLATE = 'utf8mb4_unicode_ci';
-- Xóa một cơ sở dữ liệu
DROP DATABASE app_db;
3.3. Thao tác với Bảng (Table)
Trước khi thao tác với bảng, cần chọn cơ sở dữ liệu muốn sử dụng: USE database_name;. Để xem cơ sở dữ liệu hiện tại: SELECT DATABASE();
-- Tạo bảng đơn giản
CREATE TABLE users (
user_id INT,
user_name VARCHAR(50),
user_email VARCHAR(100) DEFAULT 'unknown@example.com'
);
-- Liệt kê các bảng trong cơ sở dữ liệu hiện tại
SHOW TABLES;
-- Xem cấu trúc bảng
DESCRIBE users; -- Hoặc DESC users;
SHOW CREATE TABLE users;
-- Đổi tên bảng
ALTER TABLE users RENAME TO app_users;
-- Thêm một cột mới vào bảng
ALTER TABLE app_users ADD COLUMN registration_date DATE;
-- Xóa một cột khỏi bảng
ALTER TABLE app_users DROP COLUMN user_email;
-- Sửa đổi kiểu dữ liệu của một cột
ALTER TABLE app_users MODIFY COLUMN user_name VARCHAR(100);
-- Sửa đổi tên cột và kiểu dữ liệu
ALTER TABLE app_users CHANGE COLUMN user_name full_name VARCHAR(120);
-- Sao chép cấu trúc bảng (không bao gồm dữ liệu)
CREATE TABLE old_app_users LIKE app_users;
-- Sao chép cấu trúc và dữ liệu (không bao gồm các khóa như PRIMARY KEY, FOREIGN KEY, INDEX)
CREATE TABLE app_users_copy SELECT * FROM app_users;
3.4. Thao tác với Bản ghi (Record)
-- Chèn một bản ghi
INSERT INTO app_users (user_id, full_name, registration_date) VALUES (1, 'Nguyen Van A', '2023-01-15');
-- Chèn nhiều bản ghi
INSERT INTO app_users (user_id, full_name, registration_date) VALUES
(2, 'Le Thi B', '2023-02-20'),
(3, 'Tran Minh C', '2023-03-01');
-- Truy vấn tất cả các bản ghi và cột
SELECT * FROM app_users;
-- Truy vấn các cột cụ thể
SELECT full_name, registration_date FROM app_users;
-- Truy vấn với điều kiện lọc
SELECT * FROM app_users WHERE user_id = 2 OR full_name = 'Nguyen Van A';
-- Cập nhật bản ghi
UPDATE app_users SET full_name = 'Phan Thi H' WHERE user_id = 3;
UPDATE app_users SET full_name = 'Phan Thi H', registration_date = '2023-03-05' WHERE user_id = 3;
-- Xóa bản ghi
DELETE FROM app_users WHERE user_id = 1;
-- Xóa tất cả bản ghi khỏi bảng (không reset AUTO_INCREMENT)
DELETE FROM app_users;
-- Xóa tất cả bản ghi và reset AUTO_INCREMENT
TRUNCATE TABLE app_users;
3.5. Truy vấn dữ liệu JSON
MySQL hỗ trợ truy vấn dữ liệu JSON được lưu trữ trong cột. Ví dụ, nếu có một cột metadata kiểu JSON:
-- Giả sử bảng 'products' có cột 'details' kiểu JSON: {'product_code': 'P123', 'supplier': 'XYZ'}
-- Truy vấn một thuộc tính trong JSON (kết quả có dấu ngoặc kép)
SELECT JSON_EXTRACT(details, '$.product_code') FROM products;
-- Truy vấn một thuộc tính trong JSON (kết quả không có dấu ngoặc kép)
SELECT details->>'$.product_code' FROM products;
-- Đối với PostgreSQL:
-- SELECT details::json->>'product_code' FROM products;
3.6. Giao dịch (Transactions)
Giao dịch là một chuỗi các thao tác SQL được thực hiện như một đơn vị công việc duy nhất. Hoặc tất cả các thao tác thành công (commit), hoặc không có thao tác nào thành công (rollback).
-- Bắt đầu một giao dịch
START TRANSACTION;
-- Thực hiện các thao tác DML (INSERT, UPDATE, DELETE)
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- Hoàn tác các thay đổi nếu có lỗi hoặc không muốn lưu
ROLLBACK;
-- Lưu các thay đổi vĩnh viễn vào cơ sở dữ liệu
COMMIT;
3.7. Tạo chỉ mục (Indexes)
Chỉ mục giúp tăng tốc độ truy vấn dữ liệu bằng cách cung cấp một cấu trúc tìm kiếm hiệu quả hơn.
-- Tạo chỉ mục đơn giản (Non-unique index)
CREATE INDEX idx_product_category ON products (category_id);
-- Tạo chỉ mục duy nhất (Unique index)
CREATE UNIQUE INDEX uidx_sku ON products (sku);
-- Tạo chỉ mục tổng hợp (Composite index)
CREATE INDEX idx_order_date_customer ON orders (order_date, customer_id);
-- Lưu ý: Nếu cột kiểu VARCHAR, có thể chỉ định độ dài chỉ mục để tiết kiệm không gian
-- CREATE INDEX idx_product_name_partial ON products (product_name(20));
3.8. Đánh số thứ tự và phân trang theo nhóm
Sử dụng hàm cửa sổ ROW_NUMBER() OVER() để đánh số thứ tự các bản ghi trong từng nhóm.
-- Đánh số thứ tự người dùng theo ID giảm dần
SELECT user_id, user_name, ROW_NUMBER() OVER (ORDER BY user_id DESC) AS row_num
FROM app_users;
-- Lấy bản ghi mới nhất cho mỗi người dùng dựa trên trường 'updated_at'
SELECT * FROM (
SELECT
user_id,
user_name,
updated_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC) AS rn
FROM user_activity
WHERE status = 1
) AS ranked_users
WHERE rn = 1;
3.9. Tỷ lệ giá trị trong nhóm
Cập nhật tỷ lệ đóng góp của mỗi bản ghi vào tổng giá trị của nhóm.
UPDATE sales_records AS sr
INNER JOIN (
SELECT
sale_date,
product_type,
SUM(sale_amount) AS total_group_amount
FROM sales_records
GROUP BY sale_date, product_type
) AS group_totals
ON sr.sale_date = group_totals.sale_date
AND sr.product_type = group_totals.product_type
SET sr.distribution_ratio = (sr.sale_amount / group_totals.total_group_amount)
WHERE group_totals.total_group_amount > 0;
3.10. Mệnh đề WITH (Common Table Expressions - CTE)
Mệnh đề WITH giúp định nghĩa các biểu thức bảng tạm thời có thể được tham chiếu trong một câu lệnh SQL phức tạp hơn, làm cho truy vấn dễ đọc và quản lý hơn.
-- Ví dụ CTE đơn giản
WITH RecentUsers AS (
SELECT user_id, user_name
FROM app_users
WHERE registration_date >= '2023-01-01'
)
SELECT * FROM RecentUsers;
-- Ví dụ CTE với phân nhóm
WITH RankedProducts AS (
SELECT
product_id,
product_name,
price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS rn
FROM products
)
SELECT product_id, product_name, price
FROM RankedProducts
WHERE rn = 1;
4. Storage Engines (Công cụ lưu trữ)
MySQL hỗ trợ nhiều công cụ lưu trữ khác nhau, mỗi loại có đặc điểm riêng. Hai công cụ phổ biến nhất là InnoDB và MyISAM.
- InnoDB: Là công cụ mặc định từ MySQL 5.5 trở đi.
- Hỗ trợ giao dịch (transactions) và khóa hàng (row-level locking), giúp tăng tính an toàn và khả năng đồng thời.
- Hỗ trợ khóa ngoại (foreign keys).
- Dữ liệu và chỉ mục được lưu trữ trong cùng một tệp (
.ibd).
- MyISAM: Công cụ mặc định trong các phiên bản MySQL cũ hơn.
- Tốc độ đọc nhanh hơn trong một số trường hợp.
- Không hỗ trợ giao dịch hay khóa hàng (chỉ có khóa cấp bảng).
- Không hỗ trợ khóa ngoại.
- Lưu trữ dữ liệu trong tệp
.MYD, chỉ mục trong tệp.MYI, và cấu trúc bảng trong tệp.frm.
- Memory: Dữ liệu được lưu trữ hoàn toàn trên bộ nhớ RAM.
- Truy cập cực nhanh.
- Dữ liệu sẽ bị mất khi server tắt hoặc khởi động lại. Thích hợp cho dữ liệu tạm thời.
- Blackhole: Hoạt động như một "hố đen". Mọi dữ liệu chèn vào sẽ bị loại bỏ ngay lập tức, nhưng các câu lệnh vẫn được ghi vào binary log. Thích hợp cho việc kiểm tra hiệu suất binary log hoặc sao chép dữ liệu.
-- Tạo bảng với công cụ lưu trữ cụ thể
CREATE TABLE my_innodb_table (id INT PRIMARY KEY) ENGINE=InnoDB;
CREATE TABLE my_myisam_table (id INT PRIMARY KEY) ENGINE=MyISAM;
CREATE TABLE my_memory_table (id INT PRIMARY KEY) ENGINE=Memory;
-- Xem danh sách các công cụ lưu trữ có sẵn
SHOW ENGINES;
5. Các Kiểu dữ liệu
5.1. Các kiểu dữ liệu số
- Kiểu số nguyên:
TINYINT,SMALLINT,MEDIUMINT,INT,BIGINT.INTthường là đủ cho hầu hết các trường hợp, không cần chỉ định độ rộng hiển thị (ví dụ:INT(11)). Sử dụngUNSIGNEDđể lưu trữ số nguyên không âm. - Kiểu số thực:
FLOAT(M, D)vàDOUBLE(M, D): Số dấu phẩy động, có thể không chính xác ở độ chính xác cao.Mlà tổng số chữ số,Dlà số chữ số sau dấu thập phân.DECIMAL(M, D): Kiểu số thập phân chính xác. Dữ liệu được lưu trữ dưới dạng chuỗi, đảm bảo độ chính xác tuyệt đối. Luôn ưu tiên dùngDECIMALcho các giá trị tiền tệ hoặc các phép tính yêu cầu độ chính xác cao.
5.2. Các kiểu dữ liệu chuỗi
CHAR(N): Chuỗi có độ dài cố định. Luôn chiếmNký tự, dù giá trị lưu trữ ngắn hơn (phần còn lại được đệm bằng khoảng trắng). Hiệu suất đọc/ghi nhanh hơn cho các chuỗi có độ dài biết trước.Ntừ 0 đến 255.VARCHAR(N): Chuỗi có độ dài thay đổi. Chỉ chiếm dung lượng cần thiết để lưu trữ giá trị thực tế (cộng thêm 1 hoặc 2 byte cho độ dài). Tiết kiệm không gian nhưng có thể chậm hơnCHARmột chút.Ntừ 0 đến 65535.
5.3. Các kiểu dữ liệu thời gian
DATE: Lưu trữ ngày (YYYY-MM-DD).TIME: Lưu trữ thời gian (HH:MM:SS).DATETIME: Lưu trữ ngày và thời gian (YYYY-MM-DD HH:MM:SS).TIMESTAMP: Lưu trữ ngày và thời gian, tự động cập nhật và được lưu trữ dưới dạng số giây từ '1970-01-01 00:00:00' UTC.YEAR: Lưu trữ năm (YYYY).
5.4. Kiểu ENUM và SET
ENUM('val1', 'val2', ...): Kiểu liệt kê. Một cộtENUMchỉ có thể chứa một trong các giá trị đã định nghĩa. Giống như hộp chọn đơn trong giao diện người dùng.SET('val1', 'val2', ...): Kiểu tập hợp. Một cộtSETcó thể chứa không, một, hoặc nhiều giá trị từ danh sách đã định nghĩa, được phân tách bằng dấu phẩy. Giống như hộp chọn đa trong giao diện người dùng.
-- Ví dụ ENUM
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100),
product_size ENUM('S', 'M', 'L', 'XL')
);
INSERT INTO products (product_id, product_name, product_size) VALUES (1, 'Áo phông', 'M');
-- Ví dụ SET
CREATE TABLE user_preferences (
user_id INT PRIMARY KEY,
interests SET('reading', 'gaming', 'hiking', 'cooking')
);
INSERT INTO user_preferences (user_id, interests) VALUES (1, 'gaming,hiking');
6. Ràng buộc (Constraints)
Các ràng buộc được sử dụng để áp đặt các quy tắc lên dữ liệu trong bảng, đảm bảo tính toàn vẹn và hợp lệ của dữ liệu.
6.1. NOT NULL
Đảm bảo rằng một cột không thể chứa giá trị NULL.
CREATE TABLE employees (
employee_id INT NOT NULL,
first_name VARCHAR(50) NOT NULL
);
6.2. DEFAULT
Gán một giá trị mặc định cho một cột nếu không có giá trị nào được cung cấp khi chèn bản ghi.
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
stock_quantity INT DEFAULT 0
);
6.3. UNIQUE
Đảm bảo rằng tất cả các giá trị trong một cột (hoặc một tập hợp các cột) là duy nhất. Có thể có nhiều ràng buộc UNIQUE trên một bảng.
-- Ràng buộc UNIQUE trên một cột
CREATE TABLE users (
user_id INT PRIMARY KEY,
username VARCHAR(50) UNIQUE
);
-- Ràng buộc UNIQUE kết hợp trên nhiều cột
CREATE TABLE server_ports (
server_ip VARCHAR(15),
port_number INT,
UNIQUE (server_ip, port_number)
);
6.4. PRIMARY KEY
Khóa chính là một cột (hoặc tập hợp các cột) duy nhất định danh mỗi bản ghi trong một bảng.
Một bảng chỉ có thể có một khóa chính. Khóa chính tự động áp dụng ràng buộc NOT NULL và UNIQUE.
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
customer_name VARCHAR(100)
);
6.5. AUTO_INCREMENT
Tự động tăng giá trị của một cột số mỗi khi một bản ghi mới được chèn. Thường được sử dụng với khóa chính.
CREATE TABLE orders (
order_id INT PRIMARY KEY AUTO_INCREMENT,
customer_id INT,
order_date DATE
);
-- Khi chèn, không cần cung cấp giá trị cho order_id
INSERT INTO orders (customer_id, order_date) VALUES (101, '2023-11-01');
6.6. CHECK
Ràng buộc CHECK dùng để giới hạn phạm vi giá trị mà một cột có thể chứa.
CREATE TABLE students (
student_id INT PRIMARY KEY,
student_age INT,
grade CHAR(1),
CHECK (student_age >= 5 AND student_age <= 18),
CHECK (grade IN ('A', 'B', 'C', 'D', 'F'))
);
6.7. FOREIGN KEY
Khóa ngoại là một cột (hoặc tập hợp các cột) trong một bảng, tham chiếu đến khóa chính của một bảng khác. Nó thiết lập mối quan hệ giữa hai bảng và đảm bảo tính toàn vẹn tham chiếu.
CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50)
);
CREATE TABLE employees (
employee_id INT PRIMARY KEY AUTO_INCREMENT,
employee_name VARCHAR(100),
department_id INT,
FOREIGN KEY (department_id) REFERENCES departments(dept_id)
ON UPDATE CASCADE -- Khi dept_id trong departments thay đổi, department_id ở đây cũng thay đổi
ON DELETE RESTRICT -- Ngăn chặn xóa phòng ban nếu có nhân viên thuộc phòng đó
);
7. Khóa ngoại và Các loại Quan hệ
Khóa ngoại là công cụ quan trọng để quản lý mối quan hệ giữa các bảng trong cơ sở dữ liệu quan hệ.
7.1. Xác định mối quan hệ
Để xác định mối quan hệ giữa hai thực thể, hãy tự đặt câu hỏi:
- Một thực thể A có thể liên kết với nhiều thực thể B không?
- Một thực thể B có thể liên kết với nhiều thực thể A không?
Ví dụ: Một quyển sách và một nhà xuất bản.
- Một quyển sách có thể được xuất bản bởi nhiều nhà xuất bản không? (Thường là Không)
- Một nhà xuất bản có thể xuất bản nhiều quyển sách không? (Có)
Đây là mối quan hệ một-nhiều (one-to-many).
7.2. Cập nhật và Xóa theo tầng (Cascade Actions)
ON UPDATE CASCADE và ON DELETE CASCADE là các hành động được định nghĩa cho khóa ngoại để tự động cập nhật hoặc xóa các bản ghi liên quan khi bản ghi khóa chính bị thay đổi hoặc xóa. RESTRICT hoặc NO ACTION sẽ ngăn chặn hành động đó nếu có bản ghi con liên quan.
CREATE TABLE categories (
category_id INT PRIMARY KEY,
category_name VARCHAR(50)
);
CREATE TABLE items (
item_id INT PRIMARY KEY AUTO_INCREMENT,
item_name VARCHAR(100),
cat_id INT,
FOREIGN KEY (cat_id) REFERENCES categories(category_id)
ON UPDATE CASCADE -- Nếu category_id trong categories thay đổi, cat_id ở đây cũng thay đổi
ON DELETE SET NULL -- Nếu category bị xóa, cat_id ở đây sẽ thành NULL
);
7.3. Quan hệ Một-Nhiều (One-to-Many)
Đặc điểm: Một bản ghi ở bảng "cha" có thể liên quan đến nhiều bản ghi ở bảng "con", nhưng một bản ghi ở bảng "con" chỉ liên quan đến một bản ghi ở bảng "cha".
Thường thì khóa ngoại được đặt ở phía "nhiều".
Ví dụ: Phòng ban và Nhân viên (Một phòng ban có nhiều nhân viên, một nhân viên chỉ thuộc một phòng ban)
-- Bảng "cha" (phía một)
CREATE TABLE Departments (
dept_id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(50) NOT NULL
);
-- Bảng "con" (phía nhiều)
CREATE TABLE Employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(100) NOT NULL,
dept_ref_id INT,
FOREIGN KEY (dept_ref_id) REFERENCES Departments(dept_id)
);
7.4. Quan hệ Nhiều-Nhiều (Many-to-Many)
Đặc điểm: Một bản ghi ở bảng A có thể liên quan đến nhiều bản ghi ở bảng B, và ngược lại.
Để triển khai, cần một bảng trung gian (còn gọi là bảng liên kết hoặc bảng khớp nối) chứa khóa ngoại từ cả hai bảng chính.
Ví dụ: Học sinh và Khóa học (Một học sinh có thể tham gia nhiều khóa học, một khóa học có thể có nhiều học sinh)
-- Bảng chính 1
CREATE TABLE Students (
student_id INT PRIMARY KEY AUTO_INCREMENT,
student_name VARCHAR(100) NOT NULL
);
-- Bảng chính 2
CREATE TABLE Courses (
course_id INT PRIMARY KEY AUTO_INCREMENT,
course_title VARCHAR(100) NOT NULL
);
-- Bảng trung gian
CREATE TABLE Student_Courses (
enrollment_id INT PRIMARY KEY AUTO_INCREMENT,
student_ref_id INT,
course_ref_id INT,
FOREIGN KEY (student_ref_id) REFERENCES Students(student_id) ON DELETE CASCADE,
FOREIGN KEY (course_ref_id) REFERENCES Courses(course_id) ON DELETE CASCADE,
UNIQUE (student_ref_id, course_ref_id) -- Đảm bảo một học sinh chỉ đăng ký một khóa học một lần
);
7.5. Quan hệ Một-Một (One-to-One)
Đặc điểm: Một bản ghi ở bảng A chỉ liên quan đến một bản ghi ở bảng B, và ngược lại.
Thường được sử dụng để mở rộng thông tin của một bảng mà không làm cho bảng chính quá cồng kềnh, hoặc để phân chia các cột nhạy cảm/ít truy cập.
Thực hiện bằng cách đặt khóa ngoại ở một trong hai bảng và đảm bảo nó cũng là UNIQUE.
Ví dụ: Thông tin người dùng cơ bản và Thông tin hồ sơ chi tiết
-- Bảng chính
CREATE TABLE Users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
-- Bảng thông tin chi tiết
CREATE TABLE User_Profiles (
profile_id INT PRIMARY KEY AUTO_INCREMENT,
user_ref_id INT UNIQUE, -- Khóa ngoại cũng là UNIQUE để đảm bảo 1-1
full_name VARCHAR(100),
date_of_birth DATE,
address TEXT,
FOREIGN KEY (user_ref_id) REFERENCES Users(user_id) ON DELETE CASCADE
);
8. Cú pháp và Phương pháp Truy vấn Cơ bản
Các lệnh SQL cơ bản để truy vấn và lọc dữ liệu.
Thứ tự thực thi logic của câu lệnh SQL:
FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT
-- Bảng dữ liệu mẫu cho các ví dụ tiếp theo
CREATE TABLE EmployeeRecords (
emp_id INT NOT NULL UNIQUE AUTO_INCREMENT,
emp_name VARCHAR(50) NOT NULL,
gender ENUM('Male', 'Female') NOT NULL DEFAULT 'Male',
age INT UNSIGNED NOT NULL DEFAULT 28,
hire_date DATE NOT NULL,
position VARCHAR(50),
salary DECIMAL(15, 2),
department_id INT
);
INSERT INTO EmployeeRecords (emp_name, gender, age, hire_date, position, salary, department_id) VALUES
('Anh Binh', 'Male', 25, '2022-03-10', 'Dev', 75000.00, 101),
('Chi Lan', 'Female', 30, '2021-01-20', 'Manager', 120000.00, 101),
('Anh Hung', 'Male', 35, '2020-07-15', 'Dev', 85000.00, 101),
('Chi Mai', 'Female', 28, '2022-05-01', 'QA', 60000.00, 102),
('Anh Cuong', 'Male', 40, '2019-11-01', 'Architect', 150000.00, 101),
('Chi Huong', 'Female', 22, '2023-02-15', 'Intern', 30000.00, 103),
('Anh Khoa', 'Male', 32, '2021-09-01', 'Dev', 90000.00, 102),
('Chi Ngoc', 'Female', 29, '2022-07-01', 'HR', 70000.00, 103);
8.1. Mệnh đề WHERE (Lọc bản ghi)
Sử dụng WHERE để chỉ định các điều kiện lọc bản ghi. Chỉ những bản ghi thỏa mãn điều kiện mới được trả về.
-- Lấy nhân viên có ID từ 2 đến 5
SELECT * FROM EmployeeRecords WHERE emp_id >= 2 AND emp_id <= 5;
SELECT * FROM EmployeeRecords WHERE emp_id BETWEEN 2 AND 5;
-- Lấy nhân viên có mức lương 60000 hoặc 70000
SELECT emp_name, salary FROM EmployeeRecords WHERE salary IN (60000, 70000);
-- Lấy nhân viên có tên chứa chữ 'a'
SELECT emp_name, salary FROM EmployeeRecords WHERE emp_name LIKE '%a%';
-- Lấy nhân viên có tên gồm 5 ký tự
SELECT emp_name, salary FROM EmployeeRecords WHERE emp_name LIKE '_____'; -- 5 dấu gạch dưới
SELECT emp_name, salary FROM EmployeeRecords WHERE CHAR_LENGTH(emp_name) = 5;
-- Lấy nhân viên có vị trí chưa được xác định (NULL)
SELECT emp_name, position FROM EmployeeRecords WHERE position IS NULL;
8.2. Mệnh đề GROUP BY (Nhóm bản ghi)
GROUP BY nhóm các bản ghi có cùng giá trị trong một hoặc nhiều cột. Sau khi nhóm, có thể sử dụng các hàm tổng hợp (Aggregate functions) như COUNT(), SUM(), AVG(), MAX(), MIN() để tính toán trên mỗi nhóm.
-- Số lượng nhân viên theo vị trí
SELECT position, COUNT(emp_id) AS total_employees FROM EmployeeRecords GROUP BY position;
-- Lương trung bình theo phòng ban
SELECT department_id, AVG(salary) AS average_salary FROM EmployeeRecords GROUP BY department_id;
-- Danh sách tên nhân viên trong mỗi vị trí (kết hợp các tên lại)
SELECT position, GROUP_CONCAT(emp_name SEPARATOR '; ') AS employees_list FROM EmployeeRecords GROUP BY position;
-- Ví dụ với CONCAT_WS để hiển thị thông tin chi tiết hơn
SELECT department_id, GROUP_CONCAT(CONCAT_WS(':', emp_name, salary) SEPARATOR ', ') AS dept_details
FROM EmployeeRecords
GROUP BY department_id;
8.3. Mệnh đề HAVING (Lọc nhóm)
HAVING được sử dụng để lọc các nhóm được tạo bởi GROUP BY. Nó hoạt động tương tự như WHERE nhưng áp dụng cho các kết quả của hàm tổng hợp.
-- Lấy các phòng ban có lương trung bình trên 80000
SELECT department_id, AVG(salary) AS avg_dept_salary
FROM EmployeeRecords
GROUP BY department_id
HAVING AVG(salary) > 80000;
8.4. DISTINCT (Loại bỏ bản ghi trùng lặp)
DISTINCT được sử dụng để chỉ trả về các giá trị khác nhau (duy nhất) trong một cột hoặc một tập hợp các cột.
-- Lấy danh sách các vị trí duy nhất
SELECT DISTINCT position FROM EmployeeRecords;
-- Lấy các cặp vị trí và giới tính duy nhất
SELECT DISTINCT position, gender FROM EmployeeRecords;
8.5. ORDER BY (Sắp xếp kết quả)
ORDER BY dùng để sắp xếp các bản ghi theo một hoặc nhiều cột, theo thứ tự tăng dần (ASC) hoặc giảm dần (DESC).
-- Sắp xếp nhân viên theo lương tăng dần
SELECT * FROM EmployeeRecords ORDER BY salary ASC;
-- Sắp xếp nhân viên theo tuổi giảm dần, nếu tuổi bằng nhau thì sắp xếp theo lương tăng dần
SELECT * FROM EmployeeRecords ORDER BY age DESC, salary ASC;
8.6. LIMIT (Giới hạn số lượng bản ghi)
LIMIT giới hạn số lượng bản ghi được trả về từ câu truy vấn. Có thể chỉ định số lượng bản ghi hoặc cả vị trí bắt đầu và số lượng.
-- Lấy 3 nhân viên đầu tiên
SELECT * FROM EmployeeRecords LIMIT 3;
-- Lấy nhân viên có lương cao nhất
SELECT * FROM EmployeeRecords ORDER BY salary DESC LIMIT 1;
-- Phân trang: Lấy 5 bản ghi bắt đầu từ bản ghi thứ 6 (offset 5, count 5)
SELECT * FROM EmployeeRecords LIMIT 5, 5;
8.7. Truy vấn đa bảng (Multi-table Queries)
8.7.1. JOIN (Kết nối bảng)
JOIN dùng để kết hợp các hàng từ hai hoặc nhiều bảng dựa trên một cột liên quan giữa chúng.
-- Bảng phòng ban
CREATE TABLE Departments_New (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50)
);
INSERT INTO Departments_New VALUES
(101, 'Kỹ thuật'),
(102, 'Marketing'),
(103, 'Nhân sự'),
(104, 'Tài chính');
-- Bảng nhân viên (có cột department_id tham chiếu đến Departments_New)
CREATE TABLE Employees_New (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(100),
department_ref_id INT
);
INSERT INTO Employees_New (emp_name, department_ref_id) VALUES
('Minh', 101), ('Hoa', 101),
('Son', 102), ('Thuy', 103),
('Quang', NULL); -- Nhân viên không có phòng ban
-- INNER JOIN: Chỉ trả về các bản ghi có sự khớp nối ở cả hai bảng
SELECT E.emp_name, D.dept_name
FROM Employees_New AS E
INNER JOIN Departments_New AS D ON E.department_ref_id = D.dept_id;
-- LEFT JOIN: Trả về tất cả các bản ghi từ bảng bên trái và các bản ghi khớp nối từ bảng bên phải. Nếu không có khớp nối, các cột từ bảng bên phải sẽ là NULL.
SELECT E.emp_name, D.dept_name
FROM Employees_New AS E
LEFT JOIN Departments_New AS D ON E.department_ref_id = D.dept_id;
-- RIGHT JOIN: Tương tự LEFT JOIN, nhưng ưu tiên bảng bên phải.
SELECT E.emp_name, D.dept_name
FROM Employees_New AS E
RIGHT JOIN Departments_New AS D ON E.department_ref_id = D.dept_id;
-- FULL OUTER JOIN (Không có trong MySQL, thường mô phỏng bằng UNION của LEFT và RIGHT JOIN)
SELECT E.emp_name, D.dept_name
FROM Employees_New AS E
LEFT JOIN Departments_New AS D ON E.department_ref_id = D.dept_id
UNION
SELECT E.emp_name, D.dept_name
FROM Employees_New AS E
RIGHT JOIN Departments_New AS D ON E.department_ref_id = D.dept_id
WHERE E.department_ref_id IS NULL; -- Loại bỏ trùng lặp từ phần INNER JOIN
8.7.2. Subquery (Truy vấn con)
Truy vấn con là một câu lệnh SELECT nằm bên trong một câu lệnh SQL khác. Kết quả của truy vấn con được sử dụng làm điều kiện hoặc dữ liệu cho truy vấn bên ngoài.
-- Lấy thông tin nhân viên thuộc phòng ban 'Kỹ thuật' hoặc 'Marketing'
SELECT * FROM Employees_New
WHERE department_ref_id IN (SELECT dept_id FROM Departments_New WHERE dept_name IN ('Kỹ thuật', 'Marketing'));
-- Tìm nhân viên mới nhất trong mỗi phòng ban
SELECT E.emp_name, E.position, E.hire_date, E.department_ref_id
FROM EmployeeRecords AS E
INNER JOIN (
SELECT department_id, MAX(hire_date) AS latest_hire_date
FROM EmployeeRecords
GROUP BY department_id
) AS LatestHires ON E.department_id = LatestHires.department_id AND E.hire_date = LatestHires.latest_hire_date;
8.7.3. EXISTS
EXISTS kiểm tra sự tồn tại của các hàng được trả về bởi một truy vấn con. Nếu truy vấn con trả về bất kỳ hàng nào, EXISTS là TRUE; nếu không, FALSE.
-- Lấy tất cả phòng ban nếu có ít nhất một nhân viên được tuyển dụng sau năm 2022
SELECT dept_name FROM Departments_New D
WHERE EXISTS (SELECT 1 FROM Employees_New E WHERE E.department_ref_id = D.dept_id AND E.hire_date > '2022-01-01');
9. Các Tính năng Nâng cao của Cơ sở dữ liệu
9.1. Views (Chế độ xem)
View là một bảng ảo dựa trên tập hợp kết quả của một truy vấn SQL. View không chứa dữ liệu thực sự mà chỉ là một "cửa sổ" hiển thị dữ liệu từ một hoặc nhiều bảng cơ sở. Chúng giúp đơn giản hóa các truy vấn phức tạp và tăng cường bảo mật.
-- Tạo một view hiển thị thông tin nhân viên cùng tên phòng ban của họ
CREATE VIEW EmployeeDepartmentView AS
SELECT E.emp_id, E.emp_name, E.position, E.salary, D.dept_name
FROM Employees_New AS E
INNER JOIN Departments_New AS D ON E.department_ref_id = D.dept_id;
-- Sử dụng view như một bảng thông thường
SELECT * FROM EmployeeDepartmentView WHERE dept_name = 'Kỹ thuật';
Mặc dù view rất tiện lợi, nhưng việc sử dụng quá nhiều view phức tạp có thể gây khó khăn cho việc bảo trì và mở rộng ứng dụng.
9.2. Triggers (Bộ kích hoạt)
Trigger là một khối mã SQL tự động thực thi khi một sự kiện cụ thể (INSERT, UPDATE, DELETE) xảy ra trên một bảng. Trigger có thể được thiết lập để chạy BEFORE (trước) hoặc AFTER (sau) sự kiện.
-- Ví dụ: Tự động ghi log lỗi khi một lệnh không thành công
-- Bảng để ghi lại các lệnh SQL và kết quả của chúng
CREATE TABLE CommandLog (
log_id INT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(50),
command_text VARCHAR(255),
execute_time DATETIME,
status ENUM('SUCCESS', 'FAILED')
);
-- Bảng để ghi lại các lệnh thất bại
CREATE TABLE ErrorLog (
error_id INT PRIMARY KEY AUTO_INCREMENT,
failed_command VARCHAR(255),
error_time DATETIME
);
-- Thay đổi delimiter tạm thời để định nghĩa trigger
DELIMITER $$
CREATE TRIGGER trg_after_insert_command
AFTER INSERT ON CommandLog
FOR EACH ROW
BEGIN
IF NEW.status = 'FAILED' THEN
INSERT INTO ErrorLog (failed_command, error_time)
VALUES (NEW.command_text, NEW.execute_time);
END IF;
END $$
DELIMITER ; -- Đặt lại delimiter về ;
-- Chèn dữ liệu vào CommandLog để kiểm tra trigger
INSERT INTO CommandLog (user_name, command_text, execute_time, status)
VALUES
('admin', 'SELECT * FROM users;', NOW(), 'SUCCESS'),
('user1', 'DELETE FROM critical_data;', NOW(), 'FAILED'),
('admin', 'UPDATE config SET value = 1;', NOW(), 'SUCCESS');
-- Kiểm tra bảng ErrorLog
SELECT * FROM ErrorLog;
-- Xóa trigger
DROP TRIGGER trg_after_insert_command;
9.3. Giao dịch (Transactions)
Một giao dịch là một đơn vị công việc logic được thực hiện như một chuỗi các thao tác. Nó tuân thủ các thuộc tính ACID:
- Atomicity (Nguyên tố): Tất cả các thao tác trong giao dịch đều thành công, hoặc tất cả đều thất bại.
- Consistency (Nhất quán): Giao dịch đưa cơ sở dữ liệu từ trạng thái hợp lệ này sang trạng thái hợp lệ khác.
- Isolation (Cô lập): Các giao dịch đồng thời không gây nhiễu lẫn nhau.
- Durability (Bền vững): Các thay đổi đã cam kết sẽ tồn tại vĩnh viễn, ngay cả khi hệ thống gặp lỗi.
Sử dụng START TRANSACTION, COMMIT và ROLLBACK như đã giải thích ở mục 3.6.
9.4. Stored Procedures (Thủ tục lưu trữ)
Thủ tục lưu trữ là một tập hợp các câu lệnh SQL được lưu trữ trong cơ sở dữ liệu và có thể được thực thi bằng một lệnh gọi duy nhất. Chúng giống như các hàm tùy chỉnh, cho phép tái sử dụng mã, cải thiện hiệu suất và tăng cường bảo mật.
DELIMITER $$
CREATE PROCEDURE GetEmployeeSalaryRange (
IN min_salary_param DECIMAL(15, 2),
IN max_salary_param DECIMAL(15, 2),
OUT num_employees INT
)
BEGIN
SELECT emp_name, salary
FROM EmployeeRecords
WHERE salary BETWEEN min_salary_param AND max_salary_param;
SELECT COUNT(*) INTO num_employees
FROM EmployeeRecords
WHERE salary BETWEEN min_salary_param AND max_salary_param;
END $$
DELIMITER ;
-- Gọi thủ tục lưu trữ
SET @employee_count = 0;
CALL GetEmployeeSalaryRange(70000.00, 100000.00, @employee_count);
-- Xem giá trị trả về
SELECT @employee_count AS EmployeesInRange;
9.5. Built-in Functions (Hàm có sẵn)
MySQL cung cấp nhiều hàm có sẵn để thao tác với chuỗi, số, ngày tháng, v.v. Các hàm này chỉ có thể sử dụng trong các câu lệnh SQL, không phải là các khối mã độc lập như thủ tục lưu trữ.
-- Ví dụ: Định dạng ngày và đếm số lượng bản ghi
CREATE TABLE BlogPosts (
post_id INT PRIMARY KEY AUTO_INCREMENT,
post_title VARCHAR(255),
publish_date DATETIME
);
INSERT INTO BlogPosts (post_title, publish_date) VALUES
('Bài viết 1', '2023-01-15 10:00:00'),
('Bài viết 2', '2023-01-15 14:30:00'),
('Bài viết 3', '2023-02-20 09:00:00'),
('Bài viết 4', '2023-02-20 11:00:00');
SELECT DATE_FORMAT(publish_date, '%Y-%m-%d') AS publish_day, COUNT(post_id) AS total_posts
FROM BlogPosts
GROUP BY publish_day
ORDER BY publish_day;
9.6. Flow Control (Kiểm soát luồng)
Trong các thủ tục lưu trữ, bạn có thể sử dụng các cấu trúc kiểm soát luồng như IF...THEN...ELSE và WHILE...DO.
DELIMITER $$
CREATE PROCEDURE CheckNumber (IN input_num INT)
BEGIN
IF input_num > 10 THEN
SELECT 'Số lớn hơn 10';
ELSEIF input_num = 10 THEN
SELECT 'Số bằng 10';
ELSE
SELECT 'Số nhỏ hơn 10';
END IF;
END $$
DELIMITER ;
CALL CheckNumber(15);
CALL CheckNumber(10);
CALL CheckNumber(5);
DELIMITER $$
CREATE PROCEDURE LoopExample (IN max_val INT)
BEGIN
DECLARE counter INT DEFAULT 0;
WHILE counter < max_val DO
SELECT CONCAT('Giá trị hiện tại: ', counter);
SET counter = counter + 1;
END WHILE;
END $$
DELIMITER ;
CALL LoopExample(3);
9.7. Indexes và Tối ưu hóa truy vấn
Chỉ mục (index) là một cấu trúc dữ liệu giúp MySQL tìm và truy xuất các hàng trong một bảng một cách nhanh chóng. Các loại chỉ mục chính bao gồm:
- Chỉ mục Clustered (Primary Key): Sắp xếp vật lý các hàng dữ liệu trong bảng. Một bảng chỉ có thể có một chỉ mục clustered.
- Chỉ mục Non-clustered (Unique, Index): Tạo một cấu trúc riêng biệt chứa các giá trị cột và con trỏ đến các hàng dữ liệu thực tế.
Ảnh hưởng của chỉ mục:
- Tăng tốc độ truy vấn (đặc biệt là các câu lệnh
SELECT). - Làm chậm các thao tác ghi (
INSERT,UPDATE,DELETE) vì chỉ mục cũng phải được cập nhật. - Chiếm không gian lưu trữ bổ sung.
Nguyên tắc cơ bản của chỉ mục: B+ Tree MySQL sử dụng cấu trúc cây B+ (B+ Tree) cho các chỉ mục. Trong cây B+ Tree, tất cả các nút lá (leaf nodes) đều chứa dữ liệu thực hoặc con trỏ đến dữ liệu, và được liên kết với nhau theo thứ tự, giúp tối ưu hóa việc tìm kiếm theo dải. Các nút nhánh (internal nodes) chỉ chứa các khóa để định hướng tìm kiếm.
Chỉ mục Clustered (Khóa chính): Trong InnoDB, khóa chính mặc định là chỉ mục clustered. Dữ liệu thực của bảng được lưu trữ trực tiếp trong các nút lá của chỉ mục này. Do đó, việc truy vấn bằng khóa chính rất nhanh.
Chỉ mục Non-clustered (Khóa duy nhất, Chỉ mục thông thường): Các chỉ mục này chứa các giá trị của cột được đánh chỉ mục và con trỏ đến khóa chính của bản ghi. Khi truy vấn sử dụng chỉ mục non-clustered, MySQL sẽ tìm giá trị trong chỉ mục, sau đó sử dụng khóa chính tìm được để truy xuất toàn bộ bản ghi từ chỉ mục clustered.
Covering Index (Chỉ mục bao phủ): Một truy vấn được gọi là "bao phủ" nếu tất cả các cột cần thiết cho truy vấn đều có thể được lấy trực tiếp từ chỉ mục non-clustered, mà không cần phải truy cập bảng chính. Điều này giảm thiểu I/O và tăng tốc độ truy vấn đáng kể.
Các lưu ý khi tạo chỉ mục và tối ưu hóa truy vấn:
- Chọn các cột có độ chọn lọc cao: Cột có nhiều giá trị duy nhất (ví dụ: ID, email) là ứng viên tốt cho chỉ mục. Cột có ít giá trị duy nhất (ví dụ: giới tính) ít có giá trị làm chỉ mục.
- Tránh các phép toán trên cột được đánh chỉ mục: Ví dụ:
WHERE id * 2 = 10sẽ không sử dụng chỉ mục trên cộtid. Hãy viết lại thànhWHERE id = 10 / 2. - Nguyên tắc khớp trái nhất (Leftmost Prefix Matching): Đối với chỉ mục tổng hợp (trên nhiều cột, ví dụ:
(col1, col2, col3)), chỉ mục sẽ được sử dụng hiệu quả nhất khi các điều kiện truy vấn bao gồm các cột từ bên trái sang. Ví dụ:WHERE col1 = 'A' AND col2 = 'B'sẽ sử dụng chỉ mục, nhưngWHERE col2 = 'B'thì không. - Sử dụng
EXPLAIN: Luôn sử dụng lệnhEXPLAINtrước một câu truy vấn để xem cách MySQL dự định thực thi truy vấn đó và xác định xem chỉ mục có đang được sử dụng hay không.