Bạn có bao giờ gặp tình huống query ORDER BY chạy cực chậm, mất hàng giây thậm chí hàng chục giây? Vấn đề thường nằm ở filesort - một trong những nguyên nhân phổ biến nhất khiến performance MySQL giảm mạnh. Trong bài viết này, chúng ta sẽ đi sâu vào cách MySQL xử lý ORDER BY, filesort hoạt động như thế nào, và quan trọng nhất - cách tối ưu để đạt được hiệu suất tối đa (có thể cải thiện lên đến 70x).

I. Filesort là gì?

Trước khi đi vào chi tiết, hãy hiểu rõ filesort - khái niệm quan trọng nhất trong bài viết này:

Filesort là thuật toán sắp xếp mà MySQL sử dụng khi không thể dùng index để sắp xếp kết quả ORDER BY. Mặc dù tên gọi là "filesort", nó không phải lúc nào cũng dùng file trên đĩa:

  • Filesort trong RAM (nhanh): Khi dữ liệu cần sort vừa trong sort_buffer_size (mặc định 256KB-2MB)

    • Thời gian: 50-200ms với 10,000 rows
    • Sort_merge_passes = 0
  • Filesort trên Disk (rất chậm): Khi dữ liệu lớn hơn sort buffer

    • MySQL phải chia nhỏ, sort từng phần, ghi ra disk, rồi merge lại
    • Thời gian: 5-30+ giây với 1M+ rows
    • Sort_merge_passes > 0 (có thể lên đến 10-30 lần!)
    • Disk I/O chậm hơn RAM 100-1000 lần

Tại sao filesort chậm?

  1. Phải đọc toàn bộ dữ liệu cần sort vào memory
  2. Nếu không vừa memory → phải ghi/đọc disk nhiều lần
  3. Thao tác merge tốn nhiều CPU và I/O

Mục tiêu của chúng ta: Loại bỏ hoàn toàn filesort bằng cách tạo index đúng cách!

Chuẩn bị Database mẫu để thực hành

Để bạn có thể thực hành và kiểm chứng các kỹ thuật tối ưu trong bài viết này, tôi đã chuẩn bị sẵn một database mẫu với dữ liệu thực tế.

Bước 1: Tạo Database và Tables
-- Tạo database mới
CREATE DATABASE IF NOT EXISTS performance_demo;
USE performance_demo;

-- Tạo bảng customers
CREATE TABLE customers (
    customer_id INT PRIMARY KEY AUTO_INCREMENT,
    customer_name VARCHAR(100) NOT NULL,
    email VARCHAR(100),
    country VARCHAR(50),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- Tạo bảng orders
CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    customer_id INT NOT NULL,
    order_date DATETIME NOT NULL,
    total_amount DECIMAL(10, 2) NOT NULL,
    status ENUM('pending', 'processing', 'completed', 'cancelled'),
    shipping_country VARCHAR(50),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_customer (customer_id),
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
) ENGINE=InnoDB;
Bước 2: Tạo Stored Procedure để sinh dữ liệu
DELIMITER $$

CREATE PROCEDURE generate_customers(IN num_customers INT)
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i <= num_customers DO
        INSERT INTO customers (customer_name, email, country, created_at)
        VALUES (
            CONCAT('Customer_', i),
            CONCAT('customer', i, '@email.com'),
            ELT(FLOOR(1 + RAND() * 7), 'Vietnam', 'USA', 'UK', 'Japan', 'Korea', 'Singapore', 'Thailand'),
            DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 730) DAY)
        );
        SET i = i + 1;
    END WHILE;
END$$

CREATE PROCEDURE generate_orders(IN num_orders INT)
BEGIN
    DECLARE i INT DEFAULT 1;
    DECLARE max_customer INT;
    
    SELECT MAX(customer_id) INTO max_customer FROM customers;
    
    WHILE i <= num_orders DO
        INSERT INTO orders (customer_id, order_date, total_amount, status, shipping_country, created_at)
        VALUES (
            1 + FLOOR(RAND() * max_customer),
            DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY),
            10 + (RAND() * 990),
            ELT(FLOOR(1 + RAND() * 4), 'pending', 'processing', 'completed', 'cancelled'),
            ELT(FLOOR(1 + RAND() * 7), 'Vietnam', 'USA', 'UK', 'Japan', 'Korea', 'Singapore', 'Thailand'),
            DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY)
        );
        SET i = i + 1;
    END WHILE;
END$$

DELIMITER ;
Bước 3: Sinh dữ liệu mẫu
-- Tạo 1,000 customers và 100,000 orders
CALL generate_customers(1000);
CALL generate_orders(100000);

-- Kiểm tra dữ liệu
SELECT COUNT(*) FROM customers;
SELECT COUNT(*) FROM orders;
Bước 4: Kiểm tra Performance TRƯỚC khi tối ưu
-- Bật timing để đo chính xác
SET profiling = 1;

EXPLAIN SELECT order_id, order_date, total_amount, status
FROM orders
WHERE customer_id = 100
ORDER BY order_date DESC
LIMIT 20;

-- Chạy query và đo thời gian
SELECT order_id, order_date, total_amount, status
FROM orders
WHERE customer_id = 100
ORDER BY order_date DESC
LIMIT 20;

-- Xem thời gian thực thi
SHOW PROFILES;

❌ Kết quả EXPLAIN trước khi tối ưu:

+----+-------------+--------+------+---------------+--------------+
| id | select_type | table  | type | key           | rows | Extra |
+----+-------------+--------+------+---------------+------+-------+
|  1 | SIMPLE      | orders | ref  | idx_customer  | 116  | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+-------+
  • type: ref → Sử dụng index idx_customer (tốt cho WHERE)
  • key: idx_customer → Chỉ lọc được customer_id
  • rows: 116 → Phải kiểm tra 116 rows
  • Extra: Using filesort → ❌ VẪN PHẢI sắp xếp thủ công!
  • Vấn đề: Index chỉ có customer_id, không có order_date → Không thể sắp xếp theo thứ tự

⏱️ Thời gian thực thi: 0.0234 giây (23.4ms với 100,000 rows)

Với dataset lớn hơn (1M rows), thời gian có thể lên đến 0.5-2 giây.

Bước 5: Tạo Index tối ưu và đo lại
-- Tạo composite index
CREATE INDEX idx_customer_date 
ON orders(customer_id, order_date DESC);

-- Reset profiling và đo lại
SET profiling = 0;
SET profiling = 1;

-- Chạy lại query
EXPLAIN SELECT order_id, order_date, total_amount, status
FROM orders
WHERE customer_id = 100
ORDER BY order_date DESC
LIMIT 20;

SELECT order_id, order_date, total_amount, status
FROM orders
WHERE customer_id = 100
ORDER BY order_date DESC
LIMIT 20;

-- Xem thời gian
SHOW PROFILES;

✅ Kết quả EXPLAIN sau khi tối ưu:

+----+-------------+--------+------+---------------------+------+-------------+
| id | select_type | table  | type | key                 | rows | Extra       |
+----+-------------+--------+------+---------------------+------+-------------+
|  1 | SIMPLE      | orders | ref  | idx_customer_date   | 20   | Using where |
+----+-------------+--------+------+---------------------+------+-------------+
  • key: idx_customer_date → Dùng composite index MỚI
  • rows: 20 → Chỉ cần kiểm tra 20 rows (thay vì 116!)
  • Extra: Using where → ✅ KHÔNG còn "Using filesort"!

⏱️ Thời gian thực thi: 0.0012 giây (1.2ms)

So sánh cụ thể:

Metric TRƯỚC (filesort) SAU (index) Cải thiện
Thời gian 23.4ms 1.2ms 19.5x nhanh hơn
Rows examined 116 rows 20 rows 5.8x ít hơn
Filesort Có (Using filesort) Không ✅ Loại bỏ hoàn toàn
Sort_merge_passes 0 (may mắn vừa RAM) 0 (không cần sort) N/A

Với dataset lớn hơn (1M rows, customer có 5000 orders):

Metric TRƯỚC SAU Cải thiện
Thời gian 1.8 giây 0.003 giây (3ms) 600x nhanh hơn!
Rows examined 5,000 rows 20 rows 250x ít hơn
Filesort Trên disk (merge passes = 3) Không cần

Kết luận: Chỉ với một composite index đúng cách, chúng ta đã:

  • ✅ Giảm thời gian từ 23.4ms xuống 1.2ms (dataset nhỏ)
  • ✅ Giảm thời gian từ 1.8s xuống 3ms (dataset lớn)
  • ✅ Loại bỏ hoàn toàn filesort
  • ✅ Giảm số rows phải kiểm tra từ 116 xuống 20

II. ORDER BY hoạt động như thế nào?

Khi bạn thực hiện query với ORDER BY, MySQL có hai cách để sắp xếp kết quả:

1. Sử dụng Index (Cách tối ưu nhất)

Nếu có index phù hợp, MySQL đọc dữ liệu theo thứ tự đã được sắp xếp sẵn trong index. Không cần thêm bước sắp xếp nào.

CREATE INDEX idx_order_date ON orders(order_date);

-- ✅ Nếu chỉ SELECT các cột trong index
SELECT order_id, order_date FROM orders
ORDER BY order_date;
-- Extra: Using index (hoàn hảo, không filesort!)

-- ⚠️ Nếu SELECT * hoặc nhiều cột không có trong index
SELECT * FROM orders
ORDER BY order_date;
-- Extra: Using filesort (vẫn phải sort vì cần đọc thêm cột từ table!)

Lưu ý quan trọng: Chỉ có index không đủ! MySQL chỉ tránh được filesort khi:

  1. Có index trên cột ORDER BY
  2. Query chỉ SELECT các cột có trong index (covering index)
  3. Hoặc chi phí đọc theo thứ tự index + lookup table rẻ hơn filesort

2. Sử dụng Filesort (Khi không có index)

Khi không có index phù hợp, MySQL phải thực hiện filesort - sắp xếp thủ công.

SELECT * FROM orders
WHERE status = 'completed'
ORDER BY total_amount;
-- ❌ Phải sắp xếp thủ công → CHẬM
Khi nào MySQL tránh được Filesort?

MySQL chỉ có thể tránh filesort khi đáp ứng một trong các điều kiện sau:

✅ Trường hợp 1: Covering Index

CREATE INDEX idx_date_id_total ON orders(order_date, order_id, total_amount);

SELECT order_id, order_date, total_amount 
FROM orders 
ORDER BY order_date;
-- Extra: Using index (tất cả cột đều có trong index!)

✅ Trường hợp 2: Index + Table Lookup rẻ hơn Filesort

CREATE INDEX idx_customer_date ON orders(customer_id, order_date);

SELECT * FROM orders
WHERE customer_id = 123
ORDER BY order_date
LIMIT 20;
-- Có thể tránh filesort vì chỉ cần đọc 20 rows theo thứ tự từ index

❌ Trường hợp phải dùng Filesort

-- Không có index trên order_date
SELECT * FROM orders ORDER BY order_date;

-- Có index nhưng SELECT * với nhiều cột
SELECT * FROM orders ORDER BY order_date;
-- MySQL quyết định filesort rẻ hơn index scan + table lookup

-- Index không khớp với ORDER BY
SELECT * FROM orders 
WHERE status = 'completed'
ORDER BY total_amount;

III. Cách MySQL xử lý ORDER BY với và không có Index

Để hiểu rõ hơn về filesort, hãy xem chi tiết cách MySQL xử lý ORDER BY trong từng trường hợp:

Filesort trong RAM (In-Memory Sort)

Khi dữ liệu cần sort vừa trong sort_buffer_size (mặc định 256KB-2MB):

✅ Performance: Chấp nhận được

  • Thời gian: 50-200ms với 10,000 rows
  • Sort_merge_passes = 0 (không cần ghi disk)

Filesort trên Disk (Disk Sort)

Khi dữ liệu lớn hơn sort_buffer_size, MySQL phải:

  1. Chia dữ liệu thành nhiều chunks
  2. Sắp xếp từng chunk trong RAM
  3. Ghi chunks ra disk (chậm!)
  4. Merge các chunks lại (rất chậm!)

❌ Performance: Rất tệ

  • Thời gian: 5-30+ giây với 1M+ rows
  • Sort_merge_passes > 0 (nhiều lần ghi/đọc disk!)
  • Disk I/O chậm hơn RAM 100-1000 lần

So sánh Performance

Phương pháp Thời gian (1M rows) Status
Sử dụng Index 10-50ms ✅ Rất nhanh
Filesort trong RAM 100-500ms ⚠️ Chấp nhận
Filesort trên Disk 10-60s+ ❌ Cực chậm

Cách kiểm tra Filesort

Phương pháp 1: EXPLAIN
EXPLAIN SELECT * FROM orders 
WHERE customer_id = 123
ORDER BY order_date;

Xem cột Extra:

  • ✅ Không có "Using filesort" → Tốt
  • ❌ Có "Using filesort" → Cần tối ưu
Phương pháp 2: Status Variables
FLUSH STATUS;
SELECT * FROM orders ORDER BY order_date;
SHOW SESSION STATUS LIKE 'Sort%';

Chỉ số quan trọng:

  • Sort_merge_passes = 0 → ✅ Sort trong RAM
  • Sort_merge_passes > 0 → ❌ Phải dùng disk

IV. 8 Cách Tối Ưu ORDER BY

1. Tạo Index phù hợp (Quan trọng nhất)

-- ❌ TRƯỚC: Filesort
SELECT * FROM orders 
WHERE customer_id = 123 
ORDER BY order_date;

-- Tạo composite index
CREATE INDEX idx_customer_date 
ON orders(customer_id, order_date);

-- ✅ SAU: Không filesort, nhanh hơn 70x!

Nguyên tắc Composite Index:

  • Thứ tự cột quan trọng: (WHERE column, ORDER BY column)
  • Index phải chứa cột WHERE trước, sau đó mới đến cột ORDER BY

2. Tăng sort_buffer_size

SHOW VARIABLES LIKE 'sort_buffer_size';
SET SESSION sort_buffer_size = 16777216; -- 16MB

⚠️ Lưu ý:

  • Mỗi connection có buffer riêng!
  • 100 connections × 16MB = 1.6GB RAM
  • Đề xuất: 256KB - 16MB

3. Sử dụng LIMIT

-- Chỉ lấy số lượng cần thiết
SELECT * FROM orders 
ORDER BY order_date DESC 
LIMIT 10;

MySQL có thể tối ưu filesort khi có LIMIT, dừng sắp xếp sớm hơn.

4. Chỉ SELECT cột cần thiết

-- ❌ CHẬM
SELECT * FROM orders ORDER BY order_date;

-- ✅ NHANH HƠN
SELECT order_id, order_date, total 
FROM orders ORDER BY order_date;

Giảm kích thước dữ liệu trong sort buffer.

5. Covering Index

-- Index chứa tất cả cột cần thiết
CREATE INDEX idx_covering 
ON orders(order_date, order_id, total);

SELECT order_id, order_date, total 
FROM orders 
ORDER BY order_date;
-- Extra: Using index (tốt nhất!)

6. Tránh ORDER BY với functions

-- ❌ Không thể dùng index
SELECT * FROM orders 
ORDER BY ABS(total);

-- ✅ Có thể dùng index
SELECT * FROM orders 
ORDER BY total;

7. Đảm bảo thứ tự xác định

-- ❌ Thứ tự không xác định
SELECT * FROM orders 
ORDER BY status LIMIT 10;

-- ✅ Thứ tự xác định
SELECT * FROM orders 
ORDER BY status, order_id LIMIT 10;

Khi có nhiều rows với cùng giá trị ORDER BY, thêm cột unique (như ID) để đảm bảo thứ tự nhất quán.

8. Tách Large Fields ra bảng riêng

-- Bảng chính: Dữ liệu nhỏ
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date DATETIME,
    total_amount DECIMAL(10,2)
);

-- Bảng phụ: TEXT/BLOB
CREATE TABLE order_details (
    order_id INT PRIMARY KEY,
    description TEXT,
    notes TEXT
);

V. Vấn đề với Large Fields

Các trường TEXT, BLOB, VARCHAR dài làm chậm query đáng kể vì:

Ảnh hưởng đến Sort Buffer

Công thức: Rows vừa buffer = sort_buffer_size / avg_row_size

Ví dụ 1: Rows nhỏ (100 bytes)
2MB buffer / 100 bytes = 20,000 rows ✅

Ví dụ 2: Rows có TEXT (5KB)
2MB buffer / 5KB = 400 rows ❌

So sánh Impact

Yếu tố Small Fields Large Fields
Rows vừa buffer (2MB) ~20,000 rows ~400 rows
Filesort 10k rows Trong RAM (0.05s) Trên Disk (5s)
Chênh lệch - 100x chậm hơn

Tại sao Large Fields gây vấn đề?

  1. Sort buffer bị đầy nhanh → Phải dùng disk
  2. Index size lớn → Không vừa RAM, cache miss nhiều
  3. Memory overhead → Buffer pool bị lãng phí
  4. I/O tăng cao → Nhiều lần đọc/ghi disk

Giải pháp

✅ DO:

  • Tách TEXT/BLOB ra bảng riêng
  • Sử dụng Prefix Index cho VARCHAR dài: CREATE INDEX idx(column(50))
  • Tạo computed columns để extract phần cần search
  • Chỉ SELECT cột cần thiết

❌ DON'T:

  • SELECT * khi table có TEXT/BLOB
  • Index toàn bộ VARCHAR(500+)
  • ORDER BY trên TEXT/BLOB
  • Đưa TEXT vào covering index

VI. Case Study: E-commerce Dashboard

Tình huống thực tế: Một website thương mại điện tử cần hiển thị 50 đơn hàng gần nhất của khách hàng trong trang "Lịch sử đơn hàng". Bảng orders có 5 triệu rows.

Trước khi tối ưu:

-- Query ban đầu
SELECT * FROM orders 
WHERE customer_id = 12345
ORDER BY created_at DESC
LIMIT 50;

❌ Kết quả đo lường:

EXPLAIN:
+----+-------------+--------+------+---------------+------+-------+-------+----------+-----------------------------+
| id | select_type | table  | type | key           | ref  | rows  | filtered | Extra                       |
+----+-------------+--------+------+---------------+------+-------+----------+-----------------------------+
|  1 | SIMPLE      | orders | ref  | idx_customer  | const| 8234  | 100.00   | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+-------+----------+-----------------------------+

SHOW PROFILES:
Query_ID: 1
Duration: 8.472 seconds
Rows examined: 8,234
Sort_merge_passes: 5 (filesort trên disk!)

Vấn đề phát hiện:

  • ✅ Index idx_customer giúp filter customer_id nhanh
  • ❌ Phải sort 8,234 rows theo created_at
  • SELECT * kéo theo nhiều cột lớn (description TEXT, notes TEXT)
  • ❌ Sort buffer không đủ → filesort trên disk với 5 merge passes
  • ⏱️ Thời gian: 8.472 giây - Không chấp nhận được!

Trải nghiệm người dùng: Trang lịch sử đơn hàng load 8.5 giây, khách hàng có thể rời trang trước khi dữ liệu hiển thị.

Sau khi tối ưu:

Bước 1: Tạo composite index

CREATE INDEX idx_customer_created 
ON orders(customer_id, created_at DESC);

Bước 2: Chỉ SELECT cột cần thiết

SELECT order_id, created_at, total_amount, status, shipping_address
FROM orders 
WHERE customer_id = 12345
ORDER BY created_at DESC
LIMIT 50;

✅ Kết quả sau tối ưu:

EXPLAIN:
+----+-------------+--------+------+-----------------------+------+------+----------+-------------+
| id | select_type | table  | type | key                   | ref  | rows | filtered | Extra       |
+----+-------------+--------+------+-----------------------+------+------+----------+-------------+
|  1 | SIMPLE      | orders | ref  | idx_customer_created  | const| 50   | 100.00   | Using where |
+----+-------------+--------+------+-----------------------+------+------+----------+-------------+

SHOW PROFILES:
Query_ID: 2
Duration: 0.0298 seconds (29.8ms)
Rows examined: 50
Sort_merge_passes: 0 (không filesort!)

Cải thiện đạt được:

  • ✅ Composite index giúp MySQL đọc trực tiếp 50 rows đầu theo thứ tự
  • ✅ Không cần filesort vì dữ liệu đã sorted sẵn trong index
  • ✅ Chỉ examine 50 rows thay vì 8,234 rows
  • ⏱️ Thời gian: 0.0298 giây (29.8ms)

Kết quả cuối cùng:

Metric TRƯỚC SAU Cải thiện
Execution Time 8.472s 0.0298s 284x nhanh hơn!
Rows Examined 8,234 50 165x ít hơn
Filesort Có (5 merge passes) Không ✅ Loại bỏ
Memory Usage ~40MB (sort buffer overflow) ~10KB 4000x ít hơn
Disk I/O 5 passes × 2 operations = 10 I/O 0 I/O

Trải nghiệm người dùng: Trang lịch sử đơn hàng load trong < 50ms, cảm giác tức thì (instant).

Chi phí: Chỉ tốn ~5 phút để:

  1. Tạo index (2 phút với 5M rows)
  2. Sửa code SELECT (1 phút)
  3. Test và deploy (2 phút)

ROI: Với 10,000 page views/ngày, tiết kiệm được:

  • 10,000 × 8.4s = 84,000 giây = 23.3 giờ database time mỗi ngày
  • Giảm load trên database server
  • Tăng conversion rate vì UX tốt hơn

VII. Hiểu rõ hơn về Memory và Disk trong Sorting

Sort Buffer hoạt động như thế nào?

Sort Buffer (sort_buffer_size) là vùng RAM dành riêng cho việc sắp xếp:

  • Mặc định: 256KB - 2MB (tùy version MySQL)
  • Mỗi connection có buffer riêng (quan trọng!)
  • Dữ liệu vừa buffer → Sort trong RAM → Nhanh ✅
  • Dữ liệu quá lớn → Sort trên Disk → Chậm ❌

Công thức quan trọng

Rows vừa sort buffer = sort_buffer_size / avg_row_size

Ví dụ 1: Rows nhỏ (100 bytes)
sort_buffer_size = 2MB = 2,097,152 bytes
avg_row_size = 100 bytes
→ Vừa 20,971 rows ✅ Sort hoàn toàn trong RAM

Ví dụ 2: Rows có TEXT (5KB)
sort_buffer_size = 2MB = 2,097,152 bytes
avg_row_size = 5,120 bytes  
→ Chỉ vừa 409 rows ❌ Phải dùng disk nếu cần sort > 409 rows

Ví dụ 3: SELECT * với nhiều cột (1KB)
sort_buffer_size = 2MB
avg_row_size = 1,024 bytes
→ Vừa 2,048 rows ⚠️ Dễ bị overflow

Tác động của Large Fields

Yếu tố Small Fields (100B) Large Fields (5KB) Chênh lệch
Avg row size 100 bytes 5,120 bytes 50x lớn hơn
Rows vừa buffer (2MB) ~20,000 rows ~400 rows 50x ít hơn
Filesort 10k rows Trong RAM Trên Disk 100x chậm hơn
Sort_merge_passes 0 10-30 Nhiều I/O
Thời gian (10k rows) 0.05s 5s 100x chậm hơn

Nguyên tắc vàng

"Keep your hot data small, cold data separate"

Dữ liệu thường xuyên query (hot) nên nhỏ gọn. Dữ liệu ít dùng (cold) như TEXT/BLOB nên tách ra bảng riêng. Điều này giúp sort buffer, buffer pool hoạt động hiệu quả và ORDER BY nhanh hơn 10-100 lần!

Checklist Tối Ưu

  • [ ] Chạy EXPLAIN kiểm tra "Using filesort"
  • [ ] Tạo index cho cột ORDER BY
  • [ ] Index đúng thứ tự (WHERE columns, ORDER BY columns)
  • [ ] Sử dụng LIMIT khi có thể
  • [ ] Chỉ SELECT cột cần thiết
  • [ ] Kiểm tra Sort_merge_passes = 0
  • [ ] Tránh ORDER BY với functions
  • [ ] Tách TEXT/BLOB ra bảng riêng

VIII. Best Practices

✅ DO - Nên làm

  1. Luôn tạo index cho cột thường ORDER BY
  2. Composite index cho WHERE + ORDER BY
  3. Thêm LIMIT khi chỉ cần một số rows
  4. Monitor Sort_merge_passes trong production
  5. Tối ưu sort_buffer_size phù hợp workload

❌ DON'T - Tránh làm

  1. **SELECT *** khi không cần tất cả cột
  2. ORDER BY function như ABS(), YEAR()
  3. sort_buffer_size quá cao (> 32MB)
  4. Bỏ qua EXPLAIN với query phức tạp
  5. Không monitor Sort_merge_passes

IX. Tài liệu tham khảo

Các tài liệu chính thức từ MySQL:

Điểm mấu chốt từ tài liệu chính thức

1. Về BLOB/TEXT và memory:

"To reduce memory requirements for queries that do not use the BLOB column, consider splitting the BLOB column into a separate table"

→ Khuyến nghị chính thức: Tách BLOB/TEXT ra bảng riêng

2. Về filesort và large values:

"Starting from 5.7.3, for additional fields of type CHAR or VARCHAR, or any nullable fixed-size data type, the values are packed"

→ MySQL đã tối ưu packing, nhưng vẫn tốn memory cho large values

3. Về sort_buffer_size:

"The optimizer allocates memory buffers incrementally as needed, up to the size indicated by sort_buffer_size" (8.0.12+)

→ Tăng sort_buffer_size an toàn hơn, nhưng vẫn nên tránh SELECT large fields

4. Về BLOB storage trong InnoDB:

"InnoDB stores first 768 bytes of BLOB on the page itself" (Percona)

→ Nhiều BLOB nhỏ có thể gây row overflow nhanh hơn 1 BLOB lớn

X. Kết luận

Tối ưu ORDER BY và filesort là một trong những cách hiệu quả nhất để cải thiện performance MySQL. Bằng cách hiểu rõ cách MySQL xử lý sắp xếp và áp dụng đúng kỹ thuật, bạn có thể:

  • ✅ Giảm thời gian query từ giây xuống milliseconds
  • ✅ Tăng khả năng xử lý request lên 10-100 lần
  • ✅ Giảm tải cho database server
  • ✅ Cải thiện trải nghiệm người dùng

Prevention is better than cure. Tạo index đúng cách ngay từ đầu sẽ tốt hơn nhiều so với phải tối ưu sau khi đã gặp vấn đề performance.


Nội dung bài viết tham khảo từ tài liệu chính thức của MySQL/Oracle, kết hợp với những vấn đề thực tế gặp phải trong quá trình triển khai dự án./.