Chapter 05 · Oracle SQL Fundamentals: Joins, Subqueries, Set Operations, and Hierarchical Queries

INNER/OUTER/CROSS Joins, Legacy Join Syntax Awareness, and Cardinality Reasoning

Build joins from result-set semantics first: preserve or reject unmatched rows deliberately, recognize legacy (+) syntax safely, and reason about row multiplication before tuning.

Intermediate → Advanced105–125 minutesJoin correctness + cardinality labANSI joins first · legacy (+) maintenance literacyOracle AI Database Free · no paid options/packsLast reviewed: August 2026

Learning outcomes

ServiceHub now needs a report listing every technician, including technicians with no open tickets, plus an optional count of ticket tags. A developer writes a join that looks compact but silently removes technicians without tickets; another joins tags and mistakes the multiplied row count for duplicate data. The lesson’s central rule is simple: define the required row set before discussing join algorithms or indexes.

01

Distinguish inner, left/right/full outer, and cross joins by row-preservation semantics.

02

Place predicates deliberately in ON versus WHERE so an outer join does not collapse accidentally.

03

Recognize Oracle’s legacy (+) outer-join syntax without adopting it for new code.

04

Predict cardinality multiplication across one-to-many and many-to-many relationships.

05

Use row-count and plan evidence without confusing estimated cardinality with actual business correctness.

Prerequisite connection

Lesson 1 stabilized NULL, conversion, and ordering semantics. Those rules matter here because outer joins deliberately manufacture NULL-extended rows for the non-preserved side, and filters can either preserve or discard those rows.

Lab and version baseline

Mandatory examples target a disposable ServiceHub schema in Oracle AI Database Free 26ai. The chapter was reviewed against the July 2026 RU 23.26.3 documentation, SQL Developer 26.2, and SQLcl 26.2.1. No paid option, management pack, RAC, Data Guard, Exadata, or cloud service is required. Oracle AI Database Free remains limited to 2 foreground CPUs, 2 GB combined SGA/PGA memory, and 12 GB user data and receives no Oracle patches or Support service requests; use it as a learning engine, not a production default.

1. A join is first a rule for constructing rows

An inner join returns only row combinations that satisfy its join condition. A left outer join preserves every row from the left input and null-extends right-side columns when no match exists. A full outer join preserves unmatched rows from both sides. A cross join deliberately forms the Cartesian product: every row from one input paired with every row from the other.

These are result semantics. Oracle may implement an eligible join with nested loops, hash join, or sort merge join, but a different physical method must preserve the same SQL result. Therefore, validate the business row set before diagnosing plan shape.

sql · ANSI joins make the relationship explicit
SELECT    t.tech_id,    t.tech_name,    w.work_order_id,    w.status_codeFROM servicehub_sql_technician tLEFT JOIN servicehub_sql_work_order w  ON w.tech_id = t.tech_idORDER BY t.tech_id, w.work_order_id;

2. ON and WHERE are not interchangeable for outer joins

With an inner join, moving a simple filter between ON and WHERE often preserves the final row set. With an outer join, it can change the meaning. The ON clause decides whether right-side rows match while the left row is still preserved; a WHERE predicate is applied to the joined result and can reject the null-extended row.

sql · deliberately wrong: this loses technicians with no OPEN ticket
SELECT t.tech_id, t.tech_name, w.work_order_idFROM servicehub_sql_technician tLEFT JOIN servicehub_sql_work_order w  ON w.tech_id = t.tech_idWHERE w.status_code = 'OPEN'ORDER BY t.tech_id;

For a technician with no matching ticket, w.status_code is null. The WHERE predicate is not true, so that preserved row is removed. If the requirement is “all technicians, and their open tickets if any,” move the status predicate into the join condition.

sql · repair: constrain the match while preserving the technician
SELECT t.tech_id, t.tech_name, w.work_order_idFROM servicehub_sql_technician tLEFT JOIN servicehub_sql_work_order w  ON w.tech_id = t.tech_id AND w.status_code = 'OPEN'ORDER BY t.tech_id, w.work_order_id;

3. Cardinality multiplication is not automatically duplication

If one technician owns three work orders, joining technician to work order correctly returns three technician/work-order combinations. If each work order has two tags and you join tags too, you can get six rows. That is not necessarily corrupt data; it is the relational product implied by the relationships.

Before adding DISTINCT, ask what one output row is supposed to represent. Using DISTINCT to hide a join mistake can suppress legitimate combinations and consume extra work for sorting or hashing.

sql · measure each relationship before composing them
SELECT COUNT(*) AS techniciansFROM servicehub_sql_technician;SELECT tech_id, COUNT(*) AS work_ordersFROM servicehub_sql_work_orderGROUP BY tech_idORDER BY tech_id;SELECT work_order_id, COUNT(*) AS tagsFROM servicehub_sql_work_order_tagGROUP BY work_order_idORDER BY work_order_id;SELECT COUNT(*) AS joined_rowsFROM servicehub_sql_technician tJOIN servicehub_sql_work_order w  ON w.tech_id = t.tech_idJOIN servicehub_sql_work_order_tag x  ON x.work_order_id = w.work_order_id;

The last count is evidence about the composed relationship. It is not proof that the optimizer estimated the same cardinality, and it is not by itself evidence of duplicate base rows.

4. Accidental Cartesian products fail by being valid

A missing join predicate is dangerous because the SQL can be syntactically valid. Three technicians and four work orders produce twelve combinations. Large production tables can turn the same mistake into an explosive intermediate result.

sql · wrong result: valid SQL, missing relationship
SELECT COUNT(*) AS accidental_pairsFROM servicehub_sql_technician t,     servicehub_sql_work_order w;

Prefer explicit ANSI JOIN ... ON syntax for new code because the relationship is visible next to the join. Use CROSS JOIN when the Cartesian product is intentional; the keyword communicates intent during review.

5. Legacy Oracle (+) syntax: recognize it, do not normalize it

Older Oracle code often expresses an outer join by applying the (+) operator to columns from the optional side in the WHERE clause. Current Oracle still documents this syntax for compatibility, but it has restrictions that ANSI outer joins do not. For example, you cannot mix (+) and ANSI join syntax in the same query block, and if multiple predicates join the same two tables then (+) must be applied consistently or the query can silently become a simple join.

sql · maintenance literacy only: legacy left outer join
SELECT    t.tech_id,    t.tech_name,    w.work_order_idFROM servicehub_sql_technician t,     servicehub_sql_work_order wWHERE t.tech_id = w.tech_id(+)ORDER BY t.tech_id, w.work_order_id;
New-code rule

Write ANSI joins for new ServiceHub SQL. Translate legacy (+) code carefully during maintenance and prove row counts before and after the rewrite.

6. Plan evidence comes after semantic evidence

Once row semantics are correct, inspect the optimizer’s estimated plan. EXPLAIN PLAN is not an executed cursor and its row estimates are not actual row counts. It is useful here to identify the join tree and estimated cardinalities, but later optimizer lessons will use executed-cursor evidence where appropriate.

sql · estimated plan for the corrected outer join
EXPLAIN PLAN SET STATEMENT_ID = 'SH05L2'FORSELECT t.tech_id, t.tech_name, w.work_order_idFROM servicehub_sql_technician tLEFT JOIN servicehub_sql_work_order w  ON w.tech_id = t.tech_id AND w.status_code = 'OPEN';SELECT *FROM TABLE(DBMS_XPLAN.DISPLAY(NULL, 'SH05L2', 'BASIC +PREDICATE'));

Do not hard-code a predicted join method into an acceptance test. The optimizer can choose a different legal plan as statistics, data volume, or system conditions change.

7. Hands-on lab: preserve technicians and expose multiplication

sql · setup
DROP TABLE servicehub_sql_work_order_tag IF EXISTS PURGE;DROP TABLE servicehub_sql_work_order IF EXISTS PURGE;DROP TABLE servicehub_sql_technician IF EXISTS PURGE;CREATE TABLE servicehub_sql_technician (    tech_id    NUMBER PRIMARY KEY,    tech_name  VARCHAR2(80) NOT NULL);CREATE TABLE servicehub_sql_work_order (    work_order_id NUMBER PRIMARY KEY,    tech_id       NUMBER,    status_code   VARCHAR2(20) NOT NULL,    CONSTRAINT sh05_wo_tech_fk      FOREIGN KEY (tech_id) REFERENCES servicehub_sql_technician(tech_id));CREATE TABLE servicehub_sql_work_order_tag (    work_order_id NUMBER NOT NULL,    tag_code      VARCHAR2(30) NOT NULL,    CONSTRAINT sh05_wot_pk PRIMARY KEY (work_order_id, tag_code),    CONSTRAINT sh05_wot_wo_fk      FOREIGN KEY (work_order_id) REFERENCES servicehub_sql_work_order(work_order_id));INSERT INTO servicehub_sql_technician VALUES (1,'Asha');INSERT INTO servicehub_sql_technician VALUES (2,'Mina');INSERT INTO servicehub_sql_technician VALUES (3,'Ravi');INSERT INTO servicehub_sql_work_order VALUES (101,1,'OPEN');INSERT INTO servicehub_sql_work_order VALUES (102,1,'CLOSED');INSERT INTO servicehub_sql_work_order VALUES (103,2,'CLOSED');INSERT INTO servicehub_sql_work_order_tag VALUES (101,'URGENT');INSERT INTO servicehub_sql_work_order_tag VALUES (101,'HVAC');INSERT INTO servicehub_sql_work_order_tag VALUES (102,'HISTORY');COMMIT;
sql · verification queries
-- Requirement: every technician, plus OPEN work orders when present.SELECT t.tech_id, t.tech_name, w.work_order_idFROM servicehub_sql_technician tLEFT JOIN servicehub_sql_work_order w  ON w.tech_id = t.tech_id AND w.status_code = 'OPEN'ORDER BY t.tech_id, w.work_order_id;-- Observe relationship multiplication deliberately.SELECT t.tech_id, w.work_order_id, x.tag_codeFROM servicehub_sql_technician tJOIN servicehub_sql_work_order w  ON w.tech_id = t.tech_idLEFT JOIN servicehub_sql_work_order_tag x  ON x.work_order_id = w.work_order_idORDER BY t.tech_id, w.work_order_id, x.tag_code;
sql · cleanup
DROP TABLE servicehub_sql_work_order_tag PURGE;DROP TABLE servicehub_sql_work_order PURGE;DROP TABLE servicehub_sql_technician PURGE;

8. Production judgment and next step

For production joins, document the intended grain: one row per technician, work order, tag, or some aggregate. Enforce key relationships where possible, inspect base-table multiplicity, and test outer-join filters with unmatched rows. Only then use plan/cardinality evidence to improve performance. A query that is fast and semantically wrong is still wrong.

No paid feature, management pack, restart, or COMPATIBLE change is required. The legacy (+) operator is included only for maintenance literacy. Lesson 3 moves to subqueries, where Oracle may transform EXISTS/IN into semijoins and NOT EXISTS/NOT IN into antijoins—but NULL semantics must be correct before any transformation is considered.

Check your understanding

  1. Why can a WHERE predicate on the optional side of a LEFT JOIN remove preserved rows?
  2. Why is DISTINCT a poor first response to unexpected join multiplication?
  3. What does CROSS JOIN communicate that a comma join with no predicate does not?
  4. Why should new code prefer ANSI joins over Oracle’s (+) syntax?
  5. Does EXPLAIN PLAN show actual executed row counts?
Review the answers

The outer join first creates a null-extended row; a WHERE predicate on that right-side column can then reject it.

Multiplication may be the correct result of one-to-many or many-to-many relationships. DISTINCT can hide modeling/query mistakes and remove legitimate combinations.

CROSS JOIN states that the Cartesian product is intentional, making review and maintenance clearer.

ANSI syntax is clearer and avoids several restrictions and silent pitfalls of the legacy (+) operator.

No. EXPLAIN PLAN is estimated plan evidence, not an executed cursor with actual runtime rows.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.