Använder du SQL-underfrågor eller undviker du dem?
Låt oss säga att kredit- och inkassochefen ber dig att lista ner namnen på personer, deras obetalda saldon per månad och det aktuella löpande saldot och vill att du importerar denna datamatris till Excel. Syftet är att analysera data och komma med ett erbjudande som gör betalningarna lättare för att mildra effekterna av covid19-pandemin.
Väljer du att använda en fråga och en kapslad underfråga eller en koppling? Vilket beslut kommer du att fatta?
SQL-underfrågor – vad är de?
Innan vi gör en djupdykning i syntax, prestandapåverkan och varningar, varför inte definiera en underfråga först?
I de enklaste termerna är en underfråga en fråga i en fråga. Medan en fråga som förkroppsligar en underfråga är den yttre frågan, hänvisar vi till en underfråga som den inre frågan eller inre urval. Och parenteser omsluter en underfråga som liknar strukturen nedan:
SELECT
col1
,col2
,(subquery) as col3
FROM table1
[JOIN table2 ON table1.col1 = table2.col2]
WHERE col1 <operator> (subquery)
Vi kommer att titta på följande punkter i det här inlägget:
- SQL-underfrågesyntax beroende på olika underfrågetyper och operatorer.
- När och i vilken typ av satser man kan använda en underfråga.
- Prestandakonsekvenser kontra JOINs .
- Vanliga varningar när du använder SQL-underfrågor.
Som vanligt ger vi exempel och illustrationer för att öka förståelsen. Men kom ihåg att det här inläggets huvudfokus ligger på underfrågor i SQL Server.
Nu sätter vi igång.
Gör SQL-underfrågor som är fristående eller korrelerade
Dels kategoriseras delfrågor utifrån deras beroende av den yttre frågan.
Låt mig beskriva vad en fristående delfråga är.
Fristående underfrågor (eller ibland kallade icke-korrelerade eller enkla underfrågor) är oberoende av tabellerna i den yttre frågan. Låt mig illustrera detta:
-- 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)
Som visas i koden ovan har underfrågan (omsluten inom parentes nedan) inga referenser till någon kolumn i den yttre frågan. Dessutom kan du markera underfrågan i SQL Server Management Studio och köra den utan att få några runtime-fel.
Vilket i sin tur leder till enklare felsökning av fristående underfrågor.
Nästa sak att tänka på är korrelerade underfrågor. Jämfört med sin fristående motsvarighet har denna minst en kolumn som refereras till från den yttre frågan. För att förtydliga kommer jag att ge ett exempel:
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)
Var du tillräckligt uppmärksam för att lägga märke till hänvisningen till BusinessEntityID från Personen tabell? Bra jobbat!
När en kolumn från den yttre frågan hänvisas till i underfrågan, blir den en korrelerad underfråga. Ytterligare en punkt att tänka på:om du markerar en underfråga och kör den kommer ett fel att uppstå.
Och ja, du har helt rätt:detta gör korrelerade delfrågor ganska svårare att felsöka.
Följ dessa steg för att göra felsökning möjlig:
- isolera underfrågan.
- ersätt referensen till den yttre frågan med ett konstant värde.
Om du isolerar underfrågan för felsökning kommer det att se ut så här:
SELECT [SalesQuota]
FROM Sales.SalesPersonQuotaHistory spq
WHERE spq.BusinessEntityID = <constant value>
Låt oss nu gräva lite djupare i resultatet av underfrågor.
Gör SQL-underfrågor med 3 möjliga returnerade värden
Tja, låt oss först tänka på vilka returnerade värden vi kan förvänta oss från SQL-underfrågor.
Faktum är att det finns tre möjliga resultat:
- Ett enda värde
- Flera värden
- Hela tabeller
Enkelt värde
Låt oss börja med enstaka utdata. Den här typen av underfråga kan visas var som helst i den yttre frågan där ett uttryck förväntas, som WHERE klausul.
-- 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)
När du använder en MAX ()-funktionen hämtar du ett enda värde. Det är precis vad som hände med vår underfråga ovan. Använder lika (= ) operatören säger till SQL Server att du förväntar dig ett enda värde. En annan sak:om underfrågan returnerar flera värden med lika (= )-operatör får du ett fel, liknande den nedan:
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.
Flera värden
Därefter undersöker vi den flervärdiga produktionen. Den här typen av underfråga returnerar en lista med värden med en enda kolumn. Dessutom, operatörer som IN och INTE I förväntar sig ett eller flera värden.
-- 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')
Hela tabellvärden
Och sist men inte minst, varför inte fördjupa sig i hela tabellutdata.
-- 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]
Har du lagt märke till FRÅN klausul?
Istället för att använda en tabell använde den en underfråga. Detta kallas en härledd tabell eller en tabellunderfråga.
Och nu, låt mig presentera några grundregler när du använder den här typen av fråga:
- Alla kolumner i underfrågan ska ha unika namn. Ungefär som en fysisk tabell bör en härledd tabell ha unika kolumnnamn.
- BESTÄLL EFTER är inte tillåtet om inte TOPP anges också. Det beror på att den härledda tabellen representerar en relationstabell där rader inte har någon definierad ordning.
I det här fallet har en härledd tabell fördelarna med en fysisk tabell. Det är därför vi i vårt exempel kan använda COUNT () i en av kolumnerna i den härledda tabellen.
Det är ungefär allt när det gäller subquery-utgångar. Men innan vi kommer längre, kanske du har märkt att logiken bakom exemplet för flera värden och andra också kan göras med en 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'
Faktum är att utgången blir densamma. Men vilken presterar bäst?
Innan vi går in på det, låt mig berätta att jag har dedikerat ett avsnitt till detta heta ämne. Vi kommer att undersöka det med fullständiga utförandeplaner och titta på illustrationer.
Så håll ut med mig ett ögonblick. Låt oss diskutera ett annat sätt att placera dina underfrågor.
Andra påståenden där du kan använda SQL-underfrågor
Hittills har vi använt SQL-underfrågor på SELECT uttalanden. Och grejen är att du kan njuta av fördelarna med undersökningar på INSERT , UPPDATERA och RADERA satser eller i någon T-SQL-sats som bildar ett uttryck.
Så låt oss ta en titt på en rad fler exempel.
Använda SQL-underfrågor i UPDATE-satser
Det är tillräckligt enkelt att inkludera underfrågor i UPPDATERA uttalanden. Varför inte kolla in det här exemplet?
-- 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
Fick du syn på vad vi gjorde där?
Saken är den att du kan lägga underfrågor i VAR klausul i en UPPDATERING uttalande.
Eftersom vi inte har det i exemplet kan du också använda en underfråga för SET klausul som SET kolumn =(underfråga) . Men var varning:den bör mata ut ett enda värde eftersom annars uppstår ett fel.
Vad gör vi härnäst?
Använda SQL-underfrågor i INSERT-satser
Som du redan vet kan du infoga poster i en tabell med SELECT påstående. Jag är säker på att du har en uppfattning om vad underfrågans struktur kommer att vara, men låt oss visa detta med ett exempel:
-- 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)
Så vad tittar vi på här?
- Den första underfrågan hämtar den sista lönesatsen för en anställd innan de ytterligare 10 läggs till.
- Den andra underfrågan får den anställdes sista lönepost.
- Sistligen, resultatet av SELECT infogas i EmployeePayHistory tabell.
I andra T-SQL-uttalanden
Bortsett från SELECT , INSERT , UPPDATERA och RADERA , kan du också använda SQL-underfrågor i följande:
Variabeldeklarationer eller SET-satser i lagrade procedurer och funktioner
Låt mig förtydliga med detta exempel:
DECLARE @maxTransId int = (SELECT MAX(TransactionID)
FROM Production.TransactionHistory)
Alternativt kan du göra detta på följande sätt:
DECLARE @maxTransId int
SET @maxTransId = (SELECT MAX(TransactionID)
FROM Production.TransactionHistory)
I villkorliga uttryck
Varför tar du inte en titt på det här exemplet:
IF EXISTS(SELECT [Name] FROM sys.tables where [Name] = 'MyVendors')
BEGIN
DROP TABLE MyVendors
END
Bortsett från det kan vi göra det så här:
IF (SELECT count(*) FROM MyVendors) > 0
BEGIN
-- insert code here
END
Gör SQL-underfrågor med jämförelse- eller logiska operatörer
Hittills har vi sett likadana (= ) operatör och IN-operatör. Men det finns mycket mer att utforska.
Använda jämförelseoperatorer
När en jämförelseoperator som =, <,>, <>,>=eller <=används med en underfråga, bör underfrågan returnera ett enda värde. Dessutom uppstår ett fel om underfrågan returnerar flera värden.
Exemplet nedan kommer att generera ett körtidsfel.
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)
Vet du vad som är fel i koden ovan?
Först och främst använder koden operatorn equals (=) med underfrågan. Dessutom returnerar underfrågan en lista med startdatum.
För att åtgärda problemet, låt underfrågan använda en funktion som MAX () i startdatumkolumnen för att returnera ett enda värde.
Använda logiska operatorer
Att använda EXISTS eller NOT EXISTS
FINNS returnerar TRUE om underfrågan returnerar några rader. Annars returnerar den FALSKT . Under tiden använder du NOT FINNS returnerar TRUE om det inte finns några rader och FALSKT , annars.
Tänk på exemplet nedan:
IF EXISTS(SELECT name FROM sys.tables where name = 'Token')
BEGIN
DROP TABLE Token
END
Låt mig först förklara. Koden ovan kommer att ta bort tabellen Token om den finns i sys.tables , vilket betyder om det finns i databasen. En annan punkt:hänvisningen till kolumnnamnet är irrelevant.
Varför är det så?
Det visar sig att databasmotorn bara behöver få minst en rad med EXISTS . I vårt exempel, om underfrågan returnerar en rad, kommer tabellen att tas bort. Å andra sidan, om underfrågan inte returnerade en enda rad, kommer de efterföljande satserna inte att köras.
Alltså, oro för FINNS är bara rader och inga kolumner.
Dessutom FINNS använder tvåvärdig logik:TRUE eller FALSKT . Det finns inga fall där det kommer att returnera NULL . Samma sak händer när du nekar FINNS med NOT .
Använder IN eller NOT IN
En underfråga introducerad med IN eller INTE I returnerar en lista med noll eller fler värden. Och till skillnad från FINNS , krävs en giltig kolumn med lämplig datatyp.
Låt mig förtydliga detta med ett annat exempel:
-- 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
Som du kan se från ovanstående kod, båda IN och INTE I operatörer introduceras. Och i båda fallen kommer rader att returneras. Varje rad i den yttre frågan kommer att matchas mot resultatet av varje underfråga för att få en produkt som finns till hands och en produkt som inte är från leverantör 1676.
Inkapsling av SQL-underfrågor
Du kan kapsla underfrågor upp till 32 nivåer. Icke desto mindre beror denna förmåga på serverns tillgängliga minne och komplexiteten hos andra uttryck i frågan.
Vad tycker du om detta?
Enligt min erfarenhet kommer jag inte ihåg att jag häckade upp till 4. Jag använder sällan 2 eller 3 nivåer. Men det är bara jag och mina krav.
Vad sägs om ett bra exempel för att reda ut detta:
-- 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))
Som vi kan se i det här exemplet nådde kapslingen 2 nivåer.
Är SQL-underfrågor dåliga för prestanda?
I ett nötskal:ja och nej. Det beror med andra ord på.
Och glöm inte att detta är i sammanhanget med SQL Server.
Till att börja med kan många T-SQL-satser som använder underfrågor alternativt skrivas om med JOIN s. Och prestandan för båda är vanligtvis densamma. Trots det finns det särskilda fall då en koppling går snabbare. Och det finns fall då underfrågan fungerar snabbare.
Exempel 1
Låt oss undersöka ett exempel på delfråga. Innan du kör dem, tryck på Control-M eller aktivera Inkludera faktisk exekveringsplan från verktygsfältet i SQL Server Management Studio.
USE [AdventureWorks]
GO
SELECT Name
FROM Production.Product
WHERE ListPrice = SELECT ListPrice
FROM Production.Product
WHERE Name = 'Touring End Caps')
Alternativt kan frågan ovan skrivas om med en join som ger samma resultat.
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'
I slutändan blir resultatet för båda frågorna 200 rader.
Utöver det kan du kolla in exekveringsplanen för båda påståendena.
Figur 1:Exekveringsplan med hjälp av en underfråga
Figur 2:Exekveringsplan med hjälp av en Join
Vad tror du? Är de praktiskt taget samma? Förutom den faktiska förflutna tiden för varje nod, är allt annat i princip detsamma.
Men här är ett annat sätt att jämföra det förutom visuella skillnader. Jag föreslår att du använder Jämför Showplan .
För att utföra det, följ dessa steg:
- Högerklicka på exekveringsplanen för satsen med hjälp av underfrågan.
- Välj Spara exekveringsplan som .
- Ge filen ett namn subquery-execution-plan.sqlplan .
- Gå till exekveringsplanen för uttalandet med hjälp av en join och högerklicka på den.
- Välj Jämför Showplan .
- Välj filnamnet du sparade i #3.
Kolla in det här för mer information om Jämför Showplan .
Du borde kunna se något liknande detta:
Figur 3:Jämför Showplan för att använda en join kontra att använda en underfråga
Lägg märke till likheterna:
- Uppskattade rader och kostnader är desamma.
- QueryPlanHash är också densamma, vilket innebär att de har liknande genomförandeplaner.
Lägg ändå märke till skillnaderna:
- Cacheplanens storlek är större med hjälp av join än med hjälp av underfrågan
- Kompilering av CPU och tid (i ms), inklusive minnet i KB, som används för att analysera, binda och optimera exekveringsplanen är högre med hjälp av join än med hjälp av underfrågan
- CPU-tid och förfluten tid (i ms) för att exekvera planen är något högre med hjälp av join kontra underfrågan
I det här exemplet är underfrågan ett tic snabbare än kopplingen, även om de resulterande raderna är desamma.
Exempel 2
I det föregående exemplet använde vi bara en tabell. I exemplet som följer kommer vi att använda 3 olika tabeller.
Låt oss få detta att hända:
-- 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'
Båda frågorna matar ut samma 3806 rader.
Låt oss sedan ta en titt på deras genomförandeplaner:
Figur 4:Exekveringsplan för vårt andra exempel med en underfråga
Figur 5:Exekveringsplan för vårt andra exempel med en join
Kan du se de två genomförandeplanerna och hitta någon skillnad mellan dem? Vid ett ögonkast ser de likadana ut.
Men en mer noggrann undersökning med Jämför Showplan avslöjar vad som verkligen finns inuti.
Figur 6:Detaljer om Jämför Showplan för det andra exemplet
Låt oss börja med att analysera några likheter:
- Den rosa markeringen i exekveringsplanen avslöjar liknande operationer för båda frågorna. Eftersom den inre frågan använder en join istället för att kapsla underfrågor, är detta ganska förståeligt.
- De uppskattade operatörs- och underträdskostnaderna är desamma.
Låt oss sedan ta en titt på skillnaderna:
- För det första tog kompileringen längre tid när vi använde joins. Du kan kontrollera det i Compile CPU och Compile Time. Frågan med en underfråga tog dock ett högre kompileringsminne i KB.
- Då är QueryPlanHash för båda frågorna olika, vilket betyder att de har en annan exekveringsplan.
- Sistligen är den tid som förflutit och CPU-tiden för att köra planen snabbare med hjälp av join än att använda en underfråga.
Subquery vs. Join Performance Takeaway
Du kommer sannolikt att möta för många andra frågerelaterade problem som kan lösas genom att använda en sammanfogning eller en underfråga.
Men slutsatsen är att en underfråga inte är dålig i sig jämfört med sammanfogningar. Och det finns ingen tumregel att i en viss situation är en koppling bättre än en underfråga eller tvärtom.
Så, för att vara säker på att du har det bästa valet, kontrollera utförandeplanerna. Syftet med det är att få insikt i hur SQL Server kommer att bearbeta en viss fråga.
Men om du väljer att använda en underfråga, var medveten om att problem kan uppstå som kommer att testa din skicklighet.
Vanliga förbehåll vid användning av SQL-underfrågor
Det finns två vanliga problem som kan få dina frågor att bete sig vilt när du använder SQL-underfrågor.
The Pain of Column Name Resolution
Det här problemet introducerar logiska buggar i dina frågor och de kan vara mycket svåra att hitta. Ett exempel kan ytterligare förtydliga detta problem.
Låt oss börja med att skapa en tabell för demonstrationsändamål och fylla den med data.
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
Nu när tabellen är satt, låt oss avfyra några underfrågor med den. Men innan du kör frågan nedan, kom ihåg att leverantörs-ID:n som vi använde från den tidigare koden börjar med '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)
Ovanstående kod körs utan fel, som du kan se nedan. Hur som helst, var uppmärksam på listan över BusinessEntityIDs .
Figur 7:BusinessEntityIDs för resultatuppsättningen är oförenliga med posterna i MyVendors-tabellen
Infogade vi inte data med BusinessEntityID börjar med '14'? Vad är det då? Faktum är att vi kan se BusinessEntityIDs som börjar med '15' och '16'. Var kom dessa ifrån?
I själva verket listade frågan all data från ProductVendor bord.
I så fall kanske du tror att ett alias kommer att lösa det här problemet så att det hänvisar till MyVendors tabell precis som den nedan:
Figur 8:Att lägga till ett alias till BusinessEntityID resulterar i ett fel
Förutom att det verkliga problemet nu dök upp på grund av ett körtidsfel.
Kontrollera MyVendors tabell igen och du kommer att se det istället för BusinessEntityID , kolumnnamnet måste vara BusinessEntity_id (med ett understreck).
Om du använder det korrekta kolumnnamnet kommer det här problemet äntligen att lösas, som du kan se nedan:
Figur 9:Att ändra underfrågan med rätt kolumnnamn löste problemet
Som du kan se ovan kan vi nu observera BusinessEntityIDs börjar med '14' precis som vi har förväntat oss tidigare.
Men du kanske undrar: varför i hela friden tillät SQL Server att köra frågan på ett framgångsrikt sätt?
Här är kickern:Upplösningen av kolumnnamn utan alias fungerar i kontexten av underfrågan från att i sig själv gå ut till den yttre frågan. Det är därför hänvisningen till BusinessEntityID inuti underfrågan utlöste inte ett fel eftersom den hittas utanför underfrågan – i ProductVendor bord.
Med andra ord letar SQL Server efter den icke-aliasade kolumnen BusinessEntityID i MyVendors tabell. Eftersom den inte finns där letade den ut och hittade den i Produktleverantören tabell. Galet, eller hur?
Man kan säga att det är en bugg i SQL Server, men i själva verket är det designat i SQL-standarden och Microsoft följde det.
Okej, det är klart, vi kan inte göra något åt standarden, men hur kan vi undvika att stöta på ett fel?
- Först, prefix kolumnnamnen med tabellnamnet eller använd ett alias. Med andra ord, undvik tabellnamn utan prefix eller alias.
- För det andra, ha ett konsekvent namn på kolumner. Undvik att ha både BusinessEntityID och BusinessEntity_id , till exempel.
Låter bra? Ja, detta ger en viss förnuft i situationen.
Men detta är inte slutet på det.
Crazy NULLs
Som jag nämnde, det finns mer att täcka. T-SQL använder 3-värdig logik på grund av dess stöd för NULL . Och NULL kan nästan göra oss galna när vi använder SQL-underfrågor med NOT IN .
Låt mig börja med att presentera detta exempel:
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)
Utdata från frågan leder oss till en lista över produkter som inte finns i MyVendors tabell., enligt nedan:
Figur 10:Utdata från exempelfrågan med NOT IN
Anta nu att någon oavsiktligt infogat en post i MyVendors tabell med NULL BusinessEntity_id . Vad ska vi göra åt det?
Figur 11:Resultatuppsättningen blir tom när ett NULL BusinessEntity_id infogas i MyVendors
Vart tog all data vägen?
Du förstår, NOT operatören nekade IN predikat. Så, INTE SANT kommer nu att bli FALSKT . Men INTE NULL är OKÄNT. Det fick filtret att kassera raderna som är OKÄNDA, och detta är boven.
För att säkerställa att detta inte händer dig:
- Se till att tabellkolumnen inte tillåter NULL om data inte skulle vara så.
- Eller lägg till kolumnnamnet ÄR INTE NULL till din VAR klausul. I vårt fall är underfrågan följande:
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)
Hämtmat
Vi har pratat ganska mycket om delfrågor, och det är dags att tillhandahålla de viktigaste aspekterna av detta inlägg i form av en sammanfattad lista:
En underfråga:
- är en fråga i en fråga.
- omges inom parentes.
- kan ersätta ett uttryck var som helst.
- kan användas i SELECT , INSERT , UPPDATERA , RADERA, eller andra T-SQL-satser.
- kan vara fristående eller korrelerade.
- matar ut enstaka, multipla eller tabellvärden.
- fungerar med jämförelseoperatorer som =, <>,>, <,>=, <=och logiska operatorer som IN /INTE I och FINNS /FINNS INTE .
- är inte dålig eller ond. Det kan prestera bättre eller sämre än JOIN s beroende på en situation. Så ta mitt råd och kontrollera alltid utförandeplanerna.
- kan ha otäckt beteende på NULL s när den används med NOT IN , och när en kolumn inte är explicit identifierad med en tabell eller ett tabellalias.
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