Tối ưu database là kỹ năng quan trọng mà mọi developer và chủ doanh nghiệp cần quan tâm khi hệ thống bắt đầu chậm. Tin tốt là: phần lớn vấn đề database chậm có thể giải quyết mà không cần viết lại code — chỉ cần tối ưu cấu hình và thêm index đúng chỗ. Theo nghiên cứu từ Percona (2025), 80% vấn đề performance database đến từ thiếu index hoặc query không tối ưu, chỉ 20% cần thay đổi kiến trúc.
Bài viết này chia sẻ 5 kỹ thuật tối ưu database thực tế — mỗi kỹ thuật kèm ví dụ cụ thể và con số cải thiện.
Dấu hiệu database đang chậm
Trước khi tối ưu, cần xác định chính xác vấn đề:
| Triệu chứng | Có thể do database? | Cách xác nhận |
|---|---|---|
| Website load 5+ giây | Có | Kiểm tra TTFB > 2s |
| Admin panel chậm hơn frontend | Rất có thể | Admin query phức tạp hơn |
| Chậm hơn vào giờ cao điểm | Có | Check CPU/RAM lúc peak |
| Trang listing/search chậm | Gần chắc chắn | Query không có index |
| Export báo cáo timeout | Có | Query quét toàn bảng |
| Server CPU 100% | Có thể | Check MySQL processlist |
Cách xác nhận database là bottleneck
-- MySQL: Xem query đang chạy
SHOW PROCESSLIST;
-- Bật slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- Log query chạy > 1 giây
-- Xem slow query log
-- File thường ở /var/log/mysql/mysql-slow.log
Cách 1: Thêm Index đúng chỗ
Vấn đề phổ biến nhất
80% database chậm là do thiếu index. Khi query không có index, database phải quét toàn bộ bảng (full table scan) — giống như đọc hết 1.000 trang sách để tìm 1 từ.
Ví dụ thực tế
Bảng orders: 500.000 records
-- Query: Tìm đơn hàng của khách hàng
SELECT * FROM orders WHERE customer_id = 1234;
Không có index: Quét 500.000 rows → 2.3 giây
-- Thêm index
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
Có index: Tìm trực tiếp → 0.003 giây (nhanh hơn 766 lần)
Xác định query nào cần index
-- EXPLAIN cho biết query scan bao nhiêu rows
EXPLAIN SELECT * FROM orders WHERE customer_id = 1234;
Kết quả EXPLAIN:
| Trường | Tốt | Xấu |
|---|---|---|
| type | ref, const, eq_ref | ALL (full scan) |
| key | Tên index | NULL (không dùng index) |
| rows | Nhỏ (< 100) | Lớn (= tổng records) |
| Extra | Using index | Using filesort, Using temporary |
Composite Index
Khi query filter nhiều cột cùng lúc:
-- Query phổ biến
SELECT * FROM products
WHERE category_id = 5
AND is_active = 1
ORDER BY price ASC;
-- Composite index (thứ tự cột quan trọng!)
CREATE INDEX idx_products_cat_active_price
ON products(category_id, is_active, price);
Quy tắc thứ tự cột trong composite index:
- Cột dùng trong WHERE (equality) trước
- Cột dùng trong WHERE (range) sau
- Cột dùng trong ORDER BY cuối cùng
Cảnh báo: Đừng index quá nhiều
| Số index | Read speed | Write speed | Dung lượng |
|---|---|---|---|
| 0 index | Chậm | Nhanh nhất | Nhỏ nhất |
| 3–5 index | Nhanh | Tốt | +20–30% |
| 10+ index | Nhanh | Chậm | +50–100% |
| 20+ index | Nhanh | Rất chậm | +100–200% |
Khuyến nghị: 3–7 index/bảng là hợp lý cho hầu hết ứng dụng.
Cách 2: Tối ưu Query (không sửa code)
Slow query thường gặp và cách fix
Query 1: SELECT *
-- Sai: Lấy tất cả cột (kể cả không dùng)
SELECT * FROM products WHERE category_id = 5;
-- Đúng: Chỉ lấy cột cần thiết
SELECT id, name, price, thumbnail FROM products WHERE category_id = 5;
Cải thiện: Giảm data transfer 60–80%, đặc biệt khi bảng có cột TEXT/BLOB lớn.
Query 2: N+1 Problem
-- Sai: 1 query lấy orders + N query lấy customer cho mỗi order
SELECT * FROM orders WHERE status = 'pending';
-- Rồi loop:
SELECT * FROM customers WHERE id = ?; -- Chạy N lần
-- Đúng: 1 query với JOIN
SELECT o.*, c.name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'pending';
Cải thiện: Từ 101 queries → 1 query. Thời gian từ 3 giây → 0.05 giây.
Query 3: LIKE ‘%keyword%’
-- Sai: Leading wildcard → không dùng được index
SELECT * FROM products WHERE name LIKE '%áo thun%';
-- Tốt hơn: Full-text search
ALTER TABLE products ADD FULLTEXT(name, description);
SELECT * FROM products WHERE MATCH(name, description) AGAINST('áo thun');
Cải thiện: Từ full table scan → full-text index. 10x–100x nhanh hơn.
Query 4: Subquery có thể thay bằng JOIN
-- Chậm: Subquery chạy cho mỗi row
SELECT * FROM products
WHERE id IN (SELECT product_id FROM order_items WHERE quantity > 10);
-- Nhanh: JOIN
SELECT DISTINCT p.*
FROM products p
JOIN order_items oi ON p.id = oi.product_id
WHERE oi.quantity > 10;
Công cụ phân tích query
| Tool | Sử dụng | Miễn phí? |
|---|---|---|
| EXPLAIN / EXPLAIN ANALYZE | Xem execution plan | Có |
| MySQL Slow Query Log | Log query chậm | Có |
| pt-query-digest | Phân tích slow log chi tiết | Có (Percona) |
| phpMyAdmin | Giao diện đồ họa analyze | Có |
| MySQL Workbench | Visual EXPLAIN | Có |
| pgAdmin | PostgreSQL query analyzer | Có |
Cách 3: Cấu hình MySQL/PostgreSQL cho performance
MySQL/MariaDB tuning
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
# InnoDB Buffer Pool - CÁI QUAN TRỌNG NHẤT
# Đặt = 70-80% RAM nếu server chỉ chạy MySQL
innodb_buffer_pool_size = 1G # Cho VPS 2GB RAM
# InnoDB Log
innodb_log_file_size = 256M
innodb_log_buffer_size = 64M
# Connection
max_connections = 150
wait_timeout = 300
interactive_timeout = 300
# Query Cache (MySQL 5.7, đã bỏ ở 8.0)
# query_cache_type = 1
# query_cache_size = 64M
# Temp tables
tmp_table_size = 64M
max_heap_table_size = 64M
# Sort & Join buffer
sort_buffer_size = 4M
join_buffer_size = 4M
read_rnd_buffer_size = 4M
# Thread
thread_cache_size = 16
Mức cải thiện từ tuning cấu hình
| Thông số | Mặc định | Tối ưu | Cải thiện |
|---|---|---|---|
| innodb_buffer_pool_size | 128MB | 1GB (VPS 2GB) | Lên đến 10x cho read |
| innodb_log_file_size | 48MB | 256MB | 2–5x cho write |
| max_connections | 151 | 150 (+ connection pooling) | Tránh OOM |
| sort_buffer_size | 256KB | 4MB | 2–3x cho ORDER BY |
PostgreSQL tuning
# /etc/postgresql/15/main/postgresql.conf
# Memory
shared_buffers = 512MB # 25% RAM
effective_cache_size = 1536MB # 75% RAM
work_mem = 16MB # Per-operation memory
maintenance_work_mem = 256MB # VACUUM, CREATE INDEX
# WAL
wal_buffers = 16MB
checkpoint_completion_target = 0.9
# Planner
random_page_cost = 1.1 # SSD (mặc định 4.0 cho HDD)
effective_io_concurrency = 200 # SSD
# Connections
max_connections = 100
Cách 4: Caching Layer
Redis/Memcached: Không cần query lại database
Thay vì query database mỗi lần user truy cập, cache kết quả trong memory:
| So sánh | Database query | Redis cache |
|---|---|---|
| Thời gian | 50–500ms | 0.1–1ms |
| Throughput | 1.000 queries/giây | 100.000 queries/giây |
| CPU usage | Cao | Thấp |
Chiến lược caching
| Chiến lược | Mô tả | Phù hợp |
|---|---|---|
| Cache-aside | App check cache trước, nếu miss thì query DB rồi cache | Phổ biến nhất |
| Write-through | Ghi vào cache + DB cùng lúc | Data consistency cao |
| Write-behind | Ghi vào cache trước, async ghi DB sau | Write-heavy |
| TTL-based | Cache tự hết hạn sau N giây | Đơn giản, hiệu quả |
Dữ liệu nên cache
| Nên cache | Không nên cache |
|---|---|
| Trang chủ, category listing | Giỏ hàng (thay đổi liên tục) |
| Thông tin sản phẩm | Số lượng tồn kho real-time |
| Menu, navigation | Session data nhạy cảm |
| Kết quả tìm kiếm phổ biến | Dữ liệu cá nhân |
| Cấu hình website | Dữ liệu tài chính |
Kết quả thực tế
Một website e-commerce (200.000 products, 50.000 visitors/ngày):
| Metric | Trước cache | Sau Redis cache | Cải thiện |
|---|---|---|---|
| Page load (listing) | 3.2 giây | 0.4 giây | 8x |
| Database CPU | 85% | 25% | -71% |
| Database queries/giây | 2.500 | 400 | -84% |
| Response time P95 | 5.1 giây | 0.8 giây | 6.4x |
Cách 5: Bảo trì database định kỳ
Các tác vụ bảo trì
| Tác vụ | Tần suất | Lệnh MySQL | Hiệu quả |
|---|---|---|---|
| ANALYZE TABLE | Hàng tuần | ANALYZE TABLE products; | Cập nhật thống kê cho optimizer |
| OPTIMIZE TABLE | Hàng tháng | OPTIMIZE TABLE orders; | Giảm fragmentation, thu gọn file |
| Xóa data cũ | Hàng tháng | Archive + DELETE | Giảm kích thước bảng |
| Check slow queries | Hàng tuần | Review slow query log | Phát hiện query mới chậm |
| Backup + test restore | Hàng ngày/tuần | mysqldump, pg_dump | Đảm bảo data an toàn |
Automatic maintenance script
#!/bin/bash
# /opt/db-maintenance.sh
# Chạy hàng tuần (crontab: 0 4 * * 0)
MYSQL_USER="admin"
MYSQL_PASS="password"
DB_NAME="production_db"
echo "=== Database Maintenance: $(date) ==="
# Analyze all tables
mysql -u$MYSQL_USER -p$MYSQL_PASS $DB_NAME -e "
SELECT CONCAT('ANALYZE TABLE ', table_name, ';')
FROM information_schema.tables
WHERE table_schema = '$DB_NAME'" | mysql -u$MYSQL_USER -p$MYSQL_PASS $DB_NAME
# Check for tables needing optimization
mysql -u$MYSQL_USER -p$MYSQL_PASS $DB_NAME -e "
SELECT table_name, data_free/1024/1024 as fragmented_mb
FROM information_schema.tables
WHERE table_schema = '$DB_NAME'
AND data_free > 10*1024*1024
ORDER BY data_free DESC;"
echo "=== Maintenance Complete ==="
Archiving old data
-- Chuyển đơn hàng > 2 năm sang bảng archive
INSERT INTO orders_archive SELECT * FROM orders WHERE created_at < DATE_SUB(NOW(), INTERVAL 2 YEAR);
DELETE FROM orders WHERE created_at < DATE_SUB(NOW(), INTERVAL 2 YEAR);
-- Kết quả: Bảng orders từ 5M rows → 1.5M rows
-- Query nhanh hơn 2–3x
Tổng kết: Lộ trình tối ưu database
| Bước | Thao tác | Thời gian | Cải thiện kỳ vọng |
|---|---|---|---|
| 1 | Bật slow query log, xác định query chậm | 30 phút | Baseline |
| 2 | Thêm index cho query chậm nhất | 1–2 giờ | 5–100x |
| 3 | Tối ưu cấu hình MySQL/PostgreSQL | 1 giờ | 2–10x |
| 4 | Triển khai Redis caching | 2–4 giờ | 5–20x |
| 5 | Setup bảo trì tự động | 1 giờ | Duy trì performance |
Tổng thời gian: 5–8 giờ. Tổng chi phí: 0 VND (nếu tự làm) hoặc 5–15 triệu VND (thuê chuyên gia).
FAQ — Câu hỏi thường gặp
Database bao nhiêu records thì bắt đầu chậm?
Không có con số cụ thể — phụ thuộc vào indexing, cấu hình server, và query complexity. MySQL/PostgreSQL xử lý tốt hàng chục triệu records nếu index đúng. Nhiều hệ thống chạy mượt với 100M+ records. Nếu database chậm ở mức vài trăm nghìn records, vấn đề chắc chắn là do thiếu index hoặc query sai.
Có nên dùng ORM hay viết raw SQL?
ORM (Eloquent, Sequelize, TypeORM) tốt cho development speed và bảo trì. Nhưng ORM đôi khi generate query không tối ưu (N+1 problem). Khuyến nghị: dùng ORM cho CRUD đơn giản, viết raw SQL cho query phức tạp và báo cáo. Luôn check query mà ORM generate bằng EXPLAIN.
Redis vs Memcached — dùng cái nào?
Redis: data structures phong phú (string, list, set, hash), persistence, pub/sub. Memcached: đơn giản hơn, nhanh hơn một chút cho simple key-value. 90% trường hợp: chọn Redis — linh hoạt hơn, tính năng nhiều hơn, và có thể dùng cho cả caching, session, queue.
Kết luận
Database chậm không có nghĩa cần viết lại ứng dụng. 5 kỹ thuật trong bài viết này — indexing, query optimization, server tuning, caching, và maintenance — có thể cải thiện performance 10–100x mà không thay đổi 1 dòng code ứng dụng.
Nếu bạn cần hỗ trợ tối ưu database hoặc xây dựng hệ thống hiệu suất cao cho doanh nghiệp, hãy liên hệ Trinh Digital. Chúng tôi đã giúp nhiều SME Việt Nam tăng tốc hệ thống mà không cần thay đổi tech stack.