Working with Dates

Tuesday Jul 10th 2001 by Michael Trachtenberg

SQL Server has respectable facilities for storing, calculating, and reporting time and date information. In this article we examine them; from storage and retrieval functions to converting to and from other data types.

"Time is that great gift of nature which keeps everything from happening at once."--C.J. Overbeck(?)

All but the most rudimentary database applications store one or more expressions of time. They may record when an event occurred, its duration, the expected occurrence of a future event, or a combination of these. The values themselves are frequently less important than the calculations that depend on them: How much did we sell this month? How many hits did we get today? Is that hotel room available two weeks from now? How long before this asset is fully depreciated?

SQL Server has respectable facilities for storing, calculating, and reporting time and date information. If they do not meet your needs, the server is flexible enough to handle nearly any system you improvise.

This article won't discuss SQL Server's online analytical processing (OLAP) services. Regardless, these services almost always require time values obtained from an online transaction processing (OLTP) database. It will also omit the extensive application of time in administering SQL Server.

Out of the Box

SQL Server has four basic capabilities with regard to time values: Storage and retrieval; calculations and comparisons; formatted display; and conversion to and from other data types.

System data types

SQL Server provides two time data types: datetime and smalldatetime. They are distinguished by their range and precision, and corresponding storage requirements. Both reckon dates as the number of days either from or to January 1, 1900, the base date. Both reckon time as the period elapsed since midnight. Datetime and smalldatetime combine date and time into a single value--there is no distinct time data type in SQL Server. You can store only time values by omitting dates, or vice versa.

Datetime stores the range January 1, 1753, to December 31, 9999 with one three-hundredth of a second accuracy--it rounds to the nearest .000, .003, or .007 seconds. It requires eight bytes, four for the date, and four for the time. For additional information, see both BOL and this article.

Smalldatetime covers the range January 1, 1900, through June 6, 2079 with one-minute resolution. It requires four bytes, two for the date, and two for the time.

Click here for code example 1.

On my 7.0 system, I got the following results:

Click here for example 2.

Note the difference in precision, and the effect of rounding. Also note the various means of populating the columns. The default value uses the GETDATE() function; the remaining rows use a variety of character strings enclosed in single quotes.

The server will transparently convert properly formatted strings into their correct datetime values. These examples do not cover the full scope of its capabilities. Be warned that formatting of string constants is very particular if the database must support multiple languages. See BOL for internationalization information. Also pay attention to the settings for DATEFORMAT and DATEFIRST.

With regard to two-digit years, the server uses the two digit year cutoff setting specified by sp_configure or the Enterprise Manager. The default is 2049--'6/24/49' is equivalent to '6/24/2049,' and '6/24/50' is equivalent to '6/24/1950.' There is little rationale for using two-digit years, and they should be avoided in the interest of clarity and consistency.

Conversion and Formatting

Implicit Conversion

SQL Server implicitly converts datetime and smalldatetime both among themselves and among the various char data types. I've already shown conversions from char. Things become interesting when mathematical operators are involved.

--This works fine
SELECT 'The time is now ' + @c

-- This doesn't
SELECT 'The time is now ' + GETDATE()

The first example converts a datetime (smalldatetime , actually) to a char local variable. The server can then concatenate the variable with a string constant and announce the time.

The second example fails because the server considers the overloaded plus sign an addition operator, not a concatenation operator, and is trying to add two incompatible types. The way around this is to use the CAST function to convert the datetime to a string.

SELECT 'The time is now ' + CAST(GETDATE() AS CHAR)


CAST has two uses as regards dates: It explicitly converts converts the date data types to and from most of the other system data types, and it changes the output's length, usually to truncate it.

SELECT 'Today is ' + CAST(GETDATE() AS CHAR(11))

You can also use CAST to parse a text string into a time datatype.

SET @c = '06242001073656910'
SET @d = CAST(SUBSTRING(@c,5,4) +
         + SUBSTRING(@c,1,4) + ' '
         + SUBSTRING(@c,9,2) + ":"
         + SUBSTRING(@c,11,2) + ":"
         + SUBSTRING(@c,13,2) + "."
         + SUBSTRING(@c,15,3)
         AS DATETIME)

CAST has minimal formatting capability and isn't supported prior to 7.0. Unless you need ANSI compliance, consider using the CONVERT function instead.


CONVERT provides a number of styles corresponding to prevailing date format of each of a number of countries. Most styles allow either two or four digit years.

--CONVERT (data_type[(length)],expression [, style])
--See BOL for list of styles
SET @d = '20010624 21:48'

SELECT 108, CONVERT(VARCHAR,@d,108) -- time only

This gives the following output on my system:

NULL       Jun 24 2001 9:48PM
1          06/24/01
101        06/24/2001
2          01.06.24
104        24.06.2001
108        21:48:00
112        20010624
121        2001-06-24 21:48:00.000

Like CAST, CONVERT can truncate values if desired. Just use the appropriate data type; i.e., CHAR(4), and style. You can also nest CONVERT. The following strips the time off a datetime and converts it back to datetime.


To put it mildly, there are large number of ways to handle conversion and formatting. You do an implicit or explicit conversion, and then use one or more functions that handle that data type. You'll find some combinations run much faster than others. For instance, the above example could be rewritten as follows. The rewrite runs more than twice as quickly on my single user system.


Date Functions

SQL Server has a good collection of specialized functions for returning parts of dates. Most are also useful in calculations. The functions are DATENAME, DATEPART, DAY, MONTH, and YEAR. DATENAME returns a character string; the rest return integers. See the BOL for a full list of supported date parts and their abbreviations.

SET @c = '20010624 07:36:56.910'
SELECT 'This is Week ' + DATENAME(WEEK, @c) + ' of FY' + DATENAME(YEAR, @c)
This is Week 26 of FY2001


YEAR        MONTH       DAY
----------- ----------- -----------
2001        6           24

YEAR        MONTH       DAY
----------- ----------- -----------
2001        6           24

A few additional notes about conversion and formatting. Watch for unintentional loss of precision converting among data types. The effect will probably be subtle.

Datetime and smalldatetime have the highest precedence of all the data types. If you mix them with other data types using an operator, the server will attempt implicit conversion of the lower precedence data types to datetime or smalldatetime as required. If it can't do the conversion, it returns an error message. See the initial discussion of CAST.

With regard to formatting, if you intend to import date values into another program, keep that program's requirements in mind. In many cases, that means no formatting at all--just export the data as datetime or smalldatetime values, by casting if necessary. Otherwise, the program may treat the "dates" as nothing more than text and fail to sort or group them properly. You'll save yourself much grief with Excel pivot tables, among other things.

Calculations and Comparisons

Basic Arithmetic

SQL Server can handle addition and subtraction of time without resorting to functions, if you want. Use whichever is clearer.

-- Add 1.5 days to current time
SELECT GETDATE() AS Start, GETDATE()+1.5 AS 'Estimated Finish'
-- Done all your shopping yet?
SELECT CEILING(CAST('20011225'-GETDATE() AS FLOAT)) AS 'Shopping days left'


There are two workhorse time functions, DATEADD and DATEDIFF. As their names suggest, they add (or subtract) periods to a date, or return the difference between two dates, respectively.


DATEADD adds the specified interval to a date and returns a datetime or smalldatetime. You can use any of the usual collection of date parts.


SET @c = '20010624'
SELECT @C AS 'Start'
SELECT DATEADD(S,172800,@c) AS '+172800 seconds',
DATEADD(HH,48,@c) AS '+48 hours',
DATEADD(D,2,@c) AS '+2 days'
SELECT DATEADD(WK,-3,@c)as '-3 weeks'
SELECT DATEADD(M,1,CONVERT(CHAR(8),@c,112))-DAY(@c) as 'End of Month'
SELECT DATEADD(YY,1,'20000229') AS '1 yr. after Feb. 29, 2000'
-- Constants are evaluated at runtime. The following won't work.
SET ARITHABORT OFF -- Set this to ON and run again
SELECT DATEADD(D,1,'20010229') AS '1 day after Feb. 29, 2001'

2001-06-24 00:00:00

+172800 seconds             +48 hours                   +2 days
--------------------------- --------------------------- ---------------------------
2001-06-26 00:00:00         2001-06-26 00:00:00         2001-06-26 00:00:00

-3 weeks
2001-06-03 00:00:00

End of Month
2001-06-30 00:00:00.000

1 yr. after Feb. 29, 2000
2001-02-28 00:00:00.000

1 day. after Feb. 29, 2001

Arithmetic overflow occurred.

An interesting use for DATEADD is converting Julian dates to Gregorian ones. Strictly speaking, a Julian date is the number of days since noon, Universal Time on January 1, 4713 BCE. Some applications use Julian dates, but with a different base date. My employer's accounting system uses 1 A.D. as the base. December 31, 2000 was 730485. Since the dates we work with are within datetime's range, DATEADD can do the conversion.

SET @j = 730636
SELECT @j AS 'Julian', DATEADD(D,@j-730485,'12/31/2000') AS 'Gregorian'
Julian      Gregorian
----------- ---------------------------
730636      2001-05-31 00:00:00.000

DATEADD isn't useful for business days. It doesn't know about weekends or holidays. Rather than going through gyrations to avoid Sunday and New Years, many applications build lookup tables in advance and reference them instead.


DATEDIFF returns an integer representing the number of date parts between two dates. If the start date falls after the end date, the result is negative. Note that it only counts boundaries. If you ask it how many years elapsed between December 31, 1999, and January 1, 2001, it will reply, "2." To rehash some prior examples:

SELECT DATEDIFF(D,GETDATE(),'20011225') AS 'Shopping days left'

-- Convert from Gregorian to Julian. Must know base date and be in range.
SELECT DATEDIFF(D,'12/31/2000',GETDATE())+730485 as 'Julian'

The big gotcha with DATEDIFF is date boundaries. Even calculating age isn't straightforward.

SELECT @BIRTH = '12/10/1990', @AGEAT = '6/24/2001'
-- This is the wrong way

-- This is one correct way
    THEN 1 ELSE 0 END AS 'Age correcting DATEDIFF'

Age using straight DATEDIFF

Age correcting DATEDIFF

DATEDIFF increments whenever the year, or whatever date part you specified, changes. This could be useful for calculating how many model years old a vehicle is, but not helpful when you're trying to calculate anniversaries. It can trip up experts, as this discussion shows. (If the link breaks, look for a thread called "Finding anniversary dates - Ken Henderson Book" on Devdex, Google, etc.) Interestingly, not only is Henderson's query wrong, so is the "corrected" one propounded. See if you can understand how the second query works, and why it erroneously omits January rows once the system date reaches December 2.


SELECT statements to retrieve rows based on dates are straightforward. The main concerns are string constants, which were previously discussed, handling combined date and time values, and query optimization. Following is a simple table and some queries against it.

CREATE TABLE work_study(
r_type CHAR(1) NOT NULL,
INSERT work_study VALUES('A','1/5/2000')
INSERT work_study VALUES('B','1/5/2000 14:10')
INSERT work_study VALUES('B','1/5/2000 14:20')
INSERT work_study VALUES('A','2/8/2000')
INSERT work_study VALUES('B','2/8/2000 6:05')
INSERT work_study VALUES('B','2/8/2000 6:07')
INSERT work_study VALUES('A','4/10/2001')
INSERT work_study VALUES('B','4/10/2001 15:21')
INSERT work_study VALUES('B','4/10/2001 15:27')

SELECT COUNT(*) as '= April 10, 2001' FROM work_study WHERE r_time = '4/10/2001'

SELECT * FROM work_study WHERE r_time = '4/10/2001'

SELECT COUNT(*) as 'All April 10, 2001'
FROM work_study
WHERE r_time BETWEEN '4/10/2001' AND '4/10/2001 23:59:59'

SELECT COUNT(*) AS 'FY2000 Rows'
FROM work_study
WHERE YEAR(r_time) = 2000 and r_type = 'B'

--Use like to extract times
SELECT r_time as 'Like 6:05' FROM work_study WHERE r_time LIKE '%6:05%'

SELECT DATENAME(DW,r_time) as 'Weekday', COUNT(*) as 'Count'
FROM work_study
WHERE r_type = 'B' GROUP BY DATENAME(DW,r_time) --Crosstabs: See http://support.microsoft.com/support/kb/articles/Q175/5/74.ASP SELECT SUM(CASE WHEN YEAR(r_time) = 2000 THEN 1 ELSE 0 END) AS '2000 Rows', SUM(CASE WHEN YEAR(r_time) = 2001 THEN 1 ELSE 0 END) AS '2001 Rows' FROM work_study WHERE r_type = 'B'

= April 10, 2001 

id          r_type r_time 
----------- ------ ---------------------------
7           A      2001-04-10 00:00:00

All April 10, 2001

FY2000 Rows 

Like 6:05 
2000-02-08 06:05:00

Weekday                        Count 
------------------------------ -----------
Tuesday                        4
Wednesday                      2

2000 Rows   2001 Rows 
----------- -----------
4           2

Some things to note:

  1. Unless you never store time values with your dates, you have to make provisions for extracting all rows for the specified date.
  2. String constants must be correctly formatted for your locale or you will either get errors or strange results. Using YYYYMMDD should be foolproof.
  3. You can use LIKE to extract times. See BOL.
  4. You can use all of the usual date functions to do selection, sorting, and grouping.

One potential big issue with date functions is performance. Date columns should be indexed if date will used as a search argument. Unfortunately, the server won't use the index, except perhaps for index scans, once you use a function. Rewriting queries to avoid functions can lead to cumbersome code, but it's worth it if the query runs often. In extreme cases, splitting the date parts into separate, indexed columns, is beneficial.

-- Don't do this often.

FROM employees
WHERE YEAR(hire_date) = 1998

-- Do this instead.
FROM employees
WHERE hire_date BETWEEN '19980101' AND '19981231'

-- Same thing here. Try to avoid this.
FROM employees
WHERE MONTH(hire_date) = 11

-- If it's not too long, this is very effective.
FROM employees
WHERE hire_date BETWEEN '19901101' AND '19901130'
OR hire_date BETWEEN '19911101' AND '19911130'
OR hire_date BETWEEN '19921101' AND '19921130'
OR hire_date BETWEEN '19931101' AND '19931130'
OR hire_date BETWEEN '19941101' AND '19941130'
OR hire_date BETWEEN '19951101' AND '19951130'
OR hire_date BETWEEN '19961101' AND '19961130'
OR hire_date BETWEEN '19971101' AND '19971130'
OR hire_date BETWEEN '19981101' AND '19981130'
OR hire_date BETWEEN '19991101' AND '19991130'
OR hire_date BETWEEN '20001101' AND '20001130'
OR hire_date BETWEEN '20011101' AND '20011130'

I experimented while writing this article and got a consistent six-fold improvement using constants instead of functions. Your mileage may vary. The difference widened a little as the number of rows increased. Don't give up on functions, though. Queries on indexed columns should run quickly, no matter what approach you take. It's when you run the query 1,000 times per day that the milliseconds add up.



Dates are probably a bad idea for keys. You're betting heavily that you'll never need to insert two or more identical values into a column. Granted, datetime's resolution is 1/300th of a second, but why take the bet?


Other than keys, constraints are particularly useful with date columns. Default constraints can record the time when each row is inserted. Check constraints can meet business needs, such as prohibiting time values in a column. That way, a hire date can never be '7/2/2001 10:00.' They can meet legal needs, such as prohibiting transactions on Sunday.


Almost any non-trivial database requires working with date or time values. SQL Server has a good collection of tools for manipulating them; however, with this power, comes complexity. The developer should strive to understand the server's approach to dates so they may get the results and performance they need.

Mobile Site | Full Site