Brent buys lunch for the ladies
The purpose of this post is to show a bit of syntax that often gets overlooked in favor of using query hints to force joins to occur in a particular order. We’ll start by creating three tables. One for employees, one for orders, and one for items in the order.
/* An employees table! How novel! */ CREATE TABLE #Ozars (OzarID INT IDENTITY(1,1) NOT NULL, OzarName VARCHAR(30) NOT NULL) INSERT INTO #Ozars (OzarName) VALUES ('Brent'), ('Jeremiah'), ('Kendra'), ('Doug'), ('Jessica'), ('Erik') ALTER TABLE #Ozars ADD CONSTRAINT [PK_Ozars] PRIMARY KEY CLUSTERED (OzarID, OzarName)
/* Luuuuuuuunch */ CREATE TABLE #Lunch (LunchID INT IDENTITY(1,1) NOT NULL, OzarID INT NOT NULL) INSERT INTO #Lunch (OzarID) VALUES (1),(1),(1),(3),(5) ALTER TABLE #Lunch ADD CONSTRAINT [PK_Lunch] PRIMARY KEY CLUSTERED (LunchID, OzarID)
/* Brent called it in, so it's all under his ID. Because that's how restaurants work. By ID. Yep. */ CREATE TABLE #LunchOrders (LunchOrderID INT IDENTITY(1,1) NOT NULL, LunchID INT NOT NULL, Lunch VARCHAR(20)) INSERT INTO #LunchOrders (LunchID, Lunch) VALUES (1, 'Just Churros'), (1, 'Box of Wine'), (1, 'Kaled Kale') ALTER TABLE #LunchOrders ADD CONSTRAINT [PK_LunchOrders] PRIMARY KEY CLUSTERED (LunchOrderID, LunchID)
A SQL celebrity gossip blog got a tip that someone from BOU ordered take-out. Not exactly an earth-shattering event, but querying minds want to know!
So they write a query, and then they look at the plan.
SELECT o.* , l.* , lo.* FROM #Ozars o LEFT JOIN #Lunch l ON l.OzarID = o.OzarID INNER JOIN #LunchOrders lo ON lo.LunchID = l.LunchID
And that’s way harsh. SQL went and changed our LEFT JOIN into an INNER JOIN. What was it thinking? Now we don’t know who Brent is having lunch with.
Who ordered the KALE?
Okay, we thought about it some. No more INNER JOIN.
We’ll get this done with another LEFT JOIN.
SELECT o.* ,l.* ,lo.* FROM #Ozars o LEFT JOIN #Lunch l ON l.OzarID = o.OzarID LEFT JOIN #LunchOrders lo ON lo.LunchID = l.LunchID
Unless you’re Ernie, that’s wayyyyy too many Brents.
First of all, ew. But yeah, you can do this, and it will come back with the right results.
SELECT o.* , lol.* FROM #Ozars o LEFT JOIN ( SELECT l.*, lo.LunchOrderID, lo.Lunch FROM #Lunch l INNER JOIN #LunchOrders lo ON lo.LunchID = l.LunchID ) lol ON o.OzarID = lol.OzarID
We can even try an OUTER APPLY. That’s a little nicer looking as a query…
SELECT o.* , lol.* FROM #Ozars o OUTER APPLY ( SELECT l.LunchID , l.OzarID, lo.LunchOrderID, lo.Lunch FROM #Lunch l INNER JOIN #LunchOrders lo ON lo.LunchID = l.LunchID WHERE o.OzarID = l.OzarID ) lol
… But same yucky plan.
Hi, I’m a cool trick.
SELECT o.* , l.* , lo.* FROM #Ozars o LEFT JOIN #Lunch l --They see me LEFT JOIN... INNER JOIN #LunchOrders lo --Then INNER JOIN... ON lo.LunchID = l.LunchID --Then write both my ON o.OzarID = l.OzarID --ON clauses
Nicer plan and a little less CPU than the others, on average.
This is an interesting concept to play with. With more than a couple JOINs, you can start using parentheses to group them together, like so:
SELECT o.* , l.* , lo.* FROM #Ozars o LEFT JOIN (#Lunch l INNER JOIN #LunchOrders lo ON lo.LunchID = l.LunchID) ON o.OzarID = l.OzarID
If you switch the order of the ON clauses in the second to last query, you’ll get an error. Take a guess why in the comments!
/* Clean me up, buttercup. */ --DROP TABLE #Ozars; --DROP TABLE #Lunch; --DROP TABLE #LunchOrders;
Brent says: Okay, first off, I don’t normally drink an entire box of wine for lunch, but I had to wash down all that kale. Second off, judging by these execution plans, SQL Server intercepted my wine delivery.
Kendra says: I’ve found parentheses join hints twice in production code in SQL Server. In both cases, nobody knew why they were there, what would happen if they rewrote the query, or even if they’d been used on purpose or if it was just an accident. If you use this technique, document your code heavily as to what you’re doing and why, or you’ll be that person everyone grumbles about.