Bạn có sử dụng truy vấn con SQL hay tránh sử dụng chúng?
Giả sử giám đốc tín dụng và thu tiền yêu cầu bạn liệt kê tên của những người, số dư chưa thanh toán mỗi tháng của họ và số dư đang hoạt động hiện tại và muốn bạn nhập mảng dữ liệu này vào Excel. Mục đích là để phân tích dữ liệu và đưa ra đề nghị thanh toán nhẹ hơn để giảm thiểu tác động của đại dịch COVID19.
Bạn có chọn sử dụng một truy vấn và một truy vấn con lồng nhau hay một phép nối không? Bạn sẽ đưa ra quyết định nào?
Truy vấn con SQL - Chúng là gì?
Trước khi chúng ta tìm hiểu sâu về cú pháp, tác động đến hiệu suất và cảnh báo, tại sao không xác định một truy vấn con trước?
Theo thuật ngữ đơn giản nhất, truy vấn con là một truy vấn bên trong một truy vấn. Trong khi truy vấn thể hiện một truy vấn con là truy vấn bên ngoài, chúng tôi đề cập đến một truy vấn con là truy vấn bên trong hoặc lựa chọn bên trong. Và dấu ngoặc đơn bao quanh một truy vấn con tương tự như cấu trúc bên dưới:
SELECT
col1
,col2
,(subquery) as col3
FROM table1
[JOIN table2 ON table1.col1 = table2.col2]
WHERE col1 <operator> (subquery)
Chúng ta sẽ xem xét những điểm sau trong bài đăng này:
- Cú pháp truy vấn con SQL tùy thuộc vào các loại truy vấn con và toán tử khác nhau.
- Người ta có thể sử dụng truy vấn con khi nào và ở loại câu lệnh nào.
- Ý nghĩa về hiệu suất so với THAM GIA .
- Những lưu ý chung khi sử dụng truy vấn con SQL.
Theo thông lệ, chúng tôi cung cấp các ví dụ và hình ảnh minh họa để nâng cao hiểu biết. Nhưng hãy nhớ rằng trọng tâm chính của bài đăng này là về các truy vấn con trong SQL Server.
Bây giờ, hãy bắt đầu.
Tạo truy vấn con SQL tự chứa hoặc có liên quan
Đối với một điều, các truy vấn con được phân loại dựa trên sự phụ thuộc của chúng vào truy vấn bên ngoài.
Hãy để tôi mô tả truy vấn con tự chứa là gì.
Truy vấn con tự chứa (hoặc đôi khi được gọi là truy vấn con không tương quan hoặc đơn giản) độc lập với các bảng trong truy vấn bên ngoài. Hãy để tôi minh họa điều này:
-- Get sales orders of customers from Southwest United States
-- (TerritoryID = 4)
USE [AdventureWorks]
GO
SELECT CustomerID, SalesOrderID
FROM Sales.SalesOrderHeader
WHERE CustomerID IN (SELECT [CustomerID]
FROM [AdventureWorks].[Sales].[Customer]
WHERE TerritoryID = 4)
Như đã trình bày trong đoạn mã trên, truy vấn con (được đặt trong dấu ngoặc đơn bên dưới) không có tham chiếu đến bất kỳ cột nào trong truy vấn bên ngoài. Ngoài ra, bạn có thể đánh dấu truy vấn con trong SQL Server Management Studio và thực thi nó mà không gặp bất kỳ lỗi thời gian chạy nào.
Do đó, dẫn đến việc gỡ lỗi các truy vấn con tự chứa dễ dàng hơn.
Điều tiếp theo cần xem xét là các truy vấn con tương quan. So với bản sao tự chứa của nó, bản này có ít nhất một cột được tham chiếu từ truy vấn bên ngoài. Để làm rõ, tôi sẽ cung cấp một ví dụ:
USE [AdventureWorks]
GO
SELECT DISTINCT a.LastName, a.FirstName, b.BusinessEntityID
FROM Person.Person AS p
JOIN HumanResources.Employee AS e ON p.BusinessEntityID = e.BusinessEntityID
WHERE 1262000.00 IN
(SELECT [SalesQuota]
FROM Sales.SalesPersonQuotaHistory spq
WHERE p.BusinessEntityID = spq.BusinessEntityID)
Bạn có đủ chú ý để nhận thấy tham chiếu đến BusinessEntityID từ Người bàn? Làm tốt lắm!
Khi một cột từ truy vấn bên ngoài được tham chiếu trong truy vấn con, nó sẽ trở thành một truy vấn con tương quan. Một điểm nữa cần xem xét:nếu bạn đánh dấu một truy vấn con và thực thi nó, thì sẽ xảy ra lỗi.
Và vâng, bạn hoàn toàn đúng:điều này làm cho các truy vấn con tương quan khó gỡ lỗi hơn.
Để có thể gỡ lỗi, hãy làm theo các bước sau:
- tách biệt truy vấn con.
- thay thế tham chiếu đến truy vấn bên ngoài bằng một giá trị không đổi.
Việc cô lập truy vấn con để gỡ lỗi sẽ làm cho nó giống như sau:
SELECT [SalesQuota]
FROM Sales.SalesPersonQuotaHistory spq
WHERE spq.BusinessEntityID = <constant value>
Bây giờ, hãy tìm hiểu sâu hơn một chút về đầu ra của các truy vấn con.
Tạo truy vấn con SQL với 3 giá trị trả về có thể có
Trước tiên, hãy nghĩ xem chúng ta có thể mong đợi những giá trị trả về nào từ các truy vấn con SQL.
Trên thực tế, có 3 kết quả có thể xảy ra:
- Một giá trị duy nhất
- Nhiều giá trị
- Toàn bộ bảng
Giá trị đơn
Hãy bắt đầu với đầu ra có giá trị duy nhất. Loại truy vấn con này có thể xuất hiện ở bất kỳ đâu trong truy vấn bên ngoài mà một biểu thức được mong đợi, như WHERE mệnh đề.
-- Output a single value which is the maximum or last TransactionID
USE [AdventureWorks]
GO
SELECT TransactionID, ProductID, TransactionDate, Quantity
FROM Production.TransactionHistory
WHERE TransactionID = (SELECT MAX(t.TransactionID)
FROM Production.TransactionHistory t)
Khi bạn sử dụng MAX (), bạn truy xuất một giá trị duy nhất. Đó chính xác là những gì đã xảy ra với truy vấn con của chúng tôi ở trên. Sử dụng bằng ( = ) cho SQL Server biết rằng bạn mong đợi một giá trị duy nhất. Một điều khác:nếu truy vấn con trả về nhiều giá trị bằng cách sử dụng dấu bằng ( = ), bạn gặp lỗi, tương tự như bên dưới:
Msg 512, Level 16, State 1, Line 20
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
Nhiều giá trị
Tiếp theo, chúng tôi kiểm tra đầu ra đa giá trị. Loại truy vấn con này trả về một danh sách các giá trị với một cột duy nhất. Ngoài ra, các toán tử như IN và KHÔNG VÀO sẽ mong đợi một hoặc nhiều giá trị.
-- Output multiple values which is a list of customers with lastnames that --- start with 'I'
USE [AdventureWorks]
GO
SELECT [SalesOrderID], [OrderDate], [ShipDate], [CustomerID]
FROM Sales.SalesOrderHeader
WHERE [CustomerID] IN (SELECT c.[CustomerID] FROM Sales.Customer c
INNER JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
WHERE p.lastname LIKE N'I%' AND p.PersonType='SC')
Giá trị toàn bộ bảng
Và cuối cùng nhưng không kém phần quan trọng, tại sao không đi sâu vào toàn bộ kết quả đầu ra của bảng.
-- Output a table of values based on sales orders
USE [AdventureWorks]
GO
SELECT [ShipYear],
COUNT(DISTINCT [CustomerID]) AS CustomerCount
FROM (SELECT YEAR([ShipDate]) AS [ShipYear], [CustomerID]
FROM Sales.SalesOrderHeader) AS Shipments
GROUP BY [ShipYear]
ORDER BY [ShipYear]
Bạn có nhận thấy FROM mệnh đề?
Thay vì sử dụng một bảng, nó đã sử dụng một truy vấn con. Đây được gọi là bảng dẫn xuất hoặc truy vấn con của bảng.
Và bây giờ, hãy để tôi trình bày cho bạn một số quy tắc cơ bản khi sử dụng loại truy vấn này:
- Tất cả các cột trong truy vấn con phải có tên duy nhất. Giống như một bảng vật lý, một bảng dẫn xuất phải có các tên cột duy nhất.
- ĐẶT HÀNG BỞI không được phép trừ khi TOP cũng được chỉ định. Đó là vì bảng dẫn xuất biểu thị một bảng quan hệ trong đó các hàng không có thứ tự xác định.
Trong trường hợp này, một bảng dẫn xuất có các lợi ích của một bảng vật lý. Đó là lý do tại sao trong ví dụ của chúng tôi, chúng tôi có thể sử dụng COUNT () trong một trong các cột của bảng dẫn xuất.
Đó là tất cả những gì liên quan đến kết quả đầu ra truy vấn con. Nhưng trước khi chúng tôi tìm hiểu thêm, bạn có thể nhận thấy rằng logic đằng sau ví dụ cho nhiều giá trị và những giá trị khác cũng có thể được thực hiện bằng cách sử dụng JOIN .
-- Output multiple values which is a list of customers with lastnames that start with 'I'
USE [AdventureWorks]
GO
SELECT o.[SalesOrderID], o.[OrderDate], o.[ShipDate], o.[CustomerID]
FROM Sales.SalesOrderHeader o
INNER JOIN Sales.Customer c on o.CustomerID = c.CustomerID
INNER JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
WHERE p.LastName LIKE N'I%' AND p.PersonType = 'SC'
Trên thực tế, đầu ra sẽ giống nhau. Nhưng cái nào hoạt động tốt hơn?
Trước khi chúng ta đi sâu vào vấn đề đó, hãy để tôi nói với bạn rằng tôi đã dành một phần cho chủ đề nóng bỏng này. Chúng tôi sẽ kiểm tra nó với các kế hoạch thực hiện hoàn chỉnh và xem các hình minh họa.
Vì vậy, hãy chịu đựng tôi một chút thời gian. Hãy thảo luận về một cách khác để đặt các truy vấn phụ của bạn.
Các câu lệnh khác mà bạn có thể sử dụng truy vấn con SQL
Cho đến nay, chúng tôi đang sử dụng truy vấn con SQL trên SELECT các câu lệnh. Và vấn đề là, bạn có thể tận hưởng những lợi ích của truy vấn con trên INSERT , CẬP NHẬT và XÓA hoặc trong bất kỳ câu lệnh T-SQL nào tạo thành một biểu thức.
Vì vậy, chúng ta hãy xem xét một loạt các ví dụ khác.
Sử dụng truy vấn con SQL trong câu lệnh CẬP NHẬT
Nó đủ đơn giản để bao gồm các truy vấn phụ trong UPDATE các câu lệnh. Tại sao không kiểm tra ví dụ này?
-- In the products inventory, transfer all products of Vendor 1602 to ----
-- location 6
USE [AdventureWorks]
GO
UPDATE [Production].[ProductInventory]
SET LocationID = 6
WHERE ProductID IN
(SELECT ProductID
FROM Purchasing.ProductVendor
WHERE BusinessEntityID = 1602)
GO
Bạn có thấy chúng tôi đã làm gì ở đó không?
Vấn đề là, bạn có thể đặt các truy vấn phụ trong WHERE mệnh đề của một CẬP NHẬT tuyên bố.
Vì chúng tôi không có nó trong ví dụ, bạn cũng có thể sử dụng một truy vấn con cho SET mệnh đề như SET cột = (truy vấn con) . Nhưng được cảnh báo:nó phải xuất ra một giá trị duy nhất vì nếu không, sẽ xảy ra lỗi.
Chúng ta phải làm gì tiếp theo?
Sử dụng truy vấn con SQL trong câu lệnh INSERT
Như bạn đã biết, bạn có thể chèn bản ghi vào bảng bằng cách sử dụng CHỌN tuyên bố. Tôi chắc rằng bạn có ý tưởng về cấu trúc truy vấn con sẽ như thế nào, nhưng hãy chứng minh điều này bằng một ví dụ:
-- Impose a salary increase for all employees in DepartmentID 6
-- (Research and Development) by 10 (dollars, I think)
-- effective June 1, 2020
USE [AdventureWorks]
GO
INSERT INTO [HumanResources].[EmployeePayHistory]
([BusinessEntityID]
,[RateChangeDate]
,[Rate]
,[PayFrequency]
,[ModifiedDate])
SELECT
a.BusinessEntityID
,'06/01/2020' as RateChangeDate
,(SELECT MAX(b.Rate) FROM [HumanResources].[EmployeePayHistory] b
WHERE a.BusinessEntityID = b.BusinessEntityID) + 10 as NewRate
,2 as PayFrequency
,getdate() as ModifiedDate
FROM [HumanResources].[EmployeeDepartmentHistory] a
WHERE a.DepartmentID = 6
and StartDate = (SELECT MAX(c.StartDate)
FROM HumanResources.EmployeeDepartmentHistory c
WHERE c.BusinessEntityID = a.BusinessEntityID)
Vì vậy, chúng ta đang xem xét điều gì ở đây?
- Truy vấn con đầu tiên truy xuất mức lương cuối cùng của một nhân viên trước khi thêm vào 10.
- Truy vấn con thứ hai lấy bản ghi lương cuối cùng của nhân viên.
- Cuối cùng, kết quả của SELECT được chèn vào EmployeePayHistory bảng.
Trong các câu lệnh T-SQL khác
Ngoài SELECT , CHÈN , CẬP NHẬT và XÓA , bạn cũng có thể sử dụng truy vấn con SQL như sau:
Khai báo biến hoặc câu lệnh SET trong các thủ tục và hàm được lưu trữ
Hãy để tôi làm rõ bằng cách sử dụng ví dụ này:
DECLARE @maxTransId int = (SELECT MAX(TransactionID)
FROM Production.TransactionHistory)
Ngoài ra, bạn có thể thực hiện việc này theo cách sau:
DECLARE @maxTransId int
SET @maxTransId = (SELECT MAX(TransactionID)
FROM Production.TransactionHistory)
Trong biểu thức có điều kiện
Tại sao bạn không xem qua ví dụ này:
IF EXISTS(SELECT [Name] FROM sys.tables where [Name] = 'MyVendors')
BEGIN
DROP TABLE MyVendors
END
Ngoài ra, chúng ta có thể làm như thế này:
IF (SELECT count(*) FROM MyVendors) > 0
BEGIN
-- insert code here
END
Tạo truy vấn con SQL bằng toán tử so sánh hoặc logic
Cho đến nay, chúng tôi đã thấy bằng ( = ) và toán tử IN. Nhưng còn nhiều điều để khám phá.
Sử dụng các toán tử so sánh
Khi toán tử so sánh như =, <,>, <>,> =, hoặc <=được sử dụng với truy vấn con, truy vấn con sẽ trả về một giá trị duy nhất. Hơn nữa, lỗi xảy ra nếu truy vấn con trả về nhiều giá trị.
Ví dụ dưới đây sẽ tạo ra lỗi thời gian chạy.
USE [AdventureWorks]
GO
SELECT b.LastName, b.FirstName, b.MiddleName, a.JobTitle, a.BusinessEntityID
FROM HumanResources.Employee a
INNER JOIN Person.Person b on a.BusinessEntityID = b.BusinessEntityID
INNER JOIN HumanResources.EmployeeDepartmentHistory c on a.BusinessEntityID
= c.BusinessEntityID
WHERE c.DepartmentID = 6
and StartDate = (SELECT d.StartDate
FROM HumanResources.EmployeeDepartmentHistory d
WHERE d.BusinessEntityID = a.BusinessEntityID)
Bạn có biết điều gì sai trong đoạn mã trên không?
Trước hết, mã sử dụng toán tử bằng (=) với truy vấn con. Ngoài ra, truy vấn con trả về danh sách các ngày bắt đầu.
Để khắc phục sự cố, hãy đặt truy vấn con sử dụng hàm như MAX () trên cột ngày bắt đầu để trả về một giá trị.
Sử dụng các toán tử logic
Sử dụng TỒN TẠI hoặc KHÔNG TỒN TẠI
TỒN TẠI trả về TRUE nếu truy vấn con trả về bất kỳ hàng nào. Nếu không, nó trả về FALSE . Trong khi đó, sử dụng NOT TỒN TẠI sẽ trả về TRUE nếu không có hàng và FALSE , nếu không.
Hãy xem xét ví dụ dưới đây:
IF EXISTS(SELECT name FROM sys.tables where name = 'Token')
BEGIN
DROP TABLE Token
END
Đầu tiên, cho phép tôi giải thích. Mã ở trên sẽ thả Mã thông báo bảng nếu nó được tìm thấy trong sys.tables , nghĩa là nếu nó tồn tại trong cơ sở dữ liệu. Một điểm khác:tham chiếu đến tên cột không liên quan.
Tại sao vậy?
Hóa ra là công cụ cơ sở dữ liệu chỉ cần lấy ít nhất 1 hàng bằng cách sử dụng EXISTS . Trong ví dụ của chúng tôi, nếu truy vấn con trả về một hàng, bảng sẽ bị xóa. Mặt khác, nếu truy vấn con không trả về một hàng nào, thì các câu lệnh tiếp theo sẽ không được thực thi.
Do đó, mối quan tâm của EXISTS chỉ là hàng và không có cột.
Ngoài ra, TỒN TẠI sử dụng logic hai giá trị: TRUE hoặc FALSE . Không có trường hợp nào nó sẽ trả về NULL . Điều tương tự cũng xảy ra khi bạn phủ định TỒN TẠI sử dụng KHÔNG .
Sử dụng IN hoặc KHÔNG IN
Truy vấn con được giới thiệu với IN hoặc KHÔNG VÀO sẽ trả về danh sách không hoặc nhiều giá trị. Và không giống như TỒN TẠI , một cột hợp lệ với kiểu dữ liệu thích hợp là bắt buộc.
Hãy để tôi làm rõ điều này với một ví dụ khác:
-- From the product inventory, extract the products that are available
-- (Quantity >0)
-- except for products from Vendor 1676, and introduce a price cut for the --- whole month of June 2020.
-- Insert the results in product price history.
USE [AdventureWorks]
GO
INSERT INTO [Production].[ProductListPriceHistory]
([ProductID]
,[StartDate]
,[EndDate]
,[ListPrice]
,[ModifiedDate])
SELECT
a.ProductID
,'06/01/2020' as StartDate
,'06/30/2020' as EndDate
,a.ListPrice - 2 as ReducedListPrice
,getdate() as ModifiedDate
FROM [Production].[ProductListPriceHistory] a
WHERE a.StartDate = (SELECT MAX(StartDate)
FROM Production.ProductListPriceHistory
WHERE ProductID = a.ProductID)
AND a.ProductID IN (SELECT ProductID
FROM Production.ProductInventory
WHERE Quantity > 0)
AND a.ProductID NOT IN (SELECT ProductID
FROM [Purchasing].[ProductVendor]
WHERE BusinessEntityID = 1676
Như bạn có thể thấy từ mã trên, cả IN và KHÔNG VÀO các toán tử được giới thiệu. Và trong cả hai trường hợp, các hàng sẽ được trả về. Mỗi hàng trong truy vấn bên ngoài sẽ được đối sánh với kết quả của mỗi truy vấn con để có được sản phẩm có sẵn và sản phẩm không phải của nhà cung cấp 1676.
Lồng các truy vấn con SQL
Bạn có thể lồng các truy vấn con thậm chí lên đến 32 cấp độ. Tuy nhiên, khả năng này phụ thuộc vào bộ nhớ khả dụng của máy chủ và độ phức tạp của các biểu thức khác trong truy vấn.
Bạn đảm nhận việc này là gì?
Theo kinh nghiệm của tôi, tôi không nhớ lồng lên đến 4. Tôi hiếm khi sử dụng 2 hoặc 3 cấp độ. Nhưng đó chỉ là tôi và yêu cầu của tôi.
Làm thế nào về một ví dụ tốt để tìm ra điều này:
-- List down the names of employees who are also customers.
USE [AdventureWorks]
GO
SELECT
LastName
,FirstName
,MiddleName
FROM Person.Person
WHERE BusinessEntityID IN (SELECT BusinessEntityID
FROM Sales.Customer
WHERE BusinessEntityID IN
(SELECT BusinessEntityID
FROM HumanResources.Employee))
Như chúng ta có thể thấy trong ví dụ này, lồng đạt đến 2 cấp độ.
Truy vấn con SQL có hiệu suất kém không?
Tóm lại:có và không. Nói cách khác, nó phụ thuộc.
Và đừng quên, điều này nằm trong ngữ cảnh của SQL Server.
Đối với người mới bắt đầu, nhiều câu lệnh T-SQL sử dụng truy vấn con có thể được viết lại bằng cách sử dụng JOIN S. Và hiệu suất cho cả hai thường là như nhau. Mặc dù vậy, có những trường hợp cụ thể khi một phép nối nhanh hơn. Và có những trường hợp khi truy vấn con hoạt động nhanh hơn.
Ví dụ 1
Hãy xem xét một ví dụ về truy vấn con. Trước khi thực hiện chúng, nhấn Control-M hoặc bật Bao gồm kế hoạch thực thi thực tế từ thanh công cụ của SQL Server Management Studio.
USE [AdventureWorks]
GO
SELECT Name
FROM Production.Product
WHERE ListPrice = SELECT ListPrice
FROM Production.Product
WHERE Name = 'Touring End Caps')
Ngoài ra, truy vấn ở trên có thể được viết lại bằng cách sử dụng một phép nối mang lại kết quả tương tự.
USE [AdventureWorks]
GO
SELECT Prd1.Name
FROM Production.Product AS Prd1
INNER JOIN Production.Product AS Prd2 ON (Prd1.ListPrice = Prd2.ListPrice)
WHERE Prd2.Name = 'Touring End Caps'
Cuối cùng, kết quả cho cả hai truy vấn là 200 hàng.
Ngoài ra, bạn có thể kiểm tra kế hoạch thực thi cho cả hai câu lệnh.
Hình 1:Kế hoạch Thực thi Sử dụng Truy vấn Con
Hình 2:Kế hoạch Thực thi Sử dụng Tham gia
Bạn nghĩ sao? Thực tế chúng có giống nhau không? Ngoại trừ thời gian trôi qua thực tế của mỗi nút, mọi thứ khác về cơ bản đều giống nhau.
Nhưng đây là một cách so sánh khác ngoài sự khác biệt về hình ảnh. Tôi khuyên bạn nên sử dụng So sánh kế hoạch trưng bày .
Để thực hiện, hãy làm theo các bước sau:
- Nhấp chuột phải vào kế hoạch thực thi của câu lệnh bằng truy vấn con.
- Chọn Lưu kế hoạch thực thi dưới dạng .
- Đặt tên cho tệp là subquery-execute-plan.sqlplan .
- Chuyển đến kế hoạch thực thi của câu lệnh bằng cách sử dụng một phép nối và nhấp chuột phải vào nó.
- Chọn So sánh kế hoạch trình chiếu .
- Chọn tên tệp bạn đã lưu trong # 3.
Bây giờ, hãy kiểm tra phần này để biết thêm thông tin về So sánh Showplan .
Bạn sẽ có thể thấy một cái gì đó tương tự như sau:
Hình 3:So sánh Showplan để sử dụng kết hợp so với sử dụng truy vấn con
Lưu ý những điểm tương đồng:
- Các hàng và chi phí ước tính giống nhau.
- QueryPlanHash cũng giống nhau, nghĩa là họ có kế hoạch thực hiện tương tự.
Tuy nhiên, hãy lưu ý những điểm khác biệt:
- Kích thước gói bộ nhớ đệm khi sử dụng phép nối sẽ lớn hơn so với việc sử dụng truy vấn con
- Biên dịch CPU và thời gian (tính bằng mili giây), bao gồm cả bộ nhớ tính bằng KB, được sử dụng để phân tích cú pháp, liên kết và tối ưu hóa kế hoạch thực thi bằng cách sử dụng kết hợp cao hơn so với sử dụng truy vấn con
- Thời gian CPU và thời gian đã trôi qua (tính bằng mili giây) để thực hiện kế hoạch cao hơn một chút khi sử dụng phép nối so với truy vấn con
Trong ví dụ này, truy vấn con nhanh hơn so với phép nối, mặc dù các hàng kết quả giống nhau.
Ví dụ 2
Trong ví dụ trước, chúng tôi chỉ sử dụng một bảng. Trong ví dụ sau, chúng ta sẽ sử dụng 3 bảng khác nhau.
Hãy làm cho điều này xảy ra:
-- Subquery example
USE [AdventureWorks]
GO
SELECT [SalesOrderID], [OrderDate], [ShipDate], [CustomerID]
FROM Sales.SalesOrderHeader
WHERE [CustomerID] IN (SELECT c.[CustomerID] FROM Sales.Customer c
INNER JOIN Person.Person p ON c.PersonID =
p.BusinessEntityID
WHERE p.PersonType='SC')
-- Join example
USE [AdventureWorks]
GO
SELECT o.[SalesOrderID], o.[OrderDate], o.[ShipDate], o.[CustomerID]
FROM Sales.SalesOrderHeader o
INNER JOIN Sales.Customer c on o.CustomerID = c.CustomerID
INNER JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
WHERE p.PersonType = 'SC'
Cả hai truy vấn đều xuất ra 3806 hàng giống nhau.
Tiếp theo, hãy xem kế hoạch thực hiện của họ:
Hình 4:Kế hoạch thực thi cho ví dụ thứ hai của chúng tôi bằng cách sử dụng truy vấn con
Hình 5:Kế hoạch thực thi cho ví dụ thứ hai của chúng tôi bằng cách sử dụng phép nối
Bạn có thể xem 2 kế hoạch thực hiện và tìm thấy bất kỳ sự khác biệt nào giữa chúng không? Trong nháy mắt, chúng trông giống nhau.
Nhưng kiểm tra cẩn thận hơn với So sánh chương trình biểu diễn tiết lộ những gì thực sự bên trong.
Hình 6:Chi tiết về So sánh Showplan cho ví dụ thứ hai
Hãy bắt đầu bằng cách phân tích một số điểm tương đồng:
- Đánh dấu màu hồng trong kế hoạch thực thi hiển thị các hoạt động tương tự cho cả hai truy vấn. Vì truy vấn bên trong sử dụng phép nối thay vì lồng các truy vấn con nên điều này khá dễ hiểu.
- Chi phí toán tử ước tính và chi phí cây con giống nhau.
Tiếp theo, chúng ta hãy xem xét sự khác biệt:
- Đầu tiên, quá trình biên dịch mất nhiều thời gian hơn khi chúng tôi sử dụng các phép nối. Bạn có thể kiểm tra điều đó trong Biên dịch CPU và Thời gian biên dịch. Tuy nhiên, truy vấn có truy vấn con chiếm Bộ nhớ biên dịch tính bằng KB cao hơn.
- Sau đó, QueryPlanHash của cả hai truy vấn đều khác nhau, nghĩa là chúng có một kế hoạch thực thi khác nhau.
- Cuối cùng, thời gian đã trôi qua và thời gian CPU thực hiện kế hoạch nhanh hơn khi sử dụng kết hợp hơn là sử dụng truy vấn con.
Truy vấn phụ so với Kết quả tham gia Hiệu suất
Bạn có thể gặp phải quá nhiều vấn đề khác liên quan đến truy vấn có thể được giải quyết bằng cách sử dụng một phép nối hoặc một truy vấn con.
Nhưng điểm mấu chốt là một truy vấn con vốn dĩ không tệ so với các phép nối. Và không có quy tắc chung nào rằng trong một tình huống cụ thể, một phép nối sẽ tốt hơn một truy vấn con hoặc ngược lại.
Vì vậy, để đảm bảo rằng bạn có sự lựa chọn tốt nhất, hãy kiểm tra các kế hoạch thực hiện. Mục đích của việc đó là để hiểu sâu hơn về cách SQL Server sẽ xử lý một truy vấn cụ thể.
Tuy nhiên, nếu bạn chọn sử dụng một truy vấn con, hãy lưu ý rằng các vấn đề có thể phát sinh sẽ kiểm tra kỹ năng của bạn.
Những lưu ý thường gặp khi sử dụng truy vấn con SQL
Có 2 vấn đề phổ biến có thể khiến các truy vấn của bạn hoạt động không bình thường khi sử dụng truy vấn con SQL.
Nỗi đau của độ phân giải tên cột
Vấn đề này đưa các lỗi logic vào các truy vấn của bạn và chúng có thể rất khó tìm. Một ví dụ có thể làm rõ thêm vấn đề này.
Hãy bắt đầu bằng cách tạo một bảng cho mục đích demo và điền vào nó với dữ liệu.
USE [AdventureWorks]
GO
-- Create the table for our demonstration based on Vendors
CREATE TABLE Purchasing.MyVendors
(
BusinessEntity_id int,
AccountNumber nvarchar(15),
Name nvarchar(50)
)
GO
-- Populate some data to our new table
INSERT INTO Purchasing.MyVendors
SELECT BusinessEntityID, AccountNumber, Name
FROM Purchasing.Vendor
WHERE BusinessEntityID IN (SELECT BusinessEntityID
FROM Purchasing.ProductVendor)
AND BusinessEntityID like '14%'
GO
Bây giờ bảng đã được thiết lập, hãy kích hoạt một số truy vấn phụ bằng cách sử dụng nó. Nhưng trước khi thực hiện truy vấn bên dưới, hãy nhớ rằng ID nhà cung cấp mà chúng tôi đã sử dụng từ mã trước đó bắt đầu bằng '14'.
SELECT b.Name, b.ListPrice, a.BusinessEntityID
FROM Purchasing.ProductVendor a
INNER JOIN Production.Product b on a.ProductID = b.ProductID
WHERE a.BusinessEntityID IN (SELECT BusinessEntityID
FROM Purchasing.MyVendors)
Đoạn mã trên chạy mà không có lỗi, như bạn có thể thấy bên dưới. Dù sao, hãy chú ý đến danh sách BusinessEntityIDs .
Hình 7:BusinessEntityID của tập kết quả không nhất quán với các bản ghi của bảng MyVendors
Không phải chúng tôi đã chèn dữ liệu bằng BusinessEntityID bắt đầu bằng '14'? Sau đó, vấn đề là gì? Trên thực tế, chúng ta có thể thấy BusinessEntityIDs bắt đầu bằng '15' và '16'. Những thứ này đến từ đâu?
Trên thực tế, truy vấn đã liệt kê tất cả dữ liệu từ ProductVendor bảng.
Trong trường hợp đó, bạn có thể nghĩ rằng một bí danh sẽ giải quyết được sự cố này để nó sẽ tham chiếu đến MyVendors giống như bảng bên dưới:
Hình 8:Thêm bí danh vào BusinessEntityID dẫn đến lỗi
Ngoại trừ việc bây giờ vấn đề thực sự xuất hiện do lỗi thời gian chạy.
Kiểm tra MyVendors bảng lại và bạn sẽ thấy điều đó thay vì BusinessEntityID , tên cột phải là BusinessEntity_id (có dấu gạch dưới).
Do đó, việc sử dụng tên cột chính xác cuối cùng sẽ khắc phục được sự cố này, như bạn có thể thấy bên dưới:
Hình 9:Thay đổi truy vấn con với tên cột chính xác đã giải quyết được sự cố
Như bạn có thể thấy ở trên, bây giờ chúng ta có thể quan sát BusinessEntityIDs bắt đầu bằng '14' giống như chúng tôi đã mong đợi trước đây.
Nhưng bạn có thể thắc mắc: tại sao SQL Server lại cho phép chạy truy vấn thành công ngay từ đầu?
Đây là yếu tố khởi đầu:Việc phân giải các tên cột không có bí danh hoạt động trong ngữ cảnh của truy vấn con từ trong chính nó đi ra ngoài truy vấn. Đó là lý do tại sao tham chiếu đến BusinessEntityID bên trong truy vấn con không gây ra lỗi vì nó được tìm thấy bên ngoài truy vấn con - trong ProductVendor bảng.
Nói cách khác, SQL Server tìm kiếm cột không có bí danh BusinessEntityID trong MyVendors bàn. Vì nó không có ở đó, nó đã nhìn ra bên ngoài và tìm thấy nó trong ProductVendor bàn. Thật điên rồ, phải không?
Bạn có thể nói rằng đó là một lỗi trong SQL Server, nhưng trên thực tế, đó là do thiết kế trong tiêu chuẩn SQL và Microsoft đã tuân theo nó.
Được rồi, điều đó rõ ràng, chúng tôi không thể làm bất cứ điều gì về tiêu chuẩn, nhưng làm thế nào chúng tôi có thể tránh gặp lỗi?
- Đầu tiên, đặt tiền tố tên cột với tên bảng hoặc sử dụng bí danh. Nói cách khác, hãy tránh những tên bảng không có tiền tố hoặc không có bí danh.
- Thứ hai, đặt tên nhất quán cho các cột. Tránh có cả BusinessEntityID và BusinessEntity_id chẳng hạn.
Nghe hay không? Đúng, điều này mang lại sự tỉnh táo cho tình huống.
Nhưng đây không phải là kết thúc của nó.
NULL điên rồ
Như tôi đã đề cập, còn nhiều điều cần giải quyết. T-SQL sử dụng logic 3 giá trị vì hỗ trợ cho NULL . Và NULL gần như có thể khiến chúng ta phát điên khi chúng ta sử dụng truy vấn con SQL với NOT IN .
Hãy để tôi bắt đầu bằng cách giới thiệu ví dụ này:
SELECT b.Name, b.ListPrice, a.BusinessEntityID
FROM Purchasing.ProductVendor a
INNER JOIN Production.Product b on a.ProductID = b.ProductID
WHERE a.BusinessEntityID NOT IN (SELECT c.BusinessEntity_id
FROM Purchasing.MyVendors c)
Kết quả của truy vấn dẫn chúng ta đến danh sách các sản phẩm không có trong MyVendors bảng., như bên dưới:
Hình 10:Kết quả đầu ra của truy vấn mẫu sử dụng NOT IN
Bây giờ, giả sử ai đó vô tình chèn bản ghi vào MyVendors bảng có NULL BusinessEntity_id . Chúng ta sẽ làm gì với điều đó?
Hình 11:Tập hợp kết quả trở nên trống khi NULL BusinessEntity_id được chèn vào MyVendors
Tất cả dữ liệu đã đi đâu?
Bạn thấy đấy, KHÔNG toán tử đã phủ định IN Thuộc tính. Vì vậy, KHÔNG ĐÚNG bây giờ sẽ trở thành FALSE . Nhưng KHÔNG ĐẦY ĐỦ là không biết. Điều đó khiến bộ lọc loại bỏ các hàng KHÔNG ĐƯỢC CHỨNG MINH và đây là thủ phạm.
Để đảm bảo điều này không xảy ra với bạn:
- Làm cho cột trong bảng không cho phép NULLs nếu dữ liệu không nên như vậy.
- Hoặc thêm column_name KHÔNG ĐẦY ĐỦ tới WHERE của bạn mệnh đề. Trong trường hợp của chúng tôi, truy vấn con như sau:
SELECT b.Name, b.ListPrice, a.BusinessEntityID
FROM Purchasing.ProductVendor a
INNER JOIN Production.Product b on a.ProductID = b.ProductID
WHERE a.BusinessEntityID NOT IN (SELECT c.BusinessEntity_id
FROM Purchasing.MyVendors c
WHERE c.BusinessEntity_id IS NOT NULL)
Bài học rút ra
Chúng ta đã nói khá nhiều về các truy vấn phụ và đã đến lúc cung cấp các điểm rút ra chính của bài đăng này dưới dạng một danh sách tóm tắt:
Một truy vấn con:
- là một truy vấn trong một truy vấn.
- được đặt trong dấu ngoặc đơn.
- có thể thay thế một biểu thức ở bất kỳ đâu.
- có thể được sử dụng trong SELECT , CHÈN , CẬP NHẬT , XÓA, hoặc các câu lệnh T-SQL khác.
- có thể khép kín hoặc tương quan.
- xuất ra các giá trị đơn, nhiều hoặc giá trị bảng.
- hoạt động trên các toán tử so sánh như =, <>,>, <,> =, <=và các toán tử logic như IN / KHÔNG VÀO và TỒN TẠI / KHÔNG TỒN TẠI .
- không xấu hay xấu. Nó có thể hoạt động tốt hơn hoặc kém hơn so với JOIN tùy thuộc vào một tình huống. Vì vậy, hãy nghe theo lời khuyên của tôi và luôn kiểm tra các kế hoạch thực hiện.
- có thể có hành vi xấu trên NULL s khi được sử dụng với NOT IN và khi một cột không được xác định rõ ràng bằng bảng hoặc bí danh bảng.
Get familiarized with several additional references for your reading pleasure:
- Discussion of Subqueries from Microsoft.
- IN (Transact-SQL)
- EXISTS (Transact-SQL)
- ALL (Transact-SQL)
- SOME | ANY (Transact-SQL)
- Comparison Operators