Most useful reports need columns from more than one table: employee plus department name, part plus product description. An inner join is the matching rule that keeps only combinations where the join condition is true. Unmatched rows from either side disappear. This page is how DB2 for z/OS writes that rule: JOIN, INNER JOIN, JOIN ON, and why you should not copy JOIN USING from LUW examples.
A join operation typically matches a row of one table with a row of another on a join condition. The result of T1 INNER JOIN T2 is their paired rows. If a join operator is not specified, INNER is the default. That is why people say “join” when they mean “inner join,” and why FROM T1 JOIN T2 ON … is an inner join unless you write LEFT, RIGHT, FULL, or CROSS.
Inner join means discard. A part whose product number is not in PRODUCTS does not appear. A product with no parts listed does not appear. Outer joins exist specifically to keep those orphans; the next page covers LEFT OUTER JOIN.
The join condition can be any simple or compound search condition that does not contain a subquery. Equality of key columns is the usual case (equijoin). You can also write non-equal comparisons, AND extra local filters into ON, or even ON 1=1 to force every combination—the same Cartesian product you get from a comma join with no WHERE.
When you name the join in FROM with INNER JOIN (or JOIN), you put the join condition in ON, not in WHERE. ON is required for that syntax.
123SELECT PART, SUPPLIER, PARTS.PROD#, PRODUCT FROM PARTS INNER JOIN PRODUCTS ON PARTS.PROD# = PRODUCTS.PROD#;
IBM’s sample PARTS / PRODUCTS data yields only matching product numbers: WIRE and MAGNETS with GENERATOR (10), PLASTIC with RELAY (30), BLADES with SAW (205). OIL (product 160, not in PRODUCTS) and SCREWDRIVER (product 505, no parts) are gone.
You may AND extra predicates onto ON. They still participate in the inner-join match. For an inner join, IBM notes that ON predicates can supply both the join condition and local filtering and are semantically equivalent to WHERE predicates. Putting the key match in ON and the business filter in WHERE is still clearer:
12345678910SELECT PART, SUPPLIER, PARTS.PROD#, PRODUCT FROM PARTS INNER JOIN PRODUCTS ON PARTS.PROD# = PRODUCTS.PROD# WHERE SUPPLIER NOT LIKE 'A%'; -- Same rows for an inner join if the extra predicate is in ON instead SELECT PART, SUPPLIER, PARTS.PROD#, PRODUCT FROM PARTS INNER JOIN PRODUCTS ON PARTS.PROD# = PRODUCTS.PROD# AND SUPPLIER NOT LIKE 'A%';
Qualify column names whenever both tables have the same name (PROD#). Correlation names keep SQL readable:
12345SELECT E.EMPNO, E.LASTNAME, D.DEPTNAME FROM HR.EMPLOYEE AS E INNER JOIN HR.DEPARTMENT AS D ON E.WORKDEPT = D.DEPTNO WHERE D.LOCATION = 'DALLAS';
INNER is optional in the keyword sequence: INNER JOIN and JOIN mean the same inner join. Write INNER JOIN when the team wants the type visible next to LEFT OUTER JOIN in the same statement.
Listing tables in FROM separated by commas is an implicit inner join. The join condition belongs in WHERE. If you forget WHERE, you get every combination of rows: the product of the table sizes.
12345678SELECT PART, SUPPLIER, PARTS.PROD#, PRODUCT FROM PARTS, PRODUCTS WHERE PARTS.PROD# = PRODUCTS.PROD#; -- Cartesian product (same idea as ON 1=1) SELECT PART, SUPPLIER, PARTS.PROD#, PRODUCT FROM PARTS INNER JOIN PRODUCTS ON 1 = 1;
Self-joins use the same idea. Two correlation names, one table, a relationship between rows:
123SELECT A.PROJNO, A.PROJNAME, B.PROJNO, B.PROJNAME FROM DSN8C10.PROJ A, DSN8C10.PROJ B WHERE A.PROJNO = B.MAJPROJ;
A is the major project, B is a subproject whose MAJPROJ points at A. You can write the same self-join with INNER JOIN ON A.PROJNO = B.MAJPROJ. Prefer the explicit form in new code so the join is not mixed into a long WHERE of local filters.
| Style | Join condition | Notes |
|---|---|---|
| Explicit INNER JOIN | ON clause | Preferred when mixing joins and local filters |
| Comma (old style) | WHERE clause | Implicit inner join; missing WHERE is a Cartesian product |
| JOIN without INNER | ON clause | INNER is the default join type |
ANSI SQL and some IBM products (Db2 LUW, Db2 for i) allow:
1234-- LUW / IBM i shorthand — not how you write z/OS inner joins SELECT EMPNO, ACSTDATE FROM CORPDATA.PROJACT INNER JOIN CORPDATA.EMPPROJACT USING (PROJNO, ACTNO);
USING (PROJNO, ACTNO) means “join where those same-named columns are equal.” On those products SELECT * even coalesces the join columns so they appear once. Db2 for z/OS application programming documents inner joins with ON or with a comma and WHERE. The z/OS SQL Reference’s USING keyword in other statements is the USING clause of EXECUTE and OPEN (parameter lists), not a join shorthand.
On z/OS, write the equivalent ON predicate explicitly:
123456SELECT EMPNO, ACSTDATE FROM PROJACT AS P INNER JOIN EMPPROJACT AS E ON P.PROJNO = E.PROJNO AND P.ACTNO = E.ACTNO WHERE ACSTDATE > DATE('1982-12-31');
If you copy USING from a LUW blog into a z/OS program, expect a syntax error. Teach the idea—same-name equijoin—then always expand it to ON on this platform. NATURAL JOIN is likewise not the z/OS inner-join style; name the columns.
Multiple inner joins chain in FROM: A JOIN B ON … JOIN C ON …. Each ON refers to tables already in scope. Join order in the text is not necessarily the optimizer’s join sequence; EXPLAIN shows that. Write the statement so humans can see each relationship.
You have a box of kids’ name tags and a box of lunch boxes labeled with names. An inner join is “only keep a name tag when there is a lunch box with the same name, and only keep a lunch box when there is a tag.” A tag with no lunch, or a lunch with no tag, stays in the box. ON is the instruction “same name.” USING on other databases is a shortcut that says “the column called NAME in both boxes”; on z/OS you write that shortcut out in full.
1. What does an inner join keep?
2. When you write INNER JOIN in the FROM clause, where does the join condition go?
3. What happens if you omit WHERE on a comma join of two tables?
4. Does Db2 for z/OS support JOIN … USING (col)?
5. Can an ON join condition contain a subquery on z/OS?