Working with dates

Filter, group, shift and measure dates, and avoid the timestamp traps that make month-end numbers wrong.

Intermediate11 min readLesson 21 of 38
In this lesson 9 sections
  1. How dates look
  2. The timestamp trap
  3. Pulling out parts: year, month, weekday
  4. Grouping by month
  5. Moving a date forward or back
  6. Measuring time between dates
  7. Common mistakes
  8. Dialect notes
  9. Snowflake notes
Jump to practice

Date questions are everywhere in reporting: this month’s pipeline, deals per quarter, days to close. Every database handles dates a little differently, so this lesson teaches the ideas with the sandbox’s syntax, then maps each one to the other databases at the end.

How dates look

Column Type Example Holds
created_at date 2026-03-15 A day
close_date date 2026-05-01 A day
closed_at timestamp 2026-04-02 10:45:00 A day and a time

Always write dates as 'YYYY-MM-DD'. Every major database reads that format, and it sorts correctly even as text.

Today’s date is CURRENT_DATE on most databases, including the sandbox.

The timestamp trap

Exactly one deal closed on April 2, 2026. Watch what happens when you ask for it:

SELECT COUNT(*) FROM opportunities WHERE closed_at = '2026-04-02';          -- 0

closed_at holds 2026-04-02 10:45:00. A date on its own means the very start of that day, midnight, so only a deal closed at exactly 00:00:00 would match. Two fixes:

-- 1. A range for the whole day. Works everywhere, and is the fastest on big tables.
WHERE closed_at >= '2026-04-02'
  AND closed_at <  '2026-04-03'
 
-- 2. Keep just the date part, then compare.
WHERE date(closed_at) = '2026-04-02'

This is the date version of lesson 4’s half-open range, and it’s why a “deals closed on the last day of the quarter” report can come back empty.

Pulling out parts: year, month, weekday

In the sandbox, strftime(format, date) returns any part of a date as text:

SELECT strftime('%Y', '2026-03-15'),      -- '2026'
       strftime('%m', '2026-03-15'),      -- '03'
       strftime('%Y-%m', '2026-03-15');   -- '2026-03'
Code Gives Example
%Y Year 2026
%m Month, 01 to 12 03
%d Day of the month 15
%w Day of the week, 0 = Sunday 0
%Y-%m Year and month together 2026-03

Grouping by month

This is the most common date query there is. Turn each date into its month, then group:

SELECT strftime('%Y-%m', created_at) AS month,
       COUNT(*) AS deals
FROM opportunities
GROUP BY month
ORDER BY month;

Use year and month (%Y-%m), not just %m. Grouping by month alone puts March 2025 and March 2026 in the same group.

Moving a date forward or back

SELECT date('2026-03-15', '+30 days'),          -- 2026-04-14
       date('2026-03-15', '-1 year'),           -- 2025-03-15
       date('2026-03-15', 'start of month');    -- 2026-03-01

'start of month' snaps a date back to the first of its month, which is handy for “this month so far” filters.

Watch month ends: adding a month to January 31 has no clean answer, because February has no 31st. The sandbox overflows into March: date('2026-01-31', '+1 month') gives 2026-03-03. Most other databases give February 28. If month-end dates matter, snap to the start of the month first, then add.

Measuring time between dates

In the sandbox, julianday() turns a date into a day count, so subtracting two of them gives the days between:

SELECT opportunity_id,
       julianday(closed_at) - julianday(created_at)        AS days_with_time,
       julianday(date(closed_at)) - julianday(created_at)  AS whole_days
FROM opportunities
WHERE closed_at IS NOT NULL;

Deal 1001 was created on 2025-01-24 and closed at 2025-03-24 16:00:00. That’s 59.67 days with the time included, or 59 whole days. Decide which one the report needs. “Days to close” usually means whole days.

Common mistakes

Mistake What happens Fix
closed_at = '2026-04-02' on a timestamp Matches only midnight, so usually nothing A half-open range, or compare date(closed_at)
closed_at BETWEEN '2026-01-01' AND '2026-12-31' Misses everything on December 31 after midnight >= '2026-01-01' AND < '2027-01-01'
Grouping by month number alone Mixes the same month from different years Group by year and month
Dates in other formats, like '3/15/2026' Sorts and compares as text, so it’s wrong Store and write 'YYYY-MM-DD'
Ignoring time zones A deal closed at 7 p.m. in Chicago on March 31 is already April 1 in UTC Find out which zone the system writes before trusting month-end totals

Dialect notes

The ideas are the same everywhere. The function names aren’t:

Task Sandbox (SQLite) SQL Server PostgreSQL MySQL
Today CURRENT_DATE CAST(GETDATE() AS DATE) CURRENT_DATE CURRENT_DATE
Year strftime('%Y', d) YEAR(d) EXTRACT(YEAR FROM d) YEAR(d)
First of the month date(d, 'start of month') DATETRUNC(month, d) (2022+) date_trunc('month', d) DATE_FORMAT(d, '%Y-%m-01')
As 'YYYY-MM' text strftime('%Y-%m', d) FORMAT(d, 'yyyy-MM') to_char(d, 'YYYY-MM') DATE_FORMAT(d, '%Y-%m')
Add 30 days date(d, '+30 days') DATEADD(day, 30, d) d + INTERVAL '30 days' DATE_ADD(d, INTERVAL 30 DAY)
Days between julianday(b) - julianday(a) DATEDIFF(day, a, b) b::date - a::date DATEDIFF(b, a)
Just the date of a timestamp date(ts) CAST(ts AS DATE) ts::date DATE(ts)

Watch the argument order for days between: SQL Server puts the start first, MySQL puts the end first. Swap them and every result flips sign.

Snowflake notes

Task Snowflake
Today CURRENT_DATE
First of the month DATE_TRUNC('month', d)
Add 30 days DATEADD('day', 30, d)
Days between DATEDIFF('day', a, b)
A part EXTRACT(quarter FROM d), or YEAR(d), MONTH(d)

DATEDIFF counts boundaries crossed, not time elapsed: DATEDIFF('day', '2026-10-31 23:59', '2026-11-01 00:01') is 1, though only two minutes passed. For hours or response times, measure in minutes or seconds and divide.

Snowflake has three timestamp types. TIMESTAMP_NTZ stores no time zone, TIMESTAMP_LTZ shows each viewer their own zone, and TIMESTAMP_TZ keeps the original offset. CONVERT_TIMEZONE('UTC', 'America/Chicago', ts) converts for display.

More in the field guide: Date functions, Dates and timestamps and Elapsed time. Exercise T2 is the timestamp trap in a real report.

Try it

How many deals were created in each month of 2026, and what were they worth? Return the month as YYYY-MM, the number of deals and their total amount, in month order.

Stretch: what’s the average number of whole days to close for each lead source? That’s field guide exercise I2, written for Snowflake. Rewrite it with the sandbox’s date functions.

Write it in the sandbox below, then press Check answer.

SQL sandbox

Run queries against the sample CRM tables (accounts, opportunities,users, contacts and opportunity_history), right in your browser. Nothing you run leaves your computer.

Back to the top of the lesson