MySQL

Cú pháp MySQL cơ bản

Kiểu dữ liệu, DDL, DML, UPSERT (ON DUPLICATE KEY), function — gốc MySQL 8.4 / 9.x.

1. Kiểu dữ liệu

Numeric

KiểuRangeStorage
TINYINT-128 → 127 (hoặc 0 → 255 UNSIGNED)1 byte
SMALLINT±32K2 bytes
MEDIUMINT±8M3 bytes
INT±2.1B4 bytes
BIGINT±9.2 × 10¹⁸8 bytes
DECIMAL(p, s)Chính xácVariable
FLOAT / DOUBLEXấ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ểuGhi chú
CHAR(n)Cố định, pad space
VARCHAR(n)Biến đổi đến n. Tới ~16K char tuỳ charset.
TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXTKhông giới hạn n, lưu out-of-row
TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOBLư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)
JSONBuilt-in từ 5.7
Câu PV MySQL:"VARCHAR vs TEXT khác gì?"

"Cùng lưu chuỗi biến đổi. Khác chính: VARCHAR(n) lưu inline trong row (faster), TEXT lưu out-of-row (con trỏ + data ngoài). VARCHAR index full được, TEXT chỉ index prefix.

Dùng VARCHAR(n) cho dữ liệu < 1KB. TEXT cho 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'
ENUM — lợi và hại: Tiết kiệm storage, query nhanh. Nhưng khó mở rộng — thêm giá trị mới cần 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
Best practice: Với file lớn, không nên lưu trực tiếp BLOB trong DB. Lưu file lên object storage (S3, R2) và chỉ lưu URL/path trong MySQL. DB không phải file server.

Thời gian

KiểuRangeGhi chú
DATE1000 → 9999Ngày
TIME-838:59:59 → +838:59:59Có thể negative
DATETIME1000 → 9999Không convert timezone
TIMESTAMP1970 → 2038Convert UTC ↔ session timezone, dễ Y2038 bug
YEAR1901 → 21551 byte
DATETIME vs TIMESTAMP — câu PV gãi đầu nhiều fresher:
DATETIMETIMESTAMP
Range1000–99991970–2038 ⚠️
Storage5–8 bytes4–7 bytes
TimezoneKhông convertConvert UTC ↔ session
DefaultNULL hoặc valueCURRENT_TIMESTAMP phổ biến
Khuyến nghị: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)
);
Chi tiết về JSON functions, JSON path, multi-valued index xem tại JSON trong MySQL — file 4 trong series.MySQL 9.7+: JSON Duality Views hỗ trợ DML (INSERT / UPDATE / DELETE) trên JSON document view trong Community Edition — trước đây chỉ có ở HeatWave/Enterprise. Điều này cho phép vừa truy vấn relational, vừa thao tác JSON document qua cùng một view.

UUID

MySQL 8.0+ hỗ trợ UUID qua function UUID()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 vs AUTO_INCREMENT: UUID tránh collision khi phân tán (multi-master, sharding) nhưng index B-tree của InnoDB bị fragmentation nặng với UUID v4 (random). 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ểuMô tả
GEOMETRYBase type cho mọi spatial
POINTMột điểm (longitude, latitude)
LINESTRINGĐường thẳng/đường gấp khúc
POLYGONVùng/đa giác
MULTIPOINT, MULTILINESTRING, MULTIPOLYGONCollection
GEOMETRYCOLLECTIONTậ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;
Spatial trong MySQL — đủ dùng cho ứng dụng đơn giản (tìm cửa hàng gần nhất). Với GIS nặng (postGIS, raster), Postgres + PostGIS là lựa chọn tốt hơn. :::

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);
MySQL 8.0+ 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;
Kiểm tra operation nào được hỗ trợ INSTANT: SELECT * FROM information_schema.INNODB_COLUMNS;
MySQL trick — 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);
CHECK constraint được enforce từ MySQL 8.0.16. Trước đó, MySQL parse syntax nhưng âm thầm bỏ qua — data vi phạm vẫn insert bình thường. Đây là một trong những lý do MySQL bị chê "thiếu data integrity". Từ 8.0.16 đã full support.

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();
Câu PV:"MySQL UPSERT làm sao?"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 vs INSERT ... ON DUPLICATE KEY UPDATE: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;
LOAD DATA INFILE là cách nhanh nhất để import bulk data vào MySQL — nhanh hơn nhiều so với chạy 10,000 câu 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');
Vì sao 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;
Function trong MySQL bắt buộc khai báo deterministic property:
  • 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.
Mặc định 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 ;
MySQL hỗ trợ 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
Câu PV:"Làm sao xem schema của bảng trong MySQL?"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?".

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.