Bài 15: Phân Tích JOIN – Lựa Chọn Thuật Toán Nested Loop, Hash, Merge
"Chọn sai thuật toán JOIN, query của bạn có thể 'đắm chìm' trong biển dữ liệu!"
1. Vấn Đề: Query JOIN 2 Bảng Lớn Chậm Bất Thường
Scenario:
Hai bảng dữ liệu:
customers
(1 triệu khách hàng, khóa chínhcustomer_id
).orders
(10 triệu đơn hàng, khóa ngoạicustomer_id
,order_date
).
Thực hiện query:
SELECT c.customer_name, COUNT(o.order_id) AS total_orders
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.country = 'Vietnam'
GROUP BY c.customer_name
ORDER BY total_orders DESC;
Kết quả: Thời gian thực thi ~20 giây.
2. Phân Tích Execution Plan
Bước 1: Chạy EXPLAIN ANALYZE
(PostgreSQL)
Sort (cost=450000.00..451000.00 rows=400000 width=40)
(actual time=19000.123..19500.456 rows=50000 loops=1)
Sort Key: (COUNT(o.order_id)) DESC
Sort Method: external merge Disk: 80000KB
-> HashAggregate (cost=300000.00..340000.00 rows=400000 width=40)
(actual time=17000.123..18500.789 rows=50000 loops=1)
Group Key: c.customer_name
-> Hash Join (cost=150000.00..250000.00 rows=1000000 width=40)
(actual time=1000.123..15000.456 rows=800000 loops=1)
Hash Cond: (o.customer_id = c.customer_id)
-> Seq Scan on orders o (cost=0.00..150000.00 rows=10000000 width=8)
(actual time=0.012..8000.123 rows=10000000 loops=1)
-> Hash (cost=120000.00..120000.00 rows=200000 width=36)
(actual time=1000.012..1000.012 rows=200000 loops=1)
Buckets: 32768 Batches: 1 Memory Usage: 200MB
-> Seq Scan on customers c (cost=0.00..120000.00 rows=200000 width=36)
(actual time=0.010..500.123 rows=200000 loops=1)
Filter: (country = 'Vietnam')
Rows Removed by Filter: 800000
Bước 2: Giải Mã Vấn Đề
Hash Join:
Bảng lớn
orders
(10 triệu dòng) được quét toàn bộ (Seq Scan).Bảng
customers
: Lọc 200,000 dòng (khách hàng ở Vietnam) → Tạo hash table.
Bottleneck Chính:
Hash Join trên bảng
orders
lớn → Tốn bộ nhớ và I/O.Thiếu index trên
customers.country
vàorders.customer_id
.Sort trên Disk do dữ liệu tổng hợp lớn.
3. Tối Ưu: Lựa Chọn Thuật Toán JOIN Phù Hợp
Bước 1: Thêm Index Để Tối Ưu Điều Kiện WHERE và JOIN
-- Index trên customers.country để lọc nhanh
CREATE INDEX idx_customers_country ON customers(country);
-- Index trên orders.customer_id để tối ưu JOIN
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
Bước 2: Điều Chỉnh Join Algorithm Sang Merge Join
SET enable_hashjoin = off; -- Tắt Hash Join
SET enable_nestloop = off; -- Tắt Nested Loop
Bước 3: Execution Plan Sau Khi Tối Ưu
Sort (cost=180000.00..180500.00 rows=200000 width=40)
(actual time=3000.123..3200.456 rows=50000 loops=1)
Sort Key: (COUNT(o.order_id)) DESC
Sort Method: quicksort Memory: 50MB
-> HashAggregate (cost=120000.00..140000.00 rows=200000 width=40)
(actual time=2500.123..2800.789 rows=50000 loops=1)
Group Key: c.customer_name
-> Merge Join (cost=50000.00..100000.00 rows=200000 width=40)
(actual time=200.123..1500.456 rows=800000 loops=1)
Merge Cond: (c.customer_id = o.customer_id)
-> Index Scan using idx_customers_country on customers c
(cost=0.42..5000.00 rows=200000 width=36)
(actual time=0.032..200.123 rows=200000 loops=1)
Index Cond: (country = 'Vietnam')
-> Index Scan using idx_orders_customer_id on orders o
(cost=0.42..80000.00 rows=10000000 width=8)
(actual time=0.010..800.123 rows=800000 loops=1)
Kết Quả:
Merge Join Thay Thế Hash Join:
Index Scan trên cả hai bảng → Giảm I/O.
Dữ liệu đã được sắp xếp theo
customer_id
→ Không cần build hash table.
Giảm Execution Time: Từ 20 giây → 3.2 giây (84% cải thiện).
4. Tổng Kết
Chỉ Số | Trước Khi Tối Ưu | Sau Khi Tối Ưu |
Total Cost | 450,000 | 180,000 |
Execution Time | 19,500 ms | 3,200 ms |
Phương Pháp JOIN | Hash Join | Merge Join |
Sort Method | Disk (External Merge) | Memory (Quicksort) |
Lý Do Hiệu Quả:
Index trên
customers.country
giúp lọc nhanh 200,000 dòng.Index trên
orders.customer_id
cho phép Merge Join sử dụng Index Scan.Merge Join tận dụng dữ liệu đã sắp xếp → Tiết kiệm bộ nhớ.
5. Bài Tập Thực Hành
Dataset Mẫu:
CREATE TABLE products (
product_id INT PRIMARY KEY,
category_id INT,
product_name VARCHAR(100)
);
CREATE TABLE sales (
sale_id INT PRIMARY KEY,
product_id INT,
sale_date DATE
);
-- Insert 500,000 products và 5 triệu sales
Yêu Cầu:
Chạy query:
SELECT p.category_id, COUNT(s.sale_id) FROM products p JOIN sales s ON p.product_id = s.product_id WHERE p.category_id IN (1, 2, 3) GROUP BY p.category_id;
Phân tích Execution Plan và xác định loại JOIN được sử dụng.
Thử nghiệm tắt/bật các loại JOIN (Hash, Merge, Nested Loop) và đo hiệu suất.
Câu Hỏi:
- Tại sao Merge Join yêu cầu dữ liệu đầu vào đã được sắp xếp?
6. Mở Rộng & Thảo Luận
So Sánh 3 Thuật Toán JOIN
Thuật Toán | Ưu Điểm | Nhược Điểm | Trường Hợp Sử Dụng |
Nested Loop | Hiệu quả khi một bảng nhỏ | Chậm với bảng lớn | Bảng nhỏ + Index trên JOIN condition |
Hash Join | Nhanh khi cả hai bảng lớn | Tốn bộ nhớ để build hash table | Không cần dữ liệu sắp xếp |
Merge Join | Tiết kiệm bộ nhớ, ổn định | Yêu cầu dữ liệu đã sắp xếp | Dữ liệu đã có index phù hợp |
Cách "Hướng Dẫn" Database Chọn Thuật Toán
PostgreSQL:
SET enable_hashjoin = off; -- Tắt Hash Join SET enable_mergejoin = on; -- Bật Merge Join
MySQL:
SELECT /*+ NO_HASH_JOIN(p, s) */ ... -- Sử dụng optimizer hints
Edge Case: Khi Nào Không Thể Tối Ưu JOIN?
Dữ liệu quá lớn: Bảng hàng tỷ dòng → Cần partitioning hoặc sharding.
JOIN trên cột không có index: Không thể áp dụng Merge/Nested Loop → Buộc dùng Hash Join.
Ví Dụ Force Index Trong MySQL
SELECT c.customer_name, COUNT(o.order_id)
FROM customers c FORCE INDEX (idx_customers_country)
JOIN orders o FORCE INDEX (idx_orders_customer_id)
ON c.customer_id = o.customer_id
WHERE c.country = 'Vietnam'
GROUP BY c.customer_name;
Kết Luận
Việc lựa chọn thuật toán JOIN phù hợp là chìa khóa để tối ưu query. Trong ví dụ này, chuyển từ Hash Join sang Merge Join kết hợp index đã giảm 84% thời gian thực thi. Trong bài tiếp theo (Bài 16), chúng ta sẽ giải quyết các vấn đề phức tạp với subquery và CTE!
👉 Bài Tập Về Nhà: Tải dataset tại đây, thử nghiệm force index trên MySQL và điều chỉnh JOIN algorithm trên PostgreSQL!