One concept, four spellings
Date and time handling is where SQL portability breaks down fastest. Every major engine has its own function names, argument order and timezone defaults for the same logical operations. This reference puts the common tasks — current time, adding intervals, extracting fields, truncating and formatting — side by side across MySQL, PostgreSQL, SQLite and SQL Server so you can translate at a glance.
Common date tasks — cross-dialect comparison
Get the current timestamp
NOW() -- MySQL, PostgreSQL
GETDATE() -- SQL Server (local time)
SYSUTCDATETIME() -- SQL Server (UTC)
datetime('now') -- SQLite (UTC)
datetime('now', 'localtime') -- SQLite (local)
Add an interval
DATE_ADD(d, INTERVAL 7 DAY) -- MySQL
d + INTERVAL '7 days' -- PostgreSQL
date(d, '+7 days') -- SQLite (returns text)
DATEADD(DAY, 7, d) -- SQL Server
Watch the argument order: SQL Server puts the unit first and the count second.
Truncate to the start of the month
DATE_FORMAT(d, '%Y-%m-01') -- MySQL (returns string; CAST if needed)
DATE_TRUNC('month', d) -- PostgreSQL (returns timestamp)
date(d, 'start of month') -- SQLite (returns text)
DATETRUNC(MONTH, d) -- SQL Server 2022+
DATEFROMPARTS(YEAR(d),MONTH(d),1) -- SQL Server (pre-2022)
Extract the year or day-of-week
YEAR(d) -- MySQL / SQL Server
EXTRACT(YEAR FROM d) -- PostgreSQL / MySQL / SQL Server 2022+
strftime('%Y', d) -- SQLite (always returns text)
DAYOFWEEK(d) -- MySQL: 1=Sunday … 7=Saturday
EXTRACT(DOW FROM d) -- PostgreSQL: 0=Sunday … 6=Saturday
strftime('%w', d) -- SQLite: 0=Sunday … 6=Saturday
DATEPART(dw, d) -- SQL Server: depends on SET DATEFIRST
Conventions that bite
Day-of-week numbering is the classic portability trap: Sunday is 1 in MySQL,
0 in PostgreSQL and SQLite, and configurable in SQL Server via SET DATEFIRST.
If you are comparing day-of-week across engines, normalise to 0–6 explicitly.
Return types differ. SQLite’s strftime always returns a text string, not a
date or number. Wrap in CAST(strftime('%Y', d) AS INTEGER) before doing
arithmetic on the result. PostgreSQL’s DATE_TRUNC returns a timestamp, which
is fine for comparisons but can truncate precision unexpectedly if you cast it
directly to date.
The year 2038 problem lives in SQL Server. DATEDIFF(SECOND, '1970-01-01', d)
overflows a 32-bit INT for dates beyond roughly January 2038. Use DATEDIFF_BIG
there to get a 64-bit result.
Timezone defaults vary by session. PostgreSQL NOW() respects the session time
zone set by SET TIME ZONE. MySQL NOW() uses the server’s time_zone system
variable. SQLite datetime('now') is always UTC regardless of the host OS.
When storing timestamps, prefer UTC everywhere and convert at display time.
Practical patterns for common date tasks
Calculate age in years (birthday check)
-- PostgreSQL
EXTRACT(YEAR FROM AGE(NOW(), birth_date))
-- MySQL
TIMESTAMPDIFF(YEAR, birth_date, CURDATE())
-- SQL Server
DATEDIFF(YEAR, birth_date, GETDATE())
- CASE WHEN DATEADD(YEAR, DATEDIFF(YEAR, birth_date, GETDATE()), birth_date) > GETDATE() THEN 1 ELSE 0 END
-- SQLite
CAST((julianday('now') - julianday(birth_date)) / 365.25 AS INTEGER)
Generate the first day of the current month
DATE_FORMAT(NOW(), '%Y-%m-01') -- MySQL
DATE_TRUNC('month', NOW()) -- PostgreSQL
date(date('now'), 'start of month') -- SQLite
DATETRUNC(MONTH, GETDATE()) -- SQL Server 2022+
Check if a date is within the last 30 days
WHERE created_at >= NOW() - INTERVAL 30 DAY -- MySQL
WHERE created_at >= NOW() - INTERVAL '30 days' -- PostgreSQL
WHERE created_at >= date('now', '-30 days') -- SQLite
WHERE created_at >= DATEADD(DAY, -30, GETDATE()) -- SQL Server
These patterns cover the most common date-range filters in analytics and reporting queries. Always index the date column being filtered — range scans on unindexed timestamp columns in large tables are a frequent performance problem.