“Show the ten highest salaries” and “show each customer's newest order” sound simple, but they contain three separate decisions: how rows are ranked, what a tie means, and whether the answer must be repeatable. This beginner-friendly guide uses SQL that fits Db2 for z/OS, including FETCH FIRST and the OLAP specifications ROW_NUMBER, RANK, and DENSE_RANK.
Before writing SQL, ask whether “top three” means three rows, three finishing places, or three distinct values. Those answers differ when two employees have the same salary. Also ask whether “latest” means the greatest business event timestamp, the greatest load timestamp, the greatest sequence number, or the row most recently changed in Db2. A timestamp column does not automatically have the meaning your business wants.
| Requirement | Typical SQL tool | Tie behavior |
|---|---|---|
| Top N overall, exactly N at most | ORDER BY plus FETCH FIRST n ROWS ONLY | Cuts at N; peers at the boundary can be omitted |
| Top N places, including ties | RANK in a CTE, then rank <= n | Includes peers; ranks can have gaps |
| Top N distinct values | DENSE_RANK in a CTE, then rank <= n | Includes peers; ranks have no gaps |
| Exactly one latest row per group | ROW_NUMBER partitioned by group | Add a unique final sort key to choose predictably |
| Every row tied for latest per group | RANK partitioned by group or MAX join | Can return more than one row for a group |
Db2 applies the ordering to the result and then limits the result with the fetch clause. The following query asks for no more than five employees, ranked by salary from greatest to least. EMPNO is a final tie-breaker, so equal salaries still have a complete order.
12345SELECT EMPNO, LASTNAME, WORKDEPT, SALARY FROM DSN8D10.EMP WHERE SALARY IS NOT NULL ORDER BY SALARY DESC, EMPNO ASC FETCH FIRST 5 ROWS ONLY;
The predicate excludes unknown salaries. That matters because Db2 treats the null value as higher than non-null values for ordering; with descending order, nulls can appear before real salaries unless you exclude them or state an appropriate null ordering. Always decide explicitly whether unknown values belong in the competition.
Without ORDER BY, the result table has no guaranteed sequence, so “first five” means any five qualifying rows. An index access path that happens to produce a convenient order today is not a contract. RUNSTATS, REBIND, a new index, parallelism, or a Db2 maintenance level can change the access path and expose the mistake.
Even ORDER BY SALARY DESC is incomplete when salaries tie. IBM documents that rows with duplicate values of the final sort key have an arbitrary relative order. Add a stable, unique key such as EMPNO when exactly five repeatable rows are required. Do not use a changing or non-unique attribute as the final key.
12345SELECT PRODUCT_ID, PRODUCT_NAME, UNIT_PRICE FROM PRODUCT WHERE UNIT_PRICE IS NOT NULL ORDER BY UNIT_PRICE ASC, PRODUCT_ID ASC FETCH FIRST 10 ROWS ONLY;
“Bottom” usually means ascending order, but it might mean worst score, earliest date, or largest defect count. Encode the business meaning rather than assuming ASC always means worst. FETCH FIRST returns at most N rows; it can return fewer when fewer rows qualify.
These OLAP specifications calculate a value without collapsing detail rows. Their ORDER BY belongs inside the OVER clause and determines ranking. The outer ORDER BY is separate: it controls how the final result is displayed. Use both when consumers need a guaranteed presentation order.
12345678SELECT EMPNO, SALARY, ROW_NUMBER() OVER (ORDER BY SALARY DESC, EMPNO) AS ROW_NO, RANK() OVER (ORDER BY SALARY DESC) AS PAY_RANK, DENSE_RANK() OVER (ORDER BY SALARY DESC) AS PAY_LEVEL FROM DSN8D10.EMP WHERE SALARY IS NOT NULL ORDER BY SALARY DESC, EMPNO;
Notice that PAY_RANK and PAY_LEVEL omit EMPNO from their window ordering. That is intentional: employees with equal salaries must remain peers. Adding EMPNO there would break the tie and give each employee a different rank. The correct ORDER BY list depends on whether you are defining a row winner or a business-value tie.
A rank calculated in the SELECT list is not available to the WHERE clause of the same query block. Calculate it in a common table expression (CTE) or nested table expression, then filter it outside.
12345678910111213WITH RANKED_PAY AS ( SELECT EMPNO, LASTNAME, SALARY, RANK() OVER (ORDER BY SALARY DESC) AS PAY_RANK FROM DSN8D10.EMP WHERE SALARY IS NOT NULL ) SELECT EMPNO, LASTNAME, SALARY, PAY_RANK FROM RANKED_PAY WHERE PAY_RANK <= 3 ORDER BY PAY_RANK, EMPNO;
This returns everyone in the first three competition places, so it can return more than three rows. Replace RANK with DENSE_RANK when the requirement is the top three distinct salary values. Replace it with ROW_NUMBER ordered by salary and EMPNO when the requirement is exactly three employees.
FETCH FIRST 1 ROW ONLY on the whole query returns one row for the entire result, not one row for every customer. To restart numbering for each customer, use PARTITION BY. The following pattern returns exactly one order per customer.
12345678910111213141516171819WITH LATEST_ORDER AS ( SELECT CUSTOMER_ID, ORDER_ID, ORDER_TS, STATUS, TOTAL_AMOUNT, ROW_NUMBER() OVER ( PARTITION BY CUSTOMER_ID ORDER BY ORDER_TS DESC, ORDER_ID DESC ) AS RN FROM CUSTOMER_ORDER WHERE ORDER_TS IS NOT NULL ) SELECT CUSTOMER_ID, ORDER_ID, ORDER_TS, STATUS, TOTAL_AMOUNT FROM LATEST_ORDER WHERE RN = 1 ORDER BY CUSTOMER_ID;
PARTITION BY creates an independent numbering group for each CUSTOMER_ID. ORDER_TS chooses the latest business event. ORDER_ID resolves equal timestamps. The example assumes a larger ORDER_ID is the intended winner when timestamps tie; use that rule only if the application guarantees and accepts it. An identity value is not a substitute for event time when records can arrive late.
Greatest-N-per-group is a small change: keep RN less than or equal to N. This query returns at most three orders per customer, not merely three orders total.
123456789101112131415161718WITH ORDER_HISTORY AS ( SELECT CUSTOMER_ID, ORDER_ID, ORDER_TS, STATUS, ROW_NUMBER() OVER ( PARTITION BY CUSTOMER_ID ORDER BY ORDER_TS DESC, ORDER_ID DESC ) AS RN FROM CUSTOMER_ORDER WHERE ORDER_TS IS NOT NULL ) SELECT CUSTOMER_ID, ORDER_ID, ORDER_TS, STATUS FROM ORDER_HISTORY WHERE RN <= 3 ORDER BY CUSTOMER_ID, RN;
Use RANK instead of ROW_NUMBER when every order tied at the boundary must be returned. Be prepared for more than three rows in a customer group. Use DENSE_RANK when “three” means three distinct timestamps, including all orders at each timestamp.
An aggregate can find the greatest timestamp for each group, and a join can recover the other columns. This compact pattern is useful when all rows sharing the latest timestamp are valid answers.
12345678910111213WITH MAX_TIME AS ( SELECT CUSTOMER_ID, MAX(ORDER_TS) AS MAX_ORDER_TS FROM CUSTOMER_ORDER WHERE ORDER_TS IS NOT NULL GROUP BY CUSTOMER_ID ) SELECT O.CUSTOMER_ID, O.ORDER_ID, O.ORDER_TS, O.STATUS FROM CUSTOMER_ORDER O JOIN MAX_TIME M ON M.CUSTOMER_ID = O.CUSTOMER_ID AND M.MAX_ORDER_TS = O.ORDER_TS ORDER BY O.CUSTOMER_ID, O.ORDER_ID;
This does not guarantee one row per customer. If two orders share MAX_ORDER_TS, both join back. That may be exactly right for “show all latest ties,” but it is wrong for a one-row customer summary. Trying to select STATUS beside MAX(ORDER_TS) in the grouped query is not a solution: STATUS is neither grouped nor aggregated, and even a permissive shortcut would not say which row supplied it.
Another accurate way to describe a latest row is “there is no better row in the same group.” It is verbose but makes the tie-break rule explicit.
1234567891011121314SELECT O.CUSTOMER_ID, O.ORDER_ID, O.ORDER_TS, O.STATUS FROM CUSTOMER_ORDER O WHERE O.ORDER_TS IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM CUSTOMER_ORDER N WHERE N.CUSTOMER_ID = O.CUSTOMER_ID AND N.ORDER_TS IS NOT NULL AND (N.ORDER_TS > O.ORDER_TS OR (N.ORDER_TS = O.ORDER_TS AND N.ORDER_ID > O.ORDER_ID)) ) ORDER BY O.CUSTOMER_ID;
The optimizer can transform correlated SQL, but readable ROW_NUMBER is usually the clearest starting point. Compare access paths and measured workload performance rather than assuming one textual form is always fastest.
Top-N queries become expensive when Db2 must examine many qualifying rows and sort them before discarding nearly all of them. A useful candidate index puts columns used by equality predicates or grouping first, followed by ranking columns in the needed order. For the customer example, consider:
123CREATE INDEX APP.IX_ORDER_LATEST ON APP.CUSTOMER_ORDER (CUSTOMER_ID, ORDER_TS DESC, ORDER_ID DESC);
This order groups a customer's entries together and places the newest keys first within the customer. It can help avoid or reduce sorting and can let Db2 find a small answer quickly. If the query filters one customer with CUSTOMER_ID = ?, the leading key is especially useful. For all customers, the OLAP operation can still require substantial work; the index is not a magic “one read per group” instruction.
MAX ignores null inputs, while ordering can place nulls ahead of ordinary values in a descending sequence. That means a MAX-and-join query and an unfiltered ROW_NUMBER query can disagree. Decide whether a null timestamp means unknown, not yet occurred, or bad data, and write a predicate or null ordering that matches that decision.
A complete ORDER BY makes the winner deterministic for the rows visible to the statement. It does not freeze the table forever. A concurrent transaction can insert a newer row before the next execution. Your isolation level, commit timing, and application unit of work determine what is visible. For a stable report across several statements, consistency requirements must be designed beyond the ranking expression.
OFFSET plus FETCH FIRST is convenient, but a new row inserted ahead of the current page shifts later offsets. For a frequently changing ordered list, keyset pagination is usually more stable: remember the last ORDER_TS and ORDER_ID, then ask for rows below that complete key. Keep the same unique ordering on every page.
123456SELECT ORDER_ID, CUSTOMER_ID, ORDER_TS, STATUS FROM CUSTOMER_ORDER WHERE ORDER_TS < :LAST_ORDER_TS OR (ORDER_TS = :LAST_ORDER_TS AND ORDER_ID < :LAST_ORDER_ID) ORDER BY ORDER_TS DESC, ORDER_ID DESC FETCH FIRST 25 ROWS ONLY;
If the sort columns can be null or can change after publication, the continuation predicate needs a documented policy. Keyset pagination improves deep-page access but does not create a historical snapshot by itself.
System-period and business-time temporal tables can answer “as of” questions using temporal SQL. Manually choosing MAX(timestamp) answers a different question unless that timestamp is the approved validity boundary. Use temporal semantics when the table design provides them.
Imagine every class lines up for a race. FETCH FIRST says, “Let only five children through the gate.” ORDER BY says who stands at the front. ROW_NUMBER gives every child a different ticket. RANK lets tied children share a place and skips the next place. DENSE_RANK lets tied children share a place but does not skip a number. PARTITION BY creates a separate little race for every classroom. To pick one newest drawing from each classroom, number each classroom's drawings newest-first and keep ticket number one.
1. What makes a FETCH FIRST 10 query a meaningful Top-10 query?
2. Which OLAP specification is normally used to select exactly one latest row per customer?
3. How do RANK and DENSE_RANK differ after a tie for first place?
4. Why can MAX(event_ts) joined back to the history table return duplicate groups?
5. Which index shape is a useful candidate for latest orders by customer?
More than N. RANK or DENSE_RANK preserves every peer whose calculated rank passes the filter. Use ROW_NUMBER or FETCH FIRST n ROWS ONLY when the contract is at most N rows.
Usually not when equal business values are meant to tie. A unique key in RANK's window ORDER BY makes every row distinct. Add it to ROW_NUMBER when choosing one row, and to the final query ORDER BY when displaying peers predictably.
No. Identity values show allocation order, not necessarily business-event order. Delayed feeds, retries, data repair, and imported history can create a newer identity for an older event. Rank by the column that represents the approved meaning of latest.