New Post

Saturday, April 7, 2012


Tiếp theo bài Các Loại JOIN Trong SQL Server, bài này giới thiệu về các cơ chế bên trong SQL Server sử dụng để xử lý các câu truy vấn JOIN. Về cơ bản khi thực hiện câu lệnh JOIN, SQL Server duyệt qua hai bảng tham gia vào, lấy ra từng cặp bản ghi để so sánh, rồi trả về tập kết quả nếu thỏa mãn điều kiện JOIN hoặc loại bỏ nếu không thỏa mãn. SQL Server cài đặt một vài thuật toán khác nhau, thích hợp với các tình huống khác nhau (như số lượng bản ghi cần so sánh nhiều hay ít, cột JOIN có index hay không…). Các thuật toán đó là Nested Loop Join, Merge Join, và Hash Join.

Nested Loop Join
Đây là thuật toán rất đơn giản và cũng rất hiệu quả đối với tập dữ liệu nhỏ, nó lấy mỗi bản ghi trong một bảng (gọi là inner table) và so sánh với từng bản ghi của bảng kia (gọi là outer table) để tìm ra bản ghi thỏa mãn. Thuật toán này có thể được viết ở dạng pseodo-code như sau:
for each row R1 in the outer table
   for each row R2 in the inner table
      if R1.join_column = R2.join_column
         return (R1, R2)

(pseodo-code được copy từ Craig Freedman’s Blog)
ví dụ 1:
USE AdventureWorkds
GO
SELECT a.SalesOrderDetailID, b.Name
FROM Sales.SalesOrderDetail a
JOIN Production.Product b
ON a.ProductID = b.ProductID
WHERE a.SalesOrderID = 43659
Trong phương án thực thi ở hình trên, bạn thấy hai thao tác Clustered index seek, bảng ở thao tác trên luôn luôn là outer table, và bảng dưới luôn là inner table. Vì số bản ghi được trả về là nhỏ, bộ Optimizer đã chọn Nested Loop Join. Nói chung Nested Loop Join thích hợp khi outer table có số bản ghi nhỏ và inner table đã được sắp xếp theo trường được JOIN (ví dụ trường này có clustered index). Khi đó, việc quét bảng inner table (vòng lặp trong) trở thành index seek.
Khi xây dựng phương án thực thi, bộ Optimizer sẽ tự động chọn một bảng làm outer table và bảng kia làm inner table theo cách để đảm bảo chi phí là nhỏ nhất.
Nested Loop Join là thao tác join bạn mong đợi xuất hiện nhất trong phương án thực thi, vì nó chứng tỏ câu truy vấn được thực hiện hiệu quả và có chi phí thấp. Khi số bản ghi tăng lên hoặc bảng inner table không được sắp thứ tự (vì không có index hỗ trợ), bộ Optimizer sẽ xem xét đến Merge Join hoặc Hash Join, vì lúc đó các thuật toán này có thể sẽ hiệu quả hơn so với Nested Loop Join.
Cần phân biệt các thuật toán kể trên với các loại JOIN mà bạn dùng khi viết code như INNER JOIN, OUTER JOIN. Theo thuật ngữ của Microsoft, các loại JOIN đó được gọi là logical operator (các toán tử ở mức logic); còn các thuật toán trên gọi là physical operator (các toán tử ở mức vật lý). Khi viết code, bạn dùng các toán tử mức logic để diễn đạt yêu cầu, hệ thống sẽ xem xét và sử dụng một toán tử mức vật lý (một trong ba thuật toán) thích hợp để thực thi câu lệnh.
Merge Join
Kỹ thuật này đòi hỏi hai bảng phải cùng được sắp xếp theo thứ tự của trường JOIN. Nó đọc từng cặp bản ghi của mỗi bảng và so sánh với nhau. Nếu khớp thì gửi ra tập kết quả. Nếu không thì nó loại bản ghi có trường JOIN nhỏ hơn, đọc tới bản ghi tiếp theo của bảng tương ứng và tiếp tục quá trình. Với thuật toán này, hai bảng được đọc từ đầu và cùng tiến lên song song với nhau. Pseodo-code của thuật toán Merge Join như sau:
get first row R1 from table 1
get first row R2 from table 2
while not at the end of either table
begin
   if R1.join_column = R2.join_column
   begin
      return (R1, R2)
      get next row R2 from table 2
   end
   else if R1.join_column < R2.join_column
      get next row R1 from table 1
   else
      get next row R2 from table 2
end

(pseodo-code được copy từ Craig Freedman’s Blog )
Ví dụ 2: Cùng câu truy vấn như ở phần trước, nhưng bỏ qua mệnh đề WHERE
SELECT a.SalesOrderDetailID, b.Name
FROM Sales.SalesOrderDetail a
JOIN Production.Product b
ON a.ProductID = b.ProductID
--WHERE a.SalesOrderID = 43659

Với câu lệnh trên thì Merge Join trở nên thích hợp, vì số bản ghi trả về là lớn và cả hai bảng đều đã được sắp xếp (nói chính xác ra là, đối với bảng SalesOrderDetail nó chỉ cần đọc index trên trường ProductID, và tất nhiên index đã sắp xếp sẵn).
Số phép so sánh, và do đó, chi phí của thuật toán này, tương đương với tổng của số bản ghi trong hai bảng. Do đó thuật toán này hoạt động hiệu quả hơn Nested Loop khi số bản ghi tăng cao. Trong nhiều trường hợp, thuật toán kết thúc khi nó mới chỉ quét xong bảng nhỏ hơn, vì bảng kia nếu có quét tiếp cũng không tìm được bản ghi nào thỏa mãn nữa. Khi đó số lần so sánh chỉ bằng hai lần số bản ghi của bảng nhỏ.
Nếu một trong hai bảng không được sắp sẵn thứ tự, bộ Optimizer có hai lựa chọn: (1) sắp xếp lại bảng theo thứ tự trường JOIN rồi áp dụng Merge Join hoặc (2) chuyển sang dùng Hash Join. Phương án nào rẻ hơn sẽ được chọn.
Hash Join
Thuật toán này phát huy hiệu quả nhất đối với lượng dữ liệu lớn và không được sắp xếp sẵn. Nó được thực hiện làm hai giai đoạn: xây dựng (build) và dò tìm (probe).
Ở bước xây dựng, nó quét qua một bảng (gọi là build table), và băm (hash) các bản ghi dựa vào trường JOIN, rồi xây dựng một bảng băm (hash table) trong bộ nhớ.
Đến bước dò tìm, nó đọc bảng thứ hai (gọi là probe table) và cũng băm các bản ghi dùng trường JOIN, rồi dùng giá trị băm đó để tìm trên bảng băm. Mỗi lần tìm được nó gửi cặp bản ghi tương ứng ra tập kết quả. Pseodo-code:
for each row R1 in the build table
begin
   calculate hash value on R1.join_column
   insert R1 into the appropriate hash bucket
end
for each row R2 in the probe table
begin
   calculate hash value on R2.join_column
   for each row R1 in the corresponding hash bucket
      if R1.join_column = R2.join_column
         return (R1, R2)
end

(pseodo-code được copy từ Craig Freedman’s Blog )
Ví dụ 3: giống như ví dụ 2 nhưng thêm một trường OrderQty vào phần SELECT
SELECT a.SalesOrderDetailID, a.OrderQty, b.Name
FROM Sales.SalesOrderDetail a
JOIN Production.Product b
ON a.ProductID = b.ProductID
--WHERE a.SalesOrderID = 43659

Ở ví dụ này Hash Join đã được sử dụng, mặc dù số bản ghi được trả về giống như ở ví dụ 2. Lưu ý ở ví dụ 2, bảng SalesOrderDetail chỉ cần đọc index trên trường ProductID là đủ, và vì input cho việc join đã được sắp xêp nên Merge Join đã được dùng. Nhưng ở ví dụ 3, vì có thêm trường OrderQty nên chỉ đọc index trên trường ProductID là không đủ mà hệ thống phải đọc cả vào bảng nữa. Input cho thao tác join lúc này không còn được sắp xếp nữa và do đó, Hash Join trở nên thích hợp hơn.
Trong các tình huống như thế này Hash Join có ưu thế hơn Merge Join vì việc xây dựng bảng băm nhanh hơn sắp xếp lại bảng, hơn nữa nó chỉ cần áp dụng đối với một bảng.
Thông thường bộ Optimizer chọn bảng nhỏ để xây dựng bảng băm để không cần chiếm quá nhiều bộ nhớ. Tuy vậy so với Nested Loop Join và Merge Join thì thuật toán này đòi hỏi rất nhiều tài nguyên CPU và bộ nhớ. Khi Hash Join xuất hiện trong phương án thực thi, đó là chỉ dấu cho thấy lượng dữ liệu cần xử lý khá lớn (do không có mệnh đề WHERE, chủ ý hoặc do quên không đưa vào) và không có index hỗ trợ.
Trong các hệ thống OLTP, vốn đặc trưng bởi nhiều giao dịch được thực hiện nhanh, Hash Join cho thấy nhiều khả năng là câu lệnh chưa được thực hiện tối ưu. Còn trong môi trường data warehouse, các thao tác xử lý thường trên một lượng dữ liệu lớn, do đó Hash Join được sử dụng rất thường xuyên.
Lời kết
Trên đây giới thiệu các thuật toán SQL Server dùng để thực thi câu lệnh JOIN. Trên thực tế các thuật toán phức tạp hơn và còn có nhiều biến thể để bộ Optimizer tinh chỉnh trong từng tình huống cụ thể. Tuy nhiên mức sâu nhất mà bạn có thể nhìn vào hệ thống là biết thuật toán nào đã được sử dụng cho câu lệnh, do Microsoft che dấu toàn bộ các chi tiết bên dưới. Việc hiểu biết cơ chế hoạt động của các thuật toán giúp bạn có thêm một công cụ để tối ưu hóa câu lệnh. Ví dụ, với câu truy vấn ở phần Hash Join, khi quan sát kế hoạch thực thi và thấy Hash Join được sử dụng bạn hiểu rằng đây có thể là chỉ dấu câu lệnh chưa được thực hiện tối ưu. Bạn cố gắng tạo thay đổi để hệ thống chuyển sang chọn Merge Join (vì số bản ghi trả về lớn nên Nested Loop Join chắc chắn không thích hợp). Để dùng Merge Join thì đầu vào phải được sắp xếp. Vì thế bạn có thể sửa lại index trên trường ProductID để nó cover cả trường OrderQty. Và giờ câu lệnh đã được thực hiện bằng Merge Join và hiệu năng đã được cải thiện đáng kể:
--Tạo một bảng copy của Sales.SalesOrderDetail
SELECT *
INTO Sales.SalesOrderDetail_2
FROM Sales.SalesOrderDetail
GO
-- tạo các index trên bảng copy
CREATE CLUSTERED INDEX IX_SalesOrderDetail_SalesOrderID_SalesOrderDetailID_2
ON Sales.SalesOrderDetail_2 (
SalesOrderID,
SalesOrderDetailID)
GO
CREATE NONCLUSTERED INDEX IX_SalesOrderDetail_ProductID_2
ON Sales.SalesOrderDetail_2(ProductID)
INCLUDE(OrderQty)
 
-- so sánh hai câu lệnh:
-- Query 1: câu lệnh trên bảng cũ
SELECT a.SalesOrderDetailID, a.OrderQty, b.Name
FROM Sales.SalesOrderDetail a
JOIN Production.Product b
ON a.ProductID = b.ProductID
 
-- Query 2: câu lệnh trên bảng copy
SELECT a.SalesOrderDetailID, a.OrderQty, b.Name
FROM Sales.SalesOrderDetail_2 a
JOIN Production.Product b
ON a.ProductID = b.ProductID


Nguồn: sqlviet.com

0 nhận xét:

Post a Comment