Comparing dates in SQL is a fundamental skill for anyone working with relational databases, yet it remains a common source of bugs and performance issues. Whether you are filtering transaction logs, generating monthly reports, or archiving old records, understanding how the database engine interprets date literals, time components, and time zones is critical. This guide explores the syntax, functions, and best practices for comparing dates across major SQL dialects, ensuring your queries return accurate results and execute efficiently.
Understanding the Core Data Types
Before writing comparison logic, you must understand what you are comparing. SQL databases generally store temporal data in three distinct categories, and confusing them leads to logic errors But it adds up..
- DATE: Stores only the calendar date (Year, Month, Day). Example:
2023-10-25. This is ideal for birthdays, hire dates, or event days where time is irrelevant. - TIME: Stores only the time of day (Hour, Minute, Second, Fractional Seconds). No date context exists.
- DATETIME / TIMESTAMP: Stores both date and time components. This is the most common type for logging events (e.g.,
2023-10-25 14:30:00.123).
Critical Distinction: A DATETIME value of '2023-10-25 00:00:00' is not equal to a DATE value of '2023-10-25' in strict comparison contexts, though implicit casting often bridges this gap. Even so, a DATETIME of '2023-10-25 14:30:00' is greater than '2023-10-25' (which implicitly casts to midnight). Always verify the column data type using DESCRIBE table_name or \d table_name before writing filters.
The Standard Comparison Operators
SQL uses standard mathematical operators for temporal comparisons. These work identically across PostgreSQL, MySQL, SQL Server, Oracle, and SQLite, provided the formats are compatible.
| Operator | Meaning |
|---|---|
= |
Equal to |
!= or <> |
Not equal to |
> |
Greater than (Later than) |
>= |
Greater than or equal to |
< |
Less than (Earlier than) |
<= |
Less than or equal to |
Basic Syntax Example:
SELECT order_id, customer_id, order_date
FROM orders
WHERE order_date >= '2023-01-01';
This query retrieves all orders placed on or after January 1st, 2023. Note the use of the ISO 8601 format (YYYY-MM-DD). This is the only universally safe string literal format for dates in SQL. Avoid formats like MM/DD/YYYY or DD-MON-YY, as they depend on server locale settings and cause ambiguous errors.
Handling the "Midnight Trap" with DATETIME Columns
The most frequent mistake developers make involves DATETIME (or TIMESTAMP) columns when trying to filter for a specific day And that's really what it comes down to. And it works..
The Problem:
If you run WHERE created_at = '2023-10-25', the database interprets the string as '2023-10-25 00:00:00'. It will only return rows where the timestamp is exactly midnight. Records from 2023-10-25 10:00:00 are excluded because they are "greater than" the literal.
The Correct Approach: Range Filtering (SARGable)
To capture an entire day, use a half-open interval (closed start, open end). This method is SARGable (Search ARGument ABLE), meaning the database engine can apply an index on the created_at column.
-- Best Practice: Half-Open Interval
SELECT *
FROM audit_logs
WHERE created_at >= '2023-10-25'
AND created_at < '2023-10-26';
This captures every millisecond of October 25th without relying on functions that break index usage Took long enough..
The "Function on Column" Anti-Pattern (Avoid This): You will often see queries like this:
-- BAD: Prevents Index Usage
SELECT *
FROM audit_logs
WHERE DATE(created_at) = '2023-10-25'; -- MySQL/PostgreSQL
-- OR
WHERE CAST(created_at AS DATE) = '2023-10-25'; -- SQL Server/Standard
While logically correct, applying a function (DATE(), CAST, TRUNC, CONVERT) to the column side forces the database to scan the entire table, calculate the function for every row, and then filter. On large tables, this destroys performance. Always keep the column "naked" on one side of the operator.
Dialect-Specific Functions and Syntax
While the logic remains the same, syntax for date manipulation varies significantly between engines. Knowing the right function for your platform saves debugging time.
1. Getting the Current Date/Time
- Standard SQL / PostgreSQL / MySQL / SQLite:
CURRENT_DATE(date only),CURRENT_TIMESTAMP(date + time),NOW()(timestamp). - SQL Server:
GETDATE()(datetime),SYSDATETIME()(higher precision),GETUTCDATE()(UTC time). - Oracle:
SYSDATE(date+time),CURRENT_DATE(session time zone),SYSTIMESTAMP.
2. Date Arithmetic (Adding/Subtracting Intervals)
Calculating "30 days ago" or "next month" requires specific syntax.
PostgreSQL / Standard SQL (Interval Syntax):
-- Last 30 days (Relative to now)
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
-- First day of current month
WHERE created_at >= DATE_TRUNC('month', CURRENT_DATE)
MySQL:
-- Last 30 days
WHERE created_at >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
-- First day of current month
WHERE created_at >= DATE_FORMAT(CURRENT_DATE, '%Y-%m-01')
SQL Server (DATEADD Function):
-- Last 30 days
WHERE created_at >= DATEADD(DAY, -30, CAST(GETDATE() AS DATE))
-- First day of current month
WHERE created_at >= DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0)
-- Note: SQL Server 2022+ supports DATE_BUCKET for easier bucketing.
Oracle:
-- Last 30 days
WHERE created_at >= TRUNC(SYSDATE) - 30
-- First day of current month
WHERE created_at >= TRUNC(SYSDATE, 'MM')
3. Truncating/Rounding Timestamps to Dates
When you must group by day or compare only the date portion of a timestamp (e.g., in a GROUP BY clause), use the specific truncation function Worth knowing..
- PostgreSQL:
DATE_TRUNC('day', timestamp_col)ortimestamp_col::DATE - MySQL:
DATE(timestamp_col) - SQL Server:
CAST(timestamp_col AS DATE)(2008+) or `CONVERT(DATE