Fun with Date Ranges

I was in a discussion recently where the product manager of an application asked me how the date-matching logic should work in the application. In context, this application is implementing a general-purpose search capability against business records. We spent some time discussing just what a user might expect from a search that includes a date range.

After years working with the business users, I happen to know quite a bit about this particular data and the implied meaning of the search range provided. But this gives me an opportunity to have some fun with date-range searches and making sure we are building and delivering just what we think our business desires.

Test Tables

Here are some tables I'll use for this discussion and some test queries. At the end of this article you'll find the insert statements to load the data if you want to play with this on your own. We have a chain of gyms that have members. Each member has a membership that is valid when pre-paid for a given time period. The gyms hold regular classes and track attendance at each class.
CREATE TABLE members
( member_id integer not null
, first_name varchar(30) not null
, last_name varchar(30) not null
, email varchar(255)
, PRIMARY KEY (member_id)
);
CREATE TABLE memberships
( member_id integer not null
, gym_id integer not null
, contract_id integer not null
, begin_date date not null
, end_date date not null
, dues_amount decimal(10,2)
, PRIMARY KEY (member_id, gym_id, contract_id)
);
CREATE TABLE classes
( class_id integer not null
, class_name varchar(255) not null
, begin_date date not null
, end_date date not null
, PRIMARY KEY (class_id)
);
CREATE TABLE attendance
( class_id integer not null
, member_id integer not null
, attended_date date
, PRIMARY KEY (class_id, member_id, attended_date)
);

Questions, Queries and Answers

The owners of our gyms could ask many questions about the data in these tables. We'll start out with a few fairly easy questions.

Who attended classes on 2017-02-01?

This is easy enough. The question specifes a single date and the Attendance table should have a row with matching dates in it. A simple equal predicate gets us the list of members that attended class on that date.
SELECT m.first_name, m.last_name
FROM members m
JOIN attendance a
ON a.member_id = m.member_id
WHERE a.attended_date = '2016-02-01';
| first_name | last_name |
|------------|-----------|
| John | Smith |
| Nancy | Smith |

How many people attended each class in January 2017?

Still easy. This time we need to compare the Attendance dates against the range in the Classes table. Since we're only matching a single date to a date range a simple BETWEEN clause should cover it. Our date range is the month of January and we really just need to count the attendance in the classes for January.
SELECT c.class_id, c.class_name, count(*)
FROM classes c
JOIN attendance a
ON a.class_id = c.class_id
WHERE a.attended_date BETWEEN '2016-01-01' AND '2016-01-31'
GROUP BY c.class_id, c.class_name;
| class_id | class_name  | count |
|----------|-------------|-------|
| 1601 | Running 101 | 58 |
Maybe this wasn't so easy since the owners say we don't have 58 members yet. Maybe the question just wasn't phrased well and they really wanted the following.

How many distinct members attended each class in January 2017?

Ah, "distinct members". You can see how easy it is to answer the wrong question. Our previous answer counted people in classes, but re-counted each member for each date they attended. That's simple enough to fix.

SELECT c.class_id, c.class_name, count(distinct member_id)
FROM classes c
JOIN attendance a
ON a.class_id = c.class_id
WHERE a.attended_date BETWEEN '2016-01-01' AND '2016-01-31'
GROUP BY c.class_id, c.class_name;
| class_id | class_name  | count |
|----------|-------------|-------|
| 1601 | Running 101 | 2 |

Is this right? In this case, yes, it is. Confirms how important it is that we keep in mind the context the business is using when they ask their questions.

Which classes did John Smith attend during his initial membership?

We'll make some assumptions about the stability of the data and use that to get the "initial" membership information for John Smith. First we need to find the range of dates for John's initial membership period. There are several ways to do this, I'll just use a subselect in the predicate. And, because we're focusing on matching date logic in this article, I'll use MIN(begin_date) to find that initial membership.
SELECT c.class_id, c.class_name, a.attended_date
FROM members m
JOIN memberships s
ON s.member_id = m.member_id
JOIN attendance a
ON a.member_id = m.member_id
JOIN classes c
ON a.class_id = c.class_id
WHERE m.first_name = 'John'
AND m.last_name = 'Smith'
AND a.attended_date BETWEEN s.begin_date AND s.end_date
AND s.begin_date = ( SELECT min(begin_date)
FROM memberships x
WHERE x.member_id = m.member_id
);
| class_id | class_name  | attended_date |
|----------|-------------|---------------|
| 1301 | Running 101 | 2013-06-15 |

Just a single class. Seems like John wasn't really that interested in running his first year out.

What classes were available to John Smith?

So much for the easy questions. This is where we have to think a little bit deeper. We have two overlapping date ranges that need to be compared. John Smith had at least one membership period with distinct begin and end dates. Many Classes were held and each has its own begin and end dates. Let's take this in stages and build our complex query carefully.

First, we need the dates for each of John's each membership periods.

SELECT m.first_name, m.last_name
, s.begin_date, s.end_date
FROM members m
JOIN memberships s
ON m.member_id = s.member_id
WHERE m.member_id = 100;
| first_name | last_name | begin_date | end_date   |
|------------|-----------|------------|------------|
| John | Smith | 2014-06-01 | 2015-05-31 |
| John | Smith | 2013-06-01 | 2014-05-31 |
| John | Smith | 2015-06-01 | 2016-05-31 |
| John | Smith | 2016-06-01 | 9999-12-31 |

Second, we need to find classes that were available within a given date range. This on its own is tricky.

Can we use the BETWEEN clause on the begin dates to compare them? Let's take one of the rows from John's membership list and see.

SELECT c.class_id, c.class_name, c.begin_date, c.end_date
FROM classes c
WHERE '2015-06-01' BETWEEN c.begin_date AND c.end_date
OR '2016-05-31' BETWEEN c.begin_date AND c.end_date;
| class_id | class_name      | begin_date | end_date   |
|----------|-----------------|------------|------------|
| 10 | Running Anytime | 0001-01-01 | 9999-12-31 |
| 1501 | Running 101 | 2015-01-01 | 2015-06-30 |
| 1601 | Running 101 | 2016-01-01 | 2016-06-30 |

Is it correct? Let's see what gets returned compared with our initial range of '2015-06-01' thru '2016-05-31'. Class 1501 overlaps with John's first membership month in 2015 and class 1601 overlaps with his last 5 months in 2016. That's only 6 of the 12 months of that membership, so we are definitely missing something. I'll come back to that Running Anytime class in a few minutes. Let's try looking at it with a different BETWEEN comparison.

SELECT c.class_id, c.class_name, c.begin_date, c.end_date
FROM classes c
WHERE c.begin_date BETWEEN '2015-06-01' AND '2016-05-31'
OR c.end_date BETWEEN '2015-06-01' AND '2016-05-31';
| class_id | class_name      | begin_date | end_date   |
|----------|-----------------|------------|------------|
| 1501 | Running 101 | 2015-01-01 | 2015-06-30 |
| 1502 | Running 102 | 2015-07-01 | 2015-12-31 |
| 1601 | Running 101 | 2016-01-01 | 2016-06-30 |
| 1610 | Running Special | 2016-01-01 | 2016-01-31 |
That first query against classes definitely missed classes 1502 and 1610. So we know that using BETWEEN the first way didn't work because it was only hitting classes the overlapped membership begin/end dates.
Does this second query do it all? This one picks up two additional classes that occur completely within the membership dates. But now we're missing the Running Anytime class.
| class_id | class_name      | begin_date | end_date   |
|----------|-----------------|------------|------------|
| 10 | Running Anytime | 0001-01-01 | 9999-12-31 |
Let's step back a minute and look at the issue we have with these queries and why they missed what they did.

date-range-search-1.jpg

The first query picked up Cases B, C, E because they all overlap the edges of the search range. It overlooked Case D where the entire class was within the search range.
The second query picked up Cases B, D, E because they have an end-point inside the search range. It overlooked Case C because the end-points are both outside of the search range.

We can combine the two queries and give that a try.

SELECT c.class_id, c.class_name, c.begin_date, c.end_date
FROM classes c
WHERE '2015-06-01' BETWEEN c.begin_date AND c.end_date
OR '2016-05-31' BETWEEN c.begin_date AND c.end_date
OR c.begin_date BETWEEN '2015-06-01' AND '2016-05-31'
OR c.end_date BETWEEN '2015-06-01' AND '2016-05-31';
| class_id | class_name      | begin_date | end_date   |
|----------|-----------------|------------|------------|
| 10 | Running Anytime | 0001-01-01 | 9999-12-31 |
| 1501 | Running 101 | 2015-01-01 | 2015-06-30 |
| 1502 | Running 102 | 2015-07-01 | 2015-12-31 |
| 1601 | Running 101 | 2016-01-01 | 2016-06-30 |
| 1610 | Running Special | 2016-01-01 | 2016-01-31 |

That returns all of the rows but seems a little awkward to understand and four OR clauses can't possibly be efficient can it? Ok, let's drop the use of BETWEEN and go with basic logic.

SELECT c.class_id, c.class_name, c.begin_date, c.end_date
FROM classes c
WHERE ( c.begin_date <= '2016-05-31' AND c.end_date >= '2015-06-01' )
This compares the class begin date against the search end date, matching Cases A, B, C, D, E.
Then it compares class end date against the search begin date, matching Cases B, C, D, E, F.
The logical AND of these two sub-sets of the data gives us Cases B, C, D, E and excludes Cases E, F.
Now we just need to put it all together to answer the original question.
SELECT distinct m.first_name, m.last_name
-- , s.begin_date, s.end_date
, c.class_id, c.class_name
, c.begin_date as class_begin_date
, c.end_date as class_end_date
FROM members m
JOIN memberships s
ON m.member_id = s.member_id
LEFT JOIN classes c
ON ( c.begin_date <= s.end_date AND c.end_date >= s.begin_date )
WHERE m.member_id = 100
ORDER BY c.class_id;
| first_name | last_name | class_id | class_name      | class_begin_date | class_end_date |
|------------|-----------|----------|-----------------|------------------|----------------|
| John | Smith | 10 | Running Anytime | 0001-01-01 | 9999-12-31 |
| John | Smith | 1301 | Running 101 | 2013-01-01 | 2013-06-30 |
| John | Smith | 1302 | Running 102 | 2013-07-01 | 2013-12-31 |
| John | Smith | 1401 | Running 101 | 2014-01-01 | 2014-06-30 |
| John | Smith | 1402 | Running 102 | 2014-07-01 | 2014-12-31 |
| John | Smith | 1501 | Running 101 | 2015-01-01 | 2015-06-30 |
| John | Smith | 1502 | Running 102 | 2015-07-01 | 2015-12-31 |
| John | Smith | 1601 | Running 101 | 2016-01-01 | 2016-06-30 |
| John | Smith | 1602 | Running 102 | 2016-07-01 | 2016-12-31 |
| John | Smith | 1610 | Running Special | 2016-01-01 | 2016-01-31 |
| John | Smith | 1701 | Running 101 | 2017-01-01 | 2017-06-30 |
| John | Smith | 1702 | Running 102 | 2017-07-01 | 2017-12-31 |
| John | Smith | 1801 | Running 101 | 2018-01-01 | 2018-06-30 |
| John | Smith | 1802 | Running 102 | 2018-07-01 | 2018-12-31 |

Can I get a list of members and the date they became members?

Just another example of getting the minimum membership date and using it.

WITH list(member_id, begin_date)
AS ( SELECT member_id
, MIN(begin_date) as begin_date
FROM memberships
GROUP BY member_id
)
SELECT m.first_name, m.last_name
, s.begin_date as member_since
FROM members m
JOIN memberships s
ON m.member_id = s.member_id
WHERE (s.member_id, s.begin_date) IN ( SELECT member_id, begin_date FROM list )
ORDER BY member_since
| first_name | last_name | member_since |
|------------|-----------|--------------|
| John | Smith | 2013-06-01 |
| Nancy | Smith | 2015-06-01 |

Enough Fun

That's enough fun for today.
Keep in mind the need to think like your customer when building your queries and test your SQL. Just because it runs doesn't mean you got the right answer. And just because you received the right answer doesn't mean your SQL was correct either. It could have been that your test data didn't have those edge cases that would prove your accuracy.
One last point. I intentionally used dates '0001-01-01' and '9999-12-31' instead of nulls. If I had used nulls in the table design then the query logic would have been significantly more complicated. And, we'd have been much more likely to get it wrong. But that's a topic for another day, specifically 2017-10-02 if you're attending IDUG EMEA.

SQL to Insert Data

 INSERT INTO members(member_id, first_name, last_name, email)
VALUES (100, 'John', 'Smith', 'johnsmith123456789@gmail.com');
INSERT INTO members(member_id, first_name, last_name, email)
VALUES (200, 'Nancy', 'Smith', NULL);
INSERT INTO members(member_id, first_name, last_name, email)
VALUES (300, 'Jim', 'Brown', NULL);

INSERT INTO memberships(member_id, gym_id, contract_id, begin_date, end_date, dues_amount)
VALUES (100, 1, 200, '2014-06-01', '2015-05-31', 100.00);
INSERT INTO memberships(member_id, gym_id, contract_id, begin_date, end_date, dues_amount)
VALUES (100, 1, 100, '2013-06-01', '2014-05-31', 60.00);
INSERT INTO memberships(member_id, gym_id, contract_id, begin_date, end_date, dues_amount)
VALUES (100, 1, 300, '2015-06-01', '2016-05-31', 100.00);
INSERT INTO memberships(member_id, gym_id, contract_id, begin_date, end_date, dues_amount)
VALUES (100, 1, 400, '2016-06-01', '9999-12-31', 1000.00);
INSERT INTO memberships(member_id, gym_id, contract_id, begin_date, end_date, dues_amount)
VALUES (200, 1, 300, '2015-06-01', '2016-05-31', 100.00);

INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (0010, 'Running Anytime', '0001-01-01', '9999-12-31');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1610, 'Running Special', '2016-01-01', '2016-01-31');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1101, 'Running 101', '2011-01-01', '2011-06-30');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1102, 'Running 102', '2011-07-01', '2011-12-31');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1201, 'Running 101', '2012-01-01', '2012-06-30');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1202, 'Running 102', '2012-07-01', '2012-12-31');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1301, 'Running 101', '2013-01-01', '2013-06-30');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1302, 'Running 102', '2013-07-01', '2013-12-31');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1401, 'Running 101', '2014-01-01', '2014-06-30');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1402, 'Running 102', '2014-07-01', '2014-12-31');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1501, 'Running 101', '2015-01-01', '2015-06-30');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1502, 'Running 102', '2015-07-01', '2015-12-31');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1601, 'Running 101', '2016-01-01', '2016-06-30');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1602, 'Running 102', '2016-07-01', '2016-12-31');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1701, 'Running 101', '2017-01-01', '2017-06-30');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1702, 'Running 102', '2017-07-01', '2017-12-31');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1801, 'Running 101', '2018-01-01', '2018-06-30');
INSERT INTO classes(class_id, class_name, begin_date, end_date)
VALUES (1802, 'Running 102', '2018-07-01', '2018-12-31');

INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-02');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-03');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-04');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-05');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-06');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-07');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-08');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-09');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-10');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-11');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-12');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-13');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-14');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-15');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-16');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-17');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-18');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-19');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-20');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-21');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-22');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-23');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-24');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-25');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-26');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-27');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-28');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-29');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-01-30');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-02-01');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-02-02');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-02-03');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-02-04');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-02-05');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-02-06');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-02-07');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-02-08');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-02-09');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 100, '2016-02-10');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-02');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-03');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-04');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-05');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-06');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-07');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-08');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-09');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-10');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-11');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-12');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-13');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-14');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-15');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-16');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-17');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-18');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-19');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-20');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-21');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-22');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-23');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-24');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-25');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-26');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-27');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-28');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-29');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-01-30');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-02-01');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-02-02');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-02-03');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-02-04');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-02-05');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-02-06');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-02-07');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-02-08');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-02-09');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1601, 200, '2016-02-10');
INSERT INTO attendance(class_id, member_id, attended_date)
VALUES (1301, 100, '2013-06-15');
Recent Stories
Db2 for z/OS locking basics

A Beginners Guide to Locks, Latches, Claims and Drains in DB2 for z/OS by Steve Thomas

Fun with Date Ranges