Bỏ qua menu, tới nội dung
Thành viên The MONA Group
Thành viên The MONA Group
Sản phẩm và viết theo yêu cầu
1900 636 648Tư vấn miễn phí, 8h–22h
MONA OS 2.0Hệ thống quản trị MONA tự phát triển, nối 7 phần mềm nội bộ về một dữ liệu, có Gấu Quản Trị trả lời câu hỏi điều hành.7 phần mềm1 trục dữ liệu8 AI AgentXem MONA OS hoạt động
Dự án giáo dụcThe IELTS WorkshopPhần mềm quản lý chuỗi 11 cơ sở luyện thi, kèm app cho học viên20238 tháng12 ngườiXem hồ sơ dự án Xem thêm dự án Khoa Y
Dự án bán lẻHình minh hoạ gấu MONA bán trang sức tại quầy kính, cạnh màn hình bán hàng và tồn khoNgọc Thành JewelryPhần mềm quản lý chuỗi cửa hàng trang sức, tích hợp xuất hoá đơn20246 tháng7 ngườiXem hồ sơ dự án
Dự án sản xuấtHình minh hoạ gấu MONA hướng dẫn công nhân học bài trên phần mềm đào tạo trong phòng họp nhà máyPolytex Far Eastern Việt NamPhần mềm đào tạo công nhân viên chạy trong mạng nội bộ nhà máy20234 tháng8 ngườiXem hồ sơ dự án
Dự án nhân sựHình minh hoạ gấu MONA đứng lớp đào tạo cho bác sĩ, điều dưỡng bằng phần mềm học trực tuyếnBệnh viện Hoàn Mỹ Sài GònViệc đào tạo nhân sự chuyển từ Excel sang phần mềm MONA viết riêng20247 tuần13 moduleXem hồ sơ dự án
Sản phẩm có sẵnHình minh hoạ gấu MONA phục vụ bàn, khách quét mã QR trên bàn để gọi mónnhahang.aiGọi món tại bàn bằng mã QR, kết nối bếp, quản lý bàn, định lượng nguyên liệuNhà hàngQuán ănChuỗi ăn uốngXem nhahang.ai
Sản phẩm có sẵnMONA HouseTự lập hoá đơn tiền phòng, gửi mã QR qua Zalo, báo có khi khách chuyển khoảnNhà trọCăn hộ cho thuêXem MONA House
Sản phẩm có sẵnMONA NHTQĐặt hàng Taobao, 1688, Tmall, ví khách hàng và theo dõi vận đơn cho công ty nhập hàngTaobao1688TmallXem MONA NHTQ
Tư vấn miễn phí, 8h–22h mỗi ngày
Kiến thức phần mềm

Báo cáo chạy chậm khi dữ liệu lớn: đọc EXPLAIN và đặt index

MONA Software biên soạnCập nhật · Đăng · 14 phút đọc

Báo cáo chạy chậm khi dữ liệu lớn: đọc EXPLAIN và đặt index

Báo cáo doanh thu theo ngày của một cửa hàng chạy 181,6 ms (median của 10 lần chạy) trên bảng 1.000.000 đơn, vì MySQL đọc cả bảng để lấy ra 596 dòng cần dùng. Một index ba cột (store_id, status, created_at) đưa thời gian xuống 0,965 ms. Cột so sánh bằng đứng trước, cột khoảng ngày đứng cuối; đảo thứ tự vẫn nhanh hơn không có index, nhưng chậm hơn 5,8 đến 6,7 lần so với thứ tự đúng.

Mọi lệnh trong bài chạy trong một database riêng, đã xoá sau khi đo. Thời gian ghi là median của 10 lần chạy thường, trừ khi ghi rõ là số trong cây EXPLAIN ANALYZE (một lần chạy riêng). Các số trong bài đo trên bảng thử, trên một laptop Apple M2 (8 lõi, 8 GB RAM, macOS 14.4.1) chạy MySQL 8.4.8 với InnoDB. Đó không phải số của hệ thống MONA hay của khách hàng, và chưa kiểm trên MariaDB. Người đọc cần biết SELECT, WHERE, GROUP BY của ngôn ngữ SQL và biết index là gì; không cần biết quản trị cơ sở dữ liệu.

Dựng bảng thử 1.000.000 đơn và chạy báo cáo khi chưa có index

Bảng orders có 50 cửa hàng, ngày tạo rải đều từ 01/01/2025 đến 31/12/2026, 70,07% đơn (700.707 trên 1.000.000) ở trạng thái paid. Các cột ngẫu nhiên sinh bằng CRC32, vì RAND(n) với hạt giống liên tiếp cho giá trị gần nhau và làm cột trạng thái trùng với ngày. Nếu bảng thật được nạp từ file Excel, cách làm sạch và chuyển dữ liệu nằm trong bài chuyển dữ liệu từ Excel sang phần mềm quản lý. Lệnh dưới tạo bảng và nạp 1.000.000 dòng bằng phép nối chéo của 6 bản bảng digits (mỗi bản 10 dòng):

CREATE DATABASE blog_lab_explain;
USE blog_lab_explain;

CREATE TABLE orders (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  store_id SMALLINT UNSIGNED NOT NULL,
  customer_id INT UNSIGNED NOT NULL,
  status VARCHAR(12) NOT NULL,
  total_amount DECIMAL(12,2) NOT NULL,
  created_at DATETIME NOT NULL,
  note VARCHAR(100) NOT NULL
) ENGINE=InnoDB;

CREATE TABLE digits (d TINYINT UNSIGNED PRIMARY KEY);
INSERT INTO digits VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9);

INSERT INTO orders (store_id, customer_id, status, total_amount, created_at, note)
SELECT 1 + (n MOD 50),
       1 + CRC32(CONCAT('c',n)) MOD 200000,
       ELT(1 + CRC32(CONCAT('s',n)) MOD 10, 'paid','paid','paid','paid','paid','paid','paid','pending','cancelled','refunded'),
       50000 + CRC32(CONCAT('a',n)) MOD 4950000,
       TIMESTAMP('2025-01-01') + INTERVAL (CRC32(CONCAT('d',n)) MOD (730*86400)) SECOND,
       'don hang mau'
FROM (SELECT a.d + 10*b.d + 100*c.d + 1000*d.d + 10000*e.d + 100000*f.d AS n
      FROM digits a, digits b, digits c, digits d, digits e, digits f) t;
ANALYZE TABLE orders;

Bảng và dữ liệu giữ nguyên trong mọi lần đo sau. Báo cáo cần đo là doanh thu theo ngày của cửa hàng số 7, chỉ tính đơn paid, trong tháng 9/2026. Bộ lọc theo một cửa hàng cũng là cách phần mềm phân quyền người dùng cho quản lý chi nhánh chỉ xem dữ liệu của chi nhánh mình:

SELECT DATE(created_at) AS day, COUNT(*) AS orders, SUM(total_amount) AS revenue
FROM orders
WHERE store_id = 7 AND status = 'paid'
  AND created_at >= '2026-09-01' AND created_at < '2026-10-01'
GROUP BY DATE(created_at) ORDER BY day;

Số dòng khớp từng điều kiện là số đếm thật bằng COUNT(*) trên bảng thử, không phải ước tính của EXPLAIN, và cho thấy báo cáo chỉ cần 596 trong 1.000.000 dòng. Mỗi số là kết quả của một câu COUNT(*) với đúng các điều kiện ghi ở cột trái:

Điều kiện Số dòng khớp
Cửa hàng 7 20.000
Cửa hàng 7 và paid 14.071
Tháng 9/2026 (mọi cửa hàng, mọi trạng thái) 41.544
paid và tháng 9/2026 29.162
Cửa hàng 7 và tháng 9/2026 811
Cả ba điều kiện (kết quả của báo cáo) 596

Đọc EXPLAIN và EXPLAIN ANALYZE của báo cáo chưa có index

Đặt EXPLAIN trước câu SELECT: MySQL in kế hoạch thực thi mà không chạy câu lệnh. Kết quả trên bảng chưa có index, nguyên văn từ máy khách mysql:

+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra                                        |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------+
|  1 | SIMPLE      | orders | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 995845 |     0.11 | Using where; Using temporary; Using filesort |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+----------------------------------------------+

Khi type là ALL và rows gần bằng cả bảng trong khi kết quả chỉ có vài trăm dòng, truy vấn đang quét thừa. Bốn cột cần đọc trước, điền từ kế hoạch trên (định nghĩa từng cột theo tài liệu MySQL 8.4):

Cột Giá trị ở đây Cách đọc
type ALL Quét cả bảng. Tài liệu MySQL xếp ALL cuối danh sách kiểu truy cập, kém nhất.
key NULL Không có index nào được dùng.
rows 995.845 Số dòng MySQL ước tính phải duyệt. Bảng có đúng 1.000.000 dòng; hai lần EXPLAIN trên cùng bảng ra 995.845 (báo cáo chưa có index) và 995.638 (câu thiếu cửa hàng, ở phần sau); đây là ước tính, không phải số đếm.
Extra Using where; Using temporary; Using filesort Mỗi dòng đọc xong mới bị lọc; GROUP BY dùng bảng tạm; kết quả được sắp xếp thêm một lượt.

Tài liệu MySQL xếp các kiểu type từ tốt đến kém, gồm range, index và ALL:

Trang MySQL liệt kê các kiểu type của EXPLAIN từ range, index đến ALL
Phần “EXPLAIN Join Types” của tài liệu MySQL 8.4 mô tả range, index và ALL. Nguồn: dev.mysql.com, chụp ngày 26/10/2026.

Con số ước tính cần được đối chiếu với thực tế. EXPLAIN ANALYZE chạy câu lệnh thật và in thêm thời gian cùng số dòng thực của từng bước (tài liệu MySQL 8.4 về EXPLAIN ANALYZE). Kết quả cho báo cáo trên:

-> Sort: `day`  (actual time=213..213 rows=30 loops=1)
    -> Table scan on <temporary>  (actual time=213..213 rows=30 loops=1)
        -> Aggregate using temporary table  (actual time=213..213 rows=30 loops=1)
            -> Filter: ((orders.store_id = 7) and (orders.`status` = 'paid') and (orders.created_at >= TIMESTAMP'2026-09-01 00:00:00') and (orders.created_at < TIMESTAMP'2026-10-01 00:00:00'))  (cost=100618 rows=1106) (actual time=0.317..213 rows=596 loops=1)
                -> Table scan on orders  (cost=100618 rows=995845) (actual time=0.237..186 rows=1e+6 loops=1)

Dòng cuối cho thấy, trong lần chạy này, 186 ms nằm ở việc đọc 1.000.000 dòng (rows=1e+6), còn bước Filter phía trên chỉ giữ lại 596. MySQL đọc khoảng 1.678 dòng cho mỗi dòng trả về (1.000.000 chia 596). Cây này là một lần chạy riêng (213 ms ở nút ngoài cùng, lần hai 200 ms) và dùng để đọc số dòng; so sánh thời gian trong bài dùng median của 10 lần chạy, ở đây là 181,6 ms. Dùng EXPLAIN ANALYZE để so số dòng và lần chạy thường để so thời gian.

Thêm một index ba cột và đo lại

Báo cáo lọc bằng dấu = trên store_id và status, rồi lọc khoảng trên created_at. Đặt index theo đúng thứ tự đó. Trong khối lệnh sau, câu SELECT của EXPLAIN được viết gọn là <câu báo cáo ở trên>:

ALTER TABLE orders ADD INDEX ix_store_status_created (store_id, status, created_at);
ANALYZE TABLE orders;
EXPLAIN <câu báo cáo ở trên>;

+----+-------------+--------+------------+-------+-------------------------+-------------------------+---------+------+------+----------+--------------------------------------------------------+
| id | select_type | table  | partitions | type  | possible_keys           | key                     | key_len | ref  | rows | filtered | Extra                                                  |
+----+-------------+--------+------------+-------+-------------------------+-------------------------+---------+------+------+----------+--------------------------------------------------------+
|  1 | SIMPLE      | orders | NULL       | range | ix_store_status_created | ix_store_status_created | 57      | NULL |  596 |   100.00 | Using index condition; Using temporary; Using filesort |
+----+-------------+--------+------------+-------+-------------------------+-------------------------+---------+------+------+----------+--------------------------------------------------------+

Nhánh Index range scan thay cho nhánh Table scan của kế hoạch trước. Kết quả EXPLAIN ANALYZE của báo cáo có index, chạy lần thứ hai:

-> Sort: `day`  (actual time=1.4..1.4 rows=30 loops=1)
    -> Table scan on <temporary>  (actual time=1.39..1.39 rows=30 loops=1)
        -> Aggregate using temporary table  (actual time=1.39..1.39 rows=30 loops=1)
            -> Index range scan on orders using ix_store_status_created over (store_id = 7 AND status = 'paid' AND '2026-09-01 00:00:00' <= created_at < '2026-10-01 00:00:00'), with index condition: ((orders.store_id = 7) and (orders.`status` = 'paid') and (orders.created_at >= TIMESTAMP'2026-09-01 00:00:00') and (orders.created_at < TIMESTAMP'2026-10-01 00:00:00'))  (cost=268 rows=596) (actual time=0.171..1.26 rows=596 loops=1)

Cột key_len cho biết bao nhiêu cột của index đã được dùng. Số 57 là tổng của store_id (2 byte), status (50 byte: VARCHAR(12) ở bộ ký tự utf8mb4 là 12 × 4 + 2) và created_at (5 byte), tức cả ba cột. Nếu key_len ngắn hơn tổng này, MySQL đã dừng ở một cột phía trước, như ở trường hợp bọc hàm bên dưới. So sánh trước và sau, mỗi số có nguồn là lần đo ghi ở đầu bài:

Chỉ số Chưa có index Có index ba cột
type ALL range
key NULL ix_store_status_created
key_len NULL 57
rows (ước tính) 995.845 596
Median của 10 lần chạy thường 181,6 ms 0,965 ms
Dòng server phải xử lý (Rows_examined từ performance_schema) 1.000.030 626

Cách đo: một phiên MySQL chạy liên tiếp 11 câu giống nhau, đọc TIMER_WAIT của 10 câu cuối từ performance_schema.events_statements_history, lấy median. Bốn thứ tự index trong bảng so sánh thứ tự cột cũng được đo bằng đúng quy trình này, với một index duy nhất trên bảng ở mỗi lần đo. Đo lại với tên index cuối, ix_store_status_created, median là 0,82 ms. Hai lần EXPLAIN ANALYZE liên tiếp ngay sau khi tạo index ra 2,12 và 1,28 ms ở nút ngoài cùng, nên hãy chạy hai lần rồi mới đọc. Dòng Using temporary; Using filesort vẫn còn vì GROUP BY DATE(created_at) gom theo kết quả của một hàm, nhưng bước đó chỉ xử lý 596 dòng và nằm trong 1,4 ms của nút ngoài cùng ở khối trên.

Thứ tự cột: cột so sánh bằng trước, cột khoảng ngày sau cùng

Quy tắc nằm ở trang tối ưu khoảng giá trị của MySQL. Với index nhiều cột, bộ tối ưu chỉ dùng thêm cột kế tiếp khi phép so sánh là =. Gặp >, <, BETWEEN hay LIKE, MySQL vẫn dùng phép so sánh đó nhưng bỏ các cột phía sau (Range Optimization, MySQL 8.4). Với báo cáo này, created_at là cột khoảng nên phải đứng cuối.

Trang MySQL nêu quy tắc khoảng giá trị chặn các cột phía sau của index nhiều cột
Ví dụ trong tài liệu MySQL: key_part2 >= 10 được dùng, key_part3 > 10 thì không. Nguồn: dev.mysql.com, chụp ngày 26/10/2026.

Để kiểm quy tắc, cùng bảng và cùng câu báo cáo được chạy với bốn thứ tự cột và với hai index đơn lẻ, mỗi lần chỉ giữ index của phương án đang đo. Cột cuối là số mục index hoặc dòng nằm trong khoảng MySQL phải duyệt, lấy từ các số đếm ở bảng đầu bài và từ cây EXPLAIN ANALYZE của phương án hai index:

Thứ tự cột của index Median 10 lần rows trong EXPLAIN Mục index trong khoảng phải duyệt
Không có index 181,6 ms 995.845 1.000.000 (đọc cả bảng)
(store_id, status, created_at) 0,965 ms 596 596
(store_id, created_at, status) 1,355 ms 811 811
(status, created_at, store_id) 5,56 ms 56.052 29.162
(created_at, store_id, status) 6,435 ms 83.884 41.544
Hai index riêng (store_id) và (created_at) 20,82 ms 38.040 20.000 (toàn bộ đơn của cửa hàng 7)

Kết quả sửa giả thuyết ban đầu của bài: thứ tự sai không vô dụng. Khoảng ngày tháng 9 chỉ chứa 4,15% bảng (41.544 chia 1.000.000), nên hai index đặt khoảng ngày trước vẫn nhanh gấp 28 đến 33 lần không có index (181,6 chia 6,435 và chia 5,56). Nhưng chúng phải duyệt 29.162 đến 41.544 mục để giữ lại 596 dòng, nên chậm hơn thứ tự đúng 5,8 đến 6,7 lần (6,435 và 5,56 chia 0,965). Thứ tự (store_id, created_at, status), với status ở cuối, chỉ chậm hơn một chút (1,355 so với 0,965 ms) vì cửa hàng 7 và tháng 9 chỉ còn 811 mục, và status được kiểm trong 811 mục đó. Hai index đơn lẻ không thay được index nhiều cột: MySQL chỉ chọn ix_store, đọc 20.000 dòng của cửa hàng 7 rồi lọc status và ngày, nên median là 20,82 ms, gấp 21,6 lần 0,965 ms (20,82 chia 0,965).

Báo cáo xuất nhập của phần mềm quản lý kho, lọc theo một kho bằng = rồi theo khoảng ngày, có cùng dạng này nên dùng cùng thứ tự cột. Một index (store_id, status, created_at) còn phục vụ truy vấn chỉ có store_id, hoặc có store_id và status, vì các cột đó là tiền tố bên trái của index. Tài liệu MySQL về cách dùng index nêu đúng ví dụ này với index ba cột:

Trang MySQL giải thích index giúp tìm dòng theo WHERE và quy tắc tiền tố bên trái
Tài liệu MySQL 8.4 về cách MySQL dùng index: index (col1, col2, col3) tra được theo col1, (col1, col2) và cả ba cột. Nguồn: dev.mysql.com, chụp ngày 26/10/2026.

Hai trường hợp có index mà vẫn quét cả bảng hoặc bỏ cột cuối

Trường hợp thứ nhất: truy vấn thiếu cột đứng đầu index. Triệu chứng là type vẫn là ALL dù possible_keys liệt kê index. Cách kiểm là đọc cột key, không đọc possible_keys. Báo cáo toàn chuỗi bỏ điều kiện store_id và vẫn tính SUM(total_amount), cột không nằm trong index:

EXPLAIN SELECT DATE(created_at) AS day, COUNT(*) AS orders, SUM(total_amount) AS revenue
FROM orders WHERE status = 'paid' AND created_at >= '2026-09-01' AND created_at < '2026-10-01'
GROUP BY DATE(created_at) ORDER BY day;

+----+-------------+--------+------------+------+-------------------------+------+---------+------+--------+----------+----------------------------------------------+
| id | select_type | table  | partitions | type | possible_keys           | key  | key_len | ref  | rows   | filtered | Extra                                        |
+----+-------------+--------+------------+------+-------------------------+------+---------+------+--------+----------+----------------------------------------------+
|  1 | SIMPLE      | orders | NULL       | ALL  | ix_store_status_created | NULL | NULL    | NULL | 995638 |     1.11 | Using where; Using temporary; Using filesort |
+----+-------------+--------+------------+------+-------------------------+------+---------+------+--------+----------+----------------------------------------------+

Trong cây EXPLAIN ANALYZE, câu này ra 341 ms và 405 ms ở hai lần chạy, cao hơn 213 ms và 200 ms của báo cáo một cửa hàng khi chưa có index, vì phải gom 29.162 dòng thay cho 596. Sửa bằng cách thêm điều kiện cột đầu vào truy vấn nếu nghiệp vụ cho phép, hoặc tạo index riêng bắt đầu bằng cột có trong WHERE (cách này chưa đo trên bảng thử). Tài liệu MySQL còn một ngoại lệ gọi là skip scan: truy vấn chỉ đọc cột có trong index, một bảng, không GROUP BY và không DISTINCT. Cùng index đó, câu SELECT COUNT(*) với status và tháng 9 cho key_len 52 và Extra là Using index for skip scan: truy vấn này được index giúp, còn truy vấn kế bên thì không.

Trang Use The Index Luke giải thích index nối nhiều cột và truy vấn thiếu cột đầu
Trang “Concatenated Indexes” của Use The Index, Luke kể cùng chuyện bằng bảng nhân viên: truy vấn chỉ có subsidiary_id, cột thứ hai của index, phải đọc cả bảng. Nguồn: use-the-index-luke.com, chụp ngày 26/10/2026.

Trường hợp thứ hai: bọc cột trong hàm. Kế hoạch vẫn dùng index, vì vậy lỗi này khó thấy khi chỉ đọc cột key. Viết DATE(created_at) >= '2026-09-01' thay cho khoảng trên cột trần làm MySQL bỏ cột created_at của index. Kiểm bằng key_len:

EXPLAIN SELECT COUNT(*) FROM orders
WHERE store_id = 7 AND status = 'paid'
  AND DATE(created_at) >= '2026-09-01' AND DATE(created_at) < '2026-10-01';

+----+-------------+--------+------------+------+-------------------------+-------------------------+---------+-------------+-------+----------+--------------------------+
| id | select_type | table  | partitions | type | possible_keys           | key                     | key_len | ref         | rows  | filtered | Extra                    |
+----+-------------+--------+------------+------+-------------------------+-------------------------+---------+-------------+-------+----------+--------------------------+
|  1 | SIMPLE      | orders | NULL       | ref  | ix_store_status_created | ix_store_status_created | 52      | const,const | 28774 |   100.00 | Using where; Using index |
+----+-------------+--------+------------+------+-------------------------+-------------------------+---------+-------------+-------+----------+--------------------------+

EXPLAIN SELECT COUNT(*) FROM orders
WHERE store_id = 7 AND status = 'paid'
  AND created_at >= '2026-09-01' AND created_at < '2026-10-01';

+----+-------------+--------+------------+-------+-------------------------+-------------------------+---------+------+------+----------+--------------------------+
| id | select_type | table  | partitions | type  | possible_keys           | key                     | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+--------+------------+-------+-------------------------+-------------------------+---------+------+------+----------+--------------------------+
|  1 | SIMPLE      | orders | NULL       | range | ix_store_status_created | ix_store_status_created | 57      | NULL |  607 |   100.00 | Using where; Using index |
+----+-------------+--------+------------+-------+-------------------------+-------------------------+---------+------+------+----------+--------------------------+

key_len là 52 (store_id cộng status), không phải 57, và rows ước tính là 28.774 thay cho 607 của câu viết bằng khoảng trần (cả hai câu đều đếm ra 596 dòng). Sửa bằng cách giữ created_at >= '2026-09-01' AND created_at < '2026-10-01', đúng dạng của báo cáo ban đầu.

Index đổi lấy dung lượng và tốc độ ghi

Index ba cột chiếm 29,6 MB, trong khi dữ liệu bảng chiếm 65,6 MB (đọc từ mysql.innodb_index_stats và information_schema.tables). Thêm 100.000 dòng bằng INSERT ... SELECT, lặp 5 lần: median là 0,44 giây khi chưa có index và 1,06 giây khi có, tức chậm gấp 2,4 lần (1,06 chia 0,44). Hai index đơn lẻ chiếm 17,5 và 20,5 MB (tổng 38,0 MB) và INSERT như trên có median 0,94 giây. Bảng bị ghi liên tục phải tính khoản này trước khi thêm index thứ hai, thứ ba.

Thêm index không giúp trong hai tình huống. Thứ nhất, bảng nhỏ hoặc báo cáo cần đọc gần hết bảng: tài liệu MySQL nói đọc tuần tự có thể nhanh hơn đi qua index (How MySQL Uses Indexes). Thứ hai, bảng bị ghi liên tục: mỗi index thêm vào làm INSERT chậm đi theo số đo ở trên. Index chọn theo báo cáo hôm nay có thể không phục vụ báo cáo tháng sau, nên mỗi index mới cần đi kèm câu truy vấn đã được EXPLAIN kiểm, và nên chạy lại báo cáo theo kịch bản nghiệm thu UAT để xác nhận kết quả vẫn đủ 596 dòng như trước. Trước khi thêm index trên bảng của hệ thống đang chạy, hãy có bản sao lưu đã thử khôi phục; thời gian ALTER TABLE trên bảng thật chưa được đo.

Hỏi đáp

EXPLAIN có chạy câu truy vấn không?

EXPLAIN không chạy câu lệnh, còn EXPLAIN ANALYZE thì có. Tài liệu MySQL liệt kê EXPLAIN ANALYZE hỗ trợ SELECT, UPDATE và DELETE có nối bảng, vì vậy hai lệnh sau cần chạy trên bản sao dữ liệu. Trên bảng thử, EXPLAIN trả kết quả ngay còn EXPLAIN ANALYZE của báo cáo chưa có index ra 213 ms ở lần chạy đầu vì phải đọc hết 1.000.000 dòng. EXPLAIN thuần chỉ ước tính, số dòng thực chỉ có sau khi chạy.

Cột nào đứng đầu index nhiều cột?

Cột bị so sánh bằng = trong mọi truy vấn cần index đứng trước, cột khoảng đứng cuối. Với báo cáo ở bài, quy tắc cho ra thứ tự (store_id, status, created_at); bảng bốn thứ tự ở trên cho thấy chênh lệch 0,965 ms với 6,435 ms. Lý do là khoảng ngày chặn các cột đứng sau nó, nên index đặt khoảng ngày trước phải duyệt 41.544 mục để giữ lại 596 dòng. Nếu cột status đứng sau khoảng ngày, mức chậm chỉ tăng từ 0,965 lên 1,355 ms vì khoảng đã thu còn 811 mục.

Tạo index riêng cho từng cột có đủ không?

Không đủ cho báo cáo này. Với (store_id) và (created_at) riêng, MySQL chỉ dùng ix_store, đọc 20.000 dòng và ra median 20,82 ms, so với 0,965 ms của một index ba cột đúng thứ tự. Tài liệu MySQL nói bộ tối ưu có thể dùng Index Merge hoặc chọn index lọc chặt nhất; trên bảng thử nó chọn một index. Hai index đơn lẻ chiếm 38,0 MB, một index ba cột chiếm 29,6 MB.

Làm sao biết index đang được dùng thật?

Đọc key (tên index, NULL nghĩa là không dùng) rồi đọc key_len: 57 là dùng cả ba cột, 52 là dừng sau status như ở trường hợp bọc hàm. possible_keys chỉ cho biết index có thể chọn. Câu bọc DATE(created_at) ở trên cho key_len 52 dù index có đủ ba cột.

Báo cáo cần đọc gần hết bảng thì index có giúp không?

Gần như không. Tài liệu MySQL nói với báo cáo xử lý hầu hết dòng, đọc tuần tự nhanh hơn đi qua index. Báo cáo toàn chuỗi ở trên không lọc cửa hàng nên vẫn quét cả bảng (type là ALL) dù bảng có index, và EXPLAIN ANALYZE ra 341 và 405 ms.

Doanh nghiệp có báo cáo chạy chậm trong phần mềm đang dùng có thể trao đổi với MONA về dịch vụ viết phần mềm theo yêu cầu, hotline 1900 636 648.

Đội MONA gặp khách hàng để khảo sát quy trình trước khi làm phần mềm

Kiến thức phần mềm

Gửi MONA quy trình đang chạy để nhận hướng làm phần mềm

Đội MONA Software khảo sát nghiệp vụ, thiết kế cơ sở dữ liệu, lập trình, bàn giao và đi cùng qua giai đoạn vận hành thử.