SQL - DATEADD() / DATE_ADD() - Add Interval to Date
Date arithmetic is fundamental to almost every production database. You'll calculate subscription renewals, find overdue invoices, generate reporting periods, and implement data retention policies....
Key Insights
- Date arithmetic syntax varies significantly across databases: SQL Server uses
DATEADD(), MySQL usesDATE_ADD()or+ INTERVAL, and PostgreSQL uses+ INTERVALexclusively—knowing these differences prevents frustrating debugging sessions. - Month-end edge cases will bite you: adding one month to January 31st produces different results across databases, so always test boundary conditions in your date calculations.
- Negative intervals work universally for subtracting dates, making functions like
DATEADD(day, -90, GETDATE())the cleanest way to query historical data ranges.
Introduction & Syntax Overview
Date arithmetic is fundamental to almost every production database. You’ll calculate subscription renewals, find overdue invoices, generate reporting periods, and implement data retention policies. The challenge? Every major database handles this differently.
Here’s the syntax comparison you need to memorize:
-- SQL Server: DATEADD(interval, number, date)
SELECT DATEADD(day, 7, '2024-01-15');
-- Result: 2024-01-22
-- MySQL: DATE_ADD(date, INTERVAL number unit)
SELECT DATE_ADD('2024-01-15', INTERVAL 7 DAY);
-- Result: 2024-01-22
-- MySQL alternative syntax
SELECT '2024-01-15' + INTERVAL 7 DAY;
-- Result: 2024-01-22
-- PostgreSQL: date + INTERVAL 'number unit'
SELECT '2024-01-15'::date + INTERVAL '7 days';
-- Result: 2024-01-22 00:00:00
-- PostgreSQL shorthand
SELECT '2024-01-15'::date + 7; -- Adds days by default
-- Result: 2024-01-22
Notice that SQL Server puts the interval unit first, MySQL puts the date first, and PostgreSQL uses a string-based interval syntax. These differences matter when you’re writing queries that need to work across environments or when you’re switching between projects.
Interval Units Reference
Each database supports a core set of interval units, plus some database-specific options:
-- SQL Server interval units
SELECT
DATEADD(year, 1, '2024-01-15') AS plus_year, -- 2025-01-15
DATEADD(quarter, 1, '2024-01-15') AS plus_quarter, -- 2024-04-15
DATEADD(month, 1, '2024-01-15') AS plus_month, -- 2024-02-15
DATEADD(week, 1, '2024-01-15') AS plus_week, -- 2024-01-22
DATEADD(day, 1, '2024-01-15') AS plus_day, -- 2024-01-16
DATEADD(hour, 1, '2024-01-15') AS plus_hour, -- 2024-01-15 01:00
DATEADD(minute, 30, '2024-01-15') AS plus_minute, -- 2024-01-15 00:30
DATEADD(second, 45, '2024-01-15') AS plus_second; -- 2024-01-15 00:00:45
-- MySQL interval units
SELECT
DATE_ADD('2024-01-15', INTERVAL 1 YEAR) AS plus_year,
DATE_ADD('2024-01-15', INTERVAL 1 QUARTER) AS plus_quarter,
DATE_ADD('2024-01-15', INTERVAL 1 MONTH) AS plus_month,
DATE_ADD('2024-01-15', INTERVAL 1 WEEK) AS plus_week,
DATE_ADD('2024-01-15', INTERVAL 1 DAY) AS plus_day,
DATE_ADD('2024-01-15 10:00:00', INTERVAL 1 HOUR) AS plus_hour,
DATE_ADD('2024-01-15 10:00:00', INTERVAL 30 MINUTE) AS plus_minute,
DATE_ADD('2024-01-15 10:00:00', INTERVAL 45 SECOND) AS plus_second;
-- PostgreSQL interval units
SELECT
'2024-01-15'::date + INTERVAL '1 year' AS plus_year,
'2024-01-15'::date + INTERVAL '3 months' AS plus_quarter, -- No QUARTER keyword
'2024-01-15'::date + INTERVAL '1 month' AS plus_month,
'2024-01-15'::date + INTERVAL '1 week' AS plus_week,
'2024-01-15'::date + INTERVAL '1 day' AS plus_day,
'2024-01-15 10:00:00'::timestamp + INTERVAL '1 hour' AS plus_hour,
'2024-01-15 10:00:00'::timestamp + INTERVAL '30 minutes' AS plus_minute,
'2024-01-15 10:00:00'::timestamp + INTERVAL '45 seconds' AS plus_second;
SQL Server also supports dayofyear, weekday, millisecond, microsecond, and nanosecond. MySQL adds compound intervals like YEAR_MONTH and DAY_HOUR. PostgreSQL allows arbitrary combinations within a single interval string.
Basic Usage Examples
Real applications need both future and past date calculations. Use positive numbers for future dates and negative numbers for past dates:
-- SQL Server: Calculate business dates
SELECT
order_id,
order_date,
DATEADD(day, 3, order_date) AS estimated_ship_date,
DATEADD(day, 7, order_date) AS estimated_delivery,
DATEADD(day, 30, order_date) AS payment_due_date
FROM orders
WHERE order_date >= DATEADD(day, -7, GETDATE()); -- Orders from last 7 days
-- MySQL: Subscription management
SELECT
user_id,
subscription_start,
DATE_ADD(subscription_start, INTERVAL 1 MONTH) AS next_billing_date,
DATE_ADD(subscription_start, INTERVAL 1 YEAR) AS annual_renewal_date
FROM subscriptions
WHERE DATE_ADD(subscription_start, INTERVAL 1 MONTH) <= CURDATE(); -- Due for billing
-- PostgreSQL: Content expiration
SELECT
article_id,
published_at,
published_at + INTERVAL '90 days' AS archive_date,
published_at + INTERVAL '1 year' AS deletion_date
FROM articles
WHERE published_at + INTERVAL '90 days' < CURRENT_DATE; -- Ready to archive
Subtracting dates is equally straightforward:
-- SQL Server: Find records from the past
SELECT * FROM audit_logs
WHERE created_at >= DATEADD(month, -3, GETDATE()); -- Last 3 months
-- MySQL: Historical comparison
SELECT * FROM sales
WHERE sale_date BETWEEN
DATE_ADD(CURDATE(), INTERVAL -30 DAY)
AND CURDATE();
-- PostgreSQL: Retention policy
DELETE FROM session_logs
WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '7 days';
Working with Time Components
When scheduling matters at the hour or minute level, time-based intervals become essential:
-- SQL Server: Appointment scheduling
SELECT
appointment_id,
start_time,
DATEADD(minute, 30, start_time) AS end_time,
DATEADD(minute, -15, start_time) AS reminder_time
FROM appointments
WHERE start_time BETWEEN GETDATE() AND DATEADD(hour, 24, GETDATE());
-- MySQL: Session timeout management
SELECT
session_id,
last_activity,
DATE_ADD(last_activity, INTERVAL 30 MINUTE) AS expires_at
FROM user_sessions
WHERE DATE_ADD(last_activity, INTERVAL 30 MINUTE) < NOW(); -- Expired sessions
-- PostgreSQL: Time slot generation
SELECT
generate_series(
'2024-01-15 09:00:00'::timestamp,
'2024-01-15 17:00:00'::timestamp,
INTERVAL '30 minutes'
) AS slot_start;
Combining multiple intervals handles complex scheduling requirements:
-- MySQL: Compound intervals
SELECT DATE_ADD('2024-01-15 10:00:00', INTERVAL '1:30' HOUR_MINUTE);
-- Result: 2024-01-15 11:30:00
-- PostgreSQL: Multiple intervals in one expression
SELECT '2024-01-15'::date + INTERVAL '1 month 2 weeks 3 days';
-- Result: 2024-03-01 00:00:00
Common Real-World Applications
These patterns appear constantly in production systems:
-- Rolling 30-day window for analytics (SQL Server)
SELECT
COUNT(*) AS signups_last_30_days,
COUNT(CASE WHEN created_at >= DATEADD(day, -7, GETDATE()) THEN 1 END) AS signups_last_7_days
FROM users
WHERE created_at >= DATEADD(day, -30, GETDATE());
-- Data retention cleanup (MySQL)
DELETE FROM event_logs
WHERE created_at < DATE_ADD(CURDATE(), INTERVAL -90 DAY);
-- Next billing date calculation with trial period (PostgreSQL)
SELECT
user_id,
created_at,
CASE
WHEN trial_ends_at > CURRENT_DATE
THEN trial_ends_at
ELSE created_at + INTERVAL '1 month' * billing_cycles_completed
END AS next_billing_date
FROM subscriptions;
-- Generate monthly reporting periods (PostgreSQL)
SELECT
date_trunc('month', d) AS period_start,
date_trunc('month', d) + INTERVAL '1 month' - INTERVAL '1 day' AS period_end
FROM generate_series(
'2024-01-01'::date,
'2024-12-01'::date,
INTERVAL '1 month'
) AS d;
Edge Cases & Pitfalls
Month-end handling is where databases diverge and bugs hide:
-- What happens when you add 1 month to January 31?
-- SQL Server
SELECT DATEADD(month, 1, '2024-01-31'); -- 2024-02-29 (leap year)
SELECT DATEADD(month, 1, '2023-01-31'); -- 2023-02-28 (non-leap year)
-- MySQL
SELECT DATE_ADD('2024-01-31', INTERVAL 1 MONTH); -- 2024-02-29
SELECT DATE_ADD('2024-01-30', INTERVAL 1 MONTH); -- 2024-02-29 (clamped!)
-- PostgreSQL
SELECT '2024-01-31'::date + INTERVAL '1 month'; -- 2024-02-29
All three databases clamp to the last valid day of the target month. But watch what happens when you chain operations:
-- SQL Server: Adding then subtracting doesn't always round-trip
SELECT DATEADD(month, -1, DATEADD(month, 1, '2024-01-31'));
-- Result: 2024-01-29 (NOT 2024-01-31!)
Leap year considerations affect day-of-year calculations:
-- SQL Server: Day 60 is different in leap vs non-leap years
SELECT DATEADD(day, 59, '2024-01-01'); -- 2024-02-29 (leap year)
SELECT DATEADD(day, 59, '2023-01-01'); -- 2023-03-01 (non-leap year)
For timezone-aware applications, always use timestamp with time zone types and be explicit about conversions:
-- PostgreSQL: Timezone-aware arithmetic
SELECT
'2024-03-10 01:30:00 America/New_York'::timestamptz + INTERVAL '1 hour';
-- Correctly handles DST transition
Cross-Database Compatibility Tips
When you need portable date arithmetic, abstract it through views, functions, or your ORM:
-- Create a compatibility view (PostgreSQL example that mimics SQL Server)
CREATE OR REPLACE FUNCTION dateadd(unit text, num integer, dt timestamp)
RETURNS timestamp AS $$
BEGIN
RETURN CASE unit
WHEN 'day' THEN dt + (num || ' days')::interval
WHEN 'month' THEN dt + (num || ' months')::interval
WHEN 'year' THEN dt + (num || ' years')::interval
WHEN 'hour' THEN dt + (num || ' hours')::interval
WHEN 'minute' THEN dt + (num || ' minutes')::interval
ELSE dt
END;
END;
$$ LANGUAGE plpgsql;
-- Now works like SQL Server
SELECT dateadd('day', 7, CURRENT_TIMESTAMP);
For application code, most ORMs provide database-agnostic date functions. In Django:
from django.db.models import F
from django.db.models.functions import Now
from datetime import timedelta
# Works across all supported databases
Orders.objects.filter(
created_at__gte=Now() - timedelta(days=30)
)
The cleanest approach: handle date arithmetic in your application layer when possible, and use database-specific functions only for set-based operations that must run in SQL for performance reasons.