While fixing a performance issue with a customer report yesterday, there was a unique requirement that came up. As the items in the warehouse move through the aisles, their datetime timestamp values get changed. In one of the sub-reports, the requirement was to display both the latest record as well as the oldest record in the system i.e. both the first and the last record based on the timestamp column. Also, another thing to add to this was that if the location for the item was not decided yet, then the those items needed to be ranked lower then those items which have a location assigned to them.
There are a couple of ways to resolve this kind of a scenario using SQL. We will present the solution in this post using T-SQL syntax though the same applies (with brief changes to Oracle and DB2 LUW as well - another thing to note also is that Oracle already has analytic functions: first_value() and last_value() that can be used with window functions to easily get these results).
Even though the actual query had a lot of table joins, for the sake of simplicity and to help explain the concept and protect the IP of the client, we will take up an example of a table variable:
set nocount on
go
declare @inventory table (item_id int not null, location_id int null, create_date_time datetime not null)
insert into @inventory values (1, null, cast(’06/27/2007 12:00:00′ as datetime))
insert into @inventory values (2, 10, cast(’06/27/2007 12:15:00′ as datetime))
insert into @inventory values (3, 20, cast(’06/26/2007′ as datetime))
insert into @inventory values (4, 30, cast(’06/21/2007′ as datetime))
insert into @inventory values (5, null, cast(’06/27/2007 1:00:00′ as datetime))
insert into @inventory values (6, 50, cast(’06/23/2007′ as datetime))
insert into @inventory values (7, 70, cast(’06/24/2007′ as datetime))
insert into @inventory values (8, 80, cast(’06/25/2007′ as datetime))
insert into @inventory values (9, 40, cast(’06/27/2007 3:00:00′ as datetime))
insert into @inventory values (10, null, cast(’06/27/2007 12:30:00′ as datetime))
select ‘***ALL Records***’, * from @inventory
item_id location_id create_date_time
----------------- ----------- ----------- -----------------------
***ALL Records*** 1 NULL 2007-06-27 12:00:00.000
***ALL Records*** 2 10 2007-06-27 12:15:00.000
***ALL Records*** 3 20 2007-06-26 00:00:00.000
***ALL Records*** 4 30 2007-06-21 00:00:00.000
***ALL Records*** 5 NULL 2007-06-27 01:00:00.000
***ALL Records*** 6 50 2007-06-23 00:00:00.000
***ALL Records*** 7 70 2007-06-24 00:00:00.000
***ALL Records*** 8 80 2007-06-25 00:00:00.000
***ALL Records*** 9 40 2007-06-27 03:00:00.000
***ALL Records*** 10 NULL 2007-06-27 12:30:00.000
select top 1 ‘***LAST RECORD***’ as Last_Record, * from @inventory as I order by
case when location_id is null then 1 else 0 end ASC
,create_date_time desc
Last_Record item_id location_id create_date_time
----------------- ----------- ----------- -----------------------
***LAST RECORD*** 2 10 2007-06-27 12:15:00.000
select top 1 ‘***FIRST RECORD***’ as First_Record, * from @inventory as I order by
case when location_id is null then 1 else 0 end ASC
,create_date_time asc
First_Record item_id location_id create_date_time
------------------ ----------- ----------- -----------------------
***FIRST RECORD*** 4 30 2007-06-21 00:00:00.000
If you look at the output from above, you can see that the last record that is picked up by the logic is Item #2 even though Item #10 has the latest create_date_time timestamp value. The reason for this is that we had the requirement that if an item has not been assigned a location yet, that should rank lower. So, the above 2 SQL statements are just to show how we can get the first and the last records in a SQL statement. Now, in the report, we needed to get these two together and as you know, if one uses the UNION or UNION ALL clause between two statements, one can use only one ORDER BY clause which applies to the whole set. So, it is not as straight forward as just doing a UNION/UNION ALL operation between the two sets and applying an ORDER BY.
One of the solutions is to use derived tables:
select LR.* from
(select top 1 ‘***LAST RECORD***’ as Last_Record, * from @inventory as I order by
case when location_id is null then 1 else 0 end ASC
,create_date_time desc) as LR
UNION ALL
select FR.* from
(select top 1 ‘***FIRST RECORD***’ as First_Record, * from @inventory as I order by
case when location_id is null then 1 else 0 end ASC
,create_date_time asc) as FR
Last_Record item_id location_id create_date_time
------------------ ----------- ----------- -----------------------
***LAST RECORD*** 2 10 2007-06-27 12:15:00.000
***FIRST RECORD*** 4 30 2007-06-21 00:00:00.000
Another solution is to use CTE (Common Table Expressions):
with results as
(select top 1 ‘***LAST RECORD***’ as Last_Record, * from @inventory as I order by
case when location_id is null then 1 else 0 end ASC
,create_date_time desc
UNION ALL
select top 1 ‘***FIRST RECORD***’ as First_Record, * from @inventory as I order by
case when location_id is null then 1 else 0 end ASC
,create_date_time asc
)
select * from results;
Last_Record item_id location_id create_date_time
------------------ ----------- ----------- -----------------------
***LAST RECORD*** 2 10 2007-06-27 12:15:00.000
***FIRST RECORD*** 4 30 2007-06-21 00:00:00.000
Yet another solution can be the usage of the row_number() analytic function, for example:
select item_id, location_id, create_date_time
from
(
select
*
, row_number() over (order by case when location_id is null then 1 else 0 end ASC
,create_date_time desc) as RN_1
, row_number() over (order by case when location_id is null then 1 else 0 end ASC
,create_date_time asc) as RN_2
from @inventory) as IV
where RN_1 = 1 or RN_2 = 1
item_id location_id create_date_time
----------- ----------- -----------------------
4 30 2007-06-21 00:00:00.000
2 10 2007-06-27 12:15:00.000
There are other solutions as well to this problem. One needs to test these out in one’s scenario and make sure that you have meaningful filter criterias and good execution plans. The next challenge in this report was that the project manager wanted these records listed side by side on the same record. That is pretty simple to do as well - using either pivoting or even simple join between derived tables with a match on the row_number() analytic function value. We leave that as an exercise to the reader and if you have questions, we will be more than happy to post the answer to that as well.