sql server hub / joins guide

SQL Server joins guide

SQL Server joins are how queries combine rows from related tables. The join type decides which rows stay, which rows disappear, and where duplicates can appear.

Use the examples to check join types, ON clauses, missing matches, row multiplication, and APPLY queries without guessing from syntax alone.

INNERLEFTFULLCROSSAPPLY
SQL Server joins diagramTwo SQL Server table panels connect through join keys into result rows, with labels for common join types.CUSTOMERSORDERSJOIN RESULTCustomerIDCustomerNameRegionStatusOrderIDCustomerIDOrderDateStatusINNERLEFTFULLCROSSAPPLYCustomerID = CustomerID

Guide

Guide~12 min readUpdated 2 Jun 2026

Share

LinkedInXEmail
  1. 01What a SQL Server JOIN does
  2. 02Sample tables used in the examples
  3. 03SQL Server INNER JOIN example
  4. 04SQL Server LEFT JOIN example
  5. 05SQL Server RIGHT JOIN
  6. 06SQL Server FULL OUTER JOIN example
  7. 07SQL Server CROSS JOIN example
  8. 08SQL Server CROSS APPLY and OUTER APPLY
  9. 09SQL Server JOIN ON clauses
  10. 10SQL Server join duplicates and row multiplication
  11. 11JOINs, UNION, MERGE, PIVOT, and APPLY
  12. 12Performance notes for SQL Server joins
  13. 13Quick reference table
  14. 14Need a SQL Server performance review?

What a SQL Server JOIN does

A SQL Server JOIN combines rows from two table sources. The join predicate says how the rows relate. In most day-to-day queries that means matching a foreign key to a primary key, such as Orders.CustomerID to Customers.CustomerID.

The logical join type decides which rows can appear in the result: matching rows only, all rows from one side, all rows from both sides, or every possible combination. SQL Server then chooses a physical join algorithm to run the query. Microsoft documents nested loops, merge, hash, and adaptive joins, but those are execution choices. They do not change the logical meaning of your query.

The main types of SQL join in SQL Server are INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN, and CROSS JOIN. MSSQL is common shorthand for Microsoft SQL Server, so a join MSSQL example usually means the same T-SQL joins, sometimes typed as T SQL joins, shown here.

JOIN without another keyword means INNER JOIN. Write INNER JOIN when clarity matters. Future readers should not need to remember default syntax while debugging a production report.

Sample tables used in the examples

The examples below use temporary tables so the queries are runnable in SQL Server Management Studio or Azure Data Studio. The sample data intentionally includes customers with no orders, an order with a missing customer, and orders with multiple lines.

That gives the joins something useful to show. Perfect demo data makes joins look cleaner than real reporting data. Real data usually contains missing rows, optional relationships, one-to-many joins, and old records that do not fit the neat diagram.

Create sample tables

Creates temporary tables for customers, orders, order lines, products, and calendar dates.

IF OBJECT_ID('tempdb..#OrderLines') IS NOT NULL DROP TABLE #OrderLines;
IF OBJECT_ID('tempdb..#Orders') IS NOT NULL DROP TABLE #Orders;
IF OBJECT_ID('tempdb..#Products') IS NOT NULL DROP TABLE #Products;
IF OBJECT_ID('tempdb..#Customers') IS NOT NULL DROP TABLE #Customers;
IF OBJECT_ID('tempdb..#CalendarDates') IS NOT NULL DROP TABLE #CalendarDates;

CREATE TABLE #Customers
(
    CustomerID int NOT NULL PRIMARY KEY,
    CustomerName varchar(40) NOT NULL,
    Region varchar(20) NOT NULL
);

CREATE TABLE #Orders
(
    OrderID int NOT NULL PRIMARY KEY,
    CustomerID int NOT NULL,
    OrderDate date NOT NULL,
    Status varchar(20) NOT NULL
);

CREATE TABLE #Products
(
    ProductID int NOT NULL PRIMARY KEY,
    ProductName varchar(40) NOT NULL,
    IsActive bit NOT NULL
);

CREATE TABLE #OrderLines
(
    OrderLineID int NOT NULL PRIMARY KEY,
    OrderID int NOT NULL,
    ProductID int NOT NULL,
    Quantity int NOT NULL
);

CREATE TABLE #CalendarDates
(
    CalendarDate date NOT NULL PRIMARY KEY
);

INSERT INTO #Customers (CustomerID, CustomerName, Region)
VALUES
    (1, 'Acme Ltd', 'North'),
    (2, 'Blue Street', 'South'),
    (3, 'No Orders Co', 'North'),
    (4, 'Quiet Shop', 'West');

INSERT INTO #Orders (OrderID, CustomerID, OrderDate, Status)
VALUES
    (1001, 1, '2026-05-01', 'Paid'),
    (1002, 1, '2026-05-03', 'Open'),
    (1003, 2, '2026-05-04', 'Paid'),
    (1004, 99, '2026-05-05', 'Open');

INSERT INTO #Products (ProductID, ProductName, IsActive)
VALUES
    (10, 'Backup review', 1),
    (11, 'Performance review', 1),
    (12, 'Retired report', 0);

INSERT INTO #OrderLines (OrderLineID, OrderID, ProductID, Quantity)
VALUES
    (1, 1001, 10, 1),
    (2, 1001, 11, 2),
    (3, 1002, 11, 1),
    (4, 1003, 10, 1);

INSERT INTO #CalendarDates (CalendarDate)
VALUES
    ('2026-05-01'),
    ('2026-05-02'),
    ('2026-05-03');

Run this setup once before running the examples. Order 1004 has CustomerID 99, which does not exist in #Customers. Customer 3 and Customer 4 have no matching orders. Order 1001 has two order lines, which is useful for duplicate-row examples.

SQL Server INNER JOIN example

An INNER JOIN returns rows where the join predicate matches on both sides. If a customer has no orders, that customer is not returned. If an order points to a missing customer, that order is not returned either.

This is the common SQL Server join for normal lookups: orders with customers, order lines with products, users with departments, and other relationships where unmatched rows are not useful for the result. If someone searches for MSSQL inner join, this is the SQL Server INNER JOIN pattern they usually need.

SQL Server INNER JOIN example

Returns only customers that have matching orders.

SELECT
    c.CustomerName,
    o.OrderID,
    o.OrderDate,
    o.Status
FROM #Customers AS c
INNER JOIN #Orders AS o
    ON o.CustomerID = c.CustomerID
ORDER BY c.CustomerName, o.OrderDate;

Acme Ltd appears twice because it has two orders. No Orders Co and Quiet Shop do not appear because they have no matching orders. Order 1004 does not appear because its CustomerID is missing from #Customers.

Multiple inner joins keep narrowing the result to rows that match across all joined tables. That is often correct, but it can also hide missing data. If a report suddenly loses rows after another join is added, check which relationship is no longer optional.

Multiple INNER JOINs

Joins customers, orders, order lines, and products.

SELECT
    c.CustomerName,
    o.OrderID,
    p.ProductName,
    ol.Quantity
FROM #Customers AS c
INNER JOIN #Orders AS o
    ON o.CustomerID = c.CustomerID
INNER JOIN #OrderLines AS ol
    ON ol.OrderID = o.OrderID
INNER JOIN #Products AS p
    ON p.ProductID = ol.ProductID
ORDER BY c.CustomerName, o.OrderID, p.ProductName;

The result is now at order-line grain, not order grain. One order can appear more than once when it has several lines. That multiplication is normal for one-to-many joins.

SQL Server LEFT JOIN example

A LEFT JOIN returns all rows from the left table and matching rows from the right table. When no right-side row exists, SQL Server returns NULL for the right-side columns.

Use a left join in SQL Server when the left-side entity must stay in the result: all customers, all products, all databases, all jobs, all expected dates. The right side is optional.

SQL Server LEFT JOIN example

Returns every customer, including customers without orders.

SELECT
    c.CustomerName,
    o.OrderID,
    o.Status
FROM #Customers AS c
LEFT JOIN #Orders AS o
    ON o.CustomerID = c.CustomerID
ORDER BY c.CustomerName, o.OrderID;

Customers without orders stay in the result. Their order columns are NULL. This is the usual pattern for finding missing related data.

The common mistake is filtering the right-side table in the WHERE clause. That can remove the NULL rows and turn the practical result into an inner join.

LEFT JOIN filter placement

Shows the difference between filtering in the ON clause and filtering in the WHERE clause.

-- Keeps all customers, then matches only paid orders.
SELECT
    c.CustomerName,
    o.OrderID,
    o.Status
FROM #Customers AS c
LEFT JOIN #Orders AS o
    ON o.CustomerID = c.CustomerID
   AND o.Status = 'Paid'
ORDER BY c.CustomerName, o.OrderID;

-- This removes customers without a paid order.
-- It behaves like an INNER JOIN for this condition.
SELECT
    c.CustomerName,
    o.OrderID,
    o.Status
FROM #Customers AS c
LEFT JOIN #Orders AS o
    ON o.CustomerID = c.CustomerID
WHERE o.Status = 'Paid'
ORDER BY c.CustomerName, o.OrderID;

Put optional-side filters in the ON clause when unmatched left rows must remain. Put right-side filters in WHERE only when removing unmatched rows is intended. This is one of the fastest ways to break a report without changing the join keyword.

SQL Server RIGHT JOIN

A RIGHT JOIN preserves rows from the right table and returns matching rows from the left table. It is valid SQL Server outer join syntax, but in normal work it is usually clearer to swap the table order and write a LEFT JOIN.

People read joins from left to right. If the preserved table matters, put it first. That keeps the query easier to review when the FROM clause grows.

RIGHT JOIN example

Preserves customers from the right side. A LEFT JOIN with swapped table order is usually easier to read.

-- Same result as a LEFT JOIN after swapping table order.
SELECT
    c.CustomerName,
    o.OrderID,
    o.Status
FROM #Orders AS o
RIGHT JOIN #Customers AS c
    ON c.CustomerID = o.CustomerID
ORDER BY c.CustomerName, o.OrderID;

SQL Server FULL OUTER JOIN example

A FULL OUTER JOIN returns matching rows plus unmatched rows from both sides. It is useful for reconciliation, audit queries, and finding rows that exist in one source but not the other.

People often search for full join in SQL Server or full join MSSQL. In T-SQL, use FULL OUTER JOIN. It is less common for normal lookup queries. If the result has many nullable columns from both sides, check whether you are comparing sets or trying to build a daily report with the wrong join.

SQL Server FULL OUTER JOIN example

Finds customers without orders and orders without customers.

SELECT
    c.CustomerID AS CustomerTableID,
    c.CustomerName,
    o.OrderID,
    o.CustomerID AS OrderTableCustomerID,
    o.Status
FROM #Customers AS c
FULL OUTER JOIN #Orders AS o
    ON o.CustomerID = c.CustomerID
WHERE c.CustomerID IS NULL
   OR o.CustomerID IS NULL
ORDER BY COALESCE(c.CustomerID, o.CustomerID), o.OrderID;

Rows with a NULL customer side are orders that do not match a customer. Rows with a NULL order side are customers without orders. This is useful for reconciliation because both missing directions are visible.

SQL Server CROSS JOIN example

A CROSS JOIN returns every combination of rows from both sides. If one side has 2 rows and the other side has 3 rows, the result has 6 rows. If one side has 10,000 rows and the other side has 10,000 rows, the result has 100,000,000 rows. SQL Server will return the requested combinations, even when the row count is much larger than intended.

A planned cross join can be useful for building date/product combinations, test matrices, or expected reporting rows before left joining actual data. People often search this as cross join MSSQL; in SQL Server, the important part is the same row-count multiplication. An accidental cross join usually means a missing join predicate.

SQL Server CROSS JOIN example

Builds every active product and calendar-date combination.

SELECT
    p.ProductName,
    d.CalendarDate
FROM #Products AS p
CROSS JOIN #CalendarDates AS d
WHERE p.IsActive = 1
ORDER BY p.ProductName, d.CalendarDate;

The active product rows multiply by the calendar date rows. Check row counts before running a cross join on large tables. If a normal join suddenly returns far too many rows, look for a missing ON predicate.

SQL Server CROSS APPLY and OUTER APPLY

APPLY is a T-SQL table operator used in the FROM clause. It lets the right-side table expression reference columns from the left side. That makes it useful for top-N-per-row queries, table-valued functions, and correlated row sets.

CROSS APPLY behaves like an inner join against the applied row set: if the right side returns nothing, the left row disappears. OUTER APPLY behaves like a left join against the applied row set: the left row stays and right-side columns become NULL. Searches for cross apply SQL Server, outer apply SQL Server, or outer apply SQL usually refer to this T-SQL pattern.

CROSS APPLY and OUTER APPLY examples

Returns the latest order per customer, first with matching customers only and then with all customers preserved.

-- CROSS APPLY returns customers with a matching row from the inner query.
SELECT
    c.CustomerName,
    latest_order.OrderID,
    latest_order.OrderDate,
    latest_order.Status
FROM #Customers AS c
CROSS APPLY
(
    SELECT TOP (1)
        o.OrderID,
        o.OrderDate,
        o.Status
    FROM #Orders AS o
    WHERE o.CustomerID = c.CustomerID
    ORDER BY o.OrderDate DESC, o.OrderID DESC
) AS latest_order
ORDER BY c.CustomerName;

-- OUTER APPLY preserves customers without a matching order.
SELECT
    c.CustomerName,
    latest_order.OrderID,
    latest_order.OrderDate,
    latest_order.Status
FROM #Customers AS c
OUTER APPLY
(
    SELECT TOP (1)
        o.OrderID,
        o.OrderDate,
        o.Status
    FROM #Orders AS o
    WHERE o.CustomerID = c.CustomerID
    ORDER BY o.OrderDate DESC, o.OrderID DESC
) AS latest_order
ORDER BY c.CustomerName;

CROSS APPLY returns only customers with at least one order. OUTER APPLY preserves customers without orders. This pattern is often clearer than joining to a grouped subquery when you need one correlated row per outer row.

SQL Server JOIN ON clauses

The ON clause is where the join relationship belongs. Searches for SQL JOIN ON usually mean this part of the query: the predicate that tells SQL Server how two table sources match. Keep join predicates there so the relationship is visible before other filters. Microsoft also recommends specifying join conditions in the FROM clause because it separates join logic from search conditions.

Good join predicates use the actual key relationship. Weak predicates join on names, descriptions, dates, or partial business labels because those columns happen to look similar. That works until spelling changes, duplicates appear, or two regions use the same label.

JOIN ON with additional predicate

Keeps the key relationship and date predicate visible in the join.

SELECT
    c.CustomerName,
    o.OrderID,
    o.OrderDate
FROM #Customers AS c
INNER JOIN #Orders AS o
    ON o.CustomerID = c.CustomerID
   AND o.OrderDate >= '2026-05-01'
   AND o.OrderDate <  '2026-06-01';

Use table aliases so each column source is clear. Join key data types should match. Implicit conversions can hurt plans and estimates. Composite keys need all key columns in the join predicate.

SQL Server join duplicates and row multiplication

Duplicate rows after a join usually mean the query changed grain. Joining orders to order lines moves the result from one row per order to one row per order line. That is not SQL Server being weird. That is the relationship doing exactly what it was asked to do.

DISTINCT can hide duplicate output, but it does not fix a bad join. First decide the intended grain: one row per customer, one row per order, one row per product per date, or one row per order line. Then join and aggregate to that grain.

Check row multiplication

Finds orders that return more than one joined row after joining to order lines.

SELECT
    o.OrderID,
    COUNT(*) AS JoinedRows
FROM #Orders AS o
INNER JOIN #OrderLines AS ol
    ON ol.OrderID = o.OrderID
GROUP BY o.OrderID
HAVING COUNT(*) > 1;

This is expected for order 1001 because it has two order lines. If the multiplication is not expected, inspect the keys on both sides. Fix the join or aggregate before using DISTINCT.

JOINs, UNION, MERGE, PIVOT, and APPLY

A JOIN combines columns side by side from related table sources. UNION stacks compatible result sets. MERGE changes target data based on another source. PIVOT reshapes row values into columns. APPLY evaluates a right-side table expression for each left-side row.

Those features sometimes appear in the same search because people are trying to merge tables or reshape report data. If you search for SQL merge tables, check whether you really need a join, a UNION, or the MERGE statement. For MS SQL pivot questions, keep the intent separate: PIVOT reshapes data, while joins retrieve related columns.

FeatureUse when
JOINYou need related columns from two or more table sources.
UNIONYou need to stack compatible result sets into one list.
MERGEYou need to insert, update, or delete target rows from a source.
PIVOTYou need row values turned into output columns.
APPLYYou need a right-side table expression that can reference the left row.

Performance notes for SQL Server joins

Join performance starts with correct row counts and useful access paths. Indexes on join keys matter. Matching data types matter. Accurate statistics matter. The actual execution plan will show whether SQL Server used nested loops, merge, hash, or adaptive join behavior.

Do not tune a join only from the keyword. A slow INNER JOIN might be a missing index, bad estimate, implicit conversion, stale statistics, parameter-sensitive plan, oversized result, blocking chain, or memory grant issue.

Check the actual execution plan, not only estimated cost.

Look for implicit conversions on join keys.

Confirm row estimates near the join are close enough to reality.

Index the join keys when the workload and write cost justify it.

Filter early when it reduces rows without changing the intended result.

Use the waits and blocking guides when a join query is slow under load rather than slow in isolation.

Related reads: SQL Server indexing, SQL Server waits, and SQL Server blocking.

Quick reference table

TypeRows returnedUse it for
INNER JOINMatching rows only.Normal lookups where unmatched rows do not belong in the result.
LEFT JOINAll left rows, with matching right rows where available.Optional relationships and missing-data checks.
RIGHT JOINAll right rows, with matching left rows where available.Valid syntax, but usually clearer as a LEFT JOIN with table order swapped.
FULL OUTER JOINAll rows from both sides, matched where possible.Reconciliation and finding rows missing from either side.
CROSS JOINEvery combination of rows from both sides.Calendars, matrices, and deliberate row expansion.
CROSS APPLYLeft rows with at least one correlated right-side result.Top-N-per-row and table-valued expressions where missing rows should disappear.
OUTER APPLYAll left rows, with correlated right-side rows where available.Top-N-per-row and table-valued expressions where left rows should remain.

Need a SQL Server performance review?

Request a performance review when a join query returns the wrong rows, runs slowly under load, or keeps changing plans after small query or index changes.

Start with the symptom, urgency, and what changed recently. I can ask for query text, plans, or index details later if they are needed.

Next step

If the query returns the wrong rows or slows down under real load, start with the SQL Server performance review.

Next useful reads: SQL Server indexing for access paths, SQL Server waits for runtime diagnosis, and SQL Server blocking for lock pressure.