Skip to content

SQL Joins: All Types and Join Optimization

You will learn:

  • INNER / LEFT / RIGHT / FULL OUTER / CROSS joins
  • SELF join, multi-table joins, NON-EQUI join
  • Semi join (EXISTS) and Anti join (NOT EXISTS / LEFT JOIN ... IS NULL)
  • Filter in ON vs WHERE (very common interview question)
  • Join optimization: indexes on join keys, SARGable predicates, STATISTICS IO, execution plans, join algorithms (Nested Loop / Merge / Hash)

Run the sample database script first. Every query below was run on that data, and the Output under each query is the real result.

USE SqlLearningDb;
GO

Topic tables: SalaryGrades (for non-equi join) and Shifts (for cross join)

DROP TABLE IF EXISTS dbo.SalaryGrades;
DROP TABLE IF EXISTS dbo.Shifts;

CREATE TABLE dbo.SalaryGrades
(
    Grade     CHAR(1)       PRIMARY KEY,
    MinSalary DECIMAL(10,2) NOT NULL,
    MaxSalary DECIMAL(10,2) NOT NULL
);

INSERT INTO dbo.SalaryGrades (Grade, MinSalary, MaxSalary) VALUES
('E',      0,  49999.99),
('D',  50000,  74999.99),
('C',  75000,  99999.99),
('B', 100000, 149999.99),
('A', 150000, 999999.99);

CREATE TABLE dbo.Shifts
(
    ShiftName VARCHAR(20) PRIMARY KEY,
    StartTime TIME NOT NULL,
    EndTime   TIME NOT NULL
);

INSERT INTO dbo.Shifts (ShiftName, StartTime, EndTime) VALUES
('Morning', '06:00', '14:00'),
('Evening', '14:00', '22:00'),
('Night',   '22:00', '06:00');
GO
Messages
(5 rows affected)
(3 rows affected)

Part A: JOIN Types

INNER JOIN: only matching rows (Intern with NULL department disappears)

SELECT e.EmployeeID, e.FirstName, d.DepartmentName
FROM dbo.Employees AS e
INNER JOIN dbo.Departments AS d ON d.DepartmentID = e.DepartmentID;
Output
EmployeeIDFirstNameDepartmentName
1RajeshExecutive
2AnitaSales
3VikramIT
4MeenaHR
5SureshFinance
6KavyaMarketing
7ArjunSales
8PoojaSales
9RahulSales
10SnehaSales
11KiranIT
12DivyaIT
13AmitIT
14NehaIT
15SanjayIT
... showing 15 of 24 rows

LEFT JOIN: all employees, department may be NULL

SELECT e.EmployeeID, e.FirstName, d.DepartmentName
FROM dbo.Employees AS e
LEFT JOIN dbo.Departments AS d ON d.DepartmentID = e.DepartmentID;
Output
EmployeeIDFirstNameDepartmentName
1RajeshExecutive
2AnitaSales
3VikramIT
4MeenaHR
5SureshFinance
6KavyaMarketing
7ArjunSales
8PoojaSales
9RahulSales
10SnehaSales
11KiranIT
12DivyaIT
13AmitIT
14NehaIT
15SanjayIT
... showing 15 of 25 rows

RIGHT JOIN: all departments, even Research with no employees

SELECT d.DepartmentName, e.FirstName
FROM dbo.Employees AS e
RIGHT JOIN dbo.Departments AS d ON d.DepartmentID = e.DepartmentID;
Output
DepartmentNameFirstName
ExecutiveRajesh
SalesAnita
SalesArjun
SalesPooja
SalesRahul
SalesSneha
SalesRitu
MarketingKavya
MarketingRohit
MarketingSwati
ITVikram
ITKiran
ITDivya
ITAmit
ITNeha
... showing 15 of 25 rows

FULL OUTER JOIN: everything from both sides

(shows the Intern with no dept AND Research with no employees)
SELECT e.FirstName, d.DepartmentName
FROM dbo.Employees AS e
FULL OUTER JOIN dbo.Departments AS d ON d.DepartmentID = e.DepartmentID
WHERE e.EmployeeID IS NULL OR d.DepartmentID IS NULL;
Output
FirstNameDepartmentName
NaveenNULL
NULLResearch

CROSS JOIN: every combination (8 depts x 3 shifts = 24 rows)

SELECT d.DepartmentName, s.ShiftName
FROM dbo.Departments AS d
CROSS JOIN dbo.Shifts AS s
ORDER BY d.DepartmentName, s.StartTime;
Output
DepartmentNameShiftName
ExecutiveMorning
ExecutiveEvening
ExecutiveNight
FinanceMorning
FinanceEvening
FinanceNight
HRMorning
HREvening
HRNight
ITMorning
ITEvening
ITNight
MarketingMorning
MarketingEvening
MarketingNight
... showing 15 of 24 rows

SELF JOIN: employee with his/her manager name

SELECT e.EmployeeID,
       e.FirstName + ' ' + e.LastName AS Employee,
       m.FirstName + ' ' + m.LastName AS Manager
FROM dbo.Employees AS e
LEFT JOIN dbo.Employees AS m ON m.EmployeeID = e.ManagerID;
Output
EmployeeIDEmployeeManager
1Rajesh KumarNULL
2Anita SharmaRajesh Kumar
3Vikram ReddyRajesh Kumar
4Meena IyerRajesh Kumar
5Suresh PatelRajesh Kumar
6Kavya NairAnita Sharma
7Arjun MehtaAnita Sharma
8Pooja SinghArjun Mehta
9Rahul VermaArjun Mehta
10Sneha GuptaArjun Mehta
11Kiran RaoVikram Reddy
12Divya MenonKiran Rao
13Amit JoshiKiran Rao
14Neha KapoorVikram Reddy
15Sanjay DasVikram Reddy
... showing 15 of 25 rows

SELF JOIN: employees who earn More than their manager (should be none)

and employees who joined BEFORE their manager

SELECT e.FirstName AS Employee, e.HireDate, m.FirstName AS Manager, m.HireDate AS ManagerHireDate
FROM dbo.Employees AS e
JOIN dbo.Employees AS m ON m.EmployeeID = e.ManagerID
WHERE e.HireDate < m.HireDate;
Output
EmployeeHireDateManagerManagerHireDate
(0 rows)

Non-equi JOIN: join using BETWEEN instead of =

SELECT e.FirstName, e.Salary, g.Grade
FROM dbo.Employees AS e
JOIN dbo.SalaryGrades AS g ON e.Salary BETWEEN g.MinSalary AND g.MaxSalary
ORDER BY e.Salary DESC;
Output
FirstNameSalaryGrade
Rajesh250000.00A
Vikram190000.00A
Anita180000.00A
Suresh130000.00B
Meena120000.00B
Kavya110000.00B
Deepak105000.00B
Arjun100000.00B
Kiran95000.00C
Sanjay90000.00C
Neha75000.00C
Divya70000.00D
Geeta70000.00D
Amit68000.00D
Lakshmi62000.00D
... showing 15 of 25 rows

Multi-table JOIN: full order details (5 tables)

SELECT o.OrderID, o.OrderDate, c.CustomerName,
       e.FirstName AS SalesPerson, p.ProductName, cat.CategoryName,
       oi.Quantity, oi.UnitPrice, oi.LineTotal
FROM dbo.Orders      AS o
JOIN dbo.Customers   AS c   ON c.CustomerID  = o.CustomerID
JOIN dbo.Employees   AS e   ON e.EmployeeID  = o.EmployeeID
JOIN dbo.OrderItems  AS oi  ON oi.OrderID    = o.OrderID
JOIN dbo.Products    AS p   ON p.ProductID   = oi.ProductID
JOIN dbo.Categories  AS cat ON cat.CategoryID = p.CategoryID
ORDER BY o.OrderID;
Output
OrderIDOrderDateCustomerNameSalesPersonProductNameCategoryNameQuantityUnitPriceLineTotal
12024-01-09Ravi TradersRahulLenovo ThinkCentreDesktops355000.00165000.00
12024-01-09Ravi TradersRahulMacBook Air M3Laptops1120000.00120000.00
22024-01-17Deccan SystemsRituDesk OrganizerStationery2450.00810.00
32024-01-26Deccan SystemsSnehaDell Inspiron 15Laptops265000.00130000.00
32024-01-26Deccan SystemsSnehaSamsung Galaxy A55Mobiles125000.0025000.00
32024-01-26Deccan SystemsSnehaBasic Office ChairOffice Chairs28500.0017000.00
42024-01-29Silicon Valley Pvt LtdArjunHP Pavilion 14Laptops385000.00255000.00
42024-01-29Silicon Valley Pvt LtdArjunStanding DeskDesks222000.0039600.00
42024-01-29Silicon Valley Pvt LtdArjunLenovo ThinkCentreDesktops255000.0099000.00
52024-02-06Lakshmi StoresArjunRedmi Note 13Mobiles115000.0014250.00
62024-02-14Marina DigitalRahuliPhone 15Mobiles170000.0063000.00
62024-02-14Marina DigitalRahulWireless MouseAccessories21200.002400.00
62024-02-14Marina DigitalRahulMacBook Air M3Laptops3120000.00342000.00
72024-02-20Mumbai MartRituHP Pavilion 14Laptops285000.00161500.00
72024-02-20Mumbai MartRituRedmi Note 13Mobiles215000.0030000.00
... showing 15 of 202 rows

Part B: SEMI JOIN and ANTI JOIN

SEMI JOIN: customers who placed at least one order (no duplicates)

SELECT c.CustomerID, c.CustomerName
FROM dbo.Customers AS c
WHERE EXISTS (SELECT 1 FROM dbo.Orders AS o WHERE o.CustomerID = c.CustomerID);
Output
CustomerIDCustomerName
1Ravi Traders
2Infosoft Solutions
3Lakshmi Stores
4Mumbai Mart
5Pune Tech Park
6Capital Electronics
7Bengal Book House
8Deccan Systems
9Gujarat Office Supply
10Pink City Furnishers
11Kerala Kart
12Awadh Enterprises
13Silicon Valley Pvt Ltd
14Gateway Retail
15Marina Digital
... showing 15 of 17 rows

ANTI JOIN using NOT EXISTS: customers who never ordered

SELECT c.CustomerID, c.CustomerName
FROM dbo.Customers AS c
WHERE NOT EXISTS (SELECT 1 FROM dbo.Orders AS o WHERE o.CustomerID = c.CustomerID);
Output
CustomerIDCustomerName
18Nizam Bakers
19Coastal Exports
20Himalaya Ventures

ANTI JOIN using LEFT JOIN ... IS NULL: products never ordered

SELECT p.ProductID, p.ProductName
FROM dbo.Products AS p
LEFT JOIN dbo.OrderItems AS oi ON oi.ProductID = p.ProductID
WHERE oi.OrderItemID IS NULL;
Output
ProductIDProductName
19Old Model Tablet
20Laser Printer

Watch out! Joining 1-to-many creates duplicates.

Wrong : this counts the customer once per order

SELECT c.CustomerName, COUNT(*) AS RowsAfterJoin
FROM dbo.Customers AS c
JOIN dbo.Orders AS o ON o.CustomerID = c.CustomerID
GROUP BY c.CustomerName;
Output
CustomerNameRowsAfterJoin
Awadh Enterprises5
Bengal Book House2
Capital Electronics2
Deccan Systems3
Gateway Retail1
Gujarat Office Supply3
Infosoft Solutions6
Kerala Kart3
Lakshmi Stores13
Marina Digital6
Mumbai Mart4
Northern Distributors2
Pink City Furnishers4
Pune Tech Park8
Ravi Traders11
... showing 15 of 17 rows

Part C: Filter in "ON" vs "WHERE" with LEFT JOIN

Filter in ON: keeps All customers, only joins 2025 orders

SELECT c.CustomerName, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
LEFT JOIN dbo.Orders AS o
       ON o.CustomerID = c.CustomerID
      AND o.OrderDate >= '2025-01-01'
ORDER BY c.CustomerID;
Output
CustomerNameOrderIDOrderDate
Ravi Traders642025-03-08
Ravi Traders712025-04-25
Ravi Traders732025-05-09
Ravi Traders752025-05-21
Infosoft Solutions572025-01-18
Infosoft Solutions582025-01-26
Lakshmi Stores672025-03-28
Lakshmi Stores702025-04-17
Lakshmi Stores762025-05-27
Lakshmi Stores792025-06-15
Mumbai Mart622025-02-22
Mumbai Mart682025-04-02
Mumbai Mart692025-04-12
Pune Tech Park662025-03-22
Pune Tech Park742025-05-14
... showing 15 of 35 rows

Filter in WHERE: turns the LEFT JOIN into an INNER JOIN

(customers without 2025 orders vanish)
SELECT c.CustomerName, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
LEFT JOIN dbo.Orders AS o ON o.CustomerID = c.CustomerID
WHERE o.OrderDate >= '2025-01-01'
ORDER BY c.CustomerID;
Output
CustomerNameOrderIDOrderDate
Ravi Traders642025-03-08
Ravi Traders712025-04-25
Ravi Traders752025-05-21
Ravi Traders732025-05-09
Infosoft Solutions572025-01-18
Infosoft Solutions582025-01-26
Lakshmi Stores702025-04-17
Lakshmi Stores672025-03-28
Lakshmi Stores792025-06-15
Lakshmi Stores762025-05-27
Mumbai Mart682025-04-02
Mumbai Mart692025-04-12
Mumbai Mart622025-02-22
Pune Tech Park662025-03-22
Pune Tech Park772025-06-03
... showing 15 of 26 rows

Report: every department with employee count and total salary (including 0)

SELECT d.DepartmentName,
       COUNT(e.EmployeeID)      AS EmployeeCount,   -- COUNT(column) ignores NULL
       ISNULL(SUM(e.Salary), 0) AS TotalSalary
FROM dbo.Departments AS d
LEFT JOIN dbo.Employees AS e ON e.DepartmentID = d.DepartmentID
GROUP BY d.DepartmentName
ORDER BY TotalSalary DESC;
Output
DepartmentNameEmployeeCountTotalSalary
IT7658000.00
Sales6500000.00
Finance3252000.00
Executive1250000.00
Marketing3202000.00
HR2165000.00
Operations2145000.00
Research00.00

Part D: JOIN Optimization

Tip: In SSMS press Ctrl+M (Include Actual Execution Plan) before running.

-- Start clean (so this file can be run many times)
DROP INDEX IF EXISTS IX_Orders_CustomerID    ON dbo.Orders;
DROP INDEX IF EXISTS IX_Orders_EmployeeID    ON dbo.Orders;
DROP INDEX IF EXISTS IX_OrderItems_OrderID   ON dbo.OrderItems;
DROP INDEX IF EXISTS IX_OrderItems_ProductID ON dbo.OrderItems;
DROP INDEX IF EXISTS IX_Employees_DeptID     ON dbo.Employees;
GO

SET STATISTICS IO ON;    -- shows logical reads in the "Messages" tab
SET STATISTICS TIME ON;  -- shows CPU and elapsed time
GO

Foreign keys are NOT indexed automatically in SQL Server.

Without an index on Orders.CustomerID the join must scan Orders.
SELECT c.CustomerName, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
JOIN dbo.Orders AS o ON o.CustomerID = c.CustomerID
WHERE c.City = 'Hyderabad';
GO
Messages
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 4 ms.
Output
CustomerNameOrderIDOrderDate
Ravi Traders12024-01-09
Deccan Systems22024-01-17
Deccan Systems32024-01-26
Ravi Traders102024-03-12
Ravi Traders132024-04-02
Ravi Traders262024-06-28
Deccan Systems282024-07-09
Ravi Traders392024-09-23
Ravi Traders462024-11-06
Ravi Traders522024-12-18
Ravi Traders642025-03-08
Ravi Traders712025-04-25
Ravi Traders732025-05-09
Ravi Traders752025-05-21
Messages
Table 'Customers'. Scan count 0, logical reads 160, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Orders'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

Create indexes on the join (foreign key) columns

CREATE NONCLUSTERED INDEX IX_Orders_CustomerID     ON dbo.Orders(CustomerID) INCLUDE (OrderDate, Status);
CREATE NONCLUSTERED INDEX IX_Orders_EmployeeID     ON dbo.Orders(EmployeeID);
CREATE NONCLUSTERED INDEX IX_OrderItems_OrderID    ON dbo.OrderItems(OrderID)   INCLUDE (ProductID, Quantity, UnitPrice);
CREATE NONCLUSTERED INDEX IX_OrderItems_ProductID  ON dbo.OrderItems(ProductID);
CREATE NONCLUSTERED INDEX IX_Employees_DeptID      ON dbo.Employees(DepartmentID);
GO
Messages
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.
Table 'Orders'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 2 ms.
Table 'Orders'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 1 ms.
Table 'OrderItems'. Scan count 1, logical reads 4, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 1 ms.
Table 'OrderItems'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 2 ms.
Table 'Employees'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 1 ms.

Same query again: compare logical reads / plan (Index Seek instead of Scan)

SELECT c.CustomerName, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
JOIN dbo.Orders AS o ON o.CustomerID = c.CustomerID
WHERE c.City = 'Hyderabad';
GO
Messages
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 1 ms.
Output
CustomerNameOrderIDOrderDate
Ravi Traders12024-01-09
Ravi Traders102024-03-12
Ravi Traders132024-04-02
Ravi Traders262024-06-28
Ravi Traders392024-09-23
Ravi Traders462024-11-06
Ravi Traders522024-12-18
Ravi Traders642025-03-08
Ravi Traders712025-04-25
Ravi Traders732025-05-09
Ravi Traders752025-05-21
Deccan Systems22024-01-17
Deccan Systems32024-01-26
Deccan Systems282024-07-09
Messages
Table 'Orders'. Scan count 3, logical reads 6, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

SARGable vs Non-SARGable predicates

Non-SARGable : function on the column -> index cannot be seeked
SELECT OrderID, OrderDate FROM dbo.Orders WHERE YEAR(OrderDate) = 2025;
--     SARGable : compare the raw column with a range -> index seek possible
SELECT OrderID, OrderDate FROM dbo.Orders WHERE OrderDate >= '2025-01-01' AND OrderDate < '2026-01-01';
GO
Messages
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 1 ms.
Output
OrderIDOrderDate
642025-03-08
712025-04-25
732025-05-09
752025-05-21
572025-01-18
582025-01-26
672025-03-28
702025-04-17
762025-05-27
792025-06-15
622025-02-22
682025-04-02
692025-04-12
662025-03-22
742025-05-14
... showing 15 of 26 rows
Messages
Table 'Orders'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.
Output
OrderIDOrderDate
642025-03-08
712025-04-25
732025-05-09
752025-05-21
572025-01-18
582025-01-26
672025-03-28
702025-04-17
762025-05-27
792025-06-15
622025-02-22
682025-04-02
692025-04-12
662025-03-22
742025-05-14
... showing 15 of 26 rows
Messages
Table 'Orders'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

Pre-aggregate Before joining (less rows to join = faster)

Slower pattern : join everything, then group

SELECT c.CustomerName, SUM(oi.LineTotal) AS Revenue
FROM dbo.Customers AS c
JOIN dbo.Orders     AS o  ON o.CustomerID = c.CustomerID
JOIN dbo.OrderItems AS oi ON oi.OrderID   = o.OrderID
GROUP BY c.CustomerName;

--     Better pattern on big tables : aggregate first, then join small result
WITH OrderTotals AS
(
    SELECT o.CustomerID, SUM(oi.LineTotal) AS Revenue
    FROM dbo.Orders AS o
    JOIN dbo.OrderItems AS oi ON oi.OrderID = o.OrderID
    GROUP BY o.CustomerID
)
SELECT c.CustomerName, t.Revenue
FROM OrderTotals AS t
JOIN dbo.Customers AS c ON c.CustomerID = t.CustomerID;
GO
Messages
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 7 ms.
Output
CustomerNameRevenue
Awadh Enterprises673885.00
Bengal Book House259075.00
Capital Electronics115100.00
Deccan Systems220310.00
Gateway Retail476850.00
Gujarat Office Supply148800.00
Infosoft Solutions1163225.00
Kerala Kart644925.00
Lakshmi Stores1925592.50
Marina Digital1326609.00
Mumbai Mart365735.00
Northern Distributors275200.00
Pink City Furnishers536100.00
Pune Tech Park1649975.00
Ravi Traders1527910.00
... showing 15 of 17 rows
Messages
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'OrderItems'. Scan count 1, logical reads 4, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Orders'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 1 ms.
Output
CustomerNameRevenue
Ravi Traders1527910.00
Infosoft Solutions1163225.00
Lakshmi Stores1925592.50
Mumbai Mart365735.00
Pune Tech Park1649975.00
Capital Electronics115100.00
Bengal Book House259075.00
Deccan Systems220310.00
Gujarat Office Supply148800.00
Pink City Furnishers536100.00
Kerala Kart644925.00
Awadh Enterprises673885.00
Silicon Valley Pvt Ltd574857.00
Gateway Retail476850.00
Marina Digital1326609.00
... showing 15 of 17 rows
Messages
Table 'Customers'. Scan count 0, logical reads 34, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Orders'. Scan count 0, logical reads 404, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'OrderItems'. Scan count 1, logical reads 4, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

Join algorithms: SQL Server chooses one automatically.

You can FORCE one for learning (do NOT do this in production without reason).

SELECT c.CustomerName, o.OrderID FROM dbo.Customers c INNER LOOP  JOIN dbo.Orders o ON o.CustomerID = c.CustomerID;  -- Nested Loops : small outer input + index on inner
SELECT c.CustomerName, o.OrderID FROM dbo.Customers c INNER MERGE JOIN dbo.Orders o ON o.CustomerID = c.CustomerID;  -- Merge Join   : both inputs sorted on join key
SELECT c.CustomerName, o.OrderID FROM dbo.Customers c INNER HASH  JOIN dbo.Orders o ON o.CustomerID = c.CustomerID;  -- Hash Match   : large unsorted inputs
GO
Messages
Warning: The join order has been enforced because a local join hint is used.
Warning: The join order has been enforced because a local join hint is used.
Warning: The join order has been enforced because a local join hint is used.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 2 ms.
Output
CustomerNameOrderID
Ravi Traders1
Ravi Traders10
Ravi Traders13
Ravi Traders26
Ravi Traders39
Ravi Traders46
Ravi Traders52
Ravi Traders64
Ravi Traders71
Ravi Traders73
Ravi Traders75
Infosoft Solutions14
Infosoft Solutions20
Infosoft Solutions21
Infosoft Solutions35
... showing 15 of 80 rows
Messages
Table 'Orders'. Scan count 20, logical reads 40, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
Output
CustomerNameOrderID
Ravi Traders1
Ravi Traders10
Ravi Traders13
Ravi Traders26
Ravi Traders39
Ravi Traders46
Ravi Traders52
Ravi Traders64
Ravi Traders71
Ravi Traders73
Ravi Traders75
Infosoft Solutions14
Infosoft Solutions20
Infosoft Solutions21
Infosoft Solutions35
... showing 15 of 80 rows
Messages
Table 'Orders'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
Output
CustomerNameOrderID
Ravi Traders1
Ravi Traders10
Ravi Traders13
Ravi Traders26
Ravi Traders39
Ravi Traders46
Ravi Traders52
Ravi Traders64
Ravi Traders71
Ravi Traders73
Ravi Traders75
Infosoft Solutions14
Infosoft Solutions20
Infosoft Solutions21
Infosoft Solutions35
... showing 15 of 80 rows
Messages
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Orders'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

Data types must match on join columns.

Joining INT to VARCHAR causes CONVERT_IMPLICIT -> scans instead of seeks.
Always keep FK column type = PK column type.
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
GO
Messages
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.
SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

Practice Questions

  1. List each manager with the number of direct reports.
  2. Show customers and the total number of orders, including customers with 0 orders.
  3. Find categories that have no products (hint: anti join).
  4. Find pairs of employees who work in the same city but different departments.
  5. For each order show the customer city and ship city, and flag if they differ.