Reference Guides/Beginner/Date Filtering

Date Filtering

How to filter rows by date ranges and relative time windows.

Almost every production table has a created_at or updated_at column, and almost every report narrows down to some time window. Date filtering is not complicated - but there are a few traps that catch people even after they feel comfortable with SQL basics.

Basic date comparison

Date and timestamp columns work with the standard comparison operators directly:

SELECT * FROM orders WHERE created_at >= '2024-01-01';
SELECT * FROM orders WHERE created_at < '2024-02-01';
SELECT * FROM users  WHERE hire_date = '2023-06-15';

PostgreSQL automatically casts string literals like '2024-01-01' to dates when you compare them against a date column. Stick to ISO format (YYYY-MM-DD) - other formats work inconsistently depending on locale settings.

Filtering a date range with BETWEEN

BETWEEN is inclusive on both ends, which sounds convenient. The problem is with timestamps. BETWEEN '2024-01-01' AND '2024-01-31' stops at 2024-01-31 00:00:00 - anything that happened later that day is silently excluded. That trips people up.

The pattern that actually covers a full day or full month:

SELECT * FROM orders
WHERE created_at >= '2024-01-01'
  AND created_at <  '2024-02-01';

Filtering relative to today

This is where date filtering gets genuinely useful. Use CURRENT_DATE or NOW() with interval arithmetic and your reports stay fresh without hardcoded dates:

-- Orders in the last 30 days
SELECT * FROM orders
WHERE created_at >= NOW() - INTERVAL '30 days';

-- Users who signed up this calendar year
SELECT * FROM users
WHERE EXTRACT(YEAR FROM created_at) = EXTRACT(YEAR FROM CURRENT_DATE);

-- Records from this month
SELECT * FROM events
WHERE DATE_TRUNC('month', created_at) = DATE_TRUNC('month', NOW());

Filtering by a date component

Sometimes you want all December orders regardless of year, or only weekday activity. EXTRACT pulls out a single component:

-- All orders placed in December, any year
SELECT * FROM orders WHERE EXTRACT(MONTH FROM created_at) = 12;

-- Weekday orders only (Monday = 1, Sunday = 0 in PostgreSQL)
SELECT * FROM orders WHERE EXTRACT(DOW FROM created_at) BETWEEN 1 AND 5;

Here is the hidden performance trap: wrapping a column in a function like EXTRACT(YEAR FROM created_at) = 2024 prevents the database from using an index on created_at. On a large table that matters a lot. The equivalent explicit range - created_at >= '2024-01-01' AND created_at < '2025-01-01' - is index-friendly and usually much faster.

NULL dates

Comparison operators silently exclude NULL dates - no error, no warning, those rows just disappear from your results. If NULL means "no end date yet" (a common pattern for active subscriptions), you have to check for it explicitly:

SELECT * FROM subscriptions
WHERE end_date IS NULL OR end_date > CURRENT_DATE;

Forgetting this is the kind of bug that looks fine in testing but silently undercounts in production.