Docs /Querying /INTERVAL and RANGE
Querying

INTERVAL and RANGE types in BigQuery

INTERVAL stores a duration as months, days and time separately, so 36 hours stays 36 hours until you justify it. RANGE stores a start and an end-exclusive bound of dates or timestamps, with functions for overlaps, intersections and sessionizing.

Two BigQuery types describe spans of time rather than points. An INTERVAL is a duration, like “90 minutes” or “1 year 2 months”, that you can add to dates and timestamps. A RANGE is a span between two points, like a hotel stay from March 1 to March 5, with functions to test containment and overlap. Both save you from storing two columns and writing the comparisons yourself.

INTERVAL

Write an interval with a number and a unit, or as a string with a date-part range, or build one from parts:

four ways to write an interval
12345
SELECT
  INTERVAL 90 MINUTE AS ninety_minutes,
  INTERVAL '1-2' YEAR TO MONTH AS one_year_two_months,
  INTERVAL '3 4:05:06' DAY TO SECOND AS days_to_seconds,
  MAKE_INTERVAL(day => 10, hour => 2) AS made
Result in BigQuery
ninety_minutesone_year_two_monthsdays_to_secondsmade
10-0 0 1:30:01-2 0 0:0:00-0 3 4:5:60-0 10 2:0:0
1 row

Results show intervals in a fixed format, Y-M D H:M:S: years-months, then days, then time. 0-0 0 1:30:0 is 90 minutes.

Months, days and time are kept separately

An interval stores three parts that are not converted into each other: months, days and microseconds. 36 hours stays 36 hours, not 1 day and 12 hours, because a day isn’t always 24 hours (daylight saving) and a month isn’t a fixed number of days. The JUSTIFY_ functions convert when you want them to:

JUSTIFY_HOURS and JUSTIFY_INTERVAL
12345
SELECT
  INTERVAL 36 HOUR AS raw,
  JUSTIFY_HOURS(INTERVAL 36 HOUR) AS justified_hours,
  JUSTIFY_INTERVAL(INTERVAL 400 DAY) AS justified_days,
  EXTRACT(HOUR FROM INTERVAL 36 HOUR) AS hour_part
Result in BigQuery
rawjustified_hoursjustified_dayshour_part
10-0 0 36:0:00-0 1 12:0:01-1 10 0:0:036
1 row

JUSTIFY_HOURS turns each 24 hours into a day, JUSTIFY_DAYS each 30 days into a month, and JUSTIFY_INTERVAL does both. EXTRACT(HOUR FROM INTERVAL 36 HOUR) is 36, the hour part as stored.

Differences between timestamps

Subtracting two timestamps gives an interval of hours and minutes, not days. TIMESTAMP_DIFF gives a plain number in the unit you ask for:

26 hours 30 minutes, or 1,590 minutes
123
SELECT
  TIMESTAMP '2026-03-02 12:30:00 UTC' - TIMESTAMP '2026-03-01 10:00:00 UTC' AS as_interval,
  TIMESTAMP_DIFF(TIMESTAMP '2026-03-02 12:30:00 UTC', TIMESTAMP '2026-03-01 10:00:00 UTC', MINUTE) AS minutes
Result in BigQuery
as_intervalminutes
10-0 0 26:30:01590
1 row

For a number you can sort, sum or compare, TIMESTAMP_DIFF is usually what you want. An interval is better for adding the difference to another time.

Adding intervals

Adding an interval to a timestamp works as expected. Adding one to a DATE returns a DATETIME, not a DATE, and month arithmetic clamps to the month end:

DATE + INTERVAL 1 MONTH
123
SELECT
  DATE '2026-01-31' + INTERVAL 1 MONTH AS date_plus_month,
  TIMESTAMP '2026-03-01 00:00:00 UTC' + INTERVAL 36 HOUR AS ts_plus_hours
Result in BigQuery
date_plus_monthts_plus_hours
12026-02-28 00:00:002026-03-02 12:00:00 UTC
1 row

Use DATE_ADD(d, INTERVAL 1 MONTH) to stay a DATE. See Dates and times.

RANGE

A RANGE<DATE>, RANGE<DATETIME> or RANGE<TIMESTAMP> holds a start and an end. A NULL bound means unbounded on that side:

bookings as date ranges
1234567891011
WITH bookings AS (
  SELECT 'room-1' AS room, RANGE(DATE '2026-03-01', DATE '2026-03-05') AS stay UNION ALL
  SELECT 'room-1', RANGE(DATE '2026-03-04', DATE '2026-03-07') UNION ALL
  SELECT 'room-2', RANGE(DATE '2026-03-10', NULL)
)
SELECT
  room,
  RANGE_START(stay) AS check_in,
  RANGE_END(stay) AS check_out,
  RANGE_CONTAINS(stay, DATE '2026-03-04') AS here_on_mar_4
FROM bookings
Result in BigQuery
roomcheck_incheck_outhere_on_mar_4
1room-12026-03-012026-03-05true
2room-12026-03-042026-03-07true
3room-22026-03-10nullfalse
3 rows

The end is exclusive

RANGE(DATE '2026-03-01', DATE '2026-03-05') contains March 1 through March 4, not March 5:

is March 5 in the stay?
123
SELECT
  RANGE_CONTAINS(RANGE(DATE '2026-03-01', DATE '2026-03-05'), DATE '2026-03-05') AS end_included,
  RANGE_CONTAINS(RANGE(DATE '2026-03-01', DATE '2026-03-05'), DATE '2026-03-01') AS start_included
Result in BigQuery
end_includedstart_included
1falsetrue
1 row

That suits check-out dates and time slots: back-to-back ranges like [10:00, 11:00) and [11:00, 12:00) don’t overlap.

Overlaps and intersections

RANGE_OVERLAPS tests whether two ranges share any point, and RANGE_INTERSECT returns the shared part:

two bookings for the same room
123456
WITH a AS (SELECT RANGE(DATE '2026-03-01', DATE '2026-03-05') AS r1, RANGE(DATE '2026-03-04', DATE '2026-03-07') AS r2)
SELECT
  RANGE_OVERLAPS(r1, r2) AS overlaps,
  RANGE_START(RANGE_INTERSECT(r1, r2)) AS overlap_start,
  RANGE_END(RANGE_INTERSECT(r1, r2)) AS overlap_end
FROM a
Result in BigQuery
overlapsoverlap_startoverlap_end
1true2026-03-042026-03-05
1 row

This is the double-booking check, without the a.start < b.end AND b.start < a.end condition you would otherwise write by hand.

Merging overlapping ranges into sessions

RANGE_SESSIONIZE is a table function that groups overlapping (or adjacent) ranges into sessions, per partition key:

three visits, two sessions
12345678
WITH visits AS (
  SELECT 'u1' AS user_id, RANGE(TIMESTAMP '2026-03-01 10:00:00', TIMESTAMP '2026-03-01 10:10:00') AS visit UNION ALL
  SELECT 'u1', RANGE(TIMESTAMP '2026-03-01 10:05:00', TIMESTAMP '2026-03-01 10:20:00') UNION ALL
  SELECT 'u1', RANGE(TIMESTAMP '2026-03-01 11:00:00', TIMESTAMP '2026-03-01 11:05:00')
)
SELECT user_id, RANGE_START(visit) AS visit_start, RANGE_START(session_range) AS session_start, RANGE_END(session_range) AS session_end
FROM RANGE_SESSIONIZE(TABLE visits, 'visit', ['user_id'])
ORDER BY visit_start
Result in BigQuery
user_idvisit_startsession_startsession_end
1u12026-03-01 10:00:00 UTC2026-03-01 10:00:00 UTC2026-03-01 10:20:00 UTC
2u12026-03-01 10:05:00 UTC2026-03-01 10:00:00 UTC2026-03-01 10:20:00 UTC
3u12026-03-01 11:00:00 UTC2026-03-01 11:00:00 UTC2026-03-01 11:05:00 UTC
3 rows

The first two visits overlap, so they share one session from 10:00 to 10:20; the third is its own session. GAP_FILL fills missing points in a time series.

Comparing, sorting and storing

RANGE values can be compared with = and <: BigQuery compares the start bounds first, then the end bounds, so ranges sort by start and can be join keys. A RANGE column can also be a clustering column, which helps queries that filter on it. INTERVAL can’t be used for clustering or partitioning.

Both types take 16 bytes per value. See Data types and Clustering.