Date-range SQL looks simple until a report loses the last fraction of a second, two effective-dated rows overlap, or a function prevents an index from narrowing the scan. This beginner-friendly guide develops reliable Db2 for z/OS patterns for calendar days, timestamps, effective dates, overlap and gap detection, and temporal tables. The central idea is to state every boundary precisely: decide which endpoint is included, preserve the data type and precision, and compare the stored column directly with compatible values whenever possible.
A column named START_DATE does not explain its meaning. It might be when an order was entered, when a policy became legally effective, when a row version reached Db2, or the local date shown to a customer. Those meanings produce different answers. Before writing SQL, identify the time axis and the expected boundary behavior.
IBM recommends using Db2 DATE, TIME, and TIMESTAMP types rather than storing datetime values as unrelated numbers or strings. Native types validate values, support datetime arithmetic, and communicate intent to the optimizer. Use DATE only when a day is the complete fact. Use TIMESTAMP when ordering within a day matters. A timestamp can have fractional-second precision from 0 through 12; the default precision is 6.
The most reusable interval model is [start, end): include the start, exclude the end. A reservation from 09:00 to 10:00 contains 09:00 but not 10:00. Another reservation can begin exactly at 10:00 without colliding. A January price begins on January 1 and ends on February 1, rather than pretending to end at the last representable instant of January 31.
123456-- All events on 15 August 2026 SELECT EVENT_ID, EVENT_TS, EVENT_TYPE FROM APP.EVENT_LOG WHERE EVENT_TS >= TIMESTAMP('2026-08-15-00.00.00') AND EVENT_TS < TIMESTAMP('2026-08-16-00.00.00') ORDER BY EVENT_TS;
This query remains correct if EVENT_TS has microseconds, nanoseconds, or greater precision. In contrast, an upper boundary of 23:59:59 misses values such as 23:59:59.500000. Appending a string of 9s merely couples the query to a guessed column precision. The next-day exclusive boundary expresses the business requirement directly.
Ordinary SQL BETWEEN is inclusive at both ends. It is fine when both endpoints genuinely belong in the answer—for example, DATE values from the first through the last due date. It is risky for adjacent TIMESTAMP windows. If one query ends at midnight and the next begins at midnight, inclusive endpoints can count that boundary twice. Prefer explicit greater-than-or-equal and less-than predicates when representing a half-open window.
12345678910-- Inclusive DATE requirement: both dates are intended SELECT INVOICE_ID, DUE_DATE FROM AR.INVOICE WHERE DUE_DATE BETWEEN DATE('2026-08-01') AND DATE('2026-08-31'); -- Equivalent half-open DATE form, easier to compose with adjacent months SELECT INVOICE_ID, DUE_DATE FROM AR.INVOICE WHERE DUE_DATE >= DATE('2026-08-01') AND DUE_DATE < DATE('2026-09-01');
Application code should bind typed host variables or parameter markers for the lower and upper bounds. Compute “next day” or “next month” once, outside the column expression. That avoids locale-dependent string parsing and keeps SQL reusable. Db2 date arithmetic understands calendar rules, including month lengths and leap years, but month-end adjustment can occur when a target month lacks the original day. Test month arithmetic around January 31 and February rather than assuming every month behaves like 30 days.
123456789-- Host variables are typed as TIMESTAMP SELECT ORDER_ID, CREATED_TS FROM SALES.ORDERS WHERE CREATED_TS >= :MONTH_START AND CREATED_TS < :NEXT_MONTH_START; -- Db2 calendar arithmetic examples VALUES DATE('2024-02-28') + 1 DAY; VALUES ADD_DAYS(DATE('2024-02-28'), 1);
Suppose each booking stores START_TS and END_TS as a valid half-open range. Two ranges overlap exactly when each begins before the other ends. The compact formula handles all shapes: one interval inside another, partial overlap from either side, and identical periods.
1234567891011121314-- Generic half-open overlap test A.START_TS < B.END_TS AND B.START_TS < A.END_TS -- Find conflicting reservations for the same room SELECT A.ROOM_ID, A.RESERVATION_ID AS FIRST_ID, B.RESERVATION_ID AS SECOND_ID FROM HOTEL.RESERVATION A JOIN HOTEL.RESERVATION B ON B.ROOM_ID = A.ROOM_ID AND B.RESERVATION_ID > A.RESERVATION_ID AND A.START_TS < B.END_TS AND B.START_TS < A.END_TS;
The greater-ID condition reports each pair once and prevents a row from matching itself. Notice that [09:00, 10:00) and [10:00, 11:00) do not overlap: both strict less-than tests are not true. If your documented model includes both endpoints, touching at 10:00 might count as an overlap and the operators must change. Do not change operators casually; they encode the interval definition.
12345678CREATE TABLE HOTEL.RESERVATION ( RESERVATION_ID BIGINT NOT NULL, ROOM_ID INTEGER NOT NULL, START_TS TIMESTAMP(6) NOT NULL, END_TS TIMESTAMP(6) NOT NULL, PRIMARY KEY (RESERVATION_ID), CONSTRAINT VALID_RESERVATION_RANGE CHECK (START_TS < END_TS) );
A gap is a boundary problem between consecutive rows. For each business key, order rows by the beginning of the period and compare the current start with the previous end. LAG supplies that previous value without a self-join. If periods are already known not to overlap, CURRENT_START > PREVIOUS_END means a gap and equality means perfect adjacency.
123456789101112131415161718WITH ORDERED_PRICE AS ( SELECT PRODUCT_ID, VALID_FROM, VALID_TO, PRICE, LAG(VALID_TO) OVER ( PARTITION BY PRODUCT_ID ORDER BY VALID_FROM, VALID_TO ) AS PREVIOUS_TO FROM SALES.PRODUCT_PRICE ) SELECT PRODUCT_ID, PREVIOUS_TO AS GAP_START, VALID_FROM AS GAP_END FROM ORDERED_PRICE WHERE PREVIOUS_TO IS NOT NULL AND VALID_FROM > PREVIOUS_TO ORDER BY PRODUCT_ID, GAP_START;
A simple LAG of the immediately preceding end can misclassify data when overlapping rows are allowed and an earlier, longer row extends beyond that end. In that case compare the current start with the running maximum of all previous end values. The running maximum represents the furthest coverage reached so far.
123456789101112131415WITH COVERAGE AS ( SELECT PRODUCT_ID, VALID_FROM, VALID_TO, MAX(VALID_TO) OVER ( PARTITION BY PRODUCT_ID ORDER BY VALID_FROM, VALID_TO ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING ) AS COVERED_THROUGH FROM SALES.PRODUCT_PRICE ) SELECT PRODUCT_ID, COVERED_THROUGH AS GAP_START, VALID_FROM AS GAP_END FROM COVERAGE WHERE COVERED_THROUGH IS NOT NULL AND VALID_FROM > COVERED_THROUGH;
A sargable predicate gives Db2 a useful search argument for an access path. The clearest pattern puts the indexed datetime column by itself on one side and a constant, host variable, parameter marker, or compatible non-column expression on the other. Wrapping the column in DATE, CHAR, YEAR, or arithmetic can change predicate classification and prevent a matching index scan. Db2 can optimize some function forms and expression-based indexes exist for specialized designs, so verify with EXPLAIN; do not assume every function is always fatal. Still, the bare-column range is the reliable default.
12345678910-- Less desirable: function applied for every candidate row SELECT EVENT_ID FROM APP.EVENT_LOG WHERE DATE(EVENT_TS) = :EVENT_DATE; -- Preferred search range SELECT EVENT_ID FROM APP.EVENT_LOG WHERE EVENT_TS >= TIMESTAMP(:EVENT_DATE) AND EVENT_TS < TIMESTAMP(:EVENT_DATE + 1 DAY);
Index order should follow the workload. If every lookup supplies CUSTOMER_ID and a time range, an index beginning (CUSTOMER_ID, EVENT_TS) can support equality on the customer followed by range matching on time. An index beginning with EVENT_TS may be better for cross-customer chronological reports. There is no universal date index: collect statistics, run EXPLAIN, and compare the actual filtering pattern.
Comparing a TIMESTAMP column with a character value asks Db2 to perform conversion. Conversion rules, application encoding, and malformed values can introduce surprises. Bind a TIMESTAMP value for a TIMESTAMP column and a DATE value for a DATE column. Period specifications also restrict precision: an expression must not have greater timestamp precision than the columns that define the period. Use an explicit CAST when a deliberate precision reduction is required.
Hand-written start and end columns are appropriate for many tables. When effective-dated data is central to the design, an application-period temporal table gives those columns formal meaning. PERIOD BUSINESS_TIME declares the begin and end columns. The application supplies their values because they describe the business calendar. Db2 can also enforce non-overlap for a logical key through a unique constraint or unique index with BUSINESS_TIME WITHOUT OVERLAPS.
1234567891011121314CREATE TABLE SALES.PRODUCT_PRICE ( PRODUCT_ID INTEGER NOT NULL, PRICE DECIMAL(11,2) NOT NULL, VALID_FROM DATE NOT NULL, VALID_TO DATE NOT NULL, PERIOD BUSINESS_TIME (VALID_FROM, VALID_TO), PRIMARY KEY (PRODUCT_ID, BUSINESS_TIME WITHOUT OVERLAPS) ); INSERT INTO SALES.PRODUCT_PRICE VALUES (100, 19.95, DATE('2026-01-01'), DATE('2026-04-01')); INSERT INTO SALES.PRODUCT_PRICE VALUES (100, 21.50, DATE('2026-04-01'), DATE('2026-07-01'));
The two rows are adjacent, not overlapping, under the common inclusive-start, exclusive-end model. Without the WITHOUT OVERLAPS key, PERIOD BUSINESS_TIME ensures a valid period but does not by itself guarantee that one product has only one active price at an instant. Use database-enforced integrity when overlap would violate the business rule; a pre-insert SELECT alone has a concurrency window.
1234567891011-- Price in force at one business date SELECT PRODUCT_ID, PRICE FROM SALES.PRODUCT_PRICE FOR BUSINESS_TIME AS OF DATE('2026-05-15') WHERE PRODUCT_ID = 100; -- Versions that overlap a business window SELECT PRODUCT_ID, PRICE, VALID_FROM, VALID_TO FROM SALES.PRODUCT_PRICE FOR BUSINESS_TIME FROM DATE('2026-03-01') TO DATE('2026-06-01') WHERE PRODUCT_ID = 100;
IBM documents AS OF, FROM value1 TO value2, and BETWEEN value1 AND value2 period specifications. FROM/TO is especially natural for half-open windows. BETWEEN has endpoint behavior defined by the period specification and period type, so it should not be treated as interchangeable punctuation. Choose the form whose documented semantics match the question.
System-period temporal data answers a different question: “What version did Db2 store at this earlier system time?” A system-period table uses TIMESTAMP(12) generated row-begin and row-end columns, plus a generated transaction-start-ID column. After a compatible history table exists, ADD VERSIONING connects it to the base table. Updates and deletes then preserve prior versions in history.
12345678910111213141516CREATE TABLE HR.EMPLOYEE ( EMP_ID INTEGER NOT NULL PRIMARY KEY, DEPT_CODE CHAR(3) NOT NULL, SYS_START TIMESTAMP(12) NOT NULL GENERATED ALWAYS AS ROW BEGIN, SYS_END TIMESTAMP(12) NOT NULL GENERATED ALWAYS AS ROW END, TRANS_START TIMESTAMP(12) GENERATED ALWAYS AS TRANSACTION START ID, PERIOD SYSTEM_TIME (SYS_START, SYS_END) ); CREATE TABLE HR.EMPLOYEE_HISTORY LIKE HR.EMPLOYEE; ALTER TABLE HR.EMPLOYEE ADD VERSIONING USE HISTORY TABLE HR.EMPLOYEE_HISTORY;
Applications normally insert and update EMP_ID and DEPT_CODE, not the generated system columns. A temporal query against the base table lets Db2 search the appropriate current and history versions. AS OF selects versions whose system period contains one timestamp. FROM/TO selects versions that overlap a window.
1234567891011121314-- What did Db2 show at this system timestamp? SELECT EMP_ID, DEPT_CODE FROM HR.EMPLOYEE FOR SYSTEM_TIME AS OF TIMESTAMP('2026-03-31-23.59.59.000000000000') WHERE EMP_ID = 42; -- Which versions existed during this half-open system-time window? SELECT EMP_ID, DEPT_CODE, SYS_START, SYS_END FROM HR.EMPLOYEE FOR SYSTEM_TIME FROM TIMESTAMP('2026-03-01-00.00.00.000000000000') TO TIMESTAMP('2026-04-01-00.00.00.000000000000') WHERE EMP_ID = 42;
A bitemporal table has both BUSINESS_TIME and SYSTEM_TIME. It can answer “What price is valid for May 15 according to what the database knew on March 31?” The business date and system timestamp are intentionally different. This is useful when a fact is corrected after the fact: BUSINESS_TIME stores when the corrected fact should apply, while SYSTEM_TIME preserves when the old and corrected versions were recorded.
12345SELECT POLICY_ID, COVERAGE_AMOUNT FROM INSURANCE.POLICY FOR SYSTEM_TIME AS OF :SYSTEM_AS_OF FOR BUSINESS_TIME AS OF :BUSINESS_AS_OF WHERE POLICY_ID = :POLICY_ID;
Each period name can appear at most once for a table reference. Db2 temporal special registers can also apply AS OF behavior implicitly when the package's temporal sensitivity bind options allow it. That is convenient for a consistent month-end report, but surprising in pooled sessions if the register is not restored. Make temporal register ownership and reset behavior part of the application contract.
A TIMESTAMP WITHOUT TIME ZONE is a wall-clock value with no offset. It cannot tell whether 2026-11-01 01:30 occurred before or after a daylight-saving fallback. A TIMESTAMP WITH TIME ZONE carries an offset and represents an instant relative to UTC. Choose based on the fact being stored, not on display preference.
Do not add a fixed 24 hours to model every local next day. Resolve the next local midnight in the user's zone, then convert both boundaries to the stored timeline. Keep this conversion consistent across Java, COBOL, middleware, and SQL so two layers do not apply an offset twice.
Db2 supports timestamp fractional precision from 0 to 12. A host variable, literal, cast, period column, and comparison do not automatically share the same declared precision. Truncating a high-precision input can move it onto a boundary; rounding can move it across one. Define precision deliberately and test rows exactly at START, exactly at END, one representable unit before END, and one unit after START.
Open-ended business periods are often stored with a far-future end such as 9999-12-31. That convention works only when every writer and reader treats it consistently. It can complicate arithmetic that adds a day and can create a highly skewed index value. NULL is another option for a hand-built model, but every overlap predicate then needs explicit NULL handling. Db2 period definitions require concrete begin and end values, so use the documented maximum for that temporal feature and avoid arithmetic beyond it.
Imagine colored strips laid on a ruler. A blue strip starts at 1 and stops just before 4. A green strip can start exactly at 4, so there is no double color and no empty space. That is a half-open range. Two strips overlap when each starts before the other one ends. BUSINESS_TIME writes “this price sticker is good from spring until summer.” SYSTEM_TIME is a camera that remembers when the sticker was attached or replaced. Db2 can use both: one clock says when the sticker is supposed to be true, and the other says when the database knew about it.
For most stored intervals and search windows, use a half-open range: start is inclusive and end is exclusive. Query it with COLUMN >= :START_VALUE AND COLUMN < :END_VALUE. Adjacent ranges then meet at one boundary without overlapping, and timestamp queries do not need a guessed final fractional second.
For half-open ranges, use A.START_VALUE < B.END_VALUE AND B.START_VALUE < A.END_VALUE. Add the logical key comparison and a condition that prevents a row from joining to itself. The negation describes non-overlap: A.END_VALUE <= B.START_VALUE OR B.END_VALUE <= A.START_VALUE.
Calculate the day start and next-day start once, then use EVENT_TS >= :DAY_START AND EVENT_TS < :NEXT_DAY_START. Avoid wrapping EVENT_TS in DATE, CHAR, YEAR, or another function unless EXPLAIN confirms that the resulting access path is acceptable.
SYSTEM_TIME records when Db2 stored a row version and uses system-maintained timestamps plus a history table. BUSINESS_TIME records when a fact is valid in the real world, such as a price or policy term, and the application supplies its begin and end values. A table with both periods is bitemporal.
Ordinary SQL BETWEEN includes both endpoints. Temporal table period specifications have documented AS OF, FROM value1 TO value2, and BETWEEN value1 AND value2 semantics that also depend on whether the period is inclusive-exclusive or inclusive-inclusive. For ordinary timestamp filtering, explicit >= and < predicates are usually clearer.
Choose and document whether stored values are UTC instants, local civil times, or TIMESTAMP WITH TIME ZONE values. Convert input at a controlled boundary, preserve the original offset when it matters, and never assume a local day is always 24 hours during daylight-saving transitions.
1. Which predicate safely selects every event on 2026-08-15?
2. When do two half-open ranges [A_START, A_END) and [B_START, B_END) overlap?
3. What does FOR SYSTEM_TIME AS OF ask Db2 to return?
4. Why can DATE(EVENT_TS) = :DAY be less useful than a timestamp range?
5. Which feature prevents overlapping BUSINESS_TIME periods for the same logical key?
6. What is the main risk of ending a TIMESTAMP range at 23:59:59?
Track database row versions with SYSTEM_TIME and a Db2-managed history table
Model real-world validity with BUSINESS_TIME and non-overlapping keys
Combine business validity with the history of what Db2 stored
Choose native datetime types and understand their value ranges and precision