SWAT Blog

How To query contiguous data ranges – SQL Server

January 12th, 2011 by Shailendra Parate

Often times we are confronted with problems that seem very easy to start with but once you get your teeth into it, you realise it wasn’t as easy as initially thought.
In this blog, I am attempting to work with data ranges that are not easily groupable.

Problem Statement:


Using the data structure listed below, group the data ranges by dataKey when the dataKey is contigous, when breaks in data key are identified, create a new reporting output line.
Query input-

grp dataRange dataKey Comments
A 1000 1
A 1001 1
A 1002 1
A 1003 1
A 1004 1
A 1005 2 — Notice dataKey is not contiguous
A 1006 2
A 1007 1
A 1008 1
A 1009 1
A 1010 1
A 1011 1

Desired Query output-

dataKey DataRangeBreakDown
1 A [1000 .. 1004]
2 A [1005 .. 1006]
1 A [1007 .. 1011]

Approach:


This is an interesting puzzle, because we cannot simply group the data by “dataKey” column, doing so would ignore the breaks in data. The key to solving this refreshing problem is by “somehow” introducing a pseudo-column which does the heavy lifting for us.
Table below illustrates the new pseudo-column, if we introduce this column using SQL, rest is just simple group by.

Before peeking at the final solution, I would encourage you to have a go, but if you can’t for any reason I have presented final solution later in the blog.

grp dataRange dataKey New Column
A 1000 1 0
A 1001 1 0
A 1002 1 0
A 1003 1 0
A 1004 1 0
A 1005 2 1
A 1006 2 1
A 1007 1 2
A 1008 1 2
A 1009 1 2
A 1010 1 2
A 1011 1 2

Setup Data:

IF OBJECT_ID('tempDB..#tmpDataRange') IS NOT NULL
DROP TABLE #tmpDataRange;
WITH testData AS (
 SELECT 'A' grp, 1000 AS dataRange, 1 AS dataKey UNION ALL
 SELECT 'A', 1001, 1 UNION ALL
 SELECT 'A', 1002, 1 UNION ALL
 SELECT 'A', 1003, 1 UNION ALL
 SELECT 'A', 1004, 1 UNION ALL
 SELECT 'A', 1005, 2 UNION ALL
 SELECT 'A', 1006, 2 UNION ALL
 SELECT 'A', 1007, 1 UNION ALL
 SELECT 'A', 1008, 1 UNION ALL
 SELECT 'A', 1009, 1 UNION ALL
 SELECT 'A', 1010, 1 UNION ALL
 SELECT 'A', 1011, 1
 )
SELECT grp, dataRange, dataKey
INTO #tmpDataRange
FROM testData;

Solution:


Welcome to the solution.
I have tackled the problem using the aproach above using Common Table Expressions (CTE) feature of SQL Server 2005+ Engine. CTEs are great new addition to query toolkit, besides allowing us to do fancy stuff like hierarchical queries, I think CTEs are fantastic even for writing daily garden variety queries as they make the query so much more understandable by being written in the way we digest other information, i.e. Top to Bottom.
Overview of solution query:
Data0 – Instantiate the problem statement input data
Data1 – Assign incremental row numbers to input data
Data2 – Self join the data to the previous row
Data3 – This is where the “magic” happens – Build a cumulative data column which increments on every dataKey break
Data4 and Data5 – Are concerned with grouping and presenting the data in format requested

Presented below is the final solution:

WITH
data0 AS (SELECT grp, dataRange, dataKey FROM #tmpDataRange),
data1 AS (SELECT UPPER(grp) grp,dataRange,dataKey, ROW_NUMBER()OVER(ORDER BY dataRange ASC) rNum FROM data0),
data2 AS (SELECT A.grp,A.dataRange,A.dataKey,A.rNum,
 CASE WHEN A.dataKey=ISNULL(B.dataKey,A.dataKey)THEN 0 ELSE 1 END cumuWindow
 FROM data1 A LEFT JOIN data1 B ON A.rNum=b.rNum+1),
data3 AS (SELECT o.grp,o.dataRange,o.dataKey,
 (SELECT SUM(i.cumuWindow) FROM data2 i WHEREi.rNum <= o.rNum) cumuWindow
 FROM data2 o),
data4 AS (SELECT dataKey,cumuWindow,MIN(grp) grp, MIN(dataRange)mn, MAX(dataRange) mx
 FROM data3
 GROUP BY dataKey,cumuWindow)
SELECT dataKey, grp + '['+CAST(mn AS VARCHAR(10))+'..'+CAST(mx ASVARCHAR(10))+']' DataRangeBreakDown
FROM data4

Variations:
One of the great things about this solution is that it is data-type agnostic. Feel free to change the data-type of the dataRange column to DATE for example as shown below and it still works.

IF OBJECT_ID('tempDB..#tmpDataRange') IS NOT NULL
DROP TABLE #tmpDataRange;
WITH testData AS (
 SELECT 'A' grp, GETDATE() AS dataRange, 1 AS dataKey UNION ALL
 SELECT 'A', GETDATE()+1, 1 UNION ALL
 SELECT 'A', GETDATE()+2, 1 UNION ALL
 SELECT 'A', GETDATE()+3, 1 UNION ALL
 SELECT 'A', GETDATE()+4, 1 UNION ALL
 SELECT 'A', GETDATE()+5, 2 UNION ALL
 SELECT 'A', GETDATE()+6, 2 UNION ALL
 SELECT 'A', GETDATE()+7, 1 UNION ALL
 SELECT 'A', GETDATE()+8, 1 UNION ALL
 SELECT 'A', GETDATE()+9, 1 UNION ALL
 SELECT 'A', GETDATE()+10, 1 UNION ALL
 SELECT 'A', GETDATE()+11, 1
 )
SELECT grp, dataRange, dataKey
INTO #tmpDataRange
FROM testData;

Would love to hear your thoughts on this, or if you have a better solution why not share it with everyone.

I am sure you would like to know how the same problem can be solved in Oracle. Find it here

Tags: , ,

One Response to “How To query contiguous data ranges – SQL Server”

Leave a Reply

  • © 2004-2015 Special Work & Technology Limited