Is there a more efficient method of getting end of day? - sql

I have a DATETIME variable #DateEnd = '1/4/2011 16:43:22 PM'. I want to get the end of day: 1/4/2011 23:59:59.997.
I have the following and it works fine, but it has lots of conversions and doesn't seem efficient:
DECLARE #DateString VARCHAR(25)
DECLARE #DateEnd DATETIME = '1/4/2011 16:43:22 PM'
SET #DateString = CONVERT(VARCHAR(10), #DateEnd, 101) + ' 23:59:59.997'
SET #DateEnd = CAST(DateString AS DATETIME)
Is there a more efficient method of accomplishing this?

Typically what we do is get the 0/midnight value for the start of the next day, and then use an exclusive inequality boundary (<) for that end of the range instead of an inclusive equality boundary (<=).
DECLARE #DateEnd DATETIME = '20110104 16:43:22'
SET #DateEnd = DATEADD(day, 1, Cast(#DateEnd as Date))
It's also NEVER okay to format a datetime literal for SQL like in the question. Different cultures and languages have their own norms and expectations around how dates should look, and it's not good to force your own personal or cultural norms into those languages. SQL, as its own language, is no different. If you're writing SQL and not using SQL's format for the date values (yyyyMMdd, yyyy-MM-ddTHH:mm:ss[.fff], or yyyyMMdd HH:mm:ss[.fff]), you have a WRONG format for that context.
For example, my own preferred format, coming to SQL later after first learning a few other (procedural) languages, is yyyy-MM-dd HH:mm:ss, which doesn't quite match those acceptable formats. I admit I've used that format quite a lot over the years; you can probably find examples in my answers here on Stack Overflow. But every place where I did that was wrong, and I no longer use it. So don't take it too hard; everybody has to learn this.

Probably not more efficient, but I am a fan of using the correct functions for this sort of thing.
Also using the correct datatype, datetime2 is the recommended datatype to use these days, and removes the pesky 3ms interval.
DECLARE #DateEnd DATETIME2(3) = '1/4/2011 16:43:22 PM';
SET #DateEnd = DATEADD(millisecond, -1, CAST(DATEADD(day, 1, CAST(#DateEnd AS DATE)) AS DATETIME2(3)));

One little cheat is format(). However, if #DateEnd is being used as a date range, you may be better off using < TheNextDay
Declare #DateEnd datetime = '1/4/2011 16:43:22 PM'
Set #DateEnd = format(#DateEnd,'yyyy-MM-dd 23:59:59.997')
Just another option avoiding Format()
Declare #DateEnd datetime = '1/4/2011 16:43:22 PM'
Set #DateEnd = concat(convert(date,#DateEnd),' 23:59:59.997')

Related

Get DAY from Date

I need select day from #ReportDay
DECLARE #ReportDay DATETIME = '2017-10-02 12:00:03.140'
DECLARE #currentReportDay DATETIME = DATEPART(day, #ReportDay )
But in result I've '1900-01-03 00:00:00.000'. How it can be fix?
DECLARE #ReportDay DATETIME = FORMAT(Now(),'YYYY-MM-DD')
DECLARE #currentReportDay DATETIME = DATEPART(day, #ReportDay)
This would give you today's date in YYYY-MM-DD format
DATEPART gives you back an integer
DECLARE #ReportDay DATETIME = '2017-10-02 12:00:03.140'
DECLARE #currentReportDay INT = DATEPART(day, #ReportDay )
Others have already raised the ambiguity of your question but to add some explanation and hopefully clear up some confusion.
The reason why you are getting '1900-01-03 00:00:00.000'
is because DATEPART(day, #ReportDay) will return an integer for the day of the month, in your case 2. When converted to a DATETIME 2 is actually the 3rd date. 0 is 1/1/1900, 1 is 1/2/1900. and so forth.
So if you want the DATE itself you can use CONVERT or CAST. Some other string manipulation techniques exist and FORMAT() is one of them but they are not worth looking at as will have much poorer performance and take additional CASTS and CONVERTS anyway. What you want to do is to DROP the TIME off the DATETIME. Best way of doing that is to CAST it to a DATE which has no TIME component.
DECLARE #currentReportDay DATETIME = CAST(CAST(#ReportDay AS DATE) AS DATETIME)
Note because the second CAST back to DATETIME isn't really necessary in this code as it will inherently happen so you could shorten to:
DECLARE #currentReportDay DATETIME = CAST(#ReportDay AS DATE)
And to round this out. If you actually want the DAY of the Month then simply change from DATETIME data type to INT.
DECLARE #currentReportDay INT = DATEPART(day, #ReportDay )

How to set part of a datetime

I need to set just the hour of a datetime to a certain value. I know I can add it the difference to the desired value, or create a new datetime with parts from the original one, but isn't there a cleaner way? Something like:
declare #d datetime = '09/08/2012 09:14:55'
set #d = SETDATEPARTORSOMETHINGLIKETHAT (hour, #d, 23)
Thanks a lot.
DECLARE #TargetHour TINYINT = 23;
DECLARE #d datetime = '09/08/2012 09:14:55';
SET #d = DATEADD(HOUR, #TargetHour - DATEPART(HOUR, #d), #d);
SELECT #d;
Result:
2012-09-08 23:14:55.000
(And yes, this will work if the #TargetHour is less than the current hour, e.g. 5 or 7.)
I'd be very careful though about using ambiguous and error-prone formats like m/d/y. Case in point: I don't even know if you meant September 8th or August 9th, and SQL Server isn't going to know either - it needs to use regional settings etc. to figure it out, and if you give your code to someone with different settings, it will generate an error or, even worse, accept the wrong date silently. You should use:
DECLARE #d datetime = '2012-09-08T09:14:55';
Bad habits to kick : mis-handling date / range queries

How can I compare time in SQL Server?

I'm trying to compare time in a datetime field in a SQL query, but I don't know if it's right. I don't want to compare the date part, just the time part.
I'm doing this:
SELECT timeEvent
FROM tbEvents
WHERE convert(datetime, startHour, 8) >= convert(datetime, #startHour, 8)
Is it correct?
I'm asking this because I need to know if 08:00:00 is less or greater than 07:30:00 and I don't want to compare the date, just the time part.
Thanks!
Your compare will work, but it will be slow because the dates are converted to a string for each row. To efficiently compare two time parts, try:
declare #first datetime
set #first = '2009-04-30 19:47:16.123'
declare #second datetime
set #second = '2009-04-10 19:47:16.123'
select (cast(#first as float) - floor(cast(#first as float))) -
(cast(#second as float) - floor(cast(#second as float)))
as Difference
Long explanation: a date in SQL server is stored as a floating point number. The digits before the decimal point represent the date. The digits after the decimal point represent the time.
So here's an example date:
declare #mydate datetime
set #mydate = '2009-04-30 19:47:16.123'
Let's convert it to a float:
declare #myfloat float
set #myfloat = cast(#mydate as float)
select #myfloat
-- Shows 39931,8244921682
Now take the part after the comma character, i.e. the time:
set #myfloat = #myfloat - floor(#myfloat)
select #myfloat
-- Shows 0,824492168212601
Convert it back to a datetime:
declare #mytime datetime
set #mytime = convert(datetime,#myfloat)
select #mytime
-- Shows 1900-01-01 19:47:16.123
The 1900-01-01 is just the "zero" date; you can display the time part with convert, specifying for example format 108, which is just the time:
select convert(varchar(32),#mytime,108)
-- Shows 19:47:16
Conversions between datetime and float are pretty fast, because they're basically stored in the same way.
convert(varchar(5), thedate, 108) between #leftTime and #rightTime
Explanation:
if you have varchar(5) you will obtain HH:mm
if you have varchar(8) you obtain HH:mm ss
108 obtains only the time from the SQL date
#leftTime and #rightTime are two variables to compare
If you're using SQL Server 2008, you can do this:
WHERE CONVERT(time(0), startHour) >= CONVERT(time(0), #startTime)
Here's a full test:
DECLARE #tbEvents TABLE (
timeEvent int IDENTITY,
startHour datetime
)
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 0, GETDATE())
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 1, GETDATE())
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 2, GETDATE())
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 3, GETDATE())
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 4, GETDATE())
INSERT INTO #tbEvents (startHour) SELECT DATEADD(hh, 5, GETDATE())
--SELECT * FROM #tbEvents
DECLARE #startTime datetime
SET #startTime = DATEADD(mi, 65, GETDATE())
SELECT
timeEvent,
CONVERT(time(0), startHour) AS 'startHour',
CONVERT(time(0), #startTime) AS '#startTime'
FROM #tbEvents
WHERE CONVERT(time(0), startHour) >= CONVERT(time(0), #startTime)
Just change convert datetime to time that should do the trick:
SELECT timeEvent
FROM tbEvents
WHERE convert(time, startHour) >= convert(time, #startHour)
if (cast('2012-06-20 23:49:14.363' as time) between
cast('2012-06-20 23:49:14.363' as time) and
cast('2012-06-20 23:49:14.363' as time))
One (possibly small) issue I have noted with the solutions so far is that they all seem to require a function call to process the comparison. This means that the query engine will need to do a full table scan to seek the rows you are after - and be unable to use an index. If the table is not going to get particularly large, this probably won't have any adverse affects (and you can happily ignore this answer).
If, on the other hand, the table could get quite large, the performance of the query could suffer.
I know you stated that you do not wish to compare the date part - but is there an actual date being stored in the datetime column, or are you using it to store only the time? If the latter, you can use a simple comparison operator, and this will reduce both CPU usage, and allow the query engine to use statistics and indexes (if present) to optimise the query.
If, however, the datetime column is being used to store both the date and time of the event, this obviously won't work. In this case if you can modify the app and the table structure, separate the date and time into two separate datetime columns, or create a indexed view that selects all the (relevant) columns of the source table, and a further column that contains the time element you wish to search for (use any of the previous answers to compute this) - and alter the app to query the view instead.
Using float does not work.
DECLARE #t1 datetime, #t2 datetime
SELECT #t1 = '19000101 23:55:00', #t2 = '20001102 23:55:00'
SELECT CAST(#t1 as float) - floor(CAST(#t1 as float)), CAST(#t2 as float) - floor(CAST(#t2 as float))
You'll see that the values are not the same (SQL Server 2005). I wanted to use this method to check for times around midnight (the full method has more detail) in which I was comparing the current time for being between 23:55:00 and 00:05:00.
Adding to the other answers:
you can create a function for trimming the date from a datetime
CREATE FUNCTION dbo.f_trimdate (#dat datetime) RETURNS DATETIME AS BEGIN
RETURN CONVERT(DATETIME, CONVERT(FLOAT, #dat) - CONVERT(INT, #dat))
END
So this:
DECLARE #dat DATETIME
SELECT #dat = '20080201 02:25:46.000'
SELECT dbo.f_trimdate(#dat)
Will return
1900-01-01 02:25:46.000
Use Datepart function: DATEPART(datepart, date)
E.g#
SELECT DatePart(#YourVar, hh)*60) +
DatePart(#YourVar, mi)*60)
This will give you total time of day in minutes allowing you to compare more easily.
You can use DateDiff if your dates are going to be the same, otherwise you'll need to strip out the date as above
You can create a two variables of datetime, and set only hour of date that your need to compare.
declare #date1 datetime;
declare #date2 datetime;
select #date1 = CONVERT(varchar(20),CONVERT(datetime, '2011-02-11 08:00:00'), 114)
select #date2 = CONVERT(varchar(20),GETDATE(), 114)
The date will be "1900-01-01" you can compare it
if #date1 <= #date2
print '#date1 less then #date2'
else
print '#date1 more then #date2'
SELECT timeEvent
FROM tbEvents
WHERE CONVERT(VARCHAR,startHour,108) >= '01:01:01'
This tells SQL Server to convert the current date/time into a varchar using style 108, which is "hh:mm:ss". You can also replace '01:01:01' which another convert if necessary.
I believe you want to use DATEPART('hour', datetime).
Reference is here:
http://msdn.microsoft.com/en-us/library/ms174420.aspx
I don't love relying on storage internals (that datetime is a float with whole number = day and fractional = time), but I do the same thing as the answer Jhonny D. Cano. This is the way all of the db devs I know do it. Definitely do not convert to string. If you must avoid processing as float/int, then the best option is to pull out hour/minute/second/milliseconds with DatePart()
I am assuming your startHour column and #startHour variable are both DATETIME; In that case, you should be converting to a string:
SELECT timeEvent
FROM tbEvents
WHERE convert(VARCHAR(8), startHour, 8) >= convert(VARCHAR(8), #startHour, 8)
below query gives you time of the date
select DateAdd(day,-DateDiff(day,0,YourDateTime),YourDateTime) As NewTime from Table
#ronmurp raises a valid concern - the cast/floor approach returns different values for the same time. Along the lines of the answer by #littlechris and for a more general solution that solves for times that have a minute, seconds, milliseconds component, you could use this function to count the number of milliseconds from the start of the day.
Create Function [dbo].[MsFromStartOfDay] ( #DateTime datetime )
Returns int
As
Begin
Return (
( Datepart( ms , #DateTime ) ) +
( Datepart( ss , #DateTime ) * 1000 ) +
( Datepart( mi , #DateTime ) * 1000 * 60 ) +
( Datepart( hh , #DateTime ) * 1000 * 60 * 60 )
)
End
I've verified that it returns the same int for two different dates with the same time
declare #first datetime
set #first = '1900-01-01 23:59:39.090'
declare #second datetime
set #second = '2000-11-02 23:56:39.090'
Select dbo.MsFromStartOfDay( #first )
Select dbo.MsFromStartOfDay( #second )
This solution doesn't always return the int you would expect. For example, try the below in SQL 2005, it returns an int ending in '557' instead of '556'.
set #first = '1900-01-01 23:59:39.556'
set #second = '2000-11-02 23:56:39.556'
I think this has to do with the nature of DateTime stored as float. You can still compare the two number, though. And when I used this approach on a "real" dataset of DateTime captured in .NET using DateTime.Now() and stored in SQL, I found that the calculations were accurate.
TL;DR
Separate the time value from the date value if you want to use indexes in your search (you probably should, for performance). You can: (1) use function-based indexes or (2) create a new column for time only, index this column and use it in you SELECT clause.
Keep in mind you will lose any index performance boost if you use functions in a SQL's WHERE clause, the engine has to do a scan search. Just run your query with EXPLAIN SELECT... to confirm this. This happens because the engine has to process EVERY value in the field for EACH comparison, and the converted value is not indexed.
Most answers say to use float(), convert(), cast(), addtime(), etc.. Again, your database won't use indexes if you do this. For small tables that may be OK.
It is OK to use functions in WHERE params though (where field = func(value)), because you won't be changing EACH field's value in the table.
In case you want to keep use of indexes, you can create a function-based index for the time value. The proper way to do this (and support for it) may depend on your database engine. Another option is adding a column to store only the time value and index this column, but try the former approach first.
Edit 06-02
Do some performance tests before updating your database to have a new time column or whatever to make use of indexes. In my tests, I found out the performance boost was minimal (when I could see some improvement) and wouldn't be worth the trouble and overhead of adding a new index.

Fastest way to check date range

I store events in SQLServer 2005 where the time the event occured is important and must be stored in the datebase. What is the fastest way to write the date range check in the where clause to ensure everything on that day is selected?
Currently when #DateStart and #DateEnd are passed in I set #DateStart to midnight and set #DateEnd to the last instant before midnight as the very first thing to catch every possible event on the day.
IF (#DateStart IS NOT NULL)
BEGIN
SET #DateStart = CAST (
( CAST (DATEPART (yyyy,#DateStart) AS NVARCHAR(4)) +'/'+
CAST (DATEPART (mm,#DateStart) AS NVARCHAR(2)) +'/'+
CAST (DATEPART (dd,#DateStart) AS NVARCHAR(2)) +' '+
'00:00:00.000'
)
AS DATETIME)
END
IF (#DateEnd IS NOT NULL)
BEGIN
SET #DateEnd = CAST (
( CAST (DATEPART (yyyy,#DateEnd) AS NVARCHAR(4)) +'/'+
CAST (DATEPART (mm,#DateEnd) AS NVARCHAR(2)) +'/'+
CAST (DATEPART (dd,#DateEnd) AS NVARCHAR(2)) +' '+
'23:59:59.997'
)
AS DATETIME
)
END
So the where clause is very easy to read:
WHERE ( EventDate >= #DateStart AND EventDate <= #DateEnd )
Thanks,
You could always use the alternate syntax of WHERE EventDate BETWEEN #DateStart AND #DateEnd
Your where clause would look like;
WHERE DateCol >= DATEADD(dd, DATEDIFF(dd, 0, #DateStart), 0) --Midnight on the Start date
AND DateCol < DATEADD(dd, DATEDIFF(dd, 0, #DateEnd + 1), 0) --Midnight of the day after End date
and all your IF statement would do is handle null parameters (i.e. IF #DateEnd IS NULL THEN SET #DateEnd = #DateStart)
You probably want to Index on DATEADD(dd, DATEDIFF(dd, 0, DateCol), 0) if your table is large.
the fastest way to truncate a date, previous midnight:
DATEADD(day, DATEDIFF(day, '19010101', LastModifiedDate), '19010101')
next midnight:
DATEADD(day, DATEDIFF(day, '19010101', LastModifiedDate)+1, '19010101')
You can also wrap this up as an inline UDF:
http://sqlblog.com/blogs/alexander_kuznetsov/archive/2008/05/23/reuse-your-code-with-cross-apply.aspx
Try this:
WHERE DATEPART(yyyy, EventDate) = DATEPART(yyyy, getdate())
AND DATEPART(dy, EventDate) = DATEPART(dy, getdate()) --day of year
EDIT To address Tom H's comment:
I've never had any luck with indexes on date fields; what has always worked better for me was extra integer columns to handle the year and day of year values and indexed those instead.
AlexK's is probably the best idea all around.
The only thing I would worry about is performance of using functions in the predicate.
You should consider adding a column called searchdate, or something similar, to your table to contain the date without the time. I'd also suggest indexing this column, especially if you're going to be searching against the column's data a lot.
When you query on this column you will not have any scalar functions in your SQL to rob your indexes of their performance.
The con... well, additional storage space, time during insert to write the data (not much here though).
SQL Server 2008 has better support for date only data types. You could also check in on it.
I think this T-SQL is equivalent to the code you have:
-- set time portion of #DateStart back to midnight
SET #DateStart = CONVERT(DATETIME,CONVERT(VARCHAR(10),#DateStart,20),20)
-- advance time portion of #DateEnd to last instant before next midnight
SET #DateEnd = CONVERT(DATETIME,CONVERT(VARCHAR(11),#DateEnd,20)+'23:59:59.997',21)
The CONVERT function will handle NULLS, so there's no need for a separate test for a NULL value (unless, of course, you are doing some special handling other than what you are showing, and are not passing the NULL values through to query predicate (i.e. WHERE clause). Or, perhaps you are expecting a lot of the arguments to be NULL, and you want to avoid the overhead of the calls to CONVERT.
However, I concur with Tom H.'s recommendation, and avoid messing with subtracting milliseconds, and instead set the #DateEnd to midnight of the following day e.g.
-- advance #DateEnd to midnight of following day
SET #DateEnd = DATEADD(day,1,CONVERT(DATETIME,CONVERT(VARCHAR(10),#DateEnd,20),20))
and change the predicate to do a range test like this:
WHERE (EventDate >= #DateStart AND EventDate < #DateEnd)
You can avoid the separate SET statements, and move the expressions straight into the query, but I don't expect that will improve performance any, and make the SQL statement harder to read, you'd definitely want to keep the comments ...
WHERE (EventDate >= CONVERT(DATETIME,CONVERT(VARCHAR(10),#DateStart,20),20)
AND EventDate

Limiting a date range with exactness in MS SQL / SQL Server 2005

I want to limit a report to return records from Date A through Date B. This is what I have been doing:
declare #startDate varchar(20)
declare #endDate varchar(20)
set #startDate = '01/01/2008'
set #endDate = '04/01/2008'
-- test what are the start and end dates
select min(date),max(date) from view_Inspections
where date between #startDate and #endDate
... which I was told returned records from 12 am Jan 1st through 11:59 pm March 31st (that midnight is the default when no time is indicated). But I noticed a discrepancy, which is if a record has a time of 00:00:00 that it will be part of this set.
Is there a more exact way of doing this so it will return exactly the date range I want?*
I tried using time:
declare #startDate varchar(20)
declare #endDate varchar(20)
set #startDate = '01/01/2008 00:00:00'
set #endDate = '04/01/2008 11:59:59'
-- test what are the start and end dates
select min(date),max(date) from view_Inspections
where date between #startDate and #endDate
... but I noticed something wonky: SQL Server will ROUND the hundreth-second up by half. So I get that April 1st record (ha! April Fool's record! grr) if I use any time later than 11:59:29. Why is that?
(I feel sure there is. I'm new at this. Thanks for your help!)
There's always the easy option:
declare #startDate varchar(20)
declare #endDate varchar(20)
set #startDate = '01/01/2008'
set #endDate = '04/01/2008'
-- test what are the start and end dates
select min(date),max(date) from view_Inspections
where date >= #startDate
and date < #endDate
I suspect that the date column in view_Inspections is a SmallDateTime data type. This data type has 1 minute accuracy, which explains your unexpected results (rounding the seconds to the nearest minute).
The method Roland Shaw suggests is the best way to modify your query to accommodate your requirements.
The BETWEEN operator is inclusive, which is why you're seeing the results that you are in your first query. The rounding that you're seeing in your second query is going to be dependent on what exact datetime data type you are using in your table. (BTW, I think you're confusing seconds with hundredths of seconds). It looks like you're probably using a smalldatetime in your table, in which case the time is rounded to the nearest minute.
If your table is using datetime, try explicitly converting your #startDate and #endDate to DATETIME values (CAST(#endDate AS DATETIME)).
A quick note... even for DATETIME values, SQL Server is only accurate to the 3/100ths of a second, so 11:59:59.999 will get rounded up to 12:00:00.000.
You basically have three choices:
1) BETWEEN CAST('01/01/2008 00:00:00.000' AS DATETIME) AND CAST('03/31/2008 12:59:59.997' AS DATETIME)
2) YEAR(my_date) = 2008 AND MONTH(my_date) BETWEEN 1 AND 3
3) my_date >= CAST('01/01/2008 00:00:00.000' AS DATETIME) AND my_date < CAST('04/01/2008 00:00:00.000' AS DATETIME)
The first method isn't very intuitive and is error-prone in my opinion. The second method kills performance since indexes can't be used and it becomes much more complex if you can have searches that span years or begin/end in the middle of months. The third method, which Rowland suggested, is the best I think.
Simply try removing the time from the date field like so:
declare #startDate varchar(20)
declare #endDate varchar(20)
set #startDate = '01/01/2008'
set #endDate = '04/01/2008'
SELECT min(date),max(date) FROM view_Inspections
WHERE CAST(FLOOR(CAST(date AS FLOAT)) AS DATETIME) BETWEEN CAST(#startDate AS DATETIME) And CAST(#startDate AS DATETIME))
This will return everything from 01/01/2008 00:00:00 to 04/01/2008 11:59:59.999.
If you don't want 04/01 included, change your end date to 03/31/2008.
Your best solution is just create a BIGINT(10) field that called "julian", and store it in YYYYMMDD.
Then do the query
where julian >= '20120103' AND julian <= '20120203'