Cú pháp MySQL cơ bản
1. Kiểu dữ liệu
Numeric
| Kiểu | Range | Storage |
|---|---|---|
TINYINT | -128 → 127 (hoặc 0 → 255 UNSIGNED) | 1 byte |
SMALLINT | ±32K | 2 bytes |
MEDIUMINT | ±8M | 3 bytes |
INT | ±2.1B | 4 bytes |
BIGINT | ±9.2 × 10¹⁸ | 8 bytes |
DECIMAL(p, s) | Chính xác | Variable |
FLOAT / DOUBLE | Xấp xỉ | 4 / 8 |
-- UNSIGNED = chỉ số dương, gấp đôi range
CREATE TABLE counts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
qty SMALLINT UNSIGNED NOT NULL DEFAULT 0
);
INT(11) không phải limit số chữ số. Đó chỉ là display width — không dùng nữa (deprecated từ 8.0). Bỏ qua.Chuỗi
| Kiểu | Ghi chú |
|---|---|
CHAR(n) | Cố định, pad space |
VARCHAR(n) | Biến đổi đến n. Tới ~16K char tuỳ charset. |
TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT | Không giới hạn n, lưu out-of-row |
TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB | Lưu binary (file, ảnh). Tương tự TEXT về kích thước. |
ENUM('a','b') | Cố định trong list (1–2 bytes storage) |
SET('a','b') | Subset của list (1–8 bytes) |
JSON | Built-in từ 5.7 |
"Cùng lưu chuỗi biến đổi. Khác chính:
VARCHAR(n)lưu inline trong row (faster),TEXTlưu out-of-row (con trỏ + data ngoài).VARCHARindex full được,TEXTchỉ index prefix.Dùng
VARCHAR(n)cho dữ liệu < 1KB.TEXTcho bài viết, content dài."
ENUM
CREATE TABLE tickets (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
status ENUM('open', 'in_progress', 'resolved', 'closed') NOT NULL DEFAULT 'open'
);
-- ENUM lưu dưới dạng integer (1, 2, 3, 4) — rất tiết kiệm storage (1 byte với ≤ 255 giá trị)
INSERT INTO tickets (status) VALUES ('open'); -- OK
INSERT INTO tickets (status) VALUES ('unknown'); -- ERROR nếu strict mode
-- Query theo index sẽ nhanh hơn query theo string
SELECT * FROM tickets WHERE status = 2; -- = 'in_progress'
ALTER TABLE (có thể rất chậm với bảng lớn). Nếu danh sách giá trị thay đổi thường xuyên, nên dùng bảng lookup riêng + FK thay vì ENUM.BLOB
-- BLOB = Binary Large Object — lưu file, ảnh, binary data
-- 4 kích thước: TINYBLOB (255B), BLOB (64KB), MEDIUMBLOB (16MB), LONGBLOB (4GB)
CREATE TABLE attachments (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
filename VARCHAR(255) NOT NULL,
mime_type VARCHAR(100),
data LONGBLOB NOT NULL
);
-- BLOB không có charset, không convert encoding
-- So sánh với TEXT: TEXT có charset + collation; BLOB là binary thuần
Thời gian
| Kiểu | Range | Ghi chú |
|---|---|---|
DATE | 1000 → 9999 | Ngày |
TIME | -838:59:59 → +838:59:59 | Có thể negative |
DATETIME | 1000 → 9999 | Không convert timezone |
TIMESTAMP | 1970 → 2038 | Convert UTC ↔ session timezone, dễ Y2038 bug |
YEAR | 1901 → 2155 | 1 byte |
| DATETIME | TIMESTAMP | |
|---|---|---|
| Range | 1000–9999 | 1970–2038 ⚠️ |
| Storage | 5–8 bytes | 4–7 bytes |
| Timezone | Không convert | Convert UTC ↔ session |
| Default | NULL hoặc value | CURRENT_TIMESTAMP phổ biến |
DATETIME cho ngày sinh, ngày sự kiện. TIMESTAMP cho created_at, updated_at (cần timezone aware).Y2038 problem là vấn đề thật với TIMESTAMP — đã có patch nhưng cẩn thận.JSON
CREATE TABLE events (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
payload JSON NOT NULL,
-- Generated column từ JSON path
event_type VARCHAR(50) AS (payload->>'$.type') VIRTUAL,
INDEX idx_type (event_type)
);
UUID
MySQL 8.0+ hỗ trợ UUID qua function UUID() và UUID_TO_BIN() / BIN_TO_UUID() để tối ưu storage:
-- Cách naive — lưu dạng CHAR(36), tốn 36 byte + index phình to
CREATE TABLE t_naive (
id CHAR(36) DEFAULT (UUID()) PRIMARY KEY
);
-- Cách tối ưu — lưu BINARY(16), chỉ 16 byte, index gọn
CREATE TABLE t_optimized (
id BINARY(16) DEFAULT (UUID_TO_BIN(UUID())) PRIMARY KEY
);
-- Đọc lại dạng chuẩn
SELECT BIN_TO_UUID(id) AS uuid FROM t_optimized;
-- Dùng UUID v7 (MySQL 8.0.40+) — time-ordered, thân thiện InnoDB index hơn UUID v4
-- Cần cài plugin: INSTALL COMPONENT 'file://component_uuid_v7';
UUID v7 (time-ordered) giải quyết vấn đề này — vừa unique toàn cầu, vừa thân thiện index.Spatial (không gian)
MySQL hỗ trợ OpenGIS — dùng cho GIS, bản đồ, tìm kiếm gần vị trí:
| Kiểu | Mô tả |
|---|---|
GEOMETRY | Base type cho mọi spatial |
POINT | Một điểm (longitude, latitude) |
LINESTRING | Đường thẳng/đường gấp khúc |
POLYGON | Vùng/đa giác |
MULTIPOINT, MULTILINESTRING, MULTIPOLYGON | Collection |
GEOMETRYCOLLECTION | Tập hợp các loại trên |
CREATE TABLE locations (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200),
coord POINT NOT NULL SRID 4326, -- WGS 84 (GPS)
SPATIAL INDEX idx_coord (coord)
);
-- Tìm trong bán kính 5km
SELECT name FROM locations
WHERE ST_Distance_Sphere(coord, ST_GeomFromText('POINT(106.6297 10.8231)', 4326)) < 5000;
2. DDL
CREATE TABLE products (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
price DECIMAL(18, 2) NOT NULL CHECK (price >= 0),
category_id INT NOT NULL,
metadata JSON,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_category (category_id),
CONSTRAINT fk_products_cat FOREIGN KEY (category_id)
REFERENCES categories(id) ON DELETE RESTRICT
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
-- Add / drop / modify column
ALTER TABLE products ADD COLUMN description TEXT;
ALTER TABLE products DROP COLUMN description;
ALTER TABLE products MODIFY COLUMN name VARCHAR(300) NOT NULL;
ALTER TABLE products CHANGE COLUMN price unit_price DECIMAL(18, 2);
ALGORITHM=INSTANT — thêm column, drop column, đổi tên column, set default, modify ENUM... đều có thể thực hiện instant (không copy bảng, không lock). Trước 8.0, ALTER TABLE thường phải ALGORITHM=COPY (tạo bảng tạm, copy toàn bộ data — rất chậm với bảng lớn).-- MySQL 8.0+ tự chọn INSTANT nếu có thể, hoặc ép buộc:
ALTER TABLE products ADD COLUMN tags JSON, ALGORITHM=INSTANT;
ALTER TABLE products DROP COLUMN description, ALGORITHM=INSTANT;
SELECT * FROM information_schema.INNODB_COLUMNS;ON UPDATE CURRENT_TIMESTAMP auto-update timestamp khi row thay đổi. Không có ở SQL Server / Postgres (phải dùng trigger).CHECK constraint
-- MySQL 8.0.16+ thực sự enforce CHECK (trước 8.0.16 chỉ parse rồi ignore!)
ALTER TABLE products
ADD CONSTRAINT chk_price_positive CHECK (price >= 0);
-- CHECK có thể reference nhiều column trong cùng row
ALTER TABLE orders
ADD CONSTRAINT chk_date_order CHECK (shipped_at >= created_at);
Generated column
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
subtotal DECIMAL(18, 2),
tax_rate DECIMAL(5, 2),
total DECIMAL(18, 2) AS (subtotal * (1 + tax_rate / 100)) STORED,
INDEX idx_total (total)
);
VIRTUAL = tính khi đọc (default). STORED = lưu vật lý, index trực tiếp tốt hơn.3. DML
INSERT
INSERT INTO products (name, price) VALUES ('Laptop', 32000000);
-- Multi-row
INSERT INTO products (name, price) VALUES
('Mouse', 800000),
('Keyboard', 2200000);
-- INSERT IGNORE — bỏ qua row vi phạm unique
INSERT IGNORE INTO users (email) VALUES ('alice@x.com');
-- INSERT SELECT
INSERT INTO archive_orders SELECT * FROM orders WHERE created_at < '2025-01-01';
-- Lấy ID vừa insert
INSERT INTO users (email) VALUES ('alice@x.com');
SELECT LAST_INSERT_ID();
UPSERT — ON DUPLICATE KEY UPDATE
INSERT INTO users (email, login_count)
VALUES ('alice@x.com', 1)
ON DUPLICATE KEY UPDATE
login_count = login_count + 1,
last_login = NOW();
INSERT ... ON DUPLICATE KEY UPDATE .... Khác Postgres ON CONFLICT, khác SQL Server MERGE.Hoặc dùng REPLACE INTO (= DELETE + INSERT, không khuyến nghị vì xoá row gốc, mất FK, mất AUTO_INCREMENT).REPLACE INTO
-- REPLACE = DELETE (nếu duplicate key) + INSERT
-- Tương tự INSERT về syntax
REPLACE INTO users (id, email, login_count)
VALUES (1, 'alice@x.com', 1);
-- Dạng SET
REPLACE INTO users SET id = 1, email = 'alice@x.com', login_count = 1;
-- Nếu id=1 đã tồn tại → DELETE row cũ → INSERT row mới
-- ⚠️ Cảnh báo: DELETE rồi INSERT → mất AUTO_INCREMENT cũ, mất FK child rows (nếu ON DELETE CASCADE)
REPLACE xoá row cũ rồi chèn mới — các column không được chỉ định sẽ reset về DEFAULT. ON DUPLICATE KEY UPDATE chỉ update column được chỉ định, giữ nguyên phần còn lại. Luôn dùng ON DUPLICATE KEY UPDATE trừ khi bạn thực sự muốn xoá và tạo lại toàn bộ row.UPDATE
UPDATE products SET price = price * 1.1 WHERE category_id = 1;
-- UPDATE từ JOIN
UPDATE products p
JOIN categories c ON c.id = p.category_id
SET p.category_name = c.name;
-- UPDATE với LIMIT (đặc thù MySQL)
UPDATE products SET is_active = 0 WHERE last_used < '2025-01-01' LIMIT 1000;
DELETE
DELETE FROM products WHERE is_active = 0;
-- DELETE từ JOIN
DELETE p FROM products p
JOIN categories c ON c.id = p.category_id
WHERE c.is_archived = 1;
-- DELETE với LIMIT
DELETE FROM logs WHERE created_at < '2025-01-01' ORDER BY id LIMIT 10000;
TRUNCATE
TRUNCATE TABLE logs;
-- Reset AUTO_INCREMENT về 1.
-- FK CASCADE không trigger — chỉ DELETE mới gọi trigger.
LOAD DATA INFILE & SELECT INTO OUTFILE
-- Import CSV vào bảng (cần FILE privilege)
LOAD DATA INFILE '/tmp/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS -- bỏ qua header
(id, name, email, @created)
SET created_at = STR_TO_DATE(@created, '%Y-%m-%d %H:%i:%s');
-- Export kết quả SELECT ra file CSV
SELECT id, name, email
INTO OUTFILE '/tmp/users_export.csv'
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
FROM users WHERE is_active = 1;
INSERT riêng lẻ. CSV import là câu hỏi phỏng vấn thường gặp.Lưu ý bảo mật:local_infile bị tắt mặc định từ MySQL 8.0.22. Nếu dùng LOAD DATA LOCAL INFILE, cần bật option này ở client và server.4. NULL handling
SELECT * FROM users WHERE email IS NULL;
SELECT IFNULL(email, 'no-email') FROM users; -- MySQL function
SELECT COALESCE(email, phone, 'unknown') FROM users; -- chuẩn ANSI
-- NULL-safe equality (đặc thù MySQL)
SELECT * FROM users WHERE deleted_at <=> NULL; -- = NULL không work, <=> work
<=> null-safe equal — đặc thù MySQL. NULL <=> NULL → TRUE. 1 <=> NULL → FALSE.5. Function thường dùng
String
SELECT
LENGTH('Hello'), -- byte (5 ASCII, ~15 cho UTF-8 tiếng Việt)
CHAR_LENGTH('Xin chào'), -- ký tự (8)
LOWER('ABC'), UPPER('abc'),
SUBSTRING('Hello', 2, 3), -- 'ell'
LEFT('Hello', 3), RIGHT('Hello', 2),
TRIM(' abc '), TRIM(LEADING '0' FROM '000abc'),
REPLACE('abc', 'b', 'X'),
CONCAT('Hello', ' ', 'World'),
CONCAT_WS(', ', 'a', 'b', 'c'), -- 'a, b, c'
GROUP_CONCAT(name ORDER BY name SEPARATOR ', '); -- aggregate
Date
SELECT
NOW(), CURRENT_TIMESTAMP, UTC_TIMESTAMP(),
CURDATE(), CURTIME(),
DATE_ADD(NOW(), INTERVAL 7 DAY),
DATE_SUB(NOW(), INTERVAL 1 MONTH),
DATEDIFF('2026-12-31', NOW()), -- số ngày
TIMESTAMPDIFF(HOUR, '2026-01-01', NOW()),
YEAR(NOW()), MONTH(NOW()), DAY(NOW()),
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i'),
LAST_DAY(NOW());
JSON
SELECT
payload->'$.type', -- trả JSON
payload->>'$.type', -- trả TEXT (unquoted)
JSON_EXTRACT(payload, '$.user.name'),
JSON_UNQUOTE(JSON_EXTRACT(payload, '$.tags[0]')),
JSON_CONTAINS(payload, '"vue"', '$.tags'),
JSON_KEYS(payload),
JSON_LENGTH(payload, '$.tags');
-- Modify
UPDATE events
SET payload = JSON_SET(payload, '$.processed', true)
WHERE id = 1;
UPDATE events
SET payload = JSON_REMOVE(payload, '$.temp');
Conversion
SELECT
CAST('123' AS UNSIGNED),
CAST('2026-01-01' AS DATE),
CONVERT('abc', BINARY);
6. Variable & stored program
User-defined variable
SET @x = 10;
SELECT @x;
SELECT @max := MAX(price) FROM products; -- := trong SELECT
Stored procedure
DELIMITER //
CREATE PROCEDURE get_users_by_city(IN p_city VARCHAR(50))
BEGIN
SELECT id, name FROM users WHERE city = p_city;
END //
DELIMITER ;
CALL get_users_by_city('HCM');
DELIMITER? Vì thân SP có nhiều ;. Đổi delimiter sang // để mysql CLI không break statement khi gặp ;.Function
DELIMITER //
CREATE FUNCTION full_name(first VARCHAR(50), last VARCHAR(50))
RETURNS VARCHAR(101) DETERMINISTIC
BEGIN
RETURN CONCAT(first, ' ', last);
END //
DELIMITER ;
SELECT full_name(first_name, last_name) FROM users;
DETERMINISTIC: cùng input luôn cho cùng output.NOT DETERMINISTIC: phụ thuộc state (NOW(), random).READS SQL DATA/MODIFIES SQL DATA/NO SQL.
NOT DETERMINISTIC — không cho tạo function ở chế độ binlog mặc định nếu không khai báo. Khai báo đúng nếu cần.7. Trigger
DELIMITER //
CREATE TRIGGER trg_orders_audit
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
INSERT INTO orders_audit (order_id, old_status, new_status, changed_at)
VALUES (OLD.id, OLD.status, NEW.status, NOW());
END //
DELIMITER ;
BEFORE / AFTER × INSERT / UPDATE / DELETE. Không có INSTEAD OF (phải dùng updatable view).8. Đặc thù SQL nhỏ cần nhớ
-- LIMIT 2 cú pháp
SELECT ... LIMIT 10; -- 10 đầu
SELECT ... LIMIT 10 OFFSET 20;
SELECT ... LIMIT 20, 10; -- (offset, count) - cũ
-- IF expression
SELECT IF(price > 1000000, 'expensive', 'cheap') FROM products;
-- CASE
SELECT CASE
WHEN price < 100000 THEN 'cheap'
WHEN price < 1000000 THEN 'medium'
ELSE 'expensive'
END AS tier FROM products;
-- REGEXP (RLIKE)
SELECT * FROM products WHERE name REGEXP '^[A-Z]';
mysql CLI — các lệnh hữu ích
-- Kết nối
mysql -u root -p -h localhost mydatabase
-- Liệt kê database / table / column
SHOW DATABASES;
SHOW TABLES;
SHOW CREATE TABLE products; -- xem full DDL
DESCRIBE products; -- hoặc DESC products; xem schema
SHOW COLUMNS FROM products;
SHOW INDEX FROM products;
SHOW TABLE STATUS LIKE 'products'; -- engine, row count estimate, collation
-- Xem process đang chạy
SHOW PROCESSLIST;
SHOW FULL PROCESSLIST; -- xem full query text
KILL 42; -- kill connection ID
-- Xem biến hệ thống
SHOW VARIABLES LIKE '%innodb%';
SHOW STATUS LIKE '%connections%';
-- Import / Export từ CLI
mysql -u root -p mydb < backup.sql -- import
mysqldump -u root -p mydb > backup.sql -- export toàn bộ DB
mysqldump -u root -p mydb products > products.sql -- export 1 bảng
DESCRIBE table_name; hoặc SHOW CREATE TABLE table_name;. Khác Postgres (\d table), khác SQL Server (sp_help table). Đây là câu hỏi "bạn đã thực sự dùng MySQL chưa?".1. Giới thiệu và Cài đặt
MySQL — RDBMS open-source phổ biến nhất. Bản 9.7 LTS mới (04/2026, đến ~2034) với Hypergraph Optimizer vào Community, PBKDF2+SHA512, JavaScript in-database.
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.