Nhiều người thấy một query chậm vài giây là kết luận ngay: SQL không phù hợp với dữ liệu lớn. Cách nhìn này thường sai.
Phần lớn vấn đề không nằm ở việc MySQL hay SQL yếu, mà nằm ở chỗ:
- schema chưa hợp lý
- thiếu index đúng chỗ
- query viết làm database phải scan quá nhiều row
- join quá sớm khi chưa lọc nhỏ tập dữ liệu
- đẩy việc filter, sort, paginate về application thay vì để SQL xử lý
Một hệ thống có hàng chục triệu đến hàng trăm triệu records vẫn có thể chạy tốt nếu biết thiết kế index và viết query đúng shape.
Kết luận nhanh
Nếu cần nhớ ngắn gọn, có 5 ý quan trọng:
- SQL không yếu với dữ liệu lớn, thứ làm chậm thường là
full table scanvà query shape sai indexquyết định database có tìm đúng vùng dữ liệu nhanh hay phải quét cả bảng- với query lớn, nên
lọc trước,giới hạn trước, rồi mớijoinhoặc aggregate - nhiều hệ thống e-commerce kiểu Shopify vẫn có thể dùng MySQL cho bảng khách hàng, đơn hàng, chi tiết đơn hàng nếu schema và index đúng
- tối ưu SQL không phải thêm index bừa, mà là hiểu
WHERE,JOIN,ORDER BY,GROUP BYđang chạy trên cột nào
Sai lầm lớn nhất: nghĩ SQL phải đọc hết dữ liệu rồi mới lọc
Đây là hiểu nhầm rất phổ biến.
Người mới thường hình dung database hoạt động kiểu:
- đọc toàn bộ bảng
- join hết các bảng liên quan
- sau đó mới lọc ra kết quả cuối
Nếu đúng như vậy thì với hàng trăm triệu row, query nào cũng chết.
Nhưng SQL engine không được thiết kế để làm việc ngây thơ như vậy.
Mục tiêu của optimizer là:
- chọn index phù hợp
- giảm số row phải đọc càng sớm càng tốt
- giảm số row phải join
- giảm số row phải sort
Nên khi nghe câu "SQL không chịu nổi dữ liệu lớn", cần hỏi lại ngay:
- query có dùng index đúng không?
- filter có đủ selective không?
- join có đang diễn ra sau khi đã lọc nhỏ dataset chưa?
- có đang lấy dư cột hoặc dư row không?
Vì sao index là thứ đầu tiên phải hiểu
index giúp MySQL không phải quét toàn bộ bảng để tìm dữ liệu.
Ví dụ bảng orders có 200 triệu row:
SELECT id, user_id, total_amount, created_at
FROM orders
WHERE status = 'paid'
AND created_at >= '2026-06-01'
AND created_at < '2026-07-01'
ORDER BY created_at DESC
LIMIT 50;
Nếu không có index phù hợp, MySQL có thể:
- scan một lượng row rất lớn
- lọc những row có
status = 'paid' - sort lại theo
created_at - cuối cùng mới lấy 50 row
Nhưng nếu có composite index đúng:
CREATE INDEX idx_orders_status_created_at
ON orders (status, created_at);
thì database có cơ hội:
- nhảy thẳng vào vùng
status = 'paid' - đọc khoảng thời gian cần thiết
- lấy ra một tập row nhỏ hơn rất nhiều
Điểm quan trọng là index không chỉ giúp WHERE, mà còn giúp giảm chi phí cho ORDER BY và LIMIT nếu query shape phù hợp.
Không phải dữ liệu lớn, mà là số row bị scan mới đáng sợ
Một bảng có 300 triệu row không đáng sợ bằng một query buộc MySQL scan 300 triệu row mỗi lần.
Điều cần theo dõi không phải chỉ là:
- bảng to bao nhiêu
Mà là:
- query này đọc bao nhiêu row?
- sau filter còn bao nhiêu row?
- join trên bao nhiêu row?
- sort trên bao nhiêu row?
Ví dụ hai tình huống:
Query tệ
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-06-18';
Vấn đề:
DATE(created_at)có thể làm MySQL khó dùng index trêncreated_atSELECT *lấy quá nhiều cột- không giới hạn số row
Query tốt hơn
SELECT id, user_id, total_amount, created_at
FROM orders
WHERE created_at >= '2026-06-18 00:00:00'
AND created_at < '2026-06-19 00:00:00';
Ở đây query tốt hơn vì:
- giữ nguyên cột
created_atđể index có thể được tận dụng - chỉ lấy đúng cột cần dùng
- giảm IO giữa database và application
Sai lầm hay gặp khi join dữ liệu lớn
Sai lầm phổ biến là join ngay từ đầu khi bảng gốc còn quá lớn.
Ví dụ:
SELECT o.id, o.created_at, c.name, c.email, SUM(oi.quantity * oi.price) AS total
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status = 'paid'
AND o.created_at >= '2026-01-01'
ORDER BY o.created_at DESC
LIMIT 100;
Với dữ liệu lớn, query này có thể tốn vì:
- bảng
orderschưa được thu nhỏ trước khi join - bảng
order_itemsthường rất to - aggregate và sort phải làm trên tập dữ liệu rộng hơn mức cần thiết
Cách nghĩ đúng hơn là:
- lọc bảng gốc trước
- lấy ra tập
idcần thiết trước - sau đó mới join các bảng còn lại
Ví dụ:
SELECT o.id, o.created_at, c.name, c.email, SUM(oi.quantity * oi.price) AS total
FROM (
SELECT id, customer_id, created_at
FROM orders
WHERE status = 'paid'
AND created_at >= '2026-01-01'
ORDER BY created_at DESC
LIMIT 100
) o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id, o.created_at, c.name, c.email
ORDER BY o.created_at DESC;
Tư duy ở đây không phải "subquery cho ngầu", mà là:
- giảm số row trước khi join
- giới hạn phạm vi aggregate
- tránh kéo cả triệu order vào một phép join chỉ để cuối cùng lấy 100 dòng
Vai trò của composite index trong query thực tế
Nhiều người có index rồi nhưng vẫn chậm vì index không khớp cách query chạy.
Ví dụ query:
SELECT id, customer_id, created_at
FROM orders
WHERE store_id = 25
AND status = 'paid'
AND created_at >= '2026-06-01'
AND created_at < '2026-07-01'
ORDER BY created_at DESC
LIMIT 100;
Index hợp lý hơn thường là:
CREATE INDEX idx_orders_store_status_created_at
ON orders (store_id, status, created_at);
Lý do:
store_idvàstatuslà điều kiện lọc hẹp đầu tiêncreated_atlà điều kiện range đồng thời phục vụ sort
Ngược lại, nếu tạo index kiểu:
CREATE INDEX idx_orders_created_status_store
ON orders (created_at, status, store_id);
thì có thể không tối ưu bằng trong query trên, vì thứ tự index không bám sát cách filter thực tế.
Đây là điểm nhiều team bỏ qua:
index không chỉ cần đúng cột, mà còn cần đúng thứ tự cột.
SQL mạnh nhất khi filter dữ liệu lớn ngay trong database
Một sai lầm tốn tài nguyên là lấy hàng chục nghìn row ra backend rồi mới xử lý.
Ví dụ:
SELECT id, customer_id, status, total_amount, created_at
FROM orders
WHERE created_at >= '2026-06-01';
Sau đó ở code mới làm:
const rows = orders
.filter((item) => item.status === "paid")
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.slice(0, 50);
Kiểu này làm chậm ở cả 3 nơi:
- MySQL phải trả về quá nhiều row
- network giữa DB và app bị tốn vô ích
- app tốn RAM và CPU để làm việc mà SQL làm tốt hơn
Nên đẩy hết những phần có thể xuống SQL:
SELECT id, customer_id, total_amount, created_at
FROM orders
WHERE created_at >= '2026-06-01'
AND status = 'paid'
ORDER BY created_at DESC
LIMIT 50;
Khi dữ liệu lên hàng chục triệu row, khác biệt này rất lớn.
Một bài toán kiểu Shopify với hàng triệu khách hàng
Hãy hình dung một hệ thống e-commerce có:
customers: 5 triệu khách hàngorders: 120 triệu đơn hàngorder_items: 600 triệu dòng chi tiết đơn hàng
Đây là quy mô hoàn toàn có thể gặp ở một nền tảng kiểu Shopify hoặc một shop lớn nhiều năm dữ liệu.
Điều không nên làm là viết query theo kiểu:
SELECT c.id, c.name, COUNT(o.id) AS order_count
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.created_at >= '2026-01-01'
GROUP BY c.id, c.name
ORDER BY order_count DESC;
Nếu không có index tốt, query sẽ rất nặng vì:
- phải đọc lượng order khổng lồ
- join customer quá sớm
- group trên dataset lớn
Hướng tốt hơn là:
- xác định câu hỏi cần trả lời thật rõ
- lọc đúng khoảng thời gian trước
- chỉ aggregate trên phần dữ liệu đã lọc
- join sang bảng lớn khác khi thật sự cần thêm thông tin hiển thị
Ví dụ:
SELECT c.id, c.name, x.order_count
FROM (
SELECT customer_id, COUNT(*) AS order_count
FROM orders
WHERE created_at >= '2026-01-01'
AND created_at < '2027-01-01'
AND status = 'paid'
GROUP BY customer_id
ORDER BY order_count DESC
LIMIT 100
) x
JOIN customers c ON c.id = x.customer_id
ORDER BY x.order_count DESC;
Tư duy của query này là:
- aggregate trên bảng
orderstrước - chỉ giữ top 100 customer cần quan tâm
- rồi mới join sang
customers
Với index phù hợp như:
CREATE INDEX idx_orders_status_created_customer
ON orders (status, created_at, customer_id);
thì cơ hội tối ưu sẽ tốt hơn rất nhiều so với việc join toàn bộ từ đầu.
Khi nào MySQL vẫn chậm dù đã có index?
Có vài lý do rất thường gặp:
- index không đúng thứ tự cột
- dùng hàm lên cột đang filter
- dùng
LIKE '%keyword%'trên BTREE index - join trên cột chưa có index
- query trả về quá nhiều cột hoặc quá nhiều row
- thống kê dữ liệu lệch khiến optimizer chọn plan chưa đẹp
Vì vậy đừng kết luận kiểu:
- đã có index mà vẫn chậm, chắc MySQL không đủ mạnh
Thường câu đúng hơn là:
- đã có index, nhưng chưa đúng query pattern
Nên kiểm tra bằng gì trước khi đổ lỗi cho SQL?
Ít nhất hãy kiểm tra:
EXPLAINhoặcEXPLAIN ANALYZE- query đang scan bao nhiêu row
- query đang dùng index nào
- có
Using filesort,temporary,ALLhay không - có đang lấy
SELECT *không - có đang join bảng lớn trước khi lọc không
Ví dụ:
EXPLAIN
SELECT id, customer_id, total_amount, created_at
FROM orders
WHERE status = 'paid'
AND created_at >= '2026-06-01'
ORDER BY created_at DESC
LIMIT 50;
Nếu thấy:
- scan row quá lớn
type: ALL- key đang là
NULL
thì bài toán thường chưa phải "đổi sang hệ khác", mà là sửa query và index trước.
Checklist ngắn khi làm việc với bảng hàng chục triệu row
- Query đã lọc đủ sớm chưa?
- Cột trong
WHERE,JOIN,ORDER BYđã có index phù hợp chưa? - Composite index có đúng thứ tự theo query không?
- Có đang dùng hàm làm hỏng khả năng tận dụng index không?
- Có đang
SELECT *không? - Có thể
LIMIThoặc aggregate trước khi join không? - Có đang đẩy việc filter, sort, paginate sang code không?
Kết luận
SQL không yếu với dữ liệu lớn. Điều làm hệ thống chậm thường là cách chúng ta bắt database đọc quá nhiều dữ liệu không cần thiết.
Nếu hiểu đúng vai trò của index, biết lọc nhỏ dữ liệu trước khi join, và viết query bám sát nhu cầu thật, MySQL vẫn xử lý rất tốt những bài toán có hàng chục triệu đến hàng trăm triệu records.
Trước khi nói "SQL không phù hợp với dữ liệu lớn", hãy kiểm tra lại 3 thứ:
- index đã đúng chưa
- query shape đã đúng chưa
- database có đang phải scan quá nhiều row vô ích không