SQL Server Inner Join Basics with Examples

Total: 1 Average: 5

Introduction

T-SQL allows us to combine records from more than one table and return them as a single result set. This is achieved through the concept of joins in SQL Server.

This opportunity is often necessary because data in relational databases are typically normalized. For example, we have employee data spread across two or more tables. The first table would be the basic customer data and called employee. The second table would be the department.

The data consistency requires the correct relationship between the customer and the department. Returning the complete data for a set of employees and their departments requires to join both tables.

The SQL join operations can also include more than two tables.

Another case of such foreign Key relationships existence among tables is for summary and detail tables.

People who worked with the AdventureWorks or WideWorldImporters sample databases are familiar with the Sales.Orders and Sales.OrderDetails tables. In this case, the latter contains the details of each order recorded in the Sales.Orders table. Two tables have a relationship based on the order. Thus, we can retrieve data from both tables as a single result set using JOINS.

Types of SQL Server JOINs

T-SQL allows the following types of joins:

  1. Inner Join returns all records common to all tables involved in the query.
  2. Left (Outer) Join returns all records from the left table and all records from the right table that also occur in the left table. The terms left and right refer to the position of the table relative to the JOIN clause.
  3. Right (Outer) Join returns all records from the right table and all records from the left table that also occur in the left table. The terms are similar to the previous case.
  4. Full Outer Join returns all records common to both tables, plus all other records from both tables. Columns that don’t have corresponding rows in the other table return NULL
  5. Cross Join, also called Cartesian Join, returns the cartesian product of the data from both tables. Therefore, the final result set for each row in table A will contain a mapping of all rows in table B, and vice versa.

This article will focus on SQL INNER JOINs.

Sample Tables

To demonstrate the concept of inner joins, we use three related tables from the TSQLV4 database built by Itzik Ben-Gan.

The following listings show the structure of these tables.

-- Listing 1: Structure of the Sales.Customers Table

CREATE TABLE [Sales].[Customers](
	[custid] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
	[companyname] [nvarchar](40) NOT NULL,
	[contactname] [nvarchar](30) NOT NULL,
	[contacttitle] [nvarchar](30) NOT NULL,
	[address] [nvarchar](60) NOT NULL,
	[city] [nvarchar](15) NOT NULL,
	[region] [nvarchar](15) NULL,
	[postalcode] [nvarchar](10) NULL,
	[country] [nvarchar](15) NOT NULL,
	[phone] [nvarchar](24) NOT NULL,
	[fax] [nvarchar](24) NULL,
 CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED 
(
	[custid] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

Note the foreign key relationship between the custid column in Sales.Orders and the custid column in Sales.Customers.

To perform JOINs, we must specify such a common column as the JOIN basis.

It does not strictly require a foreign key relationship to execute JOIN queries, but the columns that determine the result set must be comparable.

Foreign Keys can also help improve JOIN queries, especially if the foreign key column is indexed.

-- Listing 2: Structure of the Sales.Orders Table

CREATE TABLE [Sales].[Orders](
	[orderid] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
	[custid] [int] NULL,
	[empid] [int] NOT NULL,
	[orderdate] [date] NOT NULL,
	[requireddate] [date] NOT NULL,
	[shippeddate] [date] NULL,
	[shipperid] [int] NOT NULL,
	[freight] [money] NOT NULL,
	[shipname] [nvarchar](40) NOT NULL,
	[shipaddress] [nvarchar](60) NOT NULL,
	[shipcity] [nvarchar](15) NOT NULL,
	[shipregion] [nvarchar](15) NULL,
	[shippostalcode] [nvarchar](10) NULL,
	[shipcountry] [nvarchar](15) NOT NULL,
 CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED 
(
	[orderid] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [Sales].[Orders] ADD  CONSTRAINT [DFT_Orders_freight]  DEFAULT ((0)) FOR [freight]
GO
ALTER TABLE [Sales].[Orders]  WITH CHECK ADD  CONSTRAINT [FK_Orders_Customers] FOREIGN KEY([custid])
REFERENCES [Sales].[Customers] ([custid])
GO
ALTER TABLE [Sales].[Orders] CHECK CONSTRAINT [FK_Orders_Customers]
GO
ALTER TABLE [Sales].[Orders]  WITH CHECK ADD  CONSTRAINT [FK_Orders_Employees] FOREIGN KEY([empid])
REFERENCES [HR].[Employees] ([empid])
GO
ALTER TABLE [Sales].[Orders] CHECK CONSTRAINT [FK_Orders_Employees]
GO
ALTER TABLE [Sales].[Orders]  WITH CHECK ADD  CONSTRAINT [FK_Orders_Shippers] FOREIGN KEY([shipperid])
REFERENCES [Sales].[Shippers] ([shipperid])
GO
ALTER TABLE [Sales].[Orders] CHECK CONSTRAINT [FK_Orders_Shippers]
GO
-- Listing 3: Structure of the Sales.OrderDetails Table

CREATE TABLE [Sales].[OrderDetails](
	[orderid] [int] NOT NULL,
	[productid] [int] NOT NULL,
	[unitprice] [money] NOT NULL,
	[qty] [smallint] NOT NULL,
	[discount] [numeric](4, 3) NOT NULL,
 CONSTRAINT [PK_OrderDetails] PRIMARY KEY CLUSTERED 
(
	[orderid] ASC,
	[productid] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [Sales].[OrderDetails] ADD  CONSTRAINT [DFT_OrderDetails_unitprice]  DEFAULT ((0)) FOR [unitprice]
GO
ALTER TABLE [Sales].[OrderDetails] ADD  CONSTRAINT [DFT_OrderDetails_qty]  DEFAULT ((1)) FOR [qty]
GO
ALTER TABLE [Sales].[OrderDetails] ADD  CONSTRAINT [DFT_OrderDetails_discount]  DEFAULT ((0)) FOR [discount]
GO
ALTER TABLE [Sales].[OrderDetails]  WITH CHECK ADD  CONSTRAINT [FK_OrderDetails_Orders] FOREIGN KEY([orderid])
REFERENCES [Sales].[Orders] ([orderid])
GO
ALTER TABLE [Sales].[OrderDetails] CHECK CONSTRAINT [FK_OrderDetails_Orders]
GO
ALTER TABLE [Sales].[OrderDetails]  WITH CHECK ADD  CONSTRAINT [FK_OrderDetails_Products] FOREIGN KEY([productid])
REFERENCES [Production].[Products] ([productid])
GO
ALTER TABLE [Sales].[OrderDetails] CHECK CONSTRAINT [FK_OrderDetails_Products]
GO
ALTER TABLE [Sales].[OrderDetails]  WITH CHECK ADD  CONSTRAINT [CHK_discount] CHECK  (([discount]>=(0) AND [discount]<=(1)))
GO
ALTER TABLE [Sales].[OrderDetails] CHECK CONSTRAINT [CHK_discount]
GO
ALTER TABLE [Sales].[OrderDetails]  WITH CHECK ADD  CONSTRAINT [CHK_qty] CHECK  (([qty]>(0)))
GO
ALTER TABLE [Sales].[OrderDetails] CHECK CONSTRAINT [CHK_qty]
GO
ALTER TABLE [Sales].[OrderDetails]  WITH CHECK ADD  CONSTRAINT [CHK_unitprice] CHECK  (([unitprice]>=(0)))
GO
ALTER TABLE [Sales].[OrderDetails] CHECK CONSTRAINT [CHK_unitprice]
GO

Sample Queries with SQL INNER JOIN

Let’s execute some sample queries using an SQL INNER JOIN.

In Listing 4, we execute a query that fetches ALL rows common to the Sales.Customers table and Sales.Orders table. We use the custid column as the condition for the join.

Notice that the ON clause is a filter very much like a WHERE clause. We have also used aliases to distinguish the tables.

-- Listing 4: Customer Orders

use TSQLV4
go
select * from Sales.Customers sc
inner join Sales.Orders so
on sc.custid=so.custid;
SQL Server INNER JOIN - Output of Listing 4
Figure 1: Output of Listing 4

In Listing 5, we narrow the query down to specific columns to the Sales.Customers table and the Sales.Orders table. We use the custid column as the condition for the join.

Observe that the ON clause is a filter very much like a WHERE clause. We have also used aliases to distinguish the tables.

-- Listing 5: Customer Orders with specific Rows
use TSQLV4
go
select
contactname
, contacttitle
, address
, orderid
, orderdate
, shipaddress
, shipcountry
from Sales.Customers sc
inner join Sales.Orders so
on sc.custid=so.custid;
SQL Server INNER JOIN - Output of Listing 5
Figure 2: Output of Listing 5

In Listing 6, we extend the thought by introducing a WHERE clause that filters data for a single customer.  We have also appended aliases to the column list.

While it is not necessary in this example, there are cases where you need to project columns with the same name from both tables. Then, the columns will need an expression as two-part names, using the table aliases or names.

-- Listing 6: Customer Orders for a Single Customer
use TSQLV4
go
select 
sc.contactname
, sc.contacttitle
, sc.address
, so.orderid
, so.orderdate
, so.shipaddress
, so.shipcountry
from Sales.Customers sc
inner join Sales.Orders so
on sc.custid=so.custid
where sc.contactname='Allen, Michael';
Output of Listing 6
Figure 3: Output of Listing 6

In listing 7, we introduce the custid column. We can distinguish the columns using the alias, but we cannot distinguish the two custid columns in the output (See Figure 4). We can fix this by using aliases:

-- Listing 7: Customer Orders for a Single Customer with Common Column
use TSQLV4
go
select 
sc.custid
, sc.contactname
, sc.contacttitle
, sc.address
, so.custid
, so.orderid
, so.orderdate
, so.shipaddress
, so.shipcountry
from Sales.Customers sc
inner join Sales.Orders so
on sc.custid=so.custid
where sc.contactname='Allen, Michael';
SQL INNER JOIN - Output of Listing 7
Figure 4: Output of Listing 7
-- Listing 8: Customer Orders for a Single Customer with Aliased Column
use TSQLV4
go
select 
sc.custid customer_custid
, sc.contactname
, sc.contacttitle
, sc.address
, so.custid order_custid
, so.orderid
, so.orderdate
, so.shipaddress
, so.shipcountry
from Sales.Customers sc
inner join Sales.Orders so
on sc.custid=so.custid
where sc.contactname='Allen, Michael';
Output of Listing 8
Figure 5: Output of Listing 8

In Listing 9, we add Sales.OrderDetails table to the mix. When joining more than two tables, the result set from the first two tables JOIN becomes the left table for the next table. However, the order of tables in a JOIN query does not affect the eventual output.

Note that we use a wild card to fetch ALL columns from the Sales.OrderDetails table.

-- Listing 9: Inner Join with Three Tables

use TSQLV4
go
select 
sc.custid customer_custid
, sc.contactname
, sc.contacttitle
, sc.address
, so.custid order_custid
, so.orderid
, so.orderdate
, so.shipaddress
, so.shipcountry
, sod.*
from Sales.Customers sc
inner join Sales.Orders so
on sc.custid=so.custid
inner join Sales.OrderDetails sod
on so.orderid=sod.orderid
where sc.contactname='Allen, Michael';

Listing 10 introduces the Production.Product table showing us the product details associated with the order.

-- Listing 10: Inner Join with Four Tables

use TSQLV4
go
select 
sc.custid customer_custid
, sc.contactname
, sc.contacttitle
, sc.address
, so.custid order_custid
, so.orderid
, so.orderdate
, so.shipaddress
, so.shipcountry
, sod.*
, pp.productname
, pp.unitprice
from Sales.Customers sc
inner join Sales.Orders so
on sc.custid=so.custid
inner join Sales.OrderDetails sod
on so.orderid=sod.orderid
inner join Production.Products pp
on sod.productid=pp.productid
where sc.contactname='Allen, Michael';
SQL INNER JOIN - Output of Listing 10
Figure 6: Output of Listing 10

Non-Equi JOINs

Since the ON clause is a filter, we can use operators other than the “=” operator. JOINs generally support the use of inequalities such as <, >, !=, =< and => in the ON clause. Listing 11 demonstrates this.

Running these queries will return different result sets.

-- Listing 11: Non-Equi JOINs, "Equal to"
use TSQLV4
go
select
contactname
, contacttitle
, address
, orderid
, orderdate
, shipaddress
, shipcountry
from Sales.Customers sc
inner join Sales.Orders so
on sc.custid=so.custid;
Output of Query 11
Figure 7: Output of Query 11
-- Listing 12: Non-Equi JOINs, "Not Equal to"
use TSQLV4
go
select
contactname
, contacttitle
, address
, orderid
, orderdate
, shipaddress
, shipcountry
from Sales.Customers sc
inner join Sales.Orders so
on sc.custid<>so.custid;
Output of Query 12
Figure 8: Output of Query 12
-- Listing 13: Non-Equi JOINs, "Less than OR Equal to"
use TSQLV4
go
select
contactname
, contacttitle
, address
, orderid
, orderdate
, shipaddress
, shipcountry
from Sales.Customers sc
inner join Sales.Orders so
on sc.custid<=so.custid;

Conclusion

This article discussed the SQL INNER JOINs and presented examples of its use. It covered scenarios with two, three, and four tables in the same query. 

Using related tables, we’ve also illustrated how we could vary the query structure to display the output according to our requirements. We have also added brief examples of Non-Equi JOINs.

Kenneth Igiri
Latest posts by Kenneth Igiri (see all)

Kenneth Igiri

Kenneth Igiri is a Database Administrator with eProcess International S.A., Ecobank Group's Shared Services Centre. Kenneth has over eight years' experience with SQL Server and Oracle databases as well as related technologies. His interests include database performance, HADR, and recently, Cloud. Also, Kenneth teaches at Children's Church and writes fiction. You can connect with Kenneth via his blog or social network account.