SQL Date and Time Functions
Current date and time, adding and subtracting intervals, differences between dates, extracting parts, and formatting - the least portable corner of SQL.
- SQL Basics
- DDL
- DML
- SELECT
- WHERE
- Functions
- NULL and Logic
- Aggregate Functions
- GROUP BY
- JOIN
- Subqueries
- Set Operations
- CTEs
- Constraints
- Keys
- Relationships
- Database Design
- Normalisation
- Views
- Window Functions
- Advanced SQL
- Procedures and Functions
- Triggers
- Temporary Tables
- Transactions
- Isolation and Locking
- Indexes
- Query Performance
- SQL Security
- SQL Dialects
- Practical SQL
Concept
Date handling is where SQL dialects diverge most. The concepts are identical everywhere - now, add an interval, difference, extract a part, format - but almost every function name differs.
Syntax
| Need | MySQL / MariaDB | PostgreSQL | SQL Server | Oracle |
|---|---|---|---|---|
| Now | NOW(), CURDATE() | NOW(), CURRENT_DATE | GETDATE() | SYSDATE |
| Add interval | DATE_ADD(d, INTERVAL 7 DAY) | d + INTERVAL '7 days' | DATEADD(DAY, 7, d) | d + 7 |
| Difference | DATEDIFF(a, b) in days | a - b | DATEDIFF(DAY, b, a) | a - b |
| Extract part | YEAR(d), EXTRACT(YEAR FROM d) | EXTRACT(YEAR FROM d) | DATEPART(YEAR, d) | EXTRACT(YEAR FROM d) |
| Format | DATE_FORMAT(d, '%d %b %Y') | TO_CHAR(d, 'DD Mon YYYY') | FORMAT(d, 'dd MMM yyyy') | TO_CHAR(d, 'DD Mon YYYY') |
EXTRACT(part FROM date) is the one form that is standard and works nearly everywhere. Prefer it.
Example
-- MySQL / MariaDB
SELECT first_name,
hire_date,
EXTRACT(YEAR FROM hire_date) AS hire_year,
DATEDIFF(CURDATE(), hire_date) AS days_served,
TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) AS years_served,
DATE_ADD(hire_date, INTERVAL 1 YEAR) AS first_review_due,
DATE_FORMAT(hire_date, '%d %b %Y') AS pretty_date
FROM employees
ORDER BY hire_date;-- Orders in the last 90 days, written so the index still works
SELECT id, order_date, total
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)
ORDER BY order_date DESC;
-- Monthly totals, grouped by the first day of each month
SELECT DATE_FORMAT(order_date, '%Y-%m-01') AS month_start,
COUNT(*) AS orders,
SUM(total) AS revenue
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m-01')
ORDER BY month_start;Explanation
Note the difference between the two date filters. The 90 day filter applies the function to CURDATE() - a constant - and leaves order_date bare, so an index on order_date can be used. Writing WHERE DATEDIFF(CURDATE(), order_date) <= 90 would compute a value for every row in the table instead.
TIMESTAMPDIFF(YEAR, ...) counts whole years, which is what "years of service" and "age" actually mean. Subtracting the year parts is wrong for anyone whose anniversary has not arrived yet this year.
Important rules
- Store dates as date types. Sorting and interval arithmetic on text is broken.
- A
DATEhas no time; aDATETIME/TIMESTAMPdoes. Comparing them mixes midnight into your logic. - In MySQL,
TIMESTAMPconverts to and from the session time zone;DATETIMEdoes not. That single sentence explains most "the report is off by one day" bugs. - Week numbering, first day of week and quarter boundaries differ by product and by configuration.
- Keep the column bare in the
WHEREclause and apply functions to the constant instead.
Common mistakes
WHERE YEAR(order_date) = 2024- correct answer, full table scan.BETWEEN '2024-01-01' AND '2024-01-31'on aDATETIME, losing everything after midnight on the 31st.- Mixing UTC storage with local time comparisons and drifting by hours.
- Computing age as
YEAR(now) - YEAR(dob).
Best practices
- Store timestamps in UTC; convert at the presentation edge.
- Use
EXTRACTwhere a standard form exists; isolate the rest of the date logic in views or a small set of well named helpers. - Filter with half open ranges built from constants.
- For "per month" reporting, group by a truncated date rather than by a formatted string where the engine offers it (
DATE_TRUNCin PostgreSQL,DATETRUNCin SQL Server 2022+).
Practice
- List employees who completed more than three years of service.
- Return orders placed in the current calendar year, written so an index on
order_datecan be used. - Produce a month by month order count for 2024, including the month name.