Đặc thù MySQL và Performance
1. InnoDB internals — cốt lõi
Buffer Pool
[mysqld]
innodb_buffer_pool_size = 6G # 50-70% RAM cho dedicated DB server
innodb_buffer_pool_instances = 8 # chia nhỏ để giảm mutex contention
Redo log + Undo log
- Redo log (file
ib_logfile): mọi thay đổi vật lý ghi vào trước khi flush data page → crash recovery. - Undo log: lưu version cũ cho MVCC và ROLLBACK.
Page (16KB default)
InnoDB lưu data + index ở page 16KB. PK là clustered → data và clustered index trong cùng B-tree.
Doublewrite Buffer
InnoDB ghi page ra doublewrite buffer trước khi ghi vào data file thật — bảo vệ chống torn page (page bị ghi dở khi crash). Trên SSD hiện đại, có thể tắt với innodb_doublewrite = 0 nếu filesystem hỗ trợ atomic write (như ZFS, ext4 với data=journal).
Change Buffer
Cache các thay đổi với secondary index page chưa có trong buffer pool. Merge khi page được load vào buffer pool → giảm IO cho secondary index update. Kiểm tra hiệu quả:
SHOW ENGINE INNODB STATUS\G
-- Xem "INSERT BUFFER AND ADAPTIVE HASH INDEX"
2. MVCC trong InnoDB
InnoDB MVCC qua undo log. UPDATE/DELETE để lại version cũ trong undo. SELECT trong REPEATABLE READ thấy snapshot tại thời điểm transaction bắt đầu.
"Postgres MVCC để row dead trong bảng chính → cần VACUUM thường xuyên reclaim space. InnoDB lưu version cũ trong undo tablespace riêng → bảng chính sạch hơn, ít bloat. Trade-off: undo log có thể phình lên nếu long transaction, gây lock và slowness."
3. Index trong InnoDB — chi tiết
3.1 B-tree index (mặc định)
InnoDB dùng B+tree làm cấu trúc index mặc định cho cả clustered index (PK) và secondary index. Đặc điểm:
- Balanced tree — mọi leaf node cùng depth → lookup O(log n).
- Sorted — data trong leaf node được sắp xếp → hỗ trợ range scan, ORDER BY, GROUP BY.
- Clustered index — PK chính là bảng: leaf node của PK index chứa toàn bộ row data.
- Secondary index — leaf node chứa PK value → mọi lookup qua secondary index cần thêm 1 lần seek về clustered index để lấy full row.
Clustered index (PK) leaf: (id=1, name='A', email='a@x.com')
Secondary index leaf (idx_email): (email='a@x.com', PK=1)
↓ seek PK=1 về clustered
→ (id=1, name='A', email='a@x.com')
"PK trong InnoDB là clustered index — data được sắp xếp vật lý theo PK. UUID ngẫu nhiên → INSERT vào giữa page → page split liên tục → fragmentation, buffer pool lãng phí. AUTO_INCREMENT tăng dần → INSERT vào cuối → page fill đều, ít page split, index compact hơn."
3.2 Composite index & leftmost prefix rule
Composite index là index trên nhiều cột. MySQL dùng index theo leftmost prefix rule — chỉ dùng được index nếu WHERE reference các cột từ trái qua phải trong định nghĩa index.
CREATE INDEX idx_abc ON orders(user_id, status, created_at);
-- ✅ Dùng được idx_abc: WHERE user_id = 1
-- ✅ Dùng được idx_abc: WHERE user_id = 1 AND status = 'done'
-- ✅ Dùng được idx_abc: WHERE user_id = 1 AND created_at > '2025-01-01'
-- ❌ KHÔNG dùng idx_abc: WHERE status = 'done' (thiếu leftmost)
-- ❌ KHÔNG dùng idx_abc: WHERE user_id = 1 OR status = 'done' (OR phá leftmost)
-- ⚠️ Dùng 1 phần: WHERE user_id = 1 AND created_at > '...' (skip cột giữa → chỉ dùng user_id prefix)
- Cột
=điều kiện equality đứng trước, cột range/sort đứng sau. - Cột có cardinality cao đứng trước (nhưng
=vẫn ưu tiên hơn cardinality). - WHERE
a = ? AND b > ? ORDER BY c→ index(a, b, c).
3.3 Covering index
Covering index là index chứa tất cả cột mà query cần → không cần lookup về clustered index. Rất quan trọng trong InnoDB vì tránh được double-lookup.
-- Query: SELECT total FROM orders WHERE user_id = ? AND created_at > ?
CREATE INDEX idx_cover ON orders(user_id, created_at, total);
-- EXPLAIN hiển thị "Using index" → covering, không đụng vào clustered index
EXPLAIN SELECT total FROM orders WHERE user_id = 1 AND created_at > '2025-01-01';
"Covering index khi query thường xuyên chỉ cần vài cột và performance là critical — tránh double-lookup của InnoDB. Trade-off: index to hơn vì chứa thêm cột data → tốn disk + buffer pool, INSERT/UPDATE chậm hơn vì phải maintain nhiều index page hơn."
3.4 Prefix index
Index chỉ trên N ký tự đầu của cột VARCHAR/TEXT — tiết kiệm không gian khi cột dài.
-- Index trên 10 ký tự đầu của email
CREATE INDEX idx_email_prefix ON users(email(10));
-- Chọn độ dài prefix với cardinality gần bằng full column:
SELECT COUNT(DISTINCT LEFT(email, 4)) / COUNT(*) AS sel_4,
COUNT(DISTINCT LEFT(email, 8)) / COUNT(*) AS sel_8,
COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS sel_10,
COUNT(DISTINCT email) / COUNT(*) AS sel_full
FROM users;
-- Chọn độ dài prefix có selectivity gần với sel_full nhất
3.5 FULLTEXT index
FULLTEXT index cho tìm kiếm văn bản. InnoDB hỗ trợ từ MySQL 5.6. Chi tiết sử dụng → xem mục 4. Full-Text Search.
CREATE FULLTEXT INDEX ft_content ON articles(title, body);
-- Với ngram parser (cần cho tiếng Việt, CJK):
CREATE FULLTEXT INDEX ft_content_ngram ON articles(title, body) WITH PARSER ngram;
MySQL có sẵn 2 parser cho FULLTEXT:
- Default parser: tách từ theo whitespace/dấu câu, bỏ qua stopword. Không hoạt động tốt với tiếng Việt (không có khoảng trắng phân tách từ đơn).
- ngram parser: tách thành các n-gram token (mặc định bigram, cấu hình qua
ngram_token_size). Phù hợp với tiếng Việt, Trung, Nhật, Hàn.
[mysqld]
ngram_token_size = 2 # bigram, mặc định
ft_min_word_len = 2 # độ dài từ tối thiểu
innodb_ft_min_token_size = 2
3.6 SPATIAL index
SPATIAL index dùng cho dữ liệu không gian (GIS/geometry). InnoDB hỗ trợ SPATIAL index từ MySQL 5.7 với R-tree.
-- Tạo cột geometry
CREATE TABLE locations (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255),
geo POINT NOT NULL SRID 4326,
SPATIAL INDEX idx_geo (geo)
);
-- Query tìm điểm trong bán kính
SELECT name, ST_Distance_Sphere(geo, ST_GeomFromText('POINT(106.7 10.8)', 4326)) AS dist
FROM locations
WHERE ST_Within(geo, ST_Buffer(ST_GeomFromText('POINT(106.7 10.8)', 4326), 0.1))
ORDER BY dist;
3.7 Functional index (8.0.13+)
Index trên biểu thức / function thay vì giá trị gốc của cột.
-- Index trên expression — giải bài "non-sargable"
CREATE INDEX idx_lower_email ON users ((LOWER(email)));
-- Query dùng được:
SELECT * FROM users WHERE LOWER(email) = 'a@b.com';
-- Index trên JSON path (kết hợp với generated column — xem mục JSON)
CREATE INDEX idx_json_city ON orders ((CAST(data->>'$.city' AS CHAR(50))));
3.8 Invisible index (8.0+)
-- Tạo index nhưng optimizer không dùng — test trước khi enable/disable
ALTER TABLE orders ALTER INDEX idx_status INVISIBLE;
ALTER TABLE orders ALTER INDEX idx_status VISIBLE;
Hữu ích để test impact trước khi drop index thật sự. optimizer_switch có flag use_invisible_indexes để override global.
3.9 Descending index (8.0+)
CREATE INDEX idx_orders_desc ON orders(created_at DESC);
-- Query "ORDER BY created_at DESC" dùng index thẳng, không cần filesort
Trước 8.0, MySQL chỉ hỗ trợ ASC index, DESC trong định nghĩa index bị bỏ qua (parser chấp nhận nhưng engine không áp dụng).
4. Full-Text Search
4.1 Tạo FULLTEXT index
-- Trên bảng articles
CREATE FULLTEXT INDEX ft_content ON articles(title, body);
-- Hoặc khi CREATE TABLE
CREATE TABLE articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255),
body TEXT,
FULLTEXT INDEX ft_articles (title, body)
) ENGINE=InnoDB;
4.2 MATCH ... AGAINST — 3 chế độ tìm kiếm
a) Natural Language Mode (mặc định)
-- Tìm kiếm theo độ liên quan (relevance score)
SELECT id, title,
MATCH(title, body) AGAINST('MySQL performance tuning') AS relevance
FROM articles
WHERE MATCH(title, body) AGAINST('MySQL performance tuning' IN NATURAL LANGUAGE MODE)
ORDER BY relevance DESC;
MySQL tính relevance dựa trên TF-IDF: từ xuất hiện nhiều trong document → score cao, từ xuất hiện ở nhiều document → bị giảm trọng số. Từ có trong > 50% số row → bị bỏ qua (50% threshold).
b) Boolean Mode
Kiểm soát chính xác logic tìm kiếm với toán tử boolean.
-- Phải có 'MySQL', có thể có 'performance', không có 'postgres'
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('+MySQL performance -postgres' IN BOOLEAN MODE);
-- Toán tử boolean:
-- +word : phải có
-- -word : không được có
-- word : optional (tăng relevance nếu có)
-- "phrase" : exact phrase
-- word* : prefix wildcard
-- (a b) : nhóm
-- >word <word : tăng/giảm trọng số
-- ~word : negative contribution (giảm relevance)
"Boolean mode cho phép kiểm soát chính xác với
+,-,*,"",~. Dùng khi cần logic phức tạp (phải có A, không được có B) hoặc tìm prefix. Đánh đổi: Boolean mode không sắp xếp theo relevance mặc định (các row match có relevance = 0 trừ khi có toán tử><). Natural Language phù hợp search kiểu Google — tìm theo độ liên quan, nhưng bị 50% threshold."
c) Query Expansion Mode
-- MySQL chạy query 2 lần:
-- Lần 1: tìm row match 'MySQL' → lấy ra các từ phổ biến trong những row đó
-- Lần 2: tìm lại với cả 'MySQL' + các từ mở rộng đó
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('MySQL' WITH QUERY EXPANSION);
Hữu ích khi user gõ từ quá ngắn hoặc không biết từ khóa chính xác. Cẩn thận — có thể mở rộng ra noise.
4.3 ngram parser cho tiếng Việt
Mặc định FULLTEXT parser tách từ theo whitespace → không hoạt động với tiếng Việt (từ đơn âm tiết không có dấu cách phân tách).
-- Tạo FULLTEXT index với ngram parser
CREATE FULLTEXT INDEX ft_vi ON articles_vn(title, body) WITH PARSER ngram;
-- ngram_token_size = 2 (bigram) — mặc định
-- Từ "MySQL" → token: "My", "yS", "SQ", "QL"
-- Từ "hiệu năng" → token: "hi", "iệ", "ệu", " n", "nă", "ăn", "ng"
-- Search bình thường:
SELECT * FROM articles_vn
WHERE MATCH(title, body) AGAINST('hiệu năng' IN BOOLEAN MODE);
[mysqld]
ngram_token_size = 2
innodb_ft_min_token_size = 2
ft_min_word_len = 2
ngram_token_size.4.4 InnoDB FULLTEXT — cơ chế hoạt động
InnoDB FULLTEXT dùng inverted index gồm 6 auxiliary table (FTS_INDEX_CACHE, FTS_COMMON_TABLE, v.v.) trong internal tablespace. FTS index được update async qua background thread — có thể có độ trễ sau INSERT/UPDATE. Force sync:
SET GLOBAL innodb_ft_cache_size = 80000000; -- cache size cho FTS
OPTIMIZE TABLE articles; -- force merge FTS index
5. JSON trong MySQL
MySQL hỗ trợ JSON data type native từ 5.7. JSON column lưu dạng binary (không phải text) → validation tự động, truy cập nhanh hơn LONGTEXT.
5.1 Tạo và truy vấn JSON
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
profile JSON
);
-- Insert
INSERT INTO users (name, profile) VALUES
('Vinh', JSON_OBJECT('age', 25, 'city', 'HCM', 'skills', JSON_ARRAY('SQL', 'Python'))),
('An', JSON_OBJECT('age', 30, 'city', 'HN', 'skills', JSON_ARRAY('Java', 'Go')));
-- JSON_EXTRACT với cú pháp path $ (hoặc dùng -> shorthand)
SELECT name,
profile->>'$.city' AS city,
profile->>'$.age' AS age,
profile->'$.skills[0]' AS first_skill
FROM users;
-- JSON_CONTAINS: kiểm tra sự tồn tại
SELECT * FROM users
WHERE JSON_CONTAINS(profile->'$.skills', '"Python"');
5.2 JSON functions quan trọng
| Function | Mục đích | Ví dụ |
|---|---|---|
JSON_EXTRACT(doc, path) / -> | Trích xuất giá trị (giữ JSON type) | profile->'$.city' |
->> | Trích xuất giá trị (unquote thành string) | profile->>'$.city' |
JSON_CONTAINS(doc, val) | Kiểm tra có chứa value không | WHERE JSON_CONTAINS(profile->'$.skills', '"Python"') |
JSON_OBJECT(k1,v1,...) | Tạo JSON object | JSON_OBJECT('name', 'Vinh') |
JSON_ARRAY(v1,v2,...) | Tạo JSON array | JSON_ARRAY('a', 'b') |
JSON_SET(doc, path, val) | Set (hoặc update) value tại path | JSON_SET(profile, '$.age', 26) |
JSON_REPLACE(doc, path, val) | Chỉ update nếu path đã tồn tại | JSON_REPLACE(profile, '$.age', 26) |
JSON_REMOVE(doc, path) | Xóa key tại path | JSON_REMOVE(profile, '$.city') |
JSON_KEYS(doc) | Lấy tất cả key của JSON object | JSON_KEYS(profile) |
JSON_LENGTH(doc) | Đếm số phần tử array hoặc key object | JSON_LENGTH(profile->'$.skills') |
5.3 JSON_TABLE() — chuyển JSON array thành bảng
-- Chuyển JSON array thành relational rows
SELECT jt.* FROM orders,
JSON_TABLE(
items,
'$[*]' COLUMNS (
product VARCHAR(100) PATH '$.product',
qty INT PATH '$.qty',
price DECIMAL(10,2) PATH '$.price'
)
) AS jt;
"Dùng JSON_TABLE() (từ MySQL 8.0.4) — convert JSON array thành relational rows với COLUMNS định nghĩa theo JSON path. Có thể JOIN với bảng khác, dùng trong subquery. Trước 8.0 phải dùng stored procedure loop hoặc JSON functions kết hợp với recursive CTE."
5.4 Generated columns từ JSON + index
-- Tạo generated column từ JSON path
ALTER TABLE users ADD COLUMN city VARCHAR(100)
GENERATED ALWAYS AS (profile->>'$.city') STORED;
-- Index trên generated column → JSON query dùng index!
CREATE INDEX idx_city ON users(city);
-- Query này giờ dùng được index
SELECT * FROM users WHERE profile->>'$.city' = 'HCM';
-- Hoặc: SELECT * FROM users WHERE city = 'HCM';
VIRTUAL generated column (không tốn disk) nếu không cần index, STORED nếu cần index.5.5 JSON Duality Views (MySQL 9.0+, DML đầy đủ từ 9.7)
JSON Duality View cho phép truy xuất cùng một data vừa dạng relational (table rows) vừa dạng JSON document, không cần ORM mapping.
-- Định nghĩa Duality View
CREATE OR REPLACE JSON RELATIONAL DUALITY VIEW user_dv AS
users {
_id: id,
name: name,
email: email,
orders @unnest @outer [{
_id: id,
amount: amount,
status: status
}]
};
-- Đọc như JSON document
SELECT * FROM user_dv WHERE JSON_CONTAINS(data, '{"name":"Vinh"}');
-- Ghi như JSON document (từ 9.7 — Community Edition)
UPDATE user_dv SET data = JSON_SET(data, '$.name', 'Vinh Updated') WHERE data->>'$._id' = '1';
6. Stored Procedures
6.1 CREATE PROCEDURE cơ bản
DELIMITER $$
CREATE PROCEDURE GetOrdersByUser(
IN p_user_id INT,
IN p_status VARCHAR(20)
)
BEGIN
SELECT id, total, created_at, status
FROM orders
WHERE user_id = p_user_id
AND (p_status IS NULL OR status = p_status)
ORDER BY created_at DESC;
END$$
DELIMITER ;
-- Gọi procedure
CALL GetOrdersByUser(1, 'done');
CALL GetOrdersByUser(1, NULL); -- trả về tất cả status
6.2 Tham số IN, OUT, INOUT
DELIMITER $$
CREATE PROCEDURE GetOrderStats(
IN p_user_id INT,
OUT p_total DECIMAL(18,2),
OUT p_count INT
)
BEGIN
SELECT SUM(amount), COUNT(*)
INTO p_total, p_count
FROM orders
WHERE user_id = p_user_id;
END$$
DELIMITER ;
-- Gọi và lấy OUT params
CALL GetOrderStats(1, @total, @count);
SELECT @total, @count;
| Loại | Mô tả |
|---|---|
IN | Mặc định. Truyền giá trị vào procedure. |
OUT | Procedure gán giá trị cho biến này trước khi kết thúc. |
INOUT | Vừa truyền vào vừa có thể gán lại. |
6.3 Biến, điều kiện, vòng lặp
DELIMITER $$
CREATE PROCEDURE ProcessOrders(IN p_user_id INT)
BEGIN
DECLARE v_order_id INT;
DECLARE v_amount DECIMAL(18,2);
DECLARE v_done INT DEFAULT 0;
-- CURSOR
DECLARE cur CURSOR FOR
SELECT id, amount FROM orders WHERE user_id = p_user_id;
-- HANDLER: khi cursor hết row → set v_done = 1
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_order_id, v_amount;
IF v_done = 1 THEN
LEAVE read_loop;
END IF;
-- Xử lý từng row
IF v_amount > 1000 THEN
UPDATE orders SET priority = 'HIGH' WHERE id = v_order_id;
ELSE
UPDATE orders SET priority = 'NORMAL' WHERE id = v_order_id;
END IF;
END LOOP read_loop;
CLOSE cur;
END$$
DELIMITER ;
6.4 Conditional logic đầy đủ
-- IF / ELSEIF / ELSE
IF v_amount > 10000 THEN
SET v_discount = 20;
ELSEIF v_amount > 5000 THEN
SET v_discount = 10;
ELSE
SET v_discount = 0;
END IF;
-- CASE expression
SET v_status = CASE
WHEN v_days_overdue > 30 THEN 'Bad Debt'
WHEN v_days_overdue > 0 THEN 'Overdue'
ELSE 'Current'
END;
-- LOOP / WHILE / REPEAT
WHILE v_counter < 10 DO
SET v_counter = v_counter + 1;
END WHILE;
REPEAT
SET v_counter = v_counter - 1;
UNTIL v_counter = 0 END REPEAT;
6.5 Procedure vs Function
| Stored Procedure | Stored Function | |
|---|---|---|
| Trả về | Nhiều result set + OUT params | 1 giá trị (scalar) |
| Dùng trong SQL | ❌ Không (CALL riêng) | ✅ Trong SELECT/WHERE |
| Transaction | ✅ Có COMMIT/ROLLBACK | ❌ Không (limited) |
| Side effect | ✅ UPDATE/INSERT/DELETE | Hạn chế (READS SQL DATA) |
| Use case | Business logic, batch process | Tính toán, transform data |
"Function khi cần tính toán trả về 1 giá trị và dùng được trong SELECT/WHERE — giống built-in function. Procedure khi cần logic phức tạp: transaction, nhiều result set, batch processing, business logic nhiều bước. Function bị hạn chế về side effect (không COMMIT/ROLLBACK, nên tránh UPDATE), Procedure thì không."
7. Triggers
7.1 CREATE TRIGGER cơ bản
CREATE TRIGGER trigger_name
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON table_name
FOR EACH ROW
trigger_body;
7.2 OLD và NEW
- INSERT: chỉ có
NEW— giá trị row sắp insert. - DELETE: chỉ có
OLD— giá trị row sắp bị xóa. - UPDATE: cả
OLDvàNEW—OLD.collà giá trị cũ,NEW.collà giá trị mới.
DELIMITER $$
CREATE TRIGGER before_order_insert
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
-- Tự động set created_at nếu NULL
IF NEW.created_at IS NULL THEN
SET NEW.created_at = NOW();
END IF;
-- Validate amount > 0
IF NEW.amount <= 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'amount must be positive';
END IF;
END$$
DELIMITER ;
7.3 Use cases điển hình
a) Audit log
CREATE TABLE orders_audit (
audit_id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT,
old_status VARCHAR(20),
new_status VARCHAR(20),
changed_at DATETIME DEFAULT NOW(),
changed_by VARCHAR(100)
);
DELIMITER $$
CREATE TRIGGER audit_order_update
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
IF OLD.status != NEW.status THEN
INSERT INTO orders_audit (order_id, old_status, new_status, changed_by)
VALUES (OLD.id, OLD.status, NEW.status, CURRENT_USER());
END IF;
END$$
DELIMITER ;
b) Derived column
-- Tự động tính total = quantity * unit_price
CREATE TRIGGER calc_total
BEFORE INSERT ON order_items
FOR EACH ROW
SET NEW.total = NEW.quantity * NEW.unit_price;
c) Validation
CREATE TRIGGER validate_stock
BEFORE INSERT ON order_items
FOR EACH ROW
BEGIN
DECLARE v_stock INT;
SELECT stock INTO v_stock FROM products WHERE id = NEW.product_id;
IF v_stock < NEW.quantity THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Insufficient stock';
END IF;
END;
8. Views
8.1 CREATE VIEW
-- View cơ bản
CREATE VIEW active_users AS
SELECT id, name, email, created_at
FROM users
WHERE status = 'active';
-- Query view như bảng thường
SELECT * FROM active_users WHERE email LIKE '%@gmail.com';
-- CREATE OR REPLACE (sửa view không cần DROP)
CREATE OR REPLACE VIEW active_users AS
SELECT id, name, email, phone, created_at
FROM users
WHERE status = 'active' AND deleted_at IS NULL;
8.2 Updatable views
View có thể UPDATE/INSERT/DELETE nếu thỏa điều kiện: không có JOIN, UNION, GROUP BY, DISTINCT, aggregation, subquery trong SELECT list.
-- Updatable view (chỉ SELECT đơn giản từ 1 bảng)
CREATE VIEW vip_users AS
SELECT id, name, email, vip_level FROM users WHERE vip_level > 0;
-- Có thể UPDATE qua view
UPDATE vip_users SET vip_level = 3 WHERE id = 1;
-- Dù cột vip_level > 0 sau UPDATE, row vẫn trong view (vip_level = 3 > 0)
-- Có thể DELETE qua view
DELETE FROM vip_users WHERE id = 2;
8.3 WITH CHECK OPTION
Ngăn UPDATE/INSERT tạo ra row không thỏa WHERE condition của view.
CREATE VIEW vip_users AS
SELECT id, name, email, vip_level FROM users WHERE vip_level > 0
WITH CHECK OPTION;
-- ✅ OK: vip_level = 3 > 0, vẫn thỏa view condition
UPDATE vip_users SET vip_level = 3 WHERE id = 1;
-- ❌ Lỗi: vip_level = 0, không thỏa view condition → bị từ chối
UPDATE vip_users SET vip_level = 0 WHERE id = 1;
-- Error: CHECK OPTION failed
"Đảm bảo mọi INSERT/UPDATE qua view đều tạo ra row thỏa mãn điều kiện WHERE của view đó. Giống constraint enforcement qua view. Ví dụ: view
vip_users WHERE vip_level > 0với CHECK OPTION → không thể UPDATEvip_level = 0qua view này. Không có CHECK OPTION, row sau UPDATE sẽ 'biến mất' khỏi view (không còn thỏa WHERE) nhưng vẫn tồn tại trong bảng."
8.4 Materialized views (workaround)
MySQL không có materialized view native. Workaround: tạo bảng snapshot + Event/Trigger refresh định kỳ.
-- Tạo bảng snapshot
CREATE TABLE mv_daily_stats AS
SELECT DATE(created_at) AS day, COUNT(*) AS order_count, SUM(amount) AS revenue
FROM orders
GROUP BY DATE(created_at);
-- Refresh bằng Event (xem mục Events bên dưới)
CREATE EVENT refresh_mv
ON SCHEDULE EVERY 1 HOUR
DO
BEGIN
TRUNCATE mv_daily_stats;
INSERT INTO mv_daily_stats
SELECT DATE(created_at), COUNT(*), SUM(amount) FROM orders GROUP BY DATE(created_at);
END;
9. Events (Scheduled Tasks)
MySQL Event Scheduler giống cron job — thực thi SQL theo lịch định sẵn trong database.
9.1 Bật Event Scheduler
-- Kiểm tra trạng thái
SHOW VARIABLES LIKE 'event_scheduler';
-- Bật
SET GLOBAL event_scheduler = ON;
-- Config permanent
-- [mysqld]
-- event_scheduler = ON
9.2 CREATE EVENT
-- Chạy 1 lần vào thời điểm cụ thể (AT)
CREATE EVENT clean_old_logs
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR
DO
DELETE FROM access_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY);
-- Chạy định kỳ (EVERY)
CREATE EVENT daily_report
ON SCHEDULE EVERY 1 DAY
STARTS '2025-01-01 00:00:00'
ENDS '2025-12-31 23:59:59'
DO
BEGIN
INSERT INTO daily_stats (day, total_orders, revenue)
SELECT CURDATE(), COUNT(*), SUM(amount)
FROM orders
WHERE DATE(created_at) = CURDATE()
ON DUPLICATE KEY UPDATE
total_orders = VALUES(total_orders),
revenue = VALUES(revenue);
END;
-- Bảo trì: OPTIMIZE mỗi tuần
CREATE EVENT weekly_optimize
ON SCHEDULE EVERY 1 WEEK
DO
OPTIMIZE TABLE orders, order_items, users;
9.3 Quản lý Events
-- Liệt kê events
SHOW EVENTS;
-- Xem chi tiết
SHOW CREATE EVENT daily_report;
-- Tạm dừng / kích hoạt
ALTER EVENT daily_report DISABLE;
ALTER EVENT daily_report ENABLE;
-- Xóa
DROP EVENT IF EXISTS daily_report;
"MySQL Event dùng cho scheduled tasks chạy trong database — partition maintenance, archive cũ, refresh materialized view workaround, aggregate report. Ưu điểm: quản lý cùng schema (migration reproducible), không phụ thuộc OS, fail cùng database. Nhược điểm: chỉ chạy SQL, không gọi external API; nếu Event Scheduler tắt → task không chạy mà không có alert; khó monitor hơn cron + monitoring tool."
10. Partitioning
-- RANGE partitioning theo year
CREATE TABLE sales (
id BIGINT,
sold_at DATE,
amount DECIMAL(18, 2)
)
PARTITION BY RANGE (YEAR(sold_at)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION p_max VALUES LESS THAN MAXVALUE
);
-- HASH partitioning
PARTITION BY HASH(user_id) PARTITIONS 8;
-- LIST
PARTITION BY LIST (region) (
PARTITION p_north VALUES IN ('HN', 'HP'),
PARTITION p_south VALUES IN ('HCM', 'CT')
);
- Bảng cực lớn (> 100M row).
- Archive theo thời gian (drop partition cũ thay vì DELETE).
- Query luôn filter theo partition key (partition pruning).
11. Replication — Source / Replica
Async replication (default)
Source Replica
↓ write binlog ↓ read binlog
COMMIT (không chờ) apply
Cấu hình ngắn gọn:
# Source
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog-format = ROW # ROW > STATEMENT > MIXED
-- Source: tạo user replication
CREATE USER 'repl'@'%' IDENTIFIED BY 'replpwd';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
-- Replica: setup
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='source.example.com',
SOURCE_USER='repl',
SOURCE_PASSWORD='replpwd',
SOURCE_LOG_FILE='mysql-bin.000001',
SOURCE_LOG_POS=0;
START REPLICA;
SHOW REPLICA STATUS\G
SOURCE/REPLICA thay cho MASTER/SLAVE cũ. Old commands deprecated nhưng vẫn chạy.Semi-sync và Group Replication
- Semi-sync: source chờ ít nhất 1 replica ack trước khi return COMMIT.
- Group Replication (InnoDB Cluster): multi-master, Paxos consensus, auto-failover.
12. Query optimization checklist
- ✅ EXPLAIN xem
type— tránhALL(full scan). - ✅ Composite index theo left-most prefix.
- ✅ Covering index khi cần (vì InnoDB secondary phải lookup PK).
- ✅ Tránh
SELECT *— phá covering. - ✅ WHERE sargable — không function lên cột (hoặc dùng functional index).
- ✅ Index trên FK column (không tự tạo, MyISAM thì không tạo được, InnoDB tạo manual).
- ✅
LIMITđể dừng sớm. - ✅ Buffer pool đủ lớn (50–70% RAM).
- ✅ Slow query log để bắt query chậm.
- ✅ Pagination keyset (
WHERE id > last_id) thay OFFSET deep.
13. Slow query log
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 # > 1 giây
log_queries_not_using_indexes = 1
# Phân tích
mysqldumpslow -s t /var/log/mysql/slow.log | head -20
# Hoặc dùng pt-query-digest (Percona Toolkit)
pt-query-digest slow.log
14. Performance schema & sys schema
-- Top query theo time
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC LIMIT 10;
-- Sys schema — view trên performance_schema
SELECT * FROM sys.statement_analysis ORDER BY total_latency DESC LIMIT 10;
-- Index không dùng (sau khi DB chạy 1 thời gian)
SELECT * FROM sys.schema_unused_indexes;
15. Backup & restore
Logical backup — mysqldump
# Single DB
mysqldump -u root -p --single-transaction --routines --triggers learnmysql > dump.sql
# All DBs
mysqldump -u root -p --all-databases --single-transaction > all.sql
# Restore
mysql -u root -p learnmysql < dump.sql
--single-transaction cho consistent snapshot (chỉ InnoDB) — không lock bảng.
Physical backup — mysqlbackup / Percona XtraBackup
Faster cho DB lớn. XtraBackup hot backup không downtime.
16. MySQL 9.7 — Cải tiến InnoDB
cpuset cgroup support
InnoDB giờ tự động phát hiện giới hạn CPU trong cgroup (container/Docker/K8s). Trước đây, innodb_buffer_pool_instances và các internal thread count dựa trên tổng CPU của host → sai trong container. Từ 9.7, InnoDB đọc cpuset.cpus.effective từ cgroup fs để xác định chính xác số CPU được cấp.
# Từ 9.7, có thể bỏ qua các setting manual này nếu chạy trong container:
# innodb_buffer_pool_instances sẽ auto-detected từ cgroup CPU limit
FTS index memory optimization
Full-Text Search index memory usage được optimize — giảm đáng kể RAM cần cho innodb_ft_cache_size khi có nhiều FULLTEXT index. Quan trọng cho ứng dụng tiếng Việt dùng ngram (token nhiều hơn → cache lớn hơn).
Parallel reader fixes
Parallel read (từ MySQL 8.0.17) được sửa lỗi — innodb_parallel_read_threads giờ ổn định hơn với CHECK TABLE, COUNT(*), và một số DDL operations. Trước 9.7, parallel reader có thể gây crash với degraded index page.
InnoDB dedicated server auto-config
innodb_dedicated_server (từ 8.0) được cải thiện — tự động detect thêm các tham số:
| Tham số | Auto-configure |
|---|---|
innodb_buffer_pool_size | 50-75% RAM |
innodb_log_file_size | Dựa trên buffer pool size |
innodb_flush_method | Tối ưu cho OS filesystem |
innodb_redo_log_capacity | Từ 9.7 — thay cho innodb_log_file_size |
[mysqld]
innodb_dedicated_server = ON
# Từ 9.7, tự động detect cả redo log capacity phù hợp
17. MySQL đặc thù — khác Postgres/SQL Server
| MySQL | Postgres | SQL Server | |
|---|---|---|---|
| Default isolation | REPEATABLE READ | READ COMMITTED | READ COMMITTED |
| Schema vs DB | DB = schema | DB chứa nhiều schema | DB chứa nhiều schema |
| Identifier | backtick `Name` | "Name" | [Name] |
| Boolean | TINYINT(1) 0/1 | BOOLEAN thật | BIT 0/1 |
| Auto-id | AUTO_INCREMENT | GENERATED AS IDENTITY / SERIAL | IDENTITY |
| UPSERT | ON DUPLICATE KEY UPDATE | ON CONFLICT DO UPDATE | MERGE |
| FULL OUTER JOIN | ❌ (UNION mô phỏng) | ✅ | ✅ |
| Window function | ✅ (từ 8.0) | ✅ | ✅ |
| Recursive CTE | ✅ (từ 8.0) | ✅ | ✅ |
| Array type | ❌ (JSON thay) | ✅ native | ❌ |
| Procedure language | SQL stored proc | PL/pgSQL + nhiều | T-SQL |
| Default storage | InnoDB | – | – |
| utf8 vs utf8mb4 | gotcha lớn | UTF-8 luôn | NVARCHAR Unicode |
3. Truy vấn nâng cao
JOIN, subquery, derived tables & LATERAL (8.0.14+), set operations, CTE recursive, window function (8.0+), regular expressions, JSON query, transaction, InnoDB locking, EXPLAIN, Hypergraph Optimizer (9.7), index.
5. Q và A phỏng vấn
65+ câu hỏi phỏng vấn MySQL cho dev backend / fullstack — InnoDB, gap lock, replication, optimizer, MySQL 8.4 & 9.7 LTS.