This is a bit rant-y, but… indulge me. I’ve been writing/refactoring a lot of old reporting queries. And, like most reports, they deal with
datetimes — as parameters, boundaries, or
join predicates. I also got way too intense with a recent SSC post (Sql Server Central), which fueled the fire even more.
SQL Server is very good at handling temporal datatypes and calculations against them. We’ve got functions like
time, etc. It supports all sorts of format conversions if you need to display them in various ways.
..even though that should be left to the presentation layer!
Here’s the issue. Well, there are several issues, but we only have time for a few.
Here’s the first problem
Report users don’t understand the “end of a time period” problem. I don’t have a good name for it; others might call it the “Day plus one” problem or the “Less than date” problem. What do I mean by this? Well, let’s back up a bit, to DBA Commandment #6, “Thou shalt not use
datetimes.” In order to first understand the issue, we have to understand why this is a commandment.
When a date range, or time period, is specified in SQL, it’s easiest, clearest, and most concise, to specify it like so:
@TheDate >= @StartOfPeriod and @TheDate < @StartOfNextPeriod. Mathematically speaking, we’re defining the range as “closed on the left, open on the right”. In other words,
Min <= X < Max.
The reason we do this with
datetimes is found right there in the name of the datatype — it has (or can have) a time component!
Let’s talk examples
Say you’d like to report on the month of March 2017. How do you determine if your data-points (stored as
datetime or, hopefully,
datetime2) are within that period, that month? Well sure, you could write
where month(MyDateColumn) = 3 and year(myDateColumn) = 2017 …
NO. That is horrible, don’t do that.
SARGable and renders your index on that column useless. (You do have an index on it, don’t you? No? Make one!) Okay, let’s stick with something
SARGable. How about
MyDateColumn between '20170301' and '2017-03-31T23:59:55.999'? (You did read this post about using culture-neutral
datetime literals right?) But wait! If your data is a
datetime, it’s not actually that precise — your literal gets rounded up to
20170401 and you’re now including dates from April 1st (at midnight)!
Oh that’ll never happen… until it does.
Many developers and report-writers assume that the values in their data will never be within the typical “1 second before midnight” or “1/300th of a second before midnight” escape window of your “3/31/2017 23:59:59.997” bounding value. But can you guarantee that? Didn’t think so. Worse, if you use the .999 fraction as given in the 2nd example, you’re either “more” or “less” correct, and nobody can actually tell you which way that pendulum swings because it depends on the statistical likelihood of your data having actual literal “midnight” values vs. realistic (millisecond-y, aka “continuous”) values. Sure, if you’re storing just a
date, these things become a lot less complicated and more predictable.
But then why aren’t you storing it as an actual
date, not a
So what’s the right answer?
As I said, “greater than or equal to ‘Start’, and less than ‘End'”, where ‘End’ is the day after the end of the period, at midnight (no later!). Hence,
MyDateColumn >= '20170301' and MyDateColumn < '20170401'. Simple, yes?
But wait, there’s more!
I mentioned “date-pickers” in the title. When it comes to UX, date-pickers are a sore subject, and rightly so — it’s difficult to truly “get it right”. On a “desktop-ish” device (i.e. something with a keyboard), it may be easiest on the user to give them a simple text-box which can handle various formats and interpret them intelligently — this is what SSRS does. But on mobile devices, you often see those “spinner” controls, which is a pain in the arse when you have to select, say, your birth date and the “Year” spinner starts at 2017. #StopIt
I mean, I’m not that old, but spinning thru a few decades is still slower than just typing 4 digits on my keyboard — especially if your input-box is smart enough to flip my keyboard into “numeric only” mode.
Another seemingly popular date-picker UX is the “calendar control”. Oh gawd. It’s horrible! Clicking thru pages and pages of months to find and click (tap?) on an itty bitty day box, only to realize “Oh crap, that was the wrong year… ok let me go back.. click, click, tap..” ad-nauseum.
The point here is, use the type of date-picker that’s right for the context. If it’s meant to be a date within a few days/weeks of today, past/future — OK, spinner or calendar is probably fine. If it’s a birth date or something that could reasonably be several years in the past or future, just give me a damn box. (Heck, I’ll take a series of 3 boxes, M/D/Y or Y/M/D, as long as they’re labeled and don’t break when I omit the leading-zero from a single-digit month #!) If there’s extra pre-validation logic that “blocks out” certain dates (think bill-payer calendars or Disneyland annual-pass blackout-days), that probably needs to be a calendar too.
..just make sure it’s responsive on a mobile device.
And in all cases, pass that “ending date” to your SQL queries in a consistent, logical, sensible manner. For reporting, where the smallest increment of a period is 1 day, that probably means automagically “adding 1 day” to their given end-date, because the end-user tends to think in those terms. I.e. if I say “show me my bank activity from 1/1/2017 to 1/31/2017”, I really mean “through the end of the month“, i.e. the end of the day of 1/31. So your query is going to end up wanting the end-date parameter to be 2/1/2017, because it’s using the correct & consistent “greater than or equal to start, and less than start-of-next” logic.
I know it’s not easy to explain to business folks, and it’s not easy to implement correctly. But it’s important. The
>= & < logic is clear, concise, and can be used consistently regardless of underlying datatype. You just need to adjust your presentation layer (whether that’s SSRS parameters or a .NET date-picker) to convey their intent to the user, whether that’s “show/enter the last day of the month, but translate to the next day to feed to the query/proc.”, or “make them enter the next-day (day after the end of the month/period) and understand the ‘less than’ logic.” I’m more inclined to the first, but it depends on your audience.
Thanks for reading, and happy