12s
200ms
MySQL Query
explain • scan • optimize

SQL có xử lý dữ liệu lớn được không? Vai trò của index và query

Vì sao SQL vẫn xử lý tốt dữ liệu lớn nếu schema, index và query đúng cách, từ lọc trước khi join đến tối ưu MySQL cho bài toán hàng triệu khách hàng.

11 phút đọc18/06/2026

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:

  1. SQL không yếu với dữ liệu lớn, thứ làm chậm thường là full table scan và query shape sai
  2. index quyết định database có tìm đúng vùng dữ liệu nhanh hay phải quét cả bảng
  3. với query lớn, nên lọc trước, giới hạn trước, rồi mới join hoặc aggregate
  4. 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
  5. 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:

  1. đọc toàn bộ bảng
  2. join hết các bảng liên quan
  3. 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 BYLIMIT 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ên created_at
  • SELECT * 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 orders chưa được thu nhỏ trước khi join
  • bảng order_items thườ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à:

  1. lọc bảng gốc trước
  2. lấy ra tập id cần thiết trước
  3. 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_idstatus là điều kiện lọc hẹp đầu tiên
  • created_at là đ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àng
  • orders: 120 triệu đơn hàng
  • order_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à:

  1. xác định câu hỏi cần trả lời thật rõ
  2. lọc đúng khoảng thời gian trước
  3. chỉ aggregate trên phần dữ liệu đã lọc
  4. 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 orders trướ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:

  1. EXPLAIN hoặc EXPLAIN ANALYZE
  2. query đang scan bao nhiêu row
  3. query đang dùng index nào
  4. Using filesort, temporary, ALL hay không
  5. có đang lấy SELECT * không
  6. 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

  1. Query đã lọc đủ sớm chưa?
  2. Cột trong WHERE, JOIN, ORDER BY đã có index phù hợp chưa?
  3. Composite index có đúng thứ tự theo query không?
  4. Có đang dùng hàm làm hỏng khả năng tận dụng index không?
  5. Có đang SELECT * không?
  6. Có thể LIMIT hoặc aggregate trước khi join không?
  7. 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ứ:

  1. index đã đúng chưa
  2. query shape đã đúng chưa
  3. database có đang phải scan quá nhiều row vô ích không