Home / Intermediate SQL / T-SQL / Mastering DATE and TIME Functions in SQL Server: A Complete Guide

Mastering DATE and TIME Functions in SQL Server: A Complete Guide

On

Working with dates and times in SQL Server can feel overwhelming at first. I remember spending hours debugging a report that showed incorrect sales figures—only to discover I was using GETDATE() instead of considering the user's timezone. After years of building ETL processes, financial reports, and analytics dashboards, I've learned that mastering SQL Server date and time functions is absolutely essential for any developer.

In this comprehensive guide, we'll explore the most important date and time functions in SQL Server, learn when to use each one, and work through practical examples using the WideWorldImporters database. By the end, you'll confidently handle date calculations, formatting, and timezone conversions in your T-SQL code.

Understanding SQL Server Date and Time Data Types

Before diving into functions, let's quickly review the date and time data types you'll encounter:

  • DATE: Stores only the date (2016-05-31)
  • TIME: Stores only the time (14:30:00.0000000)
  • DATETIME: Stores both date and time with 3.33ms precision
  • DATETIME2: Enhanced version with 100ns precision (recommended for new development)
  • DATETIMEOFFSET: Includes timezone offset information
  • SMALLDATETIME: Less precise, uses less storage (1-minute accuracy)

In WideWorldImporters, you'll see columns like OrderDate (DATE), LastEditedWhen (DATETIME2), and ValidFrom (DATETIME2). The choice of data type affects which functions work best and how precise your calculations will be.

Essential Date and Time Functions Every Developer Should Know

Getting the Current Date and Time

SQL Server offers several functions to retrieve the current date and time, and choosing the right one matters:

-- Different ways to get current date/time
SELECT 
    GETDATE() AS GetDate_Result,           -- DATETIME
    SYSDATETIME() AS SysDateTime_Result,   -- DATETIME2 (more precise)
    GETUTCDATE() AS GetUTCDate_Result,     -- DATETIME in UTC
    SYSUTCDATETIME() AS SysUTCDateTime,    -- DATETIME2 in UTC
    CURRENT_TIMESTAMP AS CurrentTimestamp; -- Same as GETDATE()

When to use which:

  • GETDATE(): Most common, returns server's local time as DATETIME
  • SYSDATETIME(): Use when you need higher precision (DATETIME2)
  • GETUTCDATE()/SYSUTCDATETIME(): Essential for applications with users in multiple timezones

Here's a practical example from WideWorldImporters:

-- Find orders placed today
SELECT 
    OrderID,
    CustomerID,
    OrderDate,
    ExpectedDeliveryDate
FROM Sales.Orders
WHERE OrderDate = CAST(GETDATE() AS DATE);

Notice how we convert GETDATE() to DATE type—this removes the time portion and allows accurate comparison.

DATEPART and DATENAME: Extracting Date Components

These functions let you extract specific parts of a date, which is invaluable for reporting and grouping:

-- Analyzing order patterns by day of week
SELECT 
    DATENAME(WEEKDAY, OrderDate) AS DayName,
    DATEPART(WEEKDAY, OrderDate) AS DayNumber,
    COUNT(*) AS OrderCount
FROM Sales.Orders
WHERE OrderDate >= '2016-01-01' 
    AND OrderDate < '2016-02-01'
GROUP BY 
    DATENAME(WEEKDAY, OrderDate),
    DATEPART(WEEKDAY, OrderDate)
ORDER BY DayNumber;

Common DATEPART arguments:

  • YEAR, MONTH, DAY
  • QUARTER, WEEK, DAYOFYEAR
  • HOUR, MINUTE, SECOND
  • WEEKDAY (1 = Sunday by default)

Here's a real-world reporting scenario:

-- Monthly sales summary for 2016
SELECT 
    YEAR(OrderDate) AS OrderYear,
    MONTH(OrderDate) AS OrderMonth,
    DATENAME(MONTH, OrderDate) AS MonthName,
    COUNT(DISTINCT OrderID) AS TotalOrders,
    COUNT(DISTINCT CustomerID) AS UniqueCustomers
FROM Sales.Orders
WHERE OrderDate >= '2016-01-01' 
    AND OrderDate < '2017-01-01'
GROUP BY 
    YEAR(OrderDate),
    MONTH(OrderDate),
    DATENAME(MONTH, OrderDate)
ORDER BY OrderYear, OrderMonth;

DATEADD: Adding or Subtracting Time Intervals

DATEADD is your go-to function for date arithmetic. The syntax is: DATEADD(interval, number, date)

-- Find orders with delivery dates more than 7 days from order date
SELECT 
    OrderID,
    CustomerID,
    OrderDate,
    ExpectedDeliveryDate,
    DATEDIFF(DAY, OrderDate, ExpectedDeliveryDate) AS DaysDifference
FROM Sales.Orders
WHERE ExpectedDeliveryDate > DATEADD(DAY, 7, OrderDate)
    AND OrderDate >= '2016-01-01'
ORDER BY DaysDifference DESC;

Practical DATEADD examples:

-- Calculate dates relative to today
SELECT 
    GETDATE() AS Today,
    DATEADD(DAY, 7, GETDATE()) AS NextWeek,
    DATEADD(MONTH, -1, GETDATE()) AS LastMonth,
    DATEADD(YEAR, 1, GETDATE()) AS NextYear,
    DATEADD(QUARTER, -1, GETDATE()) AS LastQuarter,
    DATEADD(HOUR, 48, GETDATE()) AS In48Hours;

Here's how I use DATEADD in a common business scenario:

-- Find customers who haven't placed an order in 90 days
SELECT 
    c.CustomerID,
    c.CustomerName,
    MAX(o.OrderDate) AS LastOrderDate,
    DATEDIFF(DAY, MAX(o.OrderDate), GETDATE()) AS DaysSinceLastOrder
FROM Sales.Customers c
INNER JOIN Sales.Orders o ON c.CustomerID = o.CustomerID
GROUP BY c.CustomerID, c.CustomerName
HAVING MAX(o.OrderDate) < DATEADD(DAY, -90, GETDATE())
ORDER BY LastOrderDate;

DATEDIFF: Calculating Time Between Dates

DATEDIFF calculates the difference between two dates in the specified interval. The syntax is: DATEDIFF(interval, startdate, enddate)

-- Analyze order-to-delivery performance
SELECT 
    OrderID,
    CustomerID,
    OrderDate,
    ExpectedDeliveryDate,
    DATEDIFF(DAY, OrderDate, ExpectedDeliveryDate) AS ExpectedDays,
    DATEDIFF(HOUR, OrderDate, ExpectedDeliveryDate) AS ExpectedHours,
    CASE 
        WHEN DATEDIFF(DAY, OrderDate, ExpectedDeliveryDate) <= 3 
        THEN 'Express'
        WHEN DATEDIFF(DAY, OrderDate, ExpectedDeliveryDate) <= 7 
        THEN 'Standard'
        ELSE 'Extended'
    END AS DeliveryCategory
FROM Sales.Orders
WHERE OrderDate >= '2016-05-01' 
    AND OrderDate < '2016-06-01';

Important gotcha: DATEDIFF counts boundaries, not full intervals. For example:

-- These both return 1 day, even though actual time is very different
SELECT 
    DATEDIFF(DAY, '2016-05-31 23:59:00', '2016-06-01 00:01:00') AS Example1,
    DATEDIFF(DAY, '2016-05-31 00:00:00', '2016-06-01 23:59:00') AS Example2;

Both return 1 because DATEDIFF crosses one day boundary in each case.

EOMONTH: Finding the Last Day of the Month

This function is incredibly useful for financial reporting and date range queries:

-- Get sales totals by month
SELECT 
    EOMONTH(o.OrderDate) AS MonthEnd,
    COUNT(DISTINCT o.OrderID) AS OrderCount,
    COUNT(DISTINCT o.CustomerID) AS UniqueCustomers,
    SUM(ol.Quantity * ol.UnitPrice) AS TotalSales
FROM Sales.Orders o
INNER JOIN Sales.OrderLines ol ON o.OrderID = ol.OrderID
WHERE o.OrderDate >= '2016-01-01' 
    AND o.OrderDate < '2016-07-01'
GROUP BY EOMONTH(o.OrderDate)
ORDER BY MonthEnd;

You can also use EOMONTH with an offset to get next or previous month-ends:

-- Calculate month boundaries
SELECT 
    GETDATE() AS Today,
    EOMONTH(GETDATE()) AS CurrentMonthEnd,
    EOMONTH(GETDATE(), -1) AS LastMonthEnd,
    EOMONTH(GETDATE(), 1) AS NextMonthEnd,
    DATEADD(DAY, 1, EOMONTH(GETDATE(), -1)) AS CurrentMonthStart;

Step-by-Step Tutorial: Building a Comprehensive Sales Report

Let's combine multiple date functions to create a realistic business report. We'll analyze sales patterns across different time dimensions:

-- Comprehensive sales analysis with date functions
WITH SalesData AS (
    SELECT 
        o.OrderID,
        o.OrderDate,
        o.CustomerID,
        c.CustomerName,
        YEAR(o.OrderDate) AS OrderYear,
        MONTH(o.OrderDate) AS OrderMonth,
        DATENAME(MONTH, o.OrderDate) AS MonthName,
        DATEPART(QUARTER, o.OrderDate) AS OrderQuarter,
        DATENAME(WEEKDAY, o.OrderDate) AS DayOfWeek,
        DATEDIFF(DAY, o.OrderDate, o.ExpectedDeliveryDate) AS DeliveryDays,
        SUM(ol.Quantity * ol.UnitPrice) AS OrderTotal
    FROM Sales.Orders o
    INNER JOIN Sales.OrderLines ol ON o.OrderID = ol.OrderID
    INNER JOIN Sales.Customers c ON o.CustomerID = c.CustomerID
    WHERE o.OrderDate >= '2016-01-01' 
        AND o.OrderDate < '2016-04-01'
    GROUP BY 
        o.OrderID,
        o.OrderDate,
        o.CustomerID,
        c.CustomerName,
        o.ExpectedDeliveryDate
)
SELECT 
    OrderYear,
    OrderQuarter,
    OrderMonth,
    MonthName,
    DayOfWeek,
    COUNT(OrderID) AS TotalOrders,
    COUNT(DISTINCT CustomerID) AS UniqueCustomers,
    SUM(OrderTotal) AS TotalRevenue,
    AVG(OrderTotal) AS AverageOrderValue,
    AVG(DeliveryDays) AS AvgDeliveryDays
FROM SalesData
GROUP BY 
    OrderYear,
    OrderQuarter,
    OrderMonth,
    MonthName,
    DayOfWeek
ORDER BY 
    OrderYear,
    OrderMonth,
    CASE DayOfWeek
        WHEN 'Monday' THEN 1
        WHEN 'Tuesday' THEN 2
        WHEN 'Wednesday' THEN 3
        WHEN 'Thursday' THEN 4
        WHEN 'Friday' THEN 5
        WHEN 'Saturday' THEN 6
        WHEN 'Sunday' THEN 7
    END;

Working with Purchasing Data and Supplier Lead Times

Here's another practical example using the Purchasing tables:

-- Analyze supplier performance based on delivery times
SELECT 
    s.SupplierName,
    COUNT(po.PurchaseOrderID) AS TotalOrders,
    AVG(DATEDIFF(DAY, po.OrderDate, po.ExpectedDeliveryDate)) AS AvgExpectedLeadDays,
    MIN(po.OrderDate) AS FirstOrderDate,
    MAX(po.OrderDate) AS LastOrderDate,
    DATEDIFF(MONTH, MIN(po.OrderDate), MAX(po.OrderDate)) AS MonthsAsSupplier
FROM Purchasing.PurchaseOrders po
INNER JOIN Purchasing.Suppliers s ON po.SupplierID = s.SupplierID
WHERE po.OrderDate >= DATEADD(YEAR, -1, GETDATE())
GROUP BY s.SupplierID, s.SupplierName
HAVING COUNT(po.PurchaseOrderID) >= 5
ORDER BY AvgExpectedLeadDays;

Advanced Techniques: Date Formatting and Conversion

Using CONVERT and FORMAT for Date Display

SQL Server provides multiple ways to format dates for display:

-- Different date formatting approaches
SELECT 
    OrderDate,
    -- CONVERT with style codes (older method, faster)
    CONVERT(VARCHAR(10), OrderDate, 101) AS USA_Format,        -- MM/DD/YYYY
    CONVERT(VARCHAR(10), OrderDate, 103) AS UK_Format,         -- DD/MM/YYYY
    CONVERT(VARCHAR(10), OrderDate, 120) AS ISO_Format,        -- YYYY-MM-DD
    -- FORMAT function (newer, more readable, slower)
    FORMAT(OrderDate, 'MM/dd/yyyy') AS Custom_Format1,
    FORMAT(OrderDate, 'MMMM dd, yyyy') AS Custom_Format2,
    FORMAT(OrderDate, 'ddd, MMM dd, yyyy') AS Custom_Format3
FROM Sales.Orders
WHERE OrderID BETWEEN 1 AND 5;

Performance tip: CONVERT is faster than FORMAT, especially in large datasets. Use CONVERT in production queries and FORMAT for ad-hoc reporting where readability matters more.

Creating Date Ranges for Reporting

A common requirement is generating date ranges, especially for period-over-period comparisons:

-- Compare sales: This month vs. Last month vs. Same month last year
DECLARE @CurrentDate DATE = GETDATE();
DECLARE @CurrentMonthStart DATE = DATEADD(DAY, 1, EOMONTH(@CurrentDate, -1));
DECLARE @CurrentMonthEnd DATE = EOMONTH(@CurrentDate);
DECLARE @LastMonthStart DATE = DATEADD(DAY, 1, EOMONTH(@CurrentDate, -2));
DECLARE @LastMonthEnd DATE = EOMONTH(@CurrentDate, -1);
DECLARE @SameMonthLastYearStart DATE = DATEADD(YEAR, -1, @CurrentMonthStart);
DECLARE @SameMonthLastYearEnd DATE = DATEADD(YEAR, -1, @CurrentMonthEnd);

SELECT 
    'Current Month' AS Period,
    COUNT(OrderID) AS OrderCount,
    SUM(ol.Quantity * ol.UnitPrice) AS TotalSales
FROM Sales.Orders o
INNER JOIN Sales.OrderLines ol ON o.OrderID = ol.OrderID
WHERE o.OrderDate >= @CurrentMonthStart 
    AND o.OrderDate <= @CurrentMonthEnd

UNION ALL

SELECT 
    'Last Month' AS Period,
    COUNT(OrderID) AS OrderCount,
    SUM(ol.Quantity * ol.UnitPrice) AS TotalSales
FROM Sales.Orders o
INNER JOIN Sales.OrderLines ol ON o.OrderID = ol.OrderID
WHERE o.OrderDate >= @LastMonthStart 
    AND o.OrderDate <= @LastMonthEnd

UNION ALL

SELECT 
    'Same Month Last Year' AS Period,
    COUNT(OrderID) AS OrderCount,
    SUM(ol.Quantity * ol.UnitPrice) AS TotalSales
FROM Sales.Orders o
INNER JOIN Sales.OrderLines ol ON o.OrderID = ol.OrderID
WHERE o.OrderDate >= @SameMonthLastYearStart 
    AND o.OrderDate <= @SameMonthLastYearEnd;

Common Mistakes and How to Avoid Them

Mistake 1: Ignoring Time Components When Comparing Dates

This is the most frequent mistake I see:

-- WRONG: This might miss orders if time portion exists
SELECT OrderID, OrderDate
FROM Sales.Orders
WHERE OrderDate = '2016-05-31';

-- RIGHT: Explicitly handle the time component
SELECT OrderID, OrderDate
FROM Sales.Orders
WHERE CAST(OrderDate AS DATE) = '2016-05-31';

-- BETTER: Use date range (more efficient, SARGable)
SELECT OrderID, OrderDate
FROM Sales.Orders
WHERE OrderDate >= '2016-05-31' 
    AND OrderDate < '2016-06-01';

The range approach is "SARGable" (Search ARGument able), meaning SQL Server can use indexes efficiently.

Mistake 2: Using Functions on Indexed Columns

Applying functions to columns prevents index usage:

-- WRONG: Function on column prevents index seek
SELECT OrderID, OrderDate
FROM Sales.Orders
WHERE YEAR(OrderDate) = 2016 
    AND MONTH(OrderDate) = 5;

-- RIGHT: Use range comparison instead
SELECT OrderID, OrderDate
FROM Sales.Orders
WHERE OrderDate >= '2016-05-01' 
    AND OrderDate < '2016-06-01';

Mistake 3: Not Handling NULL Dates

Always check for NULL when working with dates:

-- Find orders with missing delivery dates or overdue
SELECT 
    OrderID,
    OrderDate,
    ExpectedDeliveryDate,
    CASE 
        WHEN ExpectedDeliveryDate IS NULL 
        THEN 'No Delivery Date Set'
        WHEN ExpectedDeliveryDate < GETDATE() 
        THEN 'Overdue'
        ELSE 'On Schedule'
    END AS DeliveryStatus
FROM Sales.Orders
WHERE OrderDate >= DATEADD(MONTH, -3, GETDATE());

Mistake 4: Timezone Confusion

If your application serves multiple timezones, always think about where and when to convert:

-- Store in UTC, convert for display
SELECT 
    OrderID,
    OrderDate,
    -- If OrderDate was stored in UTC
    DATEADD(HOUR, -5, OrderDate) AS OrderDate_EST,
    DATEADD(HOUR, -8, OrderDate) AS OrderDate_PST
FROM Sales.Orders
WHERE OrderDate >= '2016-01-01';

For production applications with timezone requirements, consider using DATETIMEOFFSET data type.

Performance Tips for Date Functions

  1. Avoid functions on WHERE clause columns - Use date ranges instead
  2. Use appropriate data types - Don't store dates as VARCHAR
  3. Consider computed columns - For frequently extracted date parts
  4. Index your date columns - Especially for range queries
-- Example: Add a computed column for month-based queries
ALTER TABLE Sales.Orders
ADD OrderYearMonth AS (YEAR(OrderDate) * 100 + MONTH(OrderDate)) PERSISTED;

-- Now you can create an index on it
CREATE INDEX IX_Orders_YearMonth 
ON Sales.Orders(OrderYearMonth);

Summary: Key Takeaways

Mastering SQL Server date and time functions opens up powerful capabilities for data analysis and reporting. Here's what we've covered:

  • GETDATE() vs SYSDATETIME(): Choose based on precision needs; use UTC versions for multi-timezone apps
  • DATEPART/DATENAME: Extract components for grouping and filtering
  • DATEADD: Add or subtract time intervals; essential for calculating future/past dates
  • DATEDIFF: Calculate time between dates; remember it counts boundary crossings
  • EOMONTH: Simplifies month-end calculations for financial reporting
  • Formatting: Use CONVERT for performance, FORMAT for readability
  • Performance: Avoid functions on indexed columns; use date ranges instead
  • Best practices: Handle NULLs, consider timezones, use appropriate data types

The key to mastering these functions is practice. Start with simple queries and gradually build more complex reports. Pay attention to performance, especially when working with large datasets.

Frequently Asked Questions

Q: What's the difference between GETDATE() and SYSDATETIME()?

A: Both return the current date and time, but SYSDATETIME() returns DATETIME2 with higher precision (100 nanoseconds vs 3.33 milliseconds). Use GETDATE() for most applications and SYSDATETIME() when you need precise timestamps for logging, audit trails, or high-frequency data. For new development, DATETIME2 (and thus SYSDATETIME) is generally recommended.

Q: How do I calculate business days (excluding weekends) between two dates?

A: SQL Server doesn't have a built-in function for this, but you can create a calculation using DATEDIFF and adjusting for weekends. For a more accurate solution including holidays, create a calendar table with business day flags and count the rows between dates. Here's a simple weekend-only example:

DECLARE @StartDate DATE = '2016-05-01';
DECLARE @EndDate DATE = '2016-05-31';

SELECT 
    DATEDIFF(DAY, @StartDate, @EndDate) - 
    (DATEDIFF(WEEK, @StartDate, @EndDate) * 2) -
    CASE WHEN DATEPART(WEEKDAY, @StartDate) = 1 THEN 1 ELSE 0 END -
    CASE WHEN DATEPART(WEEKDAY, @EndDate) = 7 THEN 1 ELSE 0 END AS BusinessDays;

Q: How should I store dates for applications used across multiple timezones?

A: The best practice is to store all dates in UTC using DATETIME2 or DATETIMEOFFSET. This eliminates ambiguity and makes timezone conversions straightforward. Store user timezone preferences separately and convert for display in your application layer. Never store local times without timezone information, as this leads to confusion during daylight saving time transitions and when users travel.