ในโลกของ Backend Development ฐานข้อมูลเชิงสัมพันธ์ (Relational Database) ยังคงเป็นหัวใจสำคัญของระบบส่วนใหญ่ และ MySQL กับ MariaDB คือฐานข้อมูลที่ได้รับความนิยมสูงสุดในกลุ่มนี้ ไม่ว่าจะเป็นเว็บไซต์ขนาดเล็ก แอปพลิเคชัน e-commerce ระบบ ERP หรือแม้แต่บริการระดับ Facebook และ YouTube ก็ยังใช้ MySQL เป็นแกนหลัก
บทความนี้จะครอบคลุมทุกด้านของ MySQL และ MariaDB Administration ตั้งแต่การติดตั้ง การจัดการ User การออกแบบ Schema การ Optimize Query ไปจนถึง Replication และ Backup สำหรับ Production Server จริง
MySQL vs MariaDB คืออะไร? แตกต่างกันอย่างไร?
MySQL เป็นระบบจัดการฐานข้อมูลเชิงสัมพันธ์ (RDBMS) แบบ Open Source ที่พัฒนาโดย MySQL AB ก่อนถูก Sun Microsystems ซื้อ และสุดท้าย Oracle เข้าครอบครองในปี 2010 ปัจจุบัน MySQL อยู่ภายใต้ Oracle Corporation
MariaDB ถูกสร้างโดย Michael "Monty" Widenius ผู้ร่วมก่อตั้ง MySQL เอง เนื่องจากความกังวลว่า Oracle จะทำให้ MySQL กลายเป็น Closed Source จึงได้ Fork MySQL ออกมาเป็น MariaDB ที่เป็น Open Source ตลอดไป ตั้งชื่อตามลูกสาวคนเล็ก Maria เช่นเดียวกับ MySQL ที่ตั้งชื่อตามลูกสาวคนแรก My
ความแตกต่างหลักระหว่าง MySQL กับ MariaDB
| คุณสมบัติ | MySQL 8.x | MariaDB 11.x |
|---|---|---|
| ผู้พัฒนา | Oracle Corporation | MariaDB Foundation |
| License | GPL + Commercial | GPL เท่านั้น |
| Storage Engine เริ่มต้น | InnoDB | InnoDB (Aria สำหรับ System Tables) |
| Storage Engine เพิ่มเติม | จำกัด | ColumnStore, Spider, Mroonga, Connect |
| Optimizer | ดี | ดีกว่า (Subquery optimization ดีมาก) |
| Thread Pool | Enterprise เท่านั้น | มีฟรีใน Community |
| JSON Support | Native JSON type | JSON เป็น alias ของ LONGTEXT |
| Window Functions | MySQL 8.0+ | MariaDB 10.2+ |
| CTE (Common Table Expression) | MySQL 8.0+ | MariaDB 10.2+ |
| Temporal Tables | ไม่มี | มี (System-versioned) |
| Oracle Compatibility | บางส่วน | มี sql_mode=ORACLE |
การติดตั้ง MySQL / MariaDB
ติดตั้งด้วย Docker (แนะนำสำหรับ Development)
# MySQL 8
docker run -d \
--name mysql8 \
-e MYSQL_ROOT_PASSWORD=MyStr0ngP@ss \
-e MYSQL_DATABASE=myapp \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=AppP@ss123 \
-p 3306:3306 \
-v mysql_data:/var/lib/mysql \
mysql:8.0
# MariaDB 11
docker run -d \
--name mariadb11 \
-e MARIADB_ROOT_PASSWORD=MyStr0ngP@ss \
-e MARIADB_DATABASE=myapp \
-e MARIADB_USER=appuser \
-e MARIADB_PASSWORD=AppP@ss123 \
-p 3306:3306 \
-v mariadb_data:/var/lib/mysql \
mariadb:11
# Docker Compose
cat docker-compose.yml
version: "3.8"
services:
db:
image: mariadb:11
restart: always
environment:
MARIADB_ROOT_PASSWORD: RootP@ss
MARIADB_DATABASE: production
MARIADB_USER: webapp
MARIADB_PASSWORD: WebP@ss
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
volumes:
db_data:
ติดตั้งบน Ubuntu/Debian
# MySQL 8 บน Ubuntu 22.04+
sudo apt update
sudo apt install mysql-server mysql-client
sudo systemctl start mysql
sudo systemctl enable mysql
# รัน Security Script (สำคัญมาก!)
sudo mysql_secure_installation
# ตั้ง root password
# ลบ anonymous users
# ปิด remote root login
# ลบ test database
# MariaDB บน Ubuntu
sudo apt install mariadb-server mariadb-client
sudo systemctl start mariadb
sudo mysql_secure_installation
ติดตั้งบน CentOS/RHEL/AlmaLinux
# MariaDB จาก Official Repo
sudo dnf install mariadb-server mariadb
sudo systemctl start mariadb
sudo systemctl enable mariadb
sudo mysql_secure_installation
# MySQL 8 จาก Oracle Repo
sudo dnf install @mysql
sudo systemctl start mysqld
# ดู temporary root password
sudo grep 'temporary password' /var/log/mysqld.log
User Management และ Privileges
การจัดการ User เป็นเรื่องสำคัญมากสำหรับ DBA ผู้ดูแลฐานข้อมูลต้องเข้าใจหลักการ Least Privilege คือให้สิทธิ์น้อยที่สุดเท่าที่จำเป็น
# เข้า MySQL Console
mysql -u root -p
# สร้าง User
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'SecureP@ss123';
CREATE USER 'appuser'@'192.168.1.%' IDENTIFIED BY 'SecureP@ss123';
CREATE USER 'readonly'@'%' IDENTIFIED BY 'ReadP@ss456';
# ให้สิทธิ์ (Grants)
-- สิทธิ์ทั้งหมดบน Database เดียว
GRANT ALL PRIVILEGES ON myapp.* TO 'appuser'@'localhost';
-- สิทธิ์เฉพาะ SELECT (อ่านอย่างเดียว)
GRANT SELECT ON myapp.* TO 'readonly'@'%';
-- สิทธิ์เฉพาะบาง Table
GRANT SELECT, INSERT, UPDATE ON myapp.orders TO 'api_user'@'%';
-- สิทธิ์สำหรับ Replication
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl_user'@'10.0.0.%';
-- สิทธิ์สำหรับ Backup
GRANT SELECT, RELOAD, LOCK TABLES, SHOW VIEW, REPLICATION CLIENT,
EVENT, TRIGGER ON *.* TO 'backup_user'@'localhost';
FLUSH PRIVILEGES;
# ดูสิทธิ์ของ User
SHOW GRANTS FOR 'appuser'@'localhost';
# ลบสิทธิ์
REVOKE INSERT, UPDATE ON myapp.* FROM 'readonly'@'%';
# ลบ User
DROP USER 'olduser'@'localhost';
# เปลี่ยน Password
ALTER USER 'appuser'@'localhost' IDENTIFIED BY 'NewP@ss789';
# ดู User ทั้งหมด
SELECT user, host, plugin FROM mysql.user;
Database และ Table Creation
สร้าง Database
# สร้าง Database ด้วย UTF-8 (รองรับภาษาไทยและ Emoji)
CREATE DATABASE myapp
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
# ดู Database ทั้งหมด
SHOW DATABASES;
# เลือก Database
USE myapp;
# ดูข้อมูล Database
SHOW CREATE DATABASE myapp;
สร้าง Table ที่ดี
# ตัวอย่าง E-commerce Schema
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(200) NOT NULL,
phone VARCHAR(20) DEFAULT NULL,
avatar_url VARCHAR(500) DEFAULT NULL,
role ENUM('customer', 'admin', 'seller') DEFAULT 'customer',
email_verified_at TIMESTAMP NULL DEFAULT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_role_active (role, is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE products (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
sku VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(300) NOT NULL,
slug VARCHAR(300) NOT NULL UNIQUE,
description TEXT,
price DECIMAL(12,2) NOT NULL,
compare_price DECIMAL(12,2) DEFAULT NULL,
cost DECIMAL(12,2) DEFAULT NULL,
quantity INT UNSIGNED DEFAULT 0,
category_id INT UNSIGNED DEFAULT NULL,
brand VARCHAR(200) DEFAULT NULL,
weight DECIMAL(8,2) DEFAULT NULL COMMENT 'Weight in kg',
is_published BOOLEAN DEFAULT FALSE,
published_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FULLTEXT INDEX ft_name_desc (name, description),
INDEX idx_category (category_id),
INDEX idx_price (price),
INDEX idx_published (is_published, published_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_number VARCHAR(30) NOT NULL UNIQUE,
user_id BIGINT UNSIGNED NOT NULL,
status ENUM('pending','confirmed','processing','shipped','delivered','cancelled','refunded')
DEFAULT 'pending',
subtotal DECIMAL(12,2) NOT NULL,
shipping_fee DECIMAL(8,2) DEFAULT 0,
discount DECIMAL(12,2) DEFAULT 0,
total DECIMAL(12,2) NOT NULL,
shipping_address JSON,
payment_method VARCHAR(50),
paid_at TIMESTAMP NULL,
shipped_at TIMESTAMP NULL,
delivered_at TIMESTAMP NULL,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT,
INDEX idx_user (user_id),
INDEX idx_status (status),
INDEX idx_created (created_at),
INDEX idx_order_number (order_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Data Types เลือกอย่างไรให้เหมาะสม
การเลือก Data Type ที่ถูกต้องมีผลต่อ Performance และ Storage อย่างมาก ใช้ขนาดที่เล็กที่สุดที่เพียงพอ
| ใช้งาน | Data Type ที่แนะนำ | หมายเหตุ |
|---|---|---|
| Primary Key | BIGINT UNSIGNED AUTO_INCREMENT | รองรับ 18 quintillion rows |
| UUID | BINARY(16) + BIN_TO_UUID() | ประหยัดกว่า CHAR(36) ถึง 56% |
| ชื่อ-นามสกุล | VARCHAR(200) | อย่าใช้ TEXT สำหรับข้อมูลสั้น |
| อีเมล | VARCHAR(255) | RFC 5321 กำหนดสูงสุด 254 |
| URL | VARCHAR(2083) | ความยาวสูงสุดที่ IE รองรับ |
| ราคา/เงิน | DECIMAL(12,2) | ห้ามใช้ FLOAT/DOUBLE เพราะไม่แม่นยำ |
| จำนวนเต็มเล็ก | TINYINT UNSIGNED (0-255) | ประหยัดกว่า INT มาก |
| Boolean | TINYINT(1) หรือ BOOLEAN | MySQL ไม่มี Boolean แท้ |
| วันที่เวลา | TIMESTAMP หรือ DATETIME | TIMESTAMP ใช้ 4 bytes, DATETIME ใช้ 8 bytes |
| ข้อความยาว | TEXT | ไม่เก็บใน row โดยตรง |
| JSON | JSON (MySQL) / LONGTEXT (MariaDB) | ใช้เมื่อ schema ไม่แน่นอน |
| สถานะคงที่ | ENUM('a','b','c') | ประหยัดกว่า VARCHAR มาก |
| IP Address | INT UNSIGNED + INET_ATON() | ประหยัดกว่า VARCHAR(15) |
Indexing Strategies เชิงลึก
Index คือโครงสร้างข้อมูลที่ช่วยให้ MySQL ค้นหาข้อมูลได้เร็วขึ้นโดยไม่ต้อง Full Table Scan การทำ Index ที่ถูกต้องเป็นสิ่งสำคัญที่สุดสำหรับ Performance ของ Database
ประเภทของ Index
# 1. B-Tree Index (ค่าเริ่มต้น) - ดีสำหรับ =, >, <, >=, <=, BETWEEN, LIKE 'abc%'
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_price ON products(price);
# 2. Composite Index (หลายคอลัมน์) - สำคัญมาก!
-- ลำดับคอลัมน์มีผลต่อ Performance
-- กฎ: เรียงจาก Equality → Range → Sort
CREATE INDEX idx_status_date ON orders(status, created_at);
CREATE INDEX idx_cat_pub_price ON products(category_id, is_published, price);
# 3. Covering Index - ครอบคลุมทุกคอลัมน์ที่ Query ต้องการ
-- MySQL ไม่ต้องอ่าน Table จริง อ่านแค่ Index
CREATE INDEX idx_covering ON products(category_id, is_published, price, name);
-- Query นี้จะใช้ Index อย่างเดียว:
SELECT name, price FROM products
WHERE category_id = 5 AND is_published = 1
ORDER BY price;
# 4. Full-Text Index - สำหรับค้นหาข้อความ
CREATE FULLTEXT INDEX ft_search ON products(name, description);
-- ใช้งาน:
SELECT * FROM products
WHERE MATCH(name, description) AGAINST('กระเป๋าหนังแท้' IN NATURAL LANGUAGE MODE);
SELECT *, MATCH(name, description) AGAINST('laptop gaming') as relevance
FROM products
WHERE MATCH(name, description) AGAINST('laptop gaming')
ORDER BY relevance DESC;
# 5. Prefix Index - สำหรับคอลัมน์ที่ยาวมาก
CREATE INDEX idx_url ON pages(url(100));
# 6. Unique Index
CREATE UNIQUE INDEX idx_sku ON products(sku);
# 7. Spatial Index (สำหรับ GIS/Location)
ALTER TABLE stores ADD COLUMN location POINT NOT NULL SRID 4326;
CREATE SPATIAL INDEX idx_location ON stores(location);
Query Optimization เชิงลึก
EXPLAIN คือเครื่องมือที่สำคัญที่สุด
# ใช้ EXPLAIN วิเคราะห์ Query
EXPLAIN SELECT * FROM orders
WHERE user_id = 100 AND status = 'delivered'
ORDER BY created_at DESC LIMIT 10;
# EXPLAIN ANALYZE (MySQL 8.0.18+) - รัน Query จริง แล้วแสดงเวลาจริง
EXPLAIN ANALYZE SELECT p.name, COUNT(oi.id) as sold
FROM products p
JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id ORDER BY sold DESC LIMIT 10;
# ค่าสำคัญใน EXPLAIN:
# type: ALL (แย่) → index → range → ref → eq_ref → const (ดี)
# rows: จำนวน rows ที่ต้องอ่าน (ยิ่งน้อยยิ่งดี)
# Extra: Using index (ดี), Using filesort (ควรแก้), Using temporary (ควรแก้)
Slow Query Log
# เปิด Slow Query Log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- จับ Query ที่ใช้เวลา > 1 วินาที
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
# หรือตั้งใน my.cnf
[mysqld]
slow_query_log = 1
long_query_time = 0.5
slow_query_log_file = /var/log/mysql/slow-query.log
log_queries_not_using_indexes = 1
# วิเคราะห์ Slow Query Log
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# -s t = sort by time, -t 10 = top 10
เทคนิค Optimization ที่ใช้บ่อย
# 1. ใช้ LIMIT สำหรับ Pagination
-- แย่ (ช้ามากเมื่อ offset สูง):
SELECT * FROM products ORDER BY id LIMIT 100000, 20;
-- ดี (Cursor-based pagination):
SELECT * FROM products WHERE id > 100000 ORDER BY id LIMIT 20;
# 2. หลีกเลี่ยง SELECT *
-- แย่:
SELECT * FROM users WHERE id = 1;
-- ดี:
SELECT id, email, full_name FROM users WHERE id = 1;
# 3. ใช้ EXISTS แทน IN สำหรับ Subquery ใหญ่
-- ช้า:
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 10000);
-- เร็ว:
SELECT * FROM users u WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.total > 10000
);
# 4. Batch INSERT
-- ช้า (100 round trips):
INSERT INTO logs (msg) VALUES ('a');
INSERT INTO logs (msg) VALUES ('b');
...
-- เร็ว (1 round trip):
INSERT INTO logs (msg) VALUES ('a'), ('b'), ('c'), ..., ('z');
# 5. ใช้ FORCE INDEX เมื่อ Optimizer เลือกผิด
SELECT * FROM orders FORCE INDEX (idx_status_date)
WHERE status = 'pending' AND created_at > '2026-01-01';
Joins เชิงลึก
JOIN เป็นหัวใจของ SQL ที่ช่วยดึงข้อมูลจากหลาย Table มารวมกัน การเข้าใจ JOIN แต่ละแบบสำคัญมากสำหรับ Backend Developer
# INNER JOIN - เอาเฉพาะที่ตรงกันทั้ง 2 ฝั่ง
SELECT u.full_name, o.order_number, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'delivered';
# LEFT JOIN - เอาทุกแถวจากตารางซ้าย แม้ไม่มีคู่ในตารางขวา
-- หา User ที่ไม่เคยสั่งซื้อ
SELECT u.full_name, u.email
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
# RIGHT JOIN - เอาทุกแถวจากตารางขวา (ใช้น้อยมาก เขียน LEFT JOIN กลับด้านดีกว่า)
SELECT o.order_number, u.full_name
FROM orders o
RIGHT JOIN users u ON u.id = o.user_id;
# CROSS JOIN - ทุกคู่ที่เป็นไปได้ (Cartesian Product)
-- สร้างตาราง Calendar x Products
SELECT d.date, p.name
FROM calendar_dates d
CROSS JOIN products p
WHERE d.date BETWEEN '2026-01-01' AND '2026-01-31';
# Self JOIN - Join ตารางกับตัวเอง
-- หาพนักงานและหัวหน้า
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
# Multiple JOINs
SELECT
o.order_number,
u.full_name,
p.name AS product_name,
oi.quantity,
oi.unit_price,
(oi.quantity * oi.unit_price) AS line_total
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.created_at >= '2026-01-01'
ORDER BY o.created_at DESC;
Stored Procedures, Functions, Triggers, Views
Stored Procedure
# สร้าง Stored Procedure
DELIMITER //
CREATE PROCEDURE sp_get_user_orders(
IN p_user_id BIGINT,
IN p_status VARCHAR(20),
OUT p_total_count INT
)
BEGIN
SELECT SQL_CALC_FOUND_ROWS
o.order_number, o.total, o.status, o.created_at
FROM orders o
WHERE o.user_id = p_user_id
AND (p_status IS NULL OR o.status = p_status)
ORDER BY o.created_at DESC;
SET p_total_count = FOUND_ROWS();
END //
DELIMITER ;
# เรียกใช้
CALL sp_get_user_orders(42, 'delivered', @count);
SELECT @count;
Function
DELIMITER //
CREATE FUNCTION fn_calculate_discount(
subtotal DECIMAL(12,2),
user_tier VARCHAR(20)
) RETURNS DECIMAL(12,2)
DETERMINISTIC
BEGIN
DECLARE discount_rate DECIMAL(4,2);
CASE user_tier
WHEN 'vip' THEN SET discount_rate = 0.15;
WHEN 'gold' THEN SET discount_rate = 0.10;
WHEN 'silver' THEN SET discount_rate = 0.05;
ELSE SET discount_rate = 0.00;
END CASE;
RETURN ROUND(subtotal * discount_rate, 2);
END //
DELIMITER ;
# ใช้ใน Query
SELECT order_number, subtotal,
fn_calculate_discount(subtotal, 'vip') AS discount
FROM orders;
Trigger
# Trigger ที่อัปเดต Stock อัตโนมัติเมื่อมี Order
DELIMITER //
CREATE TRIGGER trg_after_order_item_insert
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
UPDATE products
SET quantity = quantity - NEW.quantity
WHERE id = NEW.product_id;
-- บันทึก Log
INSERT INTO stock_logs (product_id, change_qty, reason, created_at)
VALUES (NEW.product_id, -NEW.quantity, CONCAT('Order: ', NEW.order_id), NOW());
END //
DELIMITER ;
# Trigger ป้องกันราคาติดลบ
DELIMITER //
CREATE TRIGGER trg_before_product_update
BEFORE UPDATE ON products
FOR EACH ROW
BEGIN
IF NEW.price < 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Price cannot be negative';
END IF;
END //
DELIMITER ;
Views
# สร้าง View สำหรับ Report
CREATE OR REPLACE VIEW v_order_summary AS
SELECT
DATE(o.created_at) AS order_date,
COUNT(*) AS total_orders,
SUM(o.total) AS revenue,
AVG(o.total) AS avg_order_value,
COUNT(DISTINCT o.user_id) AS unique_customers
FROM orders o
WHERE o.status NOT IN ('cancelled', 'refunded')
GROUP BY DATE(o.created_at);
# ใช้งาน View เหมือน Table
SELECT * FROM v_order_summary
WHERE order_date >= '2026-01-01'
ORDER BY revenue DESC;
Transactions และ ACID
Transaction คือชุดของคำสั่ง SQL ที่ทำงานเป็นหน่วยเดียว สำเร็จทั้งหมด หรือล้มเหลวทั้งหมด หลัก ACID ประกอบด้วย Atomicity (ทำทั้งหมดหรือไม่ทำเลย) Consistency (ข้อมูลถูกต้องเสมอ) Isolation (Transaction ไม่กระทบกัน) Durability (เมื่อ Commit แล้วข้อมูลอยู่ถาวร)
# ตัวอย่าง Transaction สำหรับสั่งซื้อสินค้า
START TRANSACTION;
-- 1. ตรวจสอบ Stock (พร้อม Lock)
SELECT quantity FROM products WHERE id = 101 FOR UPDATE;
-- 2. ถ้า Stock เพียงพอ สร้าง Order
INSERT INTO orders (order_number, user_id, total, status)
VALUES ('ORD-20260408-001', 42, 1500.00, 'confirmed');
SET @order_id = LAST_INSERT_ID();
-- 3. สร้าง Order Items
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (@order_id, 101, 2, 750.00);
-- 4. ลด Stock
UPDATE products SET quantity = quantity - 2 WHERE id = 101;
-- 5. บันทึก Payment
INSERT INTO payments (order_id, amount, method, status)
VALUES (@order_id, 1500.00, 'credit_card', 'paid');
COMMIT;
-- ถ้าเกิดข้อผิดพลาด: ROLLBACK;
Isolation Levels
# ดู Isolation Level ปัจจุบัน
SELECT @@transaction_isolation;
# ตั้ง Isolation Level
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
# 4 ระดับ (จากเร็วสุดไปปลอดภัยสุด):
# 1. READ UNCOMMITTED - อ่านข้อมูลที่ยังไม่ Commit (Dirty Read)
# 2. READ COMMITTED - อ่านเฉพาะข้อมูลที่ Commit แล้ว (ใช้ใน PostgreSQL default)
# 3. REPEATABLE READ - อ่านข้อมูลเดิมตลอด Transaction (MySQL/InnoDB default)
# 4. SERIALIZABLE - ปลอดภัยที่สุดแต่ช้าที่สุด (Lock ทุกอย่าง)
# สำหรับ Production ส่วนใหญ่:
# - REPEATABLE READ (default) เหมาะกับงานทั่วไป
# - READ COMMITTED เหมาะกับ High-concurrency systems
Replication — การจำลองฐานข้อมูล
Replication ช่วยให้มีสำเนาของฐานข้อมูลบนหลาย Server เพื่อ High Availability, Read Scaling, และ Disaster Recovery
Master-Slave (Source-Replica) Replication
# === บน Master (Source) ===
# แก้ my.cnf
[mysqld]
server-id = 1
log_bin = /var/log/mysql/mysql-bin
binlog_format = ROW
binlog_do_db = myapp
expire_logs_days = 7
max_binlog_size = 100M
# สร้าง Replication User
CREATE USER 'repl'@'10.0.0.%' IDENTIFIED BY 'ReplP@ss';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.0.%';
# ดูตำแหน่ง Binlog
SHOW MASTER STATUS;
# +------------------+----------+
# | File | Position |
# +------------------+----------+
# | mysql-bin.000003 | 785 |
# +------------------+----------+
# === บน Slave (Replica) ===
# แก้ my.cnf
[mysqld]
server-id = 2
relay_log = /var/log/mysql/relay-bin
read_only = 1
# ตั้งค่า Replication
CHANGE MASTER TO
MASTER_HOST='10.0.0.1',
MASTER_USER='repl',
MASTER_PASSWORD='ReplP@ss',
MASTER_LOG_FILE='mysql-bin.000003',
MASTER_LOG_POS=785;
START SLAVE;
SHOW SLAVE STATUS\G
GTID-Based Replication (แนะนำ)
# GTID ทำให้ไม่ต้องระบุ binlog file/position ด้วยตัวเอง
# === ทั้ง Master และ Slave ===
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
log_bin = mysql-bin
binlog_format = ROW
# === บน Slave ===
CHANGE MASTER TO
MASTER_HOST='10.0.0.1',
MASTER_USER='repl',
MASTER_PASSWORD='ReplP@ss',
MASTER_AUTO_POSITION=1;
START SLAVE;
Group Replication (Multi-Master)
# Group Replication ให้ทุก Node เป็นทั้ง Reader และ Writer
# ใช้ Paxos Protocol สำหรับ Consensus
# ต้องมีอย่างน้อย 3 Nodes (แนะนำเลขคี่)
[mysqld]
server-id = 1
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_checksum = NONE
log_bin = mysql-bin
binlog_format = ROW
master_info_repository = TABLE
relay_log_info_repository = TABLE
plugin_load_add = 'group_replication.so'
group_replication_group_name = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
group_replication_start_on_boot = OFF
group_replication_local_address = "10.0.0.1:33061"
group_replication_group_seeds = "10.0.0.1:33061,10.0.0.2:33061,10.0.0.3:33061"
InnoDB Tuning สำหรับ Production
InnoDB เป็น Storage Engine หลักของ MySQL/MariaDB การ Tune InnoDB ให้เหมาะสมเป็นสิ่งสำคัญที่สุดสำหรับ Performance
# my.cnf - InnoDB Configuration สำหรับ Production
[mysqld]
# === Buffer Pool (สำคัญที่สุด!) ===
# ตั้งเป็น 70-80% ของ RAM ทั้งหมด
# Server 16GB RAM → ตั้ง 11-12GB
innodb_buffer_pool_size = 12G
innodb_buffer_pool_instances = 8 # 1 instance ต่อ 1GB
# === Redo Log ===
innodb_log_file_size = 1G # ใหญ่ขึ้น = Performance ดีขึ้นสำหรับ Write
innodb_log_buffer_size = 64M
# === Flush Settings ===
innodb_flush_log_at_trx_commit = 1 # 1=safe (default), 2=faster, 0=fastest
innodb_flush_method = O_DIRECT # ข้าม OS cache
# === I/O ===
innodb_io_capacity = 2000 # SSD
innodb_io_capacity_max = 4000
innodb_read_io_threads = 8
innodb_write_io_threads = 8
# === File Per Table ===
innodb_file_per_table = 1
# === Other ===
innodb_open_files = 4000
innodb_lock_wait_timeout = 10
innodb_thread_concurrency = 0 # Auto
innodb_sort_buffer_size = 4M
innodb_online_alter_log_max_size = 1G
# === General MySQL ===
max_connections = 500
thread_cache_size = 50
table_open_cache = 4000
table_definition_cache = 2000
tmp_table_size = 64M
max_heap_table_size = 64M
join_buffer_size = 4M
sort_buffer_size = 4M
read_buffer_size = 2M
read_rnd_buffer_size = 4M
# === Query Cache (ปิดใน MySQL 8.0+ แล้ว) ===
# query_cache_type = 0 # ปิดดีกว่า ใช้ Redis/Memcached แทน
# === Character Set ===
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
Backup Strategies
mysqldump (Logical Backup)
# Backup ทั้ง Database
mysqldump -u root -p --single-transaction --routines --triggers \
--databases myapp > backup_myapp_$(date +%Y%m%d).sql
# Backup ทุก Database
mysqldump -u root -p --all-databases --single-transaction \
--routines --triggers > full_backup_$(date +%Y%m%d).sql
# Backup เฉพาะ Table
mysqldump -u root -p --single-transaction myapp users orders > tables_backup.sql
# Backup เฉพาะ Schema (ไม่มี Data)
mysqldump -u root -p --no-data myapp > schema_only.sql
# Restore
mysql -u root -p myapp < backup_myapp_20260408.sql
# Backup + Compress
mysqldump -u root -p --single-transaction myapp | gzip > backup.sql.gz
# Restore from compressed
gunzip < backup.sql.gz | mysql -u root -p myapp
Percona XtraBackup (Physical Backup)
# XtraBackup ทำ Hot Backup ได้โดยไม่ต้อง Lock (สำหรับ InnoDB)
# ดีกว่า mysqldump มากสำหรับ Database ขนาดใหญ่
# Full Backup
xtrabackup --backup --target-dir=/backup/full \
--user=backup_user --password=BackupP@ss
# Prepare Backup (ต้องทำก่อน Restore)
xtrabackup --prepare --target-dir=/backup/full
# Restore
systemctl stop mysql
rm -rf /var/lib/mysql/*
xtrabackup --copy-back --target-dir=/backup/full
chown -R mysql:mysql /var/lib/mysql
systemctl start mysql
# Incremental Backup
xtrabackup --backup --target-dir=/backup/inc1 \
--incremental-basedir=/backup/full \
--user=backup_user --password=BackupP@ss
Automated Backup Script
#!/bin/bash
# daily_backup.sh
BACKUP_DIR="/backup/mysql"
DATE=$(date +%Y%m%d_%H%M)
RETENTION_DAYS=30
mkdir -p $BACKUP_DIR
# Backup ด้วย mysqldump
mysqldump -u backup_user -p'BackupP@ss' \
--single-transaction --routines --triggers \
--all-databases 2>/dev/null | gzip > "$BACKUP_DIR/full_$DATE.sql.gz"
# ตรวจสอบว่า Backup สำเร็จ
if [ $? -eq 0 ]; then
echo "$DATE: Backup SUCCESS ($(du -h $BACKUP_DIR/full_$DATE.sql.gz | cut -f1))"
else
echo "$DATE: Backup FAILED!" | mail -s "MySQL Backup Failed" admin@example.com
fi
# ลบ Backup เก่า
find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
# เพิ่มใน Crontab:
# 0 2 * * * /scripts/daily_backup.sh >> /var/log/mysql_backup.log 2>&1
Monitoring และ Performance Schema
# Performance Schema - ดูว่า MySQL ทำอะไรอยู่
# เปิดใน my.cnf
performance_schema = ON
# ดู Query ที่ช้าที่สุด
SELECT
DIGEST_TEXT,
COUNT_STAR AS exec_count,
ROUND(AVG_TIMER_WAIT/1000000000, 2) AS avg_ms,
ROUND(SUM_TIMER_WAIT/1000000000, 2) AS total_ms,
SUM_ROWS_EXAMINED AS rows_examined
FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;
# sys Schema - รายงานที่เข้าใจง่าย (MySQL 5.7+/MariaDB 10.6+)
# ดู Query ที่ใช้ Full Table Scan
SELECT * FROM sys.statements_with_full_table_scans LIMIT 10;
# ดู Table ที่มี Index ไม่ได้ใช้
SELECT * FROM sys.schema_unused_indexes;
# ดู Table ที่ไม่มี Primary Key
SELECT * FROM sys.schema_tables_with_full_table_scans;
# ดู Lock ที่กำลังรอ
SELECT * FROM sys.innodb_lock_waits;
# ดู Buffer Pool Usage
SELECT * FROM sys.innodb_buffer_stats_by_table
ORDER BY allocated DESC LIMIT 10;
# คำสั่ง Status ที่ใช้บ่อย
SHOW GLOBAL STATUS LIKE 'Threads%';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';
SHOW GLOBAL STATUS LIKE 'Slow_queries';
SHOW PROCESSLIST;
SHOW ENGINE INNODB STATUS\G
Security Hardening
# 1. รัน mysql_secure_installation (ทำทุกครั้งหลังติดตั้ง)
# 2. ปิด Remote Root Access
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
FLUSH PRIVILEGES;
# 3. เปลี่ยนชื่อ Root (หลอก Attacker)
RENAME USER 'root'@'localhost' TO 'dbadmin'@'localhost';
# 4. ใช้ Strong Password Policy (MySQL 8+)
INSTALL COMPONENT 'file://component_validate_password';
SET GLOBAL validate_password.policy = STRONG;
SET GLOBAL validate_password.length = 12;
# 5. Encryption at Rest (InnoDB Tablespace Encryption)
[mysqld]
early-plugin-load=keyring_file.so
keyring_file_data=/var/lib/mysql-keyring/keyring
ALTER TABLE sensitive_data ENCRYPTION='Y';
# 6. SSL/TLS สำหรับ Connection
[mysqld]
require_secure_transport = ON
ssl-ca = /etc/mysql/ssl/ca.pem
ssl-cert = /etc/mysql/ssl/server-cert.pem
ssl-key = /etc/mysql/ssl/server-key.pem
# สร้าง User ที่ต้องใช้ SSL
CREATE USER 'secure_user'@'%' IDENTIFIED BY 'P@ss'
REQUIRE SSL;
# 7. Firewall - จำกัด Port 3306
# ใช้ iptables หรือ ufw
ufw allow from 10.0.0.0/24 to any port 3306
Connection Pooling
Connection Pooling ช่วยลดเวลาในการสร้าง Connection ใหม่ทุกครั้ง ซึ่งเป็นการทำงานที่แพงมาก
# ProxySQL - Connection Pooler สำหรับ MySQL/MariaDB
# ติดตั้ง
apt install proxysql
# ตั้งค่าผ่าน Admin Interface
mysql -u admin -padmin -h 127.0.0.1 -P 6032
-- เพิ่ม Backend Server
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight)
VALUES (1, '10.0.0.1', 3306, 100);
-- เพิ่ม User
INSERT INTO mysql_users (username, password, default_hostgroup)
VALUES ('appuser', 'P@ss', 1);
-- Read/Write Split
INSERT INTO mysql_query_rules (rule_id, match_pattern, destination_hostgroup)
VALUES (1, '^SELECT.*FOR UPDATE', 1), -- Write group
(2, '^SELECT', 2); -- Read group
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL USERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
# Application เชื่อมต่อ ProxySQL แทน MySQL โดยตรง
# ProxySQL จัดการ Connection Pool, Read/Write Split, Query Cache ให้
MySQL 8+ Features ที่ควรรู้
Common Table Expression (CTE)
# CTE ช่วยให้ Query ซับซ้อนอ่านง่ายขึ้น
WITH monthly_sales AS (
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
SUM(total) AS revenue,
COUNT(*) AS order_count
FROM orders
WHERE status = 'delivered'
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
),
monthly_growth AS (
SELECT
month, revenue, order_count,
LAG(revenue) OVER (ORDER BY month) AS prev_revenue
FROM monthly_sales
)
SELECT
month, revenue, order_count,
ROUND((revenue - prev_revenue) / prev_revenue * 100, 1) AS growth_pct
FROM monthly_growth
ORDER BY month DESC;
Window Functions
# จัดอันดับสินค้าขายดีในแต่ละหมวด
SELECT
p.category_id,
p.name,
SUM(oi.quantity) AS total_sold,
ROW_NUMBER() OVER (PARTITION BY p.category_id ORDER BY SUM(oi.quantity) DESC) AS rank_in_category,
RANK() OVER (ORDER BY SUM(oi.quantity) DESC) AS overall_rank,
SUM(SUM(oi.quantity * oi.unit_price)) OVER (PARTITION BY p.category_id) AS category_revenue
FROM products p
JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id, p.category_id, p.name;
# Running Total
SELECT
DATE(created_at) AS order_date,
total,
SUM(total) OVER (ORDER BY created_at) AS running_total,
AVG(total) OVER (ORDER BY created_at ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7d
FROM orders;
JSON Support
# MySQL 8 มี Native JSON Type
CREATE TABLE events (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
event_type VARCHAR(50),
payload JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO events (event_type, payload) VALUES
('purchase', '{"user_id": 42, "items": [{"sku": "ABC", "qty": 2}], "total": 1500}');
# Query JSON Fields
SELECT
event_type,
JSON_EXTRACT(payload, '$.user_id') AS user_id,
payload->>'$.total' AS total, -- ->> คือ shortcut ที่ unquote
JSON_LENGTH(payload, '$.items') AS item_count
FROM events
WHERE payload->>'$.total' > 1000;
# JSON Index (Generated Column)
ALTER TABLE events
ADD COLUMN user_id_gen BIGINT AS (JSON_EXTRACT(payload, '$.user_id')) STORED,
ADD INDEX idx_user_id (user_id_gen);
MariaDB-Specific Features
System-Versioned (Temporal) Tables
# MariaDB รองรับ Temporal Tables - เก็บประวัติทุกการเปลี่ยนแปลงอัตโนมัติ
CREATE TABLE products_versioned (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(300),
price DECIMAL(12,2),
quantity INT
) WITH SYSTEM VERSIONING;
# Query ข้อมูลในอดีต!
SELECT * FROM products_versioned FOR SYSTEM_TIME AS OF '2026-01-15 10:00:00';
# ดูประวัติทั้งหมด
SELECT *, ROW_START, ROW_END
FROM products_versioned FOR SYSTEM_TIME ALL
WHERE id = 42;
ColumnStore Engine
# MariaDB ColumnStore สำหรับ Analytical Workload (OLAP)
# เก็บข้อมูลแบบ Column-oriented แทน Row-oriented
# เหมาะกับ Query ที่ SUM, AVG, COUNT ข้อมูลล้านแถว
CREATE TABLE analytics_events (
event_date DATE,
event_type VARCHAR(50),
user_id BIGINT,
value DECIMAL(12,2)
) ENGINE=ColumnStore;
# Query ที่เร็วมากบน ColumnStore (เพราะอ่านเฉพาะ Column ที่ต้องการ)
SELECT event_date, SUM(value) as total
FROM analytics_events
WHERE event_date BETWEEN '2026-01-01' AND '2026-03-31'
GROUP BY event_date;
คำสั่ง MySQL/MariaDB ที่ DBA ใช้บ่อย (Cheatsheet)
| คำสั่ง | หน้าที่ |
|---|---|
SHOW DATABASES; | ดู Database ทั้งหมด |
SHOW TABLES; | ดู Table ทั้งหมด |
DESCRIBE table; | ดูโครงสร้าง Table |
SHOW CREATE TABLE t; | ดู DDL ของ Table |
SHOW PROCESSLIST; | ดู Connection ที่กำลังทำงาน |
SHOW ENGINE INNODB STATUS; | ดูสถานะ InnoDB |
SHOW GLOBAL STATUS; | ดูสถิติ Server |
SHOW GLOBAL VARIABLES; | ดูค่า Config |
EXPLAIN SELECT ...; | วิเคราะห์ Query Plan |
OPTIMIZE TABLE t; | ปรับ Table ให้เร็วขึ้น |
ANALYZE TABLE t; | อัปเดต Index Statistics |
CHECK TABLE t; | ตรวจสอบ Table |
REPAIR TABLE t; | ซ่อม Table (MyISAM) |
KILL process_id; | ยกเลิก Query ที่ค้าง |
สรุป
MySQL และ MariaDB เป็นฐานข้อมูลที่ Backend Developer ทุกคนต้องเชี่ยวชาญ ไม่ว่าจะใช้ Framework อะไร ภาษาอะไร ทุกระบบต้องการฐานข้อมูลที่เร็ว เสถียร และปลอดภัย การเรียนรู้ตั้งแต่การออกแบบ Schema ที่ดี การทำ Index ให้ถูกต้อง การ Optimize Query ด้วย EXPLAIN ไปจนถึง Replication และ Backup Strategy จะทำให้คุณเป็น Developer ที่มีคุณค่าในตลาดงานสูงมาก
เริ่มต้นด้วยการ Docker run MySQL หรือ MariaDB ขึ้นมา สร้าง Schema ทดลองเขียน Query ลอง EXPLAIN ดู Query Plan แล้วคุณจะเข้าใจว่าทำไม Database Administration ถึงเป็นทักษะที่มีค่าและเป็นที่ต้องการตลอดกาลในอุตสาหกรรม IT
