DBMS 📂 SQL · 3 of 6 39 min read

Set Operators, Views, Indexes & Joins in SQL

A hands-on DBMS tutorial covering the three SQL set operators (UNION, INTERSECT, EXCEPT), Views, Indexes, and all join types (Inner, Outer, Self). Every concept is taught on one small, consistent dataset with runnable SQL, expected output, comparison tables, and animated diagrams — including a cycling Venn diagram for set operators, a full-scan-vs-index-seek animation, and animated row-matching for joins. Ends with six practice problems and eight golden rules.

Section 01

The Story That Explains Set Operators

Two Guest Lists for One Wedding
Imagine you are organising a wedding. The bride hands you her guest list, and the groom hands you his. Now you face very ordinary questions that turn out to be exactly what databases solve every day:

Everyone invited — combine both lists, but invite no one twice. That is UNION.
Friends of both — people who appear on both lists. That is INTERSECT.
Only the bride's people — on her list but not the groom's. That is EXCEPT.

A relational table is just a list of rows. Set operators let you combine two result sets the same way you would combine two guest lists — by row, treating each row as a member of a set.

This tutorial walks through the six tools every database practitioner must master to combine and accelerate queries: the three set operators (UNION, INTERSECT, EXCEPT), plus Views, Indexes, and the three families of Joins (Inner, Outer, Self). Every concept is grounded in one small, consistent dataset so you can trace each result by hand.

🧮
Set Operators vs Joins — The One-Line Distinction

Set operators stack results vertically (row on top of row, same columns). Joins stitch results horizontally (columns from one table beside columns from another, matched by a key). Confusing the two is the single most common beginner mistake.


Section 02

The Sample Database We Will Use

Every example below uses these tables. Keep them in view — tracing results by hand is how the operators truly click.

👤 online_buyers
cust_idname
1Asha
2Bilal
3Chen
4Diya
🏪 store_buyers
cust_idname
3Chen
4Diya
5Esha
6Farid
employeesemp_idnamedept_idmanager_idsalary
101Asha10NULL95000
102Bilal1010162000
103Chen2010158000
104Diya3010247000
105EshaNULL10251000
departmentsdept_iddept_name
10Engineering
20Sales
30Marketing
40Research
🔐
Notice the Deliberate Gaps

Asha is a manager with NULL manager. Esha has a NULL dept_id (no department). Department 40 (Research) has no employees. These gaps are intentional — they are exactly what reveals the difference between INNER and OUTER joins later.


Section 03

Set Operators — The Animated Venn Diagram

All three set operators act on two result sets that must be union-compatible: same number of columns, in the same order, with compatible data types. The animated diagram below cycles through what each operator keeps, using our two buyer lists.

⏳ Live — UNION → INTERSECT → EXCEPT (cycling)
online store 1, 2 3, 4 5, 6 UNION → {1,2,3,4,5,6} INTERSECT → {3,4} EXCEPT (online−store) → {1,2}

The blue circle is online_buyers; the red circle is store_buyers. The shaded region is the rows each operator returns.


Section 04

UNION — Combine and Deduplicate

UNION stacks two result sets and removes duplicate rows. UNION ALL does the same but keeps duplicates — and is much faster because it skips the deduplication step.

-- Every customer who bought through any channel
SELECT cust_id, name FROM online_buyers
UNION
SELECT cust_id, name FROM store_buyers
ORDER BY cust_id;
OUTPUT
cust_id | name 1 | Asha 2 | Bilal 3 | Chen <- appeared in both, kept once 4 | Diya <- appeared in both, kept once 5 | Esha 6 | Farid
📚
UNION
distinct rows
Merges both sets and removes duplicates. Internally it sorts or hashes every row to detect repeats, so it costs more.
UNION ALL
keeps duplicates
Concatenates both sets as-is. No dedup pass → faster. Use it whenever you know rows can't overlap, or you actually want the duplicates counted.
⚖️
Column Rule
union-compatible
Both SELECTs need the same column count and order. Column names come from the first query. Types must be compatible.
⚠️
Only One ORDER BY — At the Very End

An ORDER BY applies to the whole combined result and must appear after the final SELECT, not on the individual queries. Putting it on the first SELECT is a syntax error in standard SQL.


Section 05

INTERSECT — Rows Common to Both

INTERSECT returns only rows that appear in both result sets — our "friends of both" from the wedding story. Duplicates are removed automatically.

-- Customers who bought BOTH online and in store
SELECT cust_id, name FROM online_buyers
INTERSECT
SELECT cust_id, name FROM store_buyers;
OUTPUT
cust_id | name 3 | Chen 4 | Diya
💡
If Your Engine Lacks INTERSECT

Older MySQL versions (before 8.0.31) have no INTERSECT. You can reproduce it with an INNER JOIN on every column, or with IN / EXISTS on a subquery. The set-operator form is clearer when it is available.


Section 06

EXCEPT — In One Set but Not the Other

EXCEPT (called MINUS in Oracle) returns rows from the first query that are not present in the second. Order matters: A EXCEPT B is different from B EXCEPT A.

online EXCEPT store
cust_idname
1Asha
2Bilal
store EXCEPT online
cust_idname
5Esha
6Farid
-- Online-only customers (never bought in store)
SELECT cust_id, name FROM online_buyers
EXCEPT
SELECT cust_id, name FROM store_buyers;

-- Oracle uses MINUS instead of EXCEPT:
-- SELECT cust_id FROM online_buyers MINUS SELECT cust_id FROM store_buyers;
EXCEPT Is Directional — UNION and INTERSECT Are Not

Swap the two queries around UNION or INTERSECT and the answer is identical. Swap them around EXCEPT and you get a completely different set. Always read it as "keep the left, subtract the right."


Section 07

Set Operator Rules at a Glance

OperatorReturnsRemoves Duplicates?Order Matters?Oracle Name
UNIONAll rows from bothYesNoUNION
UNION ALLAll rows from bothNoNoUNION ALL
INTERSECTRows in bothYesNoINTERSECT
EXCEPTLeft minus rightYesYesMINUS
✅ The Four Non-Negotiable Requirements
Rule 1
Both queries must return the same number of columns.
Rule 2
Corresponding columns must have compatible data types.
Rule 3
Column names come from the first query; the rest are ignored.
Rule 4
A single ORDER BY goes last, applying to the whole result.

Section 08

Views — A Saved Query That Acts Like a Table

A Window, Not a Photocopy
A view is a window onto your data, not a copy of it. You define a query once and give it a name; every time you select from the view, the database runs the underlying query fresh against the live base tables. Nothing is duplicated — you are always looking through the glass at the real, current rows.
📐 How a View Sits Above Base Tables
employees departments VIEW: staff_dirs (virtual — no stored rows) data read live, on every query ↑

Green dots show that a view holds no data of its own — it pulls live rows from its base tables each time it is queried.

-- Create a reusable view joining employees to their department
CREATE VIEW staff_dirs AS
SELECT e.name, d.dept_name, e.salary
FROM   employees e
JOIN   departments d ON e.dept_id = d.dept_id;

-- Now query it like an ordinary table
SELECT name, dept_name
FROM   staff_dirs
WHERE  salary > 50000;
🔒
Security
Expose only safe columns. Grant access to the view, hide the salary column or the raw base table entirely.
🧰
Simplicity
Wrap a gnarly multi-table join once; everyone queries the tidy view instead of repeating the logic.
🔗
Consistency
Business rules live in one definition. Fix the view once and every report updates together.
💾
Standard View vs Materialized View

A normal view stores no data — it re-runs its query every time, always fresh but never faster. A materialized view physically stores the result and must be refreshed; it trades freshness for speed on expensive aggregations.


Section 09

Indexes — The Speed Mechanism

The Index at the Back of a Textbook
To find "transactions" in a 900-page book, you would never read every page. You flip to the index, find the word, and jump straight to page 412. A database index works identically: instead of scanning every row (a full table scan), the engine uses a sorted structure — usually a B-tree — to jump directly to the rows it needs.
🏃 Full Table Scan vs Index Seek (animated)
WITHOUT INDEX — full table scan id=4 checks every row until it finds id = 4 — O(n) WITH INDEX — direct seek id=4 B-tree jumps straight to id = 4 — O(log n)

Top: the engine walks every row in turn. Bottom: the index lets it land on the target row immediately.

-- Create an index on a frequently-filtered column
CREATE INDEX idx_emp_dept ON employees(dept_id);

-- A composite (multi-column) index — order matters!
CREATE INDEX idx_emp_dept_salary ON employees(dept_id, salary);

-- Enforce uniqueness AND speed lookups at once
CREATE UNIQUE INDEX idx_emp_email ON employees(email);
✅ Index Helps
Scenario
WHERE on the column
JOIN keys
ORDER BY / GROUP BY
High-selectivity columns
❌ Index Hurts
Scenario
Heavy INSERT / UPDATE load
Tiny tables
Low-selectivity (e.g. gender)
Rarely-queried columns
⚠️
Indexes Are Not Free

Every index must be updated on every INSERT, UPDATE, and DELETE, and it consumes disk space. Indexing every column "to be safe" slows writes and wastes storage. Index the columns you actually filter, join, or sort on.

🧮
The Composite-Index Left-Prefix Rule

An index on (dept_id, salary) can serve queries filtering on dept_id alone, or on dept_id AND salary — but not on salary alone. The leftmost column must be used for the index to apply.


Section 10

Joins — Stitching Tables Side by Side

Where set operators stack rows vertically, a join matches rows from two tables on a related column and merges their columns into one wider row. The diagram below shows the four families and exactly which rows survive each one.

🧩 The Four Join Types
INNER LEFT RIGHT FULL OUTER

Blue = left table, red = right table. Green shading = rows the join keeps.


Section 11

INNER JOIN — Only Matching Rows

An INNER JOIN returns only rows where the join key exists in both tables. Watch the animation below "draw" the matches between employees and departments.

🔗 Matching employees.dept_id → departments.dept_id
employees departments Asha · dept 10 Bilal · dept 10 Chen · dept 20 Diya · dept 30 Esha · dept NULL 10 · Engineering 20 · Sales 30 · Marketing 40 · Research

Solid green links are kept by INNER JOIN. Esha (dept NULL) and Research (no staff) are dashed — INNER JOIN drops both.

SELECT e.name, d.dept_name
FROM   employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;
OUTPUT
name | dept_name Asha | Engineering Bilal | Engineering Chen | Sales Diya | Marketing -- Esha dropped (NULL dept), Research dropped (no staff)

Section 12

OUTER JOINs — Keep the Unmatched

Outer joins preserve rows that have no match, filling the missing side with NULL. There are three flavours.

⬅️
LEFT OUTER JOIN
Keeps all left rows; matched right columns appear, unmatched become NULL. Esha survives with a NULL department.
all of LEFT + matches
➡️
RIGHT OUTER JOIN
Keeps all right rows; unmatched left becomes NULL. Research (dept 40) survives with no employee.
all of RIGHT + matches
⬌️
FULL OUTER JOIN
Keeps every row from both; fills NULLs wherever a match is missing on either side.
everything
-- LEFT: every employee, even those with no department
SELECT e.name, d.dept_name
FROM   employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;

-- FULL: every employee AND every department
SELECT e.name, d.dept_name
FROM   employees e
FULL OUTER JOIN departments d ON e.dept_id = d.dept_id;
LEFT JOIN result
namedept_name
AshaEngineering
BilalEngineering
ChenSales
DiyaMarketing
EshaNULL
RIGHT JOIN adds
namedept_name
AshaEngineering
BilalEngineering
ChenSales
DiyaMarketing
NULLResearch
🔎
The Classic "Find the Orphans" Trick

To find rows with no match — departments with no employees, customers with no orders — use a LEFT JOIN then filter WHERE right_table.key IS NULL. This is the everyday anti-join pattern.


Section 13

SELF JOIN — A Table Joined to Itself

Everyone in the Same Room
A self join is when one table plays two roles at once. In our employees table, a manager is an employee — manager_id points to another emp_id in the same table. To list each person beside their manager's name, we join employees to a second copy of itself, using two aliases.
👥 Same table, two aliases: e (worker) & m (manager)
e — as worker m — as manager Bilal (mgr_id 101) Chen (mgr_id 101) Diya (mgr_id 102) 101 · Asha 102 · Bilal e.manager_id = m.emp_id

Each worker row (left) links to the row describing its manager (right) — both drawn from the very same table.

-- List each employee next to their manager's name
SELECT e.name AS employee, m.name AS manager
FROM   employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
OUTPUT
employee | manager Asha | NULL <- top of the hierarchy Bilal | Asha Chen | Asha Diya | Bilal Esha | Bilal
⚠️
Aliases Are Mandatory in a Self Join

Because the same table appears twice, you must give each instance a distinct alias (here e and m). Without aliases the database cannot tell which copy a column refers to, and the query is ambiguous.


Section 14

Joins vs Set Operators — Side by Side

PropertySet OperatorsJoins
Direction of combinationVertical (stack rows)Horizontal (widen rows)
Column requirementSame count & types both sidesAny columns; matched on a key
Result widthSame as input columnsSum of both tables' columns
Matches onWhole-row equalityA specified ON condition
Typical question"In which list(s)?""What related detail?"

Section 15

Practice Problems

Try each before revealing the logic. Use the sample tables from Section 02.

🧠 Work These Out
Q1
List customers who bought online or in store, with no duplicates. → UNION of the two buyer tables.
Q2
Find loyal customers who used both channels. → INTERSECT.
Q3
Find store-only customers to target with an online-discount email. → store_buyers EXCEPT online_buyers.
Q4
List every department including those with no employees. → RIGHT JOIN (or LEFT JOIN with departments first).
Q5
Show each employee with their manager's salary. → SELF JOIN on e.manager_id = m.emp_id.
Q6
Speed up a report filtering employees by dept_id then sorting by salary. → Composite index (dept_id, salary).
-- Q3 solution: store-only customers
SELECT cust_id, name FROM store_buyers
EXCEPT
SELECT cust_id, name FROM online_buyers;

-- Q4 solution: all departments, staffed or not
SELECT d.dept_name, e.name
FROM   departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id;

Section 16

Golden Rules

🎯 Set Operators, Views, Indexes & Joins — The Essentials
1
Set operators need union-compatible queries: same column count, same order, compatible types. The result inherits column names from the first query.
2
Prefer UNION ALL over UNION whenever duplicates are impossible or wanted — it skips an expensive deduplication step.
3
EXCEPT is directional; UNION and INTERSECT are not. Always read EXCEPT as "keep the left, subtract the right."
4
A view stores no data — it re-runs its query live. Use it for security, simplicity, and a single source of truth. Reach for a materialized view only when you need to cache an expensive result.
5
Index the columns you filter, join, or sort on — not every column. Indexes accelerate reads but slow writes and consume space.
6
Respect the left-prefix rule: a composite index on (a, b) helps queries on a or a AND b, but never on b alone.
7
INNER keeps matches only; LEFT/RIGHT keep all of one side; FULL keeps everything. To find unmatched rows, LEFT JOIN then filter WHERE right.key IS NULL.
8
A self join always needs two distinct aliases, because the same table is referenced twice in one statement.