SQL Insert Set of Values Optimized - sql

The goal is to create a table with sample data in Teradata for a year. A way to do it, is to copy the first 6 entries of similar data and just alter the timestamp 365 times (for my usecase).
I didn't know better and wrote a procedure
REPLACE PROCEDURE stackoverflow()
BEGIN
DECLARE i INTEGER DEFAULT 0;
DELETE FROM TestTable;
WHILE i < 365 DO
INSERT INTO TestTable
SELECT
TestName
,Name_DT + i
FROM
(SELECT TOP 6
*
FROM TestTable2
WHERE Name_DT = '2021-12-15') AS sampledata;
SET i = i + 1;
END WHILE;
END;
This works, but is awfully slow. Also the IT department doesn't want us to use procedures. Is there a better way to achieve the same result without a procedure?

The generic way to get repeated data is a CROSS JOIN:
SELECT
TestName
,calendar_date
FROM
( SELECT TOP 6 *
FROM TestTable2
WHERE Name_DT = DATE '2015-12-15'
) AS sampledata
CROSS JOIN
( SELECT calendar_date
FROM sys_calendar.CALENDAR
WHERE calendar_date BETWEEN DATE '2011-12-15'
AND DATE '2011-12-15' + 364
) AS cal
;
In your case there's Teradata's proprietary EXPAND ON syntax to create time series:
SELECT TestName, Begin(pd)
FROM
( SELECT TOP 6 *
FROM TestTable2
WHERE Name_DT = DATE '2015-12-15'
) AS sampledata
-- create one row per day in the range
EXPAND ON PERIOD(Name_DT, Name_DT +365) AS pd

Related

Incorrect Syntax near With

No matter where I place my With statement inside the SQL query, the keyword in the next line always shows an error, 'Incorrect syntax near keyword'. I also tried putting semi-colon.
; WITH Commercial_subset AS
(
SELECT DISTINCT
PRDID_Clean, Value, [Year]
FROM
Reporting_db_SPKPI.DBO.[tbl_RCCP_commercial]
WHERE
MEASURE = 'Unit Growth Rate'
)
--error appears at truncate
TRUNCATE TABLE Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_dup]
Example 1:
[Example 1][1]
Example 2:
[Example 2][2]
What am I missing?
[1]: https://i.stack.imgur.com/lkfVd.png
[2]: https://i.stack.imgur.com/tZRnG.png
My Final code after getting suggestions in the comments,
--Ensure the correct database is selected for creating the views
USE Reporting_db_SPKPI
--Create the table where new values will be appended
Insert into Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_dup]
Select *, Replace(productID,'-','') as ProductID_clean from Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR]
GO
--Create a subset as view which will be used for join later
Create or Alter View QRY_Commerical_Subset AS
Select Distinct PRDID_Clean, Value, [Year] From Reporting_db_SPKPI.DBO.[tbl_RCCP_commercial] where MEASURE = 'Unit Growth Rate'
Go
--Create a view with distinct list of all SKUs
CREATE OR ALTER VIEW QRY_RCCP_TEMP AS
SELECT
PRODUCTID, ROW_NUMBER() Over (ORDER BY ProductID) AS ID
FROM (
SELECT
DISTINCT A.ProductID_clean ProductID
FROM
Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_dup] A
LEFT JOIN
Reporting_db_SPKPI.DBO.QRY_Commerical_Subset B ON A.ProductID_clean = B.PRDID_Clean
WHERE
B.PRDID_Clean IS NOT NULL --and A.filename = 'Capacity Planning_INS_Springhill' --DYNAMIC VARIABLE HERE
and Cast(A.SnapshotDate as date) =
(SELECT Max(Cast(SnapshotDate as date)) FROM reporting_db_spkpi.dbo.tbl_RCCP_3_NR)
) T
GO
SET NOCOUNT ON
-- For every product id from the distinct list iterate the following the code
DECLARE #I INT = 1
WHILE #I <= (SELECT MAX(ID) FROM QRY_RCCP_TEMP)
BEGIN
DECLARE #PRODUCT NVARCHAR(50) = (SELECT PRODUCTID FROM QRY_RCCP_TEMP WHERE ID = #I)
DROP TABLE Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_temp]
--Retrieve last 12 months of value from NR and add it to a temp table in increasing order of their months. These 12 data points will be baseline
SELECT
Top 12 A.*,
Case When B.[Value] is Null then 0 else CAST(B.[Value] as float) End GROWTH
INTO
Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_temp]
FROM
Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_dup] A
LEFT JOIN
--using the view here
QRY_Commerical_Subset B ON B.PRDID_Clean = A.ProductID_clean AND B.[YEAR] = YEAR(A.[MONTH])+1
WHERE
A.PRODUCTID= #PRODUCT
AND Cast(A.SnapshotDate AS DATE) = (SELECT Max(Cast(SnapshotDate AS DATE)) FROM reporting_db_spkpi.dbo.[tbl_RCCP_3_NR_dup])
Order by
[Month] desc
-- Generate 3 years of data
DECLARE #J INT = 1
WHILE #J<=3
BEGIN
--Calculate next year's value
UPDATE Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_temp]
SET
[Value] = [Value]*(1+ GROWTH),
[MONTH] = DATEADD(YEAR,1,[Month]),
MonthCode= 'F' + CAST(CAST(SUBSTRING(MonthCode,2,LEN(MonthCode)) AS INT) + 12 AS NVARCHAR(10))
--Add it to the NR table.
Insert into Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_dup]
(ProductID, MonthCode, Value, Month, FileName,
LastModifiedDate, SnapshotDate, Quarter, IsError, ErrorDescription)
Select
ProductID, MonthCode, Value, Month, FileName,
LastModifiedDate, SnapshotDate, Quarter, IsError, ErrorDescription
from
Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_temp]
--Update growth rate for next year
UPDATE Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_temp]
SET GROWTH = Case When B.[Value] is Null then 0 else CAST(B.[Value] as float) End
FROM Reporting_db_SPKPI.DBO.QRY_Commerical_Subset B
WHERE B.PRDID_Clean = ProductID_clean AND [YEAR] = YEAR([MONTH])+1
SET #J=#J+1
END
SET #I=#I+1
END
DROP VIEW QRY_RCCP_TEMP
DROP VIEW QRY_Commerical_Subset
The WITH is a Common Table Expression, aka CTE.
And a CTE is like a template for a sub-query.
For example this join of the same sub-query:
SELECT *
FROM (
select distinct bar
from table1
where foo = 'baz'
) AS foo1
JOIN (
select distinct bar
from table1
where foo = 'baz'
) AS foo2
ON foo1.bar > foo2.bar
Can be written as
WITH CTE_FOO AS (
select distinct bar
from table1
where foo = 'baz'
)
SELECT *
FROM CTE_FOO AS foo1
JOIN CTE_FOO AS foo2
ON foo1.bar > foo2.bar
It's meant to be used with a SELECT.
Not with a TRUNCATE TABLE or DROP TABLE.
(It can be used with an UPDATE though)
As such, treat the TRUNCATE as a seperate statement.
TRUNCATE TABLE Reporting_db_SPKPI.DBO.[tbl_RCCP_3_NR_dup];
WITH Commercial_subset AS
(
SELECT DISTINCT
PRDID_Clean, Value, [Year]
FROM
Reporting_db_SPKPI.DBO.[tbl_RCCP_commercial]
WHERE
MEASURE = 'Unit Growth Rate'
)
SELECT *
FROM Commercial_subset;
Btw, the reason why many write a CTE with a leading ; is because the WITH clause raises an error if the previous statement wasn't ended with a ;. It's just a small trick to avoid that error.

Adjusting Month Specific SQL Query to Iterate Across all Months Greater than Base Month

I've inherited a query that has parameters which specify pulls data for a single desired month. The extract then gets manually added to previous month's extract in Excel. I'd like to eliminate the manual portion by adjusting the existing query to iterate across all months greater than a given base month, then (if this is what makes most sense) unioning the individual "final" outputs.
My attempt was to add the entire block of code for each specific month to the existing code, and then run it together. The idea was that I'd just paste in a new block each new month. I knew this was very inefficient, but I don't have the luxury of learning how to do it efficiently, so if it worked I'd be happy.
I ran into problems because the existing query has two subqueries which then are used to create a final table, and I couldn't figure out how to retain the final table at the end of the code so that it could be referenced in a union later (fwiw, I was attempting to use a Select Into for that final table).
with eligibility_and_customer_type AS
(SELECT DISTINCT ON(sub_id, mbr_sfx_id)
sub_id AS subscriber_id
, mbr_sfx_id AS member_suffix_id
, src_mbr_key
, ctdv.cstmr_typ_cd
, gdv.grp_name
FROM adw_common.cstmr_typ_dim_vw ctdv
JOIN adw_common.mbr_eligty_by_mo_fact_vw
ON ctdv.cstmr_typ_key = mbr_eligty_by_mo_fact_vw.cstmr_typ_key
AND mbr_eligty_yr = '2018'
AND mbr_eligty_mo = '12'
JOIN adw_common.prod_cat_dim_vw
ON prod_cat_dim_vw.prod_cat_key = mbr_eligty_by_mo_fact_vw.prod_cat_key
AND prod_cat_dim_vw.prod_cat_cd = 'M'
JOIN adw_common.mbr_dim_abr
ON mbr_eligty_by_mo_fact_vw.mbr_key = mbr_dim_abr.mbr_key
JOIN consumer.facets_xref_abr fxf
ON mbr_dim_abr.src_mbr_key = fxf.source_member_key
JOIN adw_common.grp_dim_vw gdv
ON gdv.grp_key=mbr_eligty_by_mo_fact_vw.grp_key),
facets_ip as
(select distinct cl.meme_ck
FROM gpgen_cr_ai.cmc_clcl_claim_abr cl
/* LEFT JOIN gpgen_cr_ai.cmc_clhp_hosp_abr ch
ON cl.clcl_id = ch.clcl_id*/
LEFT JOIN gpgen_cr_ai.cmc_cdml_cl_line cd
ON cl.clcl_id = cd.clcl_id
WHERE cd.pscd_id = '21'
/*AND ch.clcl_id IS NULL*/
AND cl.clcl_cur_sts NOT IN ('91','92')
AND cl.clcl_low_svc_dt >= '20181201'
and cl.clcl_low_svc_dt <= '20181231'
group by 1)
select distinct c.meme_ck,
e.cstmr_typ_cd,
'201812' as Yearmo
from facets_ip c
left join eligibility_and_customer_type e
on c.meme_ck = e.src_mbr_key;
The code above has date parameters that get updated when necessary.
The final output would be a version of the final table created above, but with results corresponding to, say, 201801 - present.
If you provide:
DDL of the underlying tables
Sample Data of the underlying tables
Expected resultset
DBMS you are using
, then one would be able to provide the best solution here.
Without knowing them, and as you said you only care about dynamically looping through each month, here is one way you can utilize your code to loop it through in SQL Server. Please fill the variable #StartDate and #EndDate values and provide proper datatype for meme_ck and cstmr_typ_cd.
IF OBJECT_ID ('tempdb..#TempTable', N'U') IS NOT NULL
BEGIN
DROP TABLE #TempTable
END
CREATE TABLE #TempTable
(
meme_ck <ProvideProperDataTypeHere>
,cstmr_typ_cd <ProvideProperDataTypeHere>
,Yearmo VARCHAR(10)
)
DECLARE #StartDate DATE = '<Provide the first day of the start month>'
DECLARE #EndDate DATE = '<Provide the end date inclusive>'
WHILE #StartDate <= #EndDate
BEGIN
DECLARE #MonthEndDate DATE = CASE WHEN DATEADD(DAY, -1, DATEADD(MONTH, 1, #StartDate)) <= #EndDate THEN DATEADD(DAY, -1, DATEADD(MONTH, 1, #StartDate)) ELSE #EndDate END
DECLARE #MonthYear VARCHAR(6) = LEFT(CONVERT(VARCHAR(8), #StartDate, 112), 6)
--This is your code which I am not touching without not knowing any detail about it. Just feeding the variables to make it dynamic
;with eligibility_and_customer_type AS
(SELECT DISTINCT ON(sub_id, mbr_sfx_id)
sub_id AS subscriber_id
, mbr_sfx_id AS member_suffix_id
, src_mbr_key
, ctdv.cstmr_typ_cd
, gdv.grp_name
FROM adw_common.cstmr_typ_dim_vw ctdv
JOIN adw_common.mbr_eligty_by_mo_fact_vw
ON ctdv.cstmr_typ_key = mbr_eligty_by_mo_fact_vw.cstmr_typ_key
AND mbr_eligty_yr = CAST(YEAR(#StartDate) AS VARCHAR(10)) -- NO need to cast if mbr_eligty_yr is an Integer
AND mbr_eligty_mo = CAST(MONTH(#StartDate) AS VARCHAR(10)) -- NO need to cast if mbr_eligty_yr is an Integer
JOIN adw_common.prod_cat_dim_vw
ON prod_cat_dim_vw.prod_cat_key = mbr_eligty_by_mo_fact_vw.prod_cat_key
AND prod_cat_dim_vw.prod_cat_cd = 'M'
JOIN adw_common.mbr_dim_abr
ON mbr_eligty_by_mo_fact_vw.mbr_key = mbr_dim_abr.mbr_key
JOIN consumer.facets_xref_abr fxf
ON mbr_dim_abr.src_mbr_key = fxf.source_member_key
JOIN adw_common.grp_dim_vw gdv
ON gdv.grp_key=mbr_eligty_by_mo_fact_vw.grp_key),
facets_ip as
(select distinct cl.meme_ck
FROM gpgen_cr_ai.cmc_clcl_claim_abr cl
/* LEFT JOIN gpgen_cr_ai.cmc_clhp_hosp_abr ch
ON cl.clcl_id = ch.clcl_id*/
LEFT JOIN gpgen_cr_ai.cmc_cdml_cl_line cd
ON cl.clcl_id = cd.clcl_id
WHERE cd.pscd_id = '21'
/*AND ch.clcl_id IS NULL*/
AND cl.clcl_cur_sts NOT IN ('91','92')
AND cl.clcl_low_svc_dt BETWEEN #StartDate AND #MonthEndDate
group by 1)
INSERT INTO #TempTable
(
meme_ck
,cstmr_typ_cd
,Yearmo
)
select distinct c.meme_ck,
e.cstmr_typ_cd,
#MonthYear as Yearmo
from facets_ip c
left join eligibility_and_customer_type e
on c.meme_ck = e.src_mbr_key;
SET #StartDate = DATEADD(MONTH, 1, #StartDate)
END
SELECT * FROM #TempTable;
I don't have enough information on your tables to really create an optimal solution. The solutions I am providing just have a single parameter (table name) and for your solution, you will need to pass in an additional parameter for the date filter.
The idea of "looping" is not something you'll need to do in Greenplum. That is common for OLTP databases like SQL Server or Oracle that can't handle big data very well and have to process smaller amounts at a time.
For these example solutions, a table is needed with some data in it.
CREATE TABLE public.foo
(id integer,
fname text,
lname text)
DISTRIBUTED BY (id);
insert into foo values (1, 'jon', 'roberts'),
(2, 'sam', 'roberts'),
(3, 'jon', 'smith'),
(4, 'sam', 'smith'),
(5, 'jon', 'roberts'),
(6, 'sam', 'roberts'),
(7, 'jon', 'smith'),
(8, 'sam', 'smith');
Solution 1: Learn how functions work in the database. Here is a quick example of how it would work.
Create a function that does the Create Table As Select (CTAS) where you pass in a parameter.
Note: You can't execute DDL statements in a function directly so you have to use "EXECUTE" instead.
create or replace function fn_test(p_table_name text) returns void as
$$
declare
v_sql text;
begin
v_sql :='drop table if exists ' || p_table_name;
execute v_sql;
v_sql := 'create table ' || p_table_name || ' with (appendonly=true, compresstype=quicklz) as
with t as (select * from foo)
select * from t
distributed by (id)';
execute v_sql;
end;
$$
language plpgsql;
Execute the function with a simple select statement.
select fn_test('foo3');
Notice how I pass in a table name that will be created when you execute the function.
Solution 2: Use psql variables
Create a sql file name "test.sql" with the following contents.
drop table if exists :p_table_name;
create table :p_table_name with (appendonly=true, compresstype=quicklz) as
with t as (select * from foo)
select * from t
distributed by (id);
Next, you execute psql and pass in the variable p_table_name.
psql -f test.sql -v p_table_name=foo4
psql:test.sql:1: NOTICE: table "foo4" does not exist, skipping
DROP TABLE
SELECT 8

Parsing a string in SQL with If statement

I have a table with a string in some columns values that tells me if I should delete the row....however this string needs some parsing to understand whether to delete or not.
What is the string: it tells me the recurrence of meetings eg everyday starting 21st march for 10 meetings.
My table is a single column called recurrence:
Recurrence
-------------------------------
daily;1;21/03/2015;times;10
daily;1;01/02/2016;times;8
monthly;1;01/01/2016;times;2
weekly;1;21/01/2016;times;4
What to do: if the meetings are finished then remove the row.
The string is of the following format
<frequency tag>;<frequency number>;<start date>;times;<no of times>
For example
daily;1;21/03/2016;times;10
everyday starting 21st march, for 10 times
Does anybody know how I would calculate if the string indicates all meetings are in past? I want a select statement that tells me if the recurrence values are in past - true or false
I added one string ('weekly;1;21/05/2016;times;4') that definitely must not be deleted to show some output. At first try to add to temp table `#table1' all data from your table and check if all is deleted well.
DECLARE #table1 TABLE (
Recurrence nvarchar(max)
)
DECLARE #xml xml
INSERT INTO #table1 VALUES
('daily;1;21/03/2016;times;10'),
('daily;1;21/03/2015;times;10'),
('daily;1;01/02/2016;times;8'),
('monthly;1;01/01/2016;times;2'),
('weekly;1;21/01/2016;times;4'),
('weekly;1;21/05/2016;times;4')
SELECT #xml= (
SELECT CAST('<s><r>' + REPLACE(Recurrence,';','</r><r>') + '</r><r>'+ Recurrence+'</r></s>' as xml)
FROM #table1
FOR XML PATH ('')
)
;WITH cte as (
SELECT t.v.value('r[1]','nvarchar(10)') as how,
t.v.value('r[2]','nvarchar(10)') as every,
CONVERT(date,t.v.value('r[3]','nvarchar(10)'),103) as since,
t.v.value('r[4]','nvarchar(10)') as what,
t.v.value('r[5]','int') as howmany,
t.v.value('r[6]','nvarchar(max)') as Recurrence
FROM #xml.nodes('/s') as t(v)
)
DELETE t
FROM #table1 t
LEFT JOIN cte c ON c.Recurrence=t.Recurrence
WHERE
CASE WHEN how = 'daily' THEN DATEADD(day,howmany,since)
WHEN how = 'weekly' THEN DATEADD(week,howmany,since)
WHEN how = 'monthly' THEN DATEADD(month,howmany,since)
ELSE NULL END < GETDATE()
SELECT * FROM #table1
Output:
Recurrence
-----------------------------
weekly;1;21/05/2016;times;4
(1 row(s) affected)

How to execute dynamic SQL in Teradata

Is there any way to submit dynamically generated SQL to Teradata? I've written a query that will create the code to denormalize a table. Right now, I am pulling the code down to my client (SAS) and resubmitting it in a second step. I am not familiar with either Teradata macros or procedures; would something like that work?
To illustrate, I have a table defined like this:
create multiset table MYTABLE
( RECID integer generated always as identity
( start with 1
increment by 1
minvalue -2147483647
maxvalue 2147483647
no cycle )
, SNAP_DATE date format 'YYYY/MM/DD'
, EMAIL_DATE date format 'YYYY/MM/DD'
, FREQ integer
)
unique primary index ( RECID )
The table is populated every day (SNAP_DATE) and is used to monitor changes to an email_date in another table. The following query returns the code that I can run to create my denormalized view:
select RUN_THIS
from (
select RUN_THIS, RN
from (
select 'select EMAIL_DATE ' (varchar(100)) as RUN_THIS
, 0 (int) as RN
) x
union all
select ', sum( case when SNAP_DATE = date '''
|| (SNAP_DATE (format 'yyyy-mm-dd') (char(10)) )
|| ''' then FREQ else 0 end ) as D'
|| (SNAP_DATE (format 'yyyymmdd') (char(8)) ) as RUN_THIS
, row_number() over ( partition by 1 order by SNAP_DATE ) as RN
from ( select distinct SNAP_DATE
from MYTABLE
where SNAP_DATE > current_date - 30) t1
union all
select RUN_THIS, RN
from (
select 'from MYTABLE group by 1 order by 1;' as RUN_THIS
, 10000 as RN
) y
) t
order by RN
I export the result of the above query to a file on my client, then turn around and submit that file back to Teradata. I'm hoping there is some way to store this complete definition in some Teradata object so it can be executed directly.
You may find success putting this in a stored procedure using the DBC.SysExecSQL command.
Here is an overly simplified example of a stored procedure in Teradata. Obviously in production would want an error handler defined to address things like invalid database objects. Furthermore, you could return the SQLSTATE back as a parameter to test for whether the stored procedure completed successfully or not.
CREATE PROCEDURE SYSDBA.CommentDatabase
(
IN P_Database VARCHAR(30),
IN P_Comment VARCHAR(255),
OUT MSG
)
MAIN: --Label
BEGIN
DECLARE P_SQL_TEXT VARCHAR(4000);
SET P_SQL_TEXT='COMMENT ON DATABASE '||P_DATABASE||' AS '''||P_COMMENT||'''';
CALL dbc.SysExecSQL (:P_SQL_TEXT);
SET MSG = 'Database '||P_DBNAME||' commented successfully!';
END;

In SQL can a sequenced range selection be done more efficiently than my algorithm (see code) that uses a cursor?

I need to collapse multiple ranges of sequential numbers (1 or more) to sets of their minimum and maximum values. I have unique integers (no duplicates) stored in a table column.
The obvious way (to me) to solve this problem is to use a cursor (see my algorithm below) and iterate through every integer. However, it seems inefficient to me so I am wondering if there is a more efficient algorithm. Perhaps there is a way using common table expressions with recursion. I have more than 32767 integers though, so any solution will need to use option (MAXRECURSION 0) which sets unlimited recursion.
Following is a simplified test case for my existing algorithm usign a cursor. It will output the minimum and maximum for each range of sequential numbers (e.g. 1-3, 9-11, 13-13, 15-16).
I am using MS SQL Server 2008. Please note comments begin with two dashes (--).
declare #minInt int, #maxInt int
declare #nextInt int, #prevInt int
--need a temporary table to store the ranges that were found
declare #rangeTable table (minInt int, maxInt int)
declare mycursor cursor for
select * from
(
select 1 as id union
select 2 as id union
select 3 as id union
select 9 as id union
select 10 as id union
select 11 as id union
select 13 as id union
select 15 as id union
select 16 as id
) tblRanges
order by id--order is needed for this algorithm if used with generic data
open mycursor
--initialise new sequence
fetch next from mycursor into #minInt
select #maxInt = #minInt--set the min and max to the smallest value
select #prevInt = #minInt--store the last int
declare #sequenceFound int
while ##FETCH_STATUS=0
begin
select #sequenceFound=1--set the default flag value to true
--loop while sequence found
while ##FETCH_STATUS=0 and #sequenceFound = 1
begin
fetch next from mycursor into #nextInt
if #nextInt = (#prevInt + 1)
begin
select #sequenceFound = 1
end
else
begin
select #sequenceFound = 0
end
select #prevInt = #nextInt--store the current value as the previous value for the next comparison
if #sequenceFound = 1 --if the nextInt is part of a sequence, then store the new maxInt
and #maxInt < #nextInt--should always be true for ordered output containing no duplicates
begin
select #maxInt = #nextInt
end
end--while sequenceFound
--store the sequence range and then check for more sequences
insert into #rangeTable (minInt,maxInt) values (#minInt,#maxInt)
--store the current value as the new minInt and maxInt for the next sequence iteration
select #minInt = #nextInt
select #maxInt = #nextInt
end--while more table rows found
select * from #rangeTable
close mycursor
deallocate mycursor
Courtesy of Itzik Ben-Gan:
WITH tblRanges AS
(
SELECT 1 AS ID UNION
SELECT 2 AS ID UNION
SELECT 3 AS ID UNION
SELECT 9 AS ID UNION
SELECT 10 AS ID UNION
SELECT 11 AS ID UNION
SELECT 13 AS ID UNION
SELECT 15 AS ID UNION
SELECT 16 AS ID
),
StartingPoints AS
(
SELECT ID, ROW_NUMBER() OVER(ORDER BY ID) AS rownum
FROM tblRanges AS A
WHERE NOT EXISTS
(SELECT *
FROM tblRanges AS B
WHERE B.ID = A.ID - 1)
),
EndingPoints AS
(
SELECT ID, ROW_NUMBER() OVER(ORDER BY ID) AS rownum
FROM tblRanges AS A
WHERE NOT EXISTS
(SELECT *
FROM tblRanges AS B
WHERE B.ID = A.ID + 1)
)
SELECT S.ID AS start_range, E.ID AS end_range
FROM StartingPoints AS S
JOIN EndingPoints AS E
ON E.rownum = S.rownum;
You can read a full explanation from his chapter in SQL Sever MVP Deep Dives called Gaps and Islands. He explains various techniques (including cursors) and compares them in terms of performance.