MAKE_INTERVAL takes its parts by position: years, months, weeks, days, hours, minutes, seconds. DQL cannot write PostgreSQL’s named arguments, so make_interval(days => 3) becomes MAKE_INTERVAL(0, 0, 0, 3).
Date and time operators
PostgreSQL operator
Register for DQL as
Description
Implemented by
at time zone
AT_TIME_ZONE
Converts time data between different time zones (behavior depends on whether the input has a time zone offset)
PostgreSQL provides several range types for representing ranges of values. These functions create and work with range types. RANGE_MINUS_MULTI and MULTIRANGE_MINUS_MULTI return a set: one result row per remaining range, or no row when nothing remains.
The results come from these rows of Entity, with times in UTC:
id
start_date
end_date
start_tz
end_tz
created_at
active_period
1
2026-01-01
2026-01-10
2026-10-24 21:00
2026-10-26 22:00
2026-01-14 10:30
[2023-03-01,2023-06-01)
2
2026-01-20
2026-01-31
2026-08-01 08:00
2026-08-01 09:00
2026-01-28 16:00
[2024-01-01,2024-02-01)
3
2026-02-15
2026-02-20
2026-09-20 08:00
2026-09-20 08:30
2026-02-03 12:00
[2023-12-15,2024-01-15)
Each -- → line under a query is one row of its result. A query without its own ORDER BY has its rows listed in sample-row order, or by group.
-- Find gaps in date rangesSELECTe1.end_date,e2.start_date,DATERANGE(e1.end_date,e2.start_date)asgap_rangeFROMEntitye1,Entitye2WHEREe1.end_date<e2.start_dateANDNOTEXISTS(SELECT1FROMEntitye3WHEREOVERLAPS(DATERANGE(e1.end_date,e2.start_date),DATERANGE(e3.start_date,e3.end_date))=TRUE)-- → ['end_date' => DateTimeImmutable('2026-01-10'), 'start_date' => DateTimeImmutable('2026-01-20'), 'gap_range' => '[2026-01-10,2026-01-20)']-- ['end_date' => DateTimeImmutable('2026-01-31'), 'start_date' => DateTimeImmutable('2026-02-15'), 'gap_range' => '[2026-01-31,2026-02-15)']-- GENERATE_TIME_SERIES: the optional 4th argument is the time zone the steps are counted in,-- so a daily series stays on local midnight across a daylight-saving changeSELECTGENERATE_TIME_SERIES(e.start_tz,e.end_tz,'1 day','Europe/Sofia')asdayFROMEntityeWHEREe.id=1-- → ['day' => '2026-10-24 21:00:00+00']-- ['day' => '2026-10-25 22:00:00+00']-- ['day' => '2026-10-26 22:00:00+00']-- DATE_BIN: snap a timestamp to the nearest interval boundary relative to an originSELECTDATE_BIN('7 days',e.created_at,'2023-01-02')asweek_startFROMEntitye-- → ['week_start' => '2026-01-12 00:00:00+00']-- ['week_start' => '2026-01-26 00:00:00+00']-- ['week_start' => '2026-02-02 00:00:00+00']-- Range bounds: third argument controls inclusivity - default is '[)' (inclusive lower, exclusive upper)SELECTDATERANGE(e.start_date,e.end_date,'[]')asinclusive_rangeFROMEntitye-- → ['inclusive_range' => '[2026-01-01,2026-01-11)']-- ['inclusive_range' => '[2026-01-20,2026-02-01)']-- ['inclusive_range' => '[2026-02-15,2026-02-21)']-- Range operators must be compared with = TRUE / = FALSE in Doctrine DQLSELECTeFROMEntityeWHEREOVERLAPS(e.active_period,DATERANGE('2023-01-01','2023-12-31'))=TRUE-- → Entity {id: 1}-- Entity {id: 3}SELECTeFROMEntityeWHERECONTAINS(TSTZRANGE(DATE_SUBTRACT(CURRENT_TIMESTAMP(),'30 days'),CURRENT_TIMESTAMP()),e.start_tz)=TRUE-- → the rows whose start_tz falls in the 30 days before the query runs-- RANGE_MINUS_MULTI: subtracting a range that splits another in two returns one row per part,-- where the - operator would fail with "result of range difference would not be contiguous"SELECTRANGE_MINUS_MULTI(e.active_period,DATERANGE('2023-04-01','2023-05-01'))asremainingFROMEntityeWHEREe.id=1-- → ['remaining' => '[2023-03-01,2023-04-01)']-- ['remaining' => '[2023-05-01,2023-06-01)']-- Group by calendar month using DATE_TRUNC + DATE_ADDSELECTTSTZRANGE(DATE_TRUNC('month',e.created_at),DATE_ADD(DATE_TRUNC('month',e.created_at),'1 month'))asmonth_range,COUNT(e.id)asentity_countFROMEntityeGROUPBYmonth_rangeORDERBYmonth_range-- → ['month_range' => '["2026-01-01 00:00:00+00","2026-02-01 00:00:00+00")', 'entity_count' => 2]-- ['month_range' => '["2026-02-01 00:00:00+00","2026-03-01 00:00:00+00")', 'entity_count' => 1]
Empty ranges: a range containing no values, which PostgreSQL prints as empty (such as DATERANGE('2023-01-01', '2023-01-01')). In PHP, use DateRange::empty() and isEmpty() - see Empty ranges. A range with two NULL bounds is not empty: it is (,), unbounded on both sides
Infinite ranges: Use a parameter set to null for unbounded sides (DQL does not accept a bare NULL argument)
Example: DATERANGE('2023-01-01', :noEnd) with :noEnd set to null represents “from 2023-01-01 onwards”
Tips:
Compare the range operators with = TRUE or = FALSE in DQL (see Writing DQL).
A GiST index on a range column speeds up the overlap and containment operators.
DATE_PART and DATE_EXTRACT take any field PostgreSQL knows, such as year, month, dow (day of week) and doy (day of year).