<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. SQL
  3. Joins and combining tables
All lessons in this course

Lesson 18 of 54 14 minIntermediate

Relationships and foreign keys

Why BuildMart’s data is spread over five tables, how primary and foreign keys tie them together, and the three kinds of relationship: one-to-many, many-to-many and one-to-one.

An order in BuildMart is not one row. Who placed it lives in Customers, when and its status in Orders, what was bought in OrderItems, and what those products are in Products. Each fact is stored once, in the table it belongs to, and keys link the pieces back together.

KeyWhat it isIn BuildMart
Primary keythe column(s) that identify each row; unique and never NULLCustomers.CustomerId, Orders.OrderId
Foreign keya column holding another table’s primary key value, so it points at one row thereOrders.CustomerId → Customers.CustomerId
Composite keya primary key made of more than one columnOrderItems (OrderId, ProductId)
Interactive

Follow the keys

Pick an order. Each foreign key value is a pointer: look it up in the other table’s primary key to find the row it means.

dbo.OrdersWHERE OrderId = 7
OrderIdCustomerIdOrderDateStatus
1742024-05-05Shipped
CustomerId 4 is a foreign key to Customers: many orders → one customer
dbo.CustomersWHERE CustomerId = 4
CustomerIdFirstNameLastNameCity
14DevSinghBradford
The same customer’s other orders:
OrderItems rows with OrderId 7 point back at this order: one order → many lines
dbo.OrderItemsWHERE OrderId = 7
OrderIdProductIdQuantityUnitPrice
174207.00
278511.90
each ProductId is a foreign key to Products
dbo.ProductsWHERE ProductId IN (4, 8)
ProductIdNameCategoryIdPrice
14Cement 25 kg27.20
28Wood screws (200)411.90
Four lookups, each matching a foreign key to a primary key, turned order #7 into “Dev Singh from Bradford bought 2 products”. A JOIN does exactly these lookups for you, in one query. Note the UnitPrice on product 4: the order line keeps the price actually paid, which can differ from today’s price in Products.

Following the keys by hand

Without joins, putting order 7 back together takes four queries. Each one takes a value from the previous result and looks it up in the next table:

Query.sql
SELECT OrderId, CustomerId, OrderDate, Status FROM dbo.Orders WHERE OrderId = 7;SELECT CustomerId, FirstName, LastName, City FROM dbo.Customers WHERE CustomerId = 4;SELECT OrderId, ProductId, Quantity, UnitPrice FROM dbo.OrderItems WHERE OrderId = 7;SELECT ProductId, Name, Price FROM dbo.Products WHERE ProductId IN (4, 8);
Output
OrderId  CustomerId  OrderDate   Status
-------  ----------  ----------  -------
      7           4  2024-05-05  Shipped

CustomerId  FirstName  LastName  City
----------  ---------  --------  --------
         4  Dev        Singh     Bradford

OrderId  ProductId  Quantity  UnitPrice
-------  ---------  --------  ---------
      7          4        20       7.00
      7          8         5      11.90

ProductId  Name               Price
---------  -----------------  -----
        4  Cement 25 kg        7.20
        8  Wood screws (200)  11.90

You had to read the 4 and the 4, 8 off the screen and type them into the next query. A join does those lookups inside one query, which is the rest of this module.

Three kinds of relationship

KindMeaningHow it is builtExample
One-to-manyone row here, any number of rows therea foreign key on the “many” sideone customer, many orders
Many-to-manymany on both sidesa third junction table with two foreign keysorders ↔ products, through OrderItems
One-to-oneat most one row on each sidea foreign key that is also the primary key (or UNIQUE)a customer and their login

One-to-many is by far the most common. The foreign key always goes on the “many” side: an order holds its CustomerId, because a customer cannot hold a list of order numbers in one column.

Many-to-many needs a table in the middle. An order contains many products and a product appears on many orders, so each pair gets a row in OrderItems. Its composite primary key (OrderId, ProductId) stops the same product being listed twice on one order, and the pair is a good place for facts that belong to it, such as Quantity and UnitPrice.

A one-to-one relationship

Login details are only needed by the website, so they can live in their own table. Making CustomerId both the primary key and a foreign key allows at most one login per customer:

Query.sql
CREATE TABLE dbo.CustomerLogins (    CustomerId int          NOT NULL        CONSTRAINT PK_CustomerLogins PRIMARY KEY        CONSTRAINT FK_CustomerLogins_Customers REFERENCES dbo.Customers (CustomerId),    UserName   nvarchar(40) NOT NULL UNIQUE,    LastLogin  datetime2(0) NULL); INSERT INTO dbo.CustomerLogins (CustomerId, UserName) VALUES (1, N'asha.p');INSERT INTO dbo.CustomerLogins (CustomerId, UserName) VALUES (1, N'asha.second');
Output
Msg 2627, Level 14, State 1, Line 10
Violation of PRIMARY KEY constraint 'PK_CustomerLogins'. Cannot insert duplicate key in object 'dbo.CustomerLogins'. The duplicate key value is (1).

Natural or surrogate keys?

A natural key is a real-world value that is already unique, such as an email address or a product barcode. A surrogate key is a number made up by the database, such as an IDENTITY column. BuildMart uses surrogate keys everywhere, and that is the usual choice:

  • Natural values change: people change email address, and every foreign key pointing at the old one would have to change too. A surrogate never needs to.
  • A small int makes foreign keys and indexes smaller and faster than a long text value.
  • Keep the natural value unique anyway with a UNIQUE constraint, as CustomerLogins does with UserName.

Listing the relationships of a database

SQL Server keeps every foreign key in its catalog views, so you can list the relationships of a database you have never seen before:

Query.sql
SELECT OBJECT_NAME(parent_object_id) AS ChildTable,       COL_NAME(parent_object_id, parent_column_id) AS ForeignKey,       OBJECT_NAME(referenced_object_id) AS ParentTable,       COL_NAME(referenced_object_id, referenced_column_id) AS PointsAtFROM sys.foreign_key_columnsORDER BY ChildTable, ForeignKey;
Output
ChildTable  ForeignKey  ParentTable  PointsAt
----------  ----------  -----------  ----------
OrderItems  OrderId     Orders       OrderId
OrderItems  ProductId   Products     ProductId
Orders      CustomerId  Customers    CustomerId
Products    CategoryId  Categories   CategoryId
Quick check

Teachers teach many classes, and each class has one teacher. Where does the foreign key go?

Quick check

Students take many courses, and courses have many students. What do you need?

Practice

Without using a join, find out who placed order 9 (first and last name) and the names of the products on it. Use one query per lookup.

Show a hint

Orders gives you the CustomerId; OrderItems with OrderId = 9 gives the ProductIds; then look those up in Customers and Products.

Show the solution
Solution.sql
SELECT CustomerId FROM dbo.Orders WHERE OrderId = 9;SELECT FirstName, LastName FROM dbo.Customers WHERE CustomerId = 2;SELECT ProductId, Quantity FROM dbo.OrderItems WHERE OrderId = 9;SELECT Name FROM dbo.Products WHERE ProductId = 9;
Output
CustomerId
----------
         2

FirstName  LastName
---------  --------
Ben        Carter

ProductId  Quantity
---------  --------
        9         4

Name
----------------
Wall plugs (100)

Remember

  • A primary key identifies each row; a foreign key holds another table’s primary key value and points at one row there.
  • One-to-many: the foreign key goes on the “many” side.
  • Many-to-many: a junction table with two foreign keys and a composite primary key.
  • One-to-one: a foreign key that is also the primary key (or UNIQUE).
  • Surrogate keys (IDENTITY) are the usual choice; keep natural values unique with UNIQUE.
  • Foreign keys are not indexed automatically.
0 of 54 lessons completed