In a relational database, you typically have data spread in multiple tables. Each table represents a set of entities of the same kind, such as employees in the examples you have seen so far. In order to make result sets meaningful for the business your database supports, most of the time you need to retrieve data from multiple tables in the same query. You need to join two or more tables based on some conditions. The most frequent kind of a join is the inner join. An inner join returns only rows for which the condition in the join predicate for the two joined tables evaluates to true. Note that in a relational database, you have three-valued logic, because there is always a possibility that a piece of data is unknown. You mark the unknown with the NULL keyword. A predicate can thus evaluate to true, false, or NULL. For an inner join, the order of the tables involved in the join is not important.
In the following example, you can see the dbo.DimEmployee table joined with an inner join to the dbo.FactResellerSales table:
SELECT e.EmployeeKey, e.FirstName, e.LastName,
fr.SalesAmount
FROM dbo.DimEmployee AS e
INNER JOIN dbo.FactResellerSales AS fr
ON e.EmployeeKey = fr.EmployeeKey;
Here are the partial results:
EmployeeKey FirstName LastName SalesAmount
----------- --------- -------- -----------
285 Tsvi Reiter 2024.994
285 Tsvi Reiter 6074.982
285 Tsvi Reiter 2024.994
In the previous query, you can see that table aliases are used. If a column's name is unique across all tables in the query, you can use it without a table name. If not, you need to use table name in front of the column, to avoid ambiguous column names, in the table.column format. In the previous query, the EmployeeKey column appears in both tables. Therefore, you need to precede this column name with the table name of its origin to avoid ambiguity. You can shorten the two-part column names by using table aliases. You specify table aliases in the FROM clause. Once you specify table aliases, you must always use the aliases; you can't refer to the original table names in that query anymore. Please note that a column name might be unique in the query at the moment when you write the query. However, later somebody could add a column with the same name in another table involved in the query. If the column name is not preceded by an alias or by the table name, you would get an error when executing the query because of the ambiguous column name. In order to make the code more stable and more readable, you should always use table aliases for each column in the query.
The previous query returned 60,855 rows. It is always recommended to know at least approximately the number of rows your query should return. This number is the first control of the correctness of the result set, or said differently, whether the query is written in a logically correct way. If every sale has an employee, as it should have, then the previous query should have returned exactly the number of rows dbo.FactResellerSales has. You can quickly check the number of rows in the dbo.FactResellerSales table with the help of the COUNT(*) aggregate function, as the following query shows:
SELECT COUNT(*) AS ResellerSalesCount
FROM dbo.FactResellerSales;
The result is, as you probably expected, 60,855 rows.
You can join multiple tables in a single query. The following code joins seven tables in a single query. Note that all of the joins are still inner joins. The query returns 60,855 rows again, with at least 1 column from each table involved in the query:
SELECT e.EmployeeKey, e.FirstName, e.LastName,
r.ResellerKey, r.ResellerName,
d.DateKey, d.CalendarYear, d.CalendarQuarter,
p.ProductKey, p.EnglishProductName,
ps.EnglishProductSubcategoryName,
pc.EnglishProductCategoryName,
fr.OrderQuantity, fr.SalesAmount
FROM dbo.DimEmployee AS e
INNER JOIN dbo.FactResellerSales AS fr
ON e.EmployeeKey = fr.EmployeeKey
INNER JOIN dbo.DimReseller AS r
ON r.ResellerKey = fr.ResellerKey
INNER JOIN dbo.DimDate AS d
ON fr.OrderDateKey = d.DateKey
INNER JOIN dbo.DimProduct AS p
ON fr.ProductKey = p.ProductKey
INNER JOIN dbo.DimProductSubcategory AS ps
ON p.ProductSubcategoryKey = ps.ProductSubcategoryKey
INNER JOIN dbo.DimProductCategory AS pc
ON ps.ProductCategoryKey = pc.ProductCategoryKey;
In the dbo.Employees table, there are 17 salespeople. Do all of them have at least one sale, at least one row from the dbo.FactResellerSales table associated with the employee key of that salesperson? You can check how many distinct employees have sales associated with them with the help of the DISTINCT keyword:
SELECT DISTINCT fr.EmployeeKey
FROM dbo.FactResellerSales AS fr;
The query returns 17 rows. Now imagine that you would like to list all sales rows together with the employees' data, but you also need to include in the result the employees that are not salespeople, that do now have any row associated with their EmployeeKey column in the fact table. You can use an outer join to fulfill this task.
With an outer join, you preserve the rows from one or both tables, even if they don't have a match in the other table. The result set returned includes all of the matched rows, like what you get from an inner join plus the preserved rows. Within an outer join, the order of the tables involved in the join might be important. If you use LEFT OUTER JOIN, the rows from the left table are preserved. If you use RIGHT OUTER JOIN, the rows from the right table are preserved. Of course, in both cases, the order of the tables involved in the join is important. With FULL OUTER JOIN, you preserve the rows from both tables, and the order of the tables is not important. The following query uses a left outer join to preserve the rows from the dbo.DimEmployee table:
SELECT e.EmployeeKey, e.FirstName, e.LastName,
fr.SalesAmount
FROM dbo.DimEmployee AS e
LEFT OUTER JOIN dbo.FactResellerSales AS fr
ON e.EmployeeKey = fr.EmployeeKey;
The query returns 61,134 rows. Did we get all of the employees in the result? You can check this by checking the distinct EmployeeKey after the outer join:
SELECT DISTINCT e.EmployeeKey
FROM dbo.DimEmployee AS e
LEFT OUTER JOIN dbo.FactResellerSales AS fr
ON e.EmployeeKey = fr.EmployeeKey;
The query returns 296 rows, which is the number of employees.
Joining more than two tables is not tricky if all of the joins are inner joins. The order of joins is not important. However, you might want to execute an outer join after all of the inner joins. If you don't control the join order with the outer joins, it might happen that a subsequent inner join filters out the preserved rows of an outer join. You can control the join order with parentheses. The following query uses the right outer join to preserve all employees and makes sure that this join is executed after all inner joins:
SELECT e.EmployeeKey, e.FirstName, e.LastName,
r.ResellerKey, r.ResellerName,
d.DateKey, d.CalendarYear, d.CalendarQuarter,
p.ProductKey, p.EnglishProductName,
ps.EnglishProductSubcategoryName,
pc.EnglishProductCategoryName,
fr.OrderQuantity, fr.SalesAmount
FROM (dbo.FactResellerSales AS fr
INNER JOIN dbo.DimReseller AS r
ON r.ResellerKey = fr.ResellerKey
INNER JOIN dbo.DimDate AS d
ON fr.OrderDateKey = d.DateKey
INNER JOIN dbo.DimProduct AS p
ON fr.ProductKey = p.ProductKey
INNER JOIN dbo.DimProductSubcategory AS ps
ON p.ProductSubcategoryKey = ps.ProductSubcategoryKey
INNER JOIN dbo.DimProductCategory AS pc
ON ps.ProductCategoryKey = pc.ProductCategoryKey)
RIGHT OUTER JOIN dbo.DimEmployee AS e
ON e.EmployeeKey = fr.EmployeeKey;
The query returns 61,134 rows, as it should. Note that with the usage of the parenthesis, the order of joins is defined in the following way:
-
Perform all inner joins, with an arbitrary order among them
-
Execute the left outer join after all of the inner joins