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
- Avoid functions on WHERE clause columns - Use date ranges instead
- Use appropriate data types - Don't store dates as VARCHAR
- Consider computed columns - For frequently extracted date parts
- 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.