Tóm lược
SQL Server sử dụng phép nối chính xác (bên trong hoặc bên ngoài) và thêm các phép chiếu khi cần để tôn vinh tất cả các ngữ nghĩa của truy vấn ban đầu khi thực hiện các bản dịch nội bộ giữa áp dụng và tham gia .
Sự khác biệt trong các kế hoạch đều có thể được giải thích bằng các ngữ nghĩa khác nhau của các tập hợp có và không có nhóm theo mệnh đề trong SQL Server.
Chi tiết
Tham gia vs Áp dụng
Chúng ta sẽ cần có khả năng phân biệt giữa ứng dụng và tham gia :
Ứng dụng
Đầu vào bên trong (phía dưới) của ứng dụng được chạy cho mỗi hàng của đầu vào bên ngoài (phía trên), với một hoặc nhiều giá trị tham số bên trong được cung cấp bởi hàng ngoài hiện tại. Kết quả tổng thể của ứng dụng là sự kết hợp (kết hợp tất cả) của tất cả các hàng được tạo bởi các thực thi bên trong được tham số hóa. Sự hiện diện của các tham số có nghĩa là áp dụng đôi khi được gọi là một tham gia tương quan.
Một ứng dụng luôn được triển khai trong các kế hoạch thực hiện bởi toán tử Nested Loops . Toán tử sẽ có thuộc tính Tham chiếu ngoài thay vì tham gia các biến vị ngữ. Các tham chiếu bên ngoài là các tham số được truyền từ phía bên ngoài sang phía bên trong trên mỗi lần lặp của vòng lặp.
Tham gia
Một phép nối đánh giá vị từ nối của nó tại toán tử nối. Việc tham gia thường có thể được thực hiện bởi các toán tử Hash Match , Merge hoặc Nested Loops trong SQL Server.
Khi các vòng lặp lồng nhau được chọn, nó có thể được phân biệt với một ứng dụng do thiếu các Tham chiếu ngoài (và thường là sự hiện diện của một vị từ tham gia). Đầu vào bên trong của phép nối không bao giờ tham chiếu các giá trị từ đầu vào bên ngoài - bên trong vẫn được thực hiện một lần cho mỗi hàng bên ngoài, nhưng thực thi bên trong không phụ thuộc vào bất kỳ giá trị nào từ hàng ngoài hiện tại.
Để biết thêm chi tiết, xem bài đăng của tôi Áp dụng so với Vòng lặp lồng nhau Tham gia .
... Tại sao có một tham gia bên ngoài trong kế hoạch thực hiện thay vì tham gia bên trong ?
Phép nối ngoài phát sinh khi trình tối ưu hóa chuyển đổi áp dụng cho phép nối (sử dụng quy tắc được gọi ApplyHandler
) để xem liệu nó có thể tìm thấy gói dựa trên phép nối rẻ hơn không. Phép nối được yêu cầu là phép nối ngoài cho chính xác khi ứng dụng chứa tổng hợp vô hướng . Một kết nối bên trong sẽ không được đảm bảo để tạo ra kết quả giống như áp dụng ban đầu như chúng ta sẽ thấy.
Tổng hợp vô hướng và vectơ
- Một tổng hợp không có
GROUP BY
mệnh đề tương ứng là tổng hợp vô hướng .
- Một tổng hợp với một
GROUP BY
mệnh đề tương ứng là một tổng hợp vector .
Trong SQL Server, tổng hợp vô hướng sẽ luôn tạo ra một hàng, ngay cả khi nó không có hàng nào để tổng hợp. Ví dụ, COUNT
tổng vô hướng của không có hàng nào bằng không. Tập hợp vectơ COUNT
không có hàng là tập hợp trống (không có hàng nào cả).
Các truy vấn đồ chơi sau đây minh họa sự khác biệt. Bạn cũng có thể đọc thêm về tổng hợp vô hướng và vectơ trong bài viết của tôi Vui với vô hướng và vectơ tổng hợp .
-- Produces a single zero value
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1;
-- Produces no rows
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1 GROUP BY ();
db <> fiddle demo
Chuyển đổi áp dụng để tham gia
Tôi đã đề cập trước đó rằng phép nối được yêu cầu là phép nối ngoài cho chính xác khi áp dụng ban đầu chứa tổng hợp vô hướng . Để chỉ ra lý do tại sao đây là trường hợp chi tiết, tôi sẽ sử dụng một ví dụ đơn giản về truy vấn câu hỏi:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
SELECT * FROM @A AS A
CROSS APPLY (SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A) AS CA;
Kết quả chính xác cho cột c
là không , bởi vì COUNT_BIG
là một đại lượng vô hướng tổng hợp. Khi dịch truy vấn áp dụng này thành biểu mẫu tham gia, SQL Server tạo ra một thay thế bên trong trông giống như sau nếu nó được thể hiện trong T-SQL:
SELECT A.*, c = COALESCE(J1.c, 0)
FROM @A AS A
LEFT JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Để viết lại ứng dụng dưới dạng tham gia không tương thích, chúng tôi phải giới thiệu một GROUP BY
trong bảng dẫn xuất (nếu không thì không thể có A
cột để tham gia). Phép nối phải là phép nối ngoài để mỗi hàng từ bảng @A
tiếp tục tạo ra một hàng trong đầu ra. Phép nối trái sẽ tạo ra một NULL
cột for c
khi vị từ nối không đánh giá là đúng. Điều đó NULL
cần được dịch thành 0 bằng cách COALESCE
hoàn thành một chuyển đổi chính xác từ áp dụng .
Bản demo dưới đây cho thấy cả hai phép nối ngoài và COALESCE
được yêu cầu để tạo ra cùng một kết quả bằng cách sử dụng phép nối như truy vấn áp dụng ban đầu :
db <> fiddle demo
Với GROUP BY
... tại sao việc không chú ý đến nhóm theo mệnh đề dẫn đến sự tham gia bên trong?
Tiếp tục ví dụ đơn giản hóa, nhưng thêm một GROUP BY
:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
-- Original
SELECT * FROM @A AS A
CROSS APPLY
(SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A GROUP BY B.A) AS CA;
Các COUNT_BIG
bây giờ là một véc tơ tổng hợp, vì vậy kết quả chính xác cho một bộ đầu vào trống rỗng không còn bằng không, nó là không có hàng ở tất cả . Nói cách khác, chạy các câu lệnh trên không tạo ra đầu ra.
Các ngữ nghĩa này dễ dàng hơn nhiều để tôn vinh khi dịch từ áp dụng sang tham gia , vì CROSS APPLY
tự nhiên từ chối bất kỳ hàng bên ngoài nào không tạo ra các hàng bên trong. Do đó, chúng ta có thể sử dụng một phép nối bên trong một cách an toàn ngay bây giờ, không có phép chiếu biểu thức phụ:
-- Rewrite
SELECT A.*, J1.c
FROM @A AS A
JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Bản demo dưới đây cho thấy việc viết lại tham gia bên trong tạo ra kết quả giống như áp dụng ban đầu với tổng hợp vectơ:
db <> fiddle demo
Trình tối ưu hóa tình cờ chọn một phép nối bên trong hợp nhất với bảng nhỏ vì nó tìm thấy một kế hoạch tham gia giá rẻ một cách nhanh chóng (tìm thấy kế hoạch đủ tốt). Trình tối ưu hóa dựa trên chi phí có thể tiếp tục viết lại liên kết trở lại cho một ứng dụng - có thể tìm thấy một kế hoạch áp dụng rẻ hơn, vì nó sẽ ở đây nếu sử dụng một tham gia vòng lặp hoặc gợi ý lực lượng - nhưng nó không đáng để nỗ lực trong trường hợp này.
Ghi chú
Các ví dụ đơn giản sử dụng các bảng khác nhau với các nội dung khác nhau để hiển thị sự khác biệt về ngữ nghĩa rõ ràng hơn.
Người ta có thể lập luận rằng trình tối ưu hóa phải có khả năng lý giải về việc tự tham gia không có khả năng tạo ra bất kỳ hàng nào không khớp (không tham gia), nhưng ngày nay nó không chứa logic đó. Truy cập cùng một bảng nhiều lần trong một truy vấn không được đảm bảo để tạo ra cùng một kết quả nói chung, tùy thuộc vào mức độ cô lập và hoạt động đồng thời.
Trình tối ưu hóa lo lắng về các ngữ nghĩa và trường hợp cạnh này vì vậy bạn không cần phải làm vậy.
Tiền thưởng: Gói áp dụng nội bộ
SQL Server có thể tạo ra một kế hoạch áp dụng bên trong (không phải là một kế hoạch tham gia bên trong !) Cho truy vấn mẫu, nó chỉ chọn không vì lý do chi phí. Chi phí của kế hoạch tham gia bên ngoài được hiển thị trong câu hỏi là 0,02898 đơn vị trên phiên bản SQL Server 2017 của máy tính xách tay của tôi.
Bạn có thể buộc một kế hoạch áp dụng (tham gia tương quan) bằng cách sử dụng cờ theo dõi không có giấy tờ và không được hỗ trợ 9114 (vô hiệu hóa, ApplyHandler
v.v.) chỉ để minh họa:
SELECT *
FROM #MyTable AS mt
CROSS APPLY
(
SELECT COUNT_BIG(DISTINCT mt2.Col_B) AS dc
FROM #MyTable AS mt2
WHERE mt2.Col_A = mt.Col_A
--GROUP BY mt2.Col_A
) AS ca
OPTION (QUERYTRACEON 9114);
Điều này tạo ra một kế hoạch vòng lặp lồng nhau áp dụng với một chỉ số lười biếng. Tổng chi phí ước tính là 0,0463983 (cao hơn gói đã chọn):
Lưu ý rằng kế hoạch thực hiện sử dụng các vòng lặp lồng nhau áp dụng tạo ra kết quả chính xác bằng cách sử dụng ngữ nghĩa "nối bên trong" bất kể sự hiện diện của GROUP BY
mệnh đề.
Trong thế giới thực, chúng ta thường có một chỉ mục để hỗ trợ tìm kiếm ở phía bên trong ứng dụng để khuyến khích SQL Server chọn tùy chọn này một cách tự nhiên, ví dụ:
CREATE INDEX i ON #MyTable (Col_A, Col_B);
db <> fiddle demo