SQL Dates
Handling date and time values effectively is a core skill in SQL database design and querying. Because date formats, time zones, and manipulation functions differ across database engines, understanding storage types, literal formats, and query patterns ensures clean data integrity and accurate reporting.
1. Standard Date & Time Data Types
Most relational database engines provide dedicated data types to store calendar dates, times of day, or combined timestamps.
| Data Type | Standard Format | Description | Storage Size (Approx) |
|---|---|---|---|
DATE |
YYYY-MM-DD |
Stores date values only (year, month, day). | 3 bytes |
TIME |
HH:MI:SS[.nnnnnn] |
Stores time of day only (hours, minutes, seconds, optional fractional seconds). | 3 to 5 bytes |
DATETIME |
YYYY-MM-DD HH:MI:SS[.nnn] |
Stores both date and time values. | 8 bytes |
TIMESTAMP |
YYYY-MM-DD HH:MI:SS[.nnnnnn] |
Stores date, time, and microsecond precision. Frequently used for tracking row creation/modification. | 4 to 8 bytes |
TIMESTAMPTZ / DATETIMEOFFSET |
YYYY-MM-DD HH:MI:SS+TZ |
Stores timestamp with explicit Time Zone offset from UTC. | 8 to 10 bytes |
2. Writing Portable Date Literals
To prevent regional interpretation errors (for example, whether 01/02/2026 means January 2nd or February 1st), always use ISO 8601 string formats when writing queries or inserting data:
-- ISO 8601 Date Format (Universal)
'2026-09-27'
-- ISO 8601 Timestamp / DATETIME Format
'2026-09-27 14:30:00'
'2026-09-27T14:30:00Z' -- UTC indicator
3. Getting Current Date & Time Across Engines
Retrieving system timestamps varies depending on the database platform:
| Database Engine | Date & Time | Date Only | UTC Timestamp |
|---|---|---|---|
| PostgreSQL | NOW() or CURRENT_TIMESTAMP |
CURRENT_DATE |
CLOCK_TIMESTAMP() |
| SQL Server | GETDATE() or SYSDATETIME() |
CAST(GETDATE() AS DATE) |
GETUTCDATE() |
| MySQL | NOW() or CURRENT_TIMESTAMP() |
CURDATE() |
UTC_TIMESTAMP() |
| Oracle | SYSDATE or LOCALTIMESTAMP |
TRUNC(SYSDATE) |
SYS_EXTRACT_UTC(SYSTIMESTAMP) |
| MS Access | Now() |
Date() |
N/A |
4. Querying Date Ranges
When filtering rows by date, filtering full days on columns that contain time components requires careful boundary handling.
The Problem with BETWEEN on Timestamps
If an OrderDate column contains time components (e.g., '2026-09-27 15:45:00'), using BETWEEN with short date strings can omit records:
-- RISKY: Misses any orders placed after 00:00:00 on September 27th!
SELECT *
FROM Orders
WHERE OrderDate BETWEEN '2026-09-01' AND '2026-09-27';
Best Practice: Half-Open Interval (>= and <)
Using an inclusive start date (>=) and an exclusive end date (<) guarantees that all timestamps throughout the entire final day are captured cleanly:
-- RECOMMENDED: Captures every timestamp up to 23:59:59.999 on September 27th
SELECT *
FROM Orders
WHERE OrderDate >= '2026-09-01'
AND OrderDate < '2026-09-28';
Performance Tip: Avoid wrapping table columns in date functions inside the
WHEREclause (e.g.,WHERE YEAR(OrderDate) = 2026). Doing so makes the query non-sargable, preventing the database optimizer from using indexes onOrderDate.
5. Adding & Subtracting Intervals
Calculating relative dates (e.g., finding orders placed within the last 30 days) uses engine-specific interval syntax:
-- PostgreSQL
SELECT * FROM Orders WHERE OrderDate >= NOW() - INTERVAL '30 days';
-- SQL Server
SELECT * FROM Orders WHERE OrderDate >= DATEADD(day, -30, GETDATE());
-- MySQL
SELECT * FROM Orders WHERE OrderDate >= DATE_SUB(NOW(), INTERVAL 30 DAY);
-- Oracle
SELECT * FROM Orders WHERE OrderDate >= SYSDATE - 30;
-- MS Access
SELECT * FROM Orders WHERE OrderDate >= DateAdd("d", -30, Date());
Engine Feature Comparison Matrix
| Feature | PostgreSQL | SQL Server | MySQL | Oracle |
|---|---|---|---|---|
| Time Zone Support | Native (TIMESTAMPTZ) |
Native (DATETIMEOFFSET) |
Session-level conversion | Native (TIMESTAMP WITH TIME ZONE) |
| Date Truncation | DATE_TRUNC('month', col) |
DATETRUNC(month, col) |
DATE_FORMAT(col, '%Y-%m-01') |
TRUNC(col, 'MM') |
| Date Formatting | TO_CHAR(col, 'YYYY-MM-DD') |
FORMAT(col, 'yyyy-MM-dd') |
DATE_FORMAT(col, '%Y-%m-%d') |
TO_CHAR(col, 'YYYY-MM-DD') |