DISTINCT and ORDER BY are valid, important parts of SQL. The problem begins when they are added as cleanup operations without a business requirement. Db2 for z/OS must then prove uniqueness or establish an order, potentially consuming CPU, memory, work-file space, and elapsed time. Worse, DISTINCT can make an incorrect join appear correct by removing duplicate-looking output after the join has already produced too many rows. This tutorial shows how to preserve required semantics while removing accidental work.
Suppose a screen needs customers who placed an open order. CUSTOMER is the parent and SALES_ORDER is the child. A customer with four open orders matches four child rows. If the query projects only customer columns, those four joined combinations look identical. Adding DISTINCT reduces them to one displayed customer, but Db2 still had to perform the join and then enforce uniqueness.
123456789101112131415161718-- Symptom treatment: multiply rows, then remove duplicates SELECT DISTINCT C.CUSTOMER_ID, C.CUSTOMER_NAME FROM CUSTOMER AS C JOIN SALES_ORDER AS O ON O.CUSTOMER_ID = C.CUSTOMER_ID WHERE O.ORDER_STATUS = 'OPEN'; -- Intentional existence test SELECT C.CUSTOMER_ID, C.CUSTOMER_NAME FROM CUSTOMER AS C WHERE EXISTS ( SELECT 1 FROM SALES_ORDER AS O WHERE O.CUSTOMER_ID = C.CUSTOMER_ID AND O.ORDER_STATUS = 'OPEN' );
EXISTS states the real question: does at least one qualifying order exist? It does not promise that the rewrite is always faster; Db2 remains cost based and can transform SQL internally. It does, however, remove accidental row multiplication from the logical request. A useful index beginning with CUSTOMER_ID and including or continuing with ORDER_STATUS can make the existence probe efficient, depending on cardinality and statistics.
| Construct | Correct purpose | Anti-pattern risk |
|---|---|---|
| DISTINCT | Return unique projected rows | Masks join multiplication; duplicate-elimination work |
| ORDER BY | Guarantee final result sequence | Sorts large intermediate results when ordering is not consumed |
| GROUP BY | Create groups, normally for aggregation | Used as a disguised DISTINCT without a grouping requirement |
| UNION | Combine inputs and remove duplicate rows | Pays for uniqueness when UNION ALL semantics are sufficient |
| Window ordering | Define order inside a window calculation | Repeated or incompatible window specifications add sort pressure |
A sort reads input rows, compares sort keys, and produces ordered runs or a final ordered result. If the input exceeds available sort resources, processing can use the Db2 work file database. Work files are shared subsystem resources used for sorts and other intermediate results. A large or concurrent sort workload can therefore affect more than one statement. The exact implementation can include in-memory work, work-file pages, merge phases, and optimizer techniques that avoid a separate sort altogether.
Cost grows with more than row count. Wide sort rows require more storage and movement. Long character keys cost more to compare than compact numeric keys. Duplicate elimination may inspect many rows before producing a small answer. Concurrent reports compete for CPU, buffer pools, and work-file capacity. The right goal is not “ban sorting”; it is “request only the ordering and uniqueness the result actually needs.”
DISTINCT applies to the complete projected row. If the query selects DEPARTMENT_ID and JOB_CODE, Db2 returns each unique pair, not one row per department and not one row per job. This is correct when the business result really is a set of combinations, such as the job codes currently represented in each department.
123456789101112-- Correct: the requirement is one row per represented pair SELECT DISTINCT DEPARTMENT_ID, JOB_CODE FROM EMPLOYEE WHERE EMPLOYMENT_STATUS = 'ACTIVE'; -- Also correct: count unique active job codes per department SELECT DEPARTMENT_ID, COUNT(DISTINCT JOB_CODE) AS ACTIVE_JOB_CODE_COUNT FROM EMPLOYEE WHERE EMPLOYMENT_STATUS = 'ACTIVE' GROUP BY DEPARTMENT_ID;
COUNT(DISTINCT expression) has different semantics from SELECT DISTINCT. It counts distinct non-null values of the expression within the applicable group. Do not remove it merely because uniqueness costs resources. First establish whether repeated values should count once or many times. Performance tuning must not change a business metric.
SQL joins do not randomly create duplicates. They produce one row for every combination satisfying the join predicates. Duplicate-looking output usually means the selected columns hide the columns that distinguish those combinations. Before adding DISTINCT, temporarily select identifiers from every joined table and inspect the relationships.
1234567891011-- Diagnostic query: expose the rows that differ SELECT C.CUSTOMER_ID, C.CUSTOMER_NAME, O.ORDER_ID, O.ORDER_STATUS FROM CUSTOMER AS C JOIN SALES_ORDER AS O ON O.CUSTOMER_ID = C.CUSTOMER_ID WHERE O.ORDER_STATUS = 'OPEN' ORDER BY C.CUSTOMER_ID, O.ORDER_ID;
Never “fix” duplicates by adding a join predicate that merely happens to discard data. The predicate must represent the actual relationship. Likewise, adding a unique index is safe only when existing data and future business rules permit exactly one row for the key. A performance symptom often reveals a missing data-model decision.
When the result needs parent columns and only asks whether matching children exist, EXISTS is usually the clearest expression. For customers without open orders, use NOT EXISTS. Avoid a left join followed by DISTINCT unless the outer-join result itself is required.
12345678910-- Customers with no open orders SELECT C.CUSTOMER_ID, C.CUSTOMER_NAME FROM CUSTOMER AS C WHERE NOT EXISTS ( SELECT 1 FROM SALES_ORDER AS O WHERE O.CUSTOMER_ID = C.CUSTOMER_ID AND O.ORDER_STATUS = 'OPEN' );
If the screen needs one row per customer plus order facts, summarize the child rows. GROUP BY is not a magic faster DISTINCT; here it is correct because COUNT and SUM are defined per customer. Confirm whether a customer with no orders should appear, and use an outer join only when that requirement exists.
12345678910SELECT C.CUSTOMER_ID, C.CUSTOMER_NAME, COUNT(*) AS OPEN_ORDER_COUNT, SUM(O.ORDER_TOTAL) AS OPEN_ORDER_TOTAL FROM CUSTOMER AS C JOIN SALES_ORDER AS O ON O.CUSTOMER_ID = C.CUSTOMER_ID WHERE O.ORDER_STATUS = 'OPEN' GROUP BY C.CUSTOMER_ID, C.CUSTOMER_NAME;
DISTINCT cannot choose the latest order because orders with different dates are not duplicates, and projecting fewer columns loses required detail. Use a window function or another valid greatest-per-group technique. Window ordering can itself require a sort, but that sort implements a real rule rather than hiding arbitrary duplication.
12345678910111213141516SELECT CUSTOMER_ID, ORDER_ID, ORDER_TIMESTAMP FROM ( SELECT O.CUSTOMER_ID, O.ORDER_ID, O.ORDER_TIMESTAMP, ROW_NUMBER() OVER ( PARTITION BY O.CUSTOMER_ID ORDER BY O.ORDER_TIMESTAMP DESC, O.ORDER_ID DESC ) AS RN FROM SALES_ORDER AS O WHERE O.ORDER_STATUS = 'OPEN' ) AS R WHERE RN = 1;
ORDER_ID is a tie-breaker, making the selected row deterministic when timestamps match. An index whose leading columns align with status, customer, and descending order may help, but key order should be designed from the full workload. Do not add a large index for one statement without measuring insert, update, logging, utility, storage, and buffer-pool costs.
SELECT DISTINCT A, B and SELECT A, B GROUP BY A, B commonly describe the same set of unique pairs when no aggregates are present. Replacing one spelling with the other does not remove the uniqueness requirement. Db2 may choose the same or a similar access path. Use DISTINCT to communicate unique projection and GROUP BY to communicate grouping for aggregates or group-level HAVING conditions.
1234567891011121314151617181920-- Unique projection SELECT DISTINCT REGION_CODE, PRODUCT_CODE FROM SALES; -- Logically equivalent grouping when no aggregate is present SELECT REGION_CODE, PRODUCT_CODE FROM SALES GROUP BY REGION_CODE, PRODUCT_CODE; -- GROUP BY has a real purpose here SELECT REGION_CODE, PRODUCT_CODE, SUM(SALES_AMOUNT) AS TOTAL_SALES FROM SALES GROUP BY REGION_CODE, PRODUCT_CODE HAVING SUM(SALES_AMOUNT) > 100000;
Do not infer performance from syntax alone. Existing index order, estimated cardinality, parallelism, selected join sequence, and Db2 optimizer transformations influence the implementation. Compare explained plans with the same catalog statistics and bind environment.
ORDER BY is the only SQL clause that guarantees final result order. Rows read through an index may appear ordered today, but Db2 can select a different index, scan a table space, use parallelism, change join methods, or merge partitions differently tomorrow. Keep ORDER BY whenever a user, file, API, cursor process, ranking rule, or FETCH FIRST request depends on sequence.
1234567891011121314151617-- Deterministic newest orders SELECT ORDER_ID, CUSTOMER_ID, ORDER_TIMESTAMP FROM SALES_ORDER WHERE CUSTOMER_ID = :CUSTOMER_ID ORDER BY ORDER_TIMESTAMP DESC, ORDER_ID DESC FETCH FIRST 25 ROWS ONLY; -- Any 25 qualifying rows: valid only if the caller truly has no preference SELECT ORDER_ID, CUSTOMER_ID, ORDER_TIMESTAMP FROM SALES_ORDER WHERE CUSTOMER_ID = :CUSTOMER_ID FETCH FIRST 25 ROWS ONLY;
Unnecessary ordering often appears inside reusable views, subqueries, exports that later sort again, or service queries whose consumers treat results as an unordered set. Remove it only after tracing every consumer. Conversely, never remove ORDER BY from a top-N query that means “latest 25”; without ordering, it means “some 25.”
A compatible index can let Db2 retrieve keys in the requested sequence and avoid a separate sort. Equality predicates on leading keys followed by ordering keys often form a useful pattern. Yet index-only coverage, filtering, clustering, prefetch, reverse scans, mixed ASC and DESC requirements, and the number of qualifying rows affect the decision. ORDER BY describes semantics; the optimizer decides whether sorting is needed.
123456789-- UNION removes duplicates SELECT CUSTOMER_ID FROM CURRENT_CUSTOMER UNION SELECT CUSTOMER_ID FROM ARCHIVE_CUSTOMER; -- Use only when duplicate IDs are acceptable or impossible SELECT CUSTOMER_ID FROM CURRENT_CUSTOMER UNION ALL SELECT CUSTOMER_ID FROM ARCHIVE_CUSTOMER;
If current and archive tables can overlap during migration, UNION ALL changes the answer. Prove disjointness through time ranges, status rules, constraints, or data governance before choosing it. “UNION ALL is faster” is not enough.
Indexes can provide useful ordering because their keys are maintained in a defined sequence. For a query filtering one CUSTOMER_ID and ordering by ORDER_TIMESTAMP descending, an index beginning with CUSTOMER_ID followed by ORDER_TIMESTAMP can be a candidate. Additional selected columns might permit index-only access. However, a wider index costs disk, logging, buffer space, INSERT and UPDATE processing, COPY, RECOVER, REORG, and RUNSTATS resources.
When no access path naturally supplies the required order or uniqueness, Db2 can use the work file database for intermediate data. Work-file tuning and capacity matter, but adding capacity does not excuse wasteful SQL. First remove semantics-free operations, reduce rows early with correct predicates, project only needed columns, maintain representative statistics, and then size subsystem resources for legitimate workloads.
Explain both the original and proposed SQL under comparable conditions. PLAN_TABLE has indicators that describe sorts for ordering, grouping, unique elimination, joins, and composite table processing. Column names and interpretation depend on the installed Db2 version and PLAN_TABLE format, so use the IBM documentation and tooling for that level. Also inspect access type, selected index, matching columns, join method, join sequence, index-only access, estimated rows, and cost.
12345678910111213141516171819202122232425EXPLAIN PLAN SET QUERYNO = 9401 FOR SELECT C.CUSTOMER_ID, C.CUSTOMER_NAME FROM CUSTOMER AS C WHERE EXISTS ( SELECT 1 FROM SALES_ORDER AS O WHERE O.CUSTOMER_ID = C.CUSTOMER_ID AND O.ORDER_STATUS = 'OPEN' ); SELECT QUERYNO, PLANNO, METHOD, ACCESSTYPE, MATCHCOLS, ACCESSNAME, INDEXONLY, SORTN_UNIQ, SORTN_ORDERBY, SORTN_GROUPBY, SORTN_JOIN FROM PLAN_TABLE WHERE QUERYNO = 9401 ORDER BY PLANNO;
An N value in a sort indicator does not prove the statement is fast, and a Y value does not prove it is wrong. A required sort over 500 rows can be harmless; an index access that reads millions of poorly clustered rows can be expensive. EXPLAIN is a model of the chosen path, not a replacement for measurement.
Imagine dumping a box of mixed cards onto the floor. DISTINCT says, “Put matching cards together and keep only one of each.” ORDER BY says, “Arrange every card from smallest to largest.” Those jobs take time and space. If someone accidentally copied every card four times, removing copies makes the pile look right, but fixing the copying machine is better. Sometimes you truly need one card of each kind or a sorted deck. Then the work is correct. EXPLAIN tells you how Db2 plans to organize the cards.
1. When is SELECT DISTINCT semantically correct?
2. Why can an unnecessary DISTINCT be expensive in Db2 for z/OS?
3. Which rewrite often fits a query that only asks whether a child row exists?
4. Is GROUP BY automatically faster than DISTINCT?
5. What does ORDER BY guarantee that an index alone does not?
6. Which set operator should be considered when duplicate removal is not required?
Inspect sort indicators, selected indexes, matching columns, and estimated costs
Understand nested loop, merge scan, and hybrid join access-path decisions
Choose index key sequences for equality, range, joining, and ordering needs
Learn how Db2 uses work files for sorts and intermediate query results