SQL iterative joining within while loop - sql-server

I know that it's not best practice to apply a while loop within SQL code, but I can't really imagine a way around it.
I have an ever-growing set of historical tables that I want to summarize, and join, using a common ID. All of these historical tables have matching structure.
The table names contain the month and year; I iterate through all tables found between two periods - determined using the fixed timestamp #StartDate, and the timestamp #EndDate via GETDATE().
The while loop is intended to continually append additional columns from each historic file. There is one month per iteration, and each column contains the pertaining month's summary.
The problem is, every left join seems to erase what was there in the previous iteration. I use a conditional statement to determine if the temp table exists, and build it using a select ... into statement if it doesn't, and an insert into statement if it does.
The following is what I have:
while #MonthCount > 0
begin
select #Month = DateSuffix from #DateTable where Row = #MonthCount
select #Table = table_name from information_schema.tables where table_name like concat('USA_RETHHDs_%', #Month)
if #Table is not null
begin
if object_id('tempdb.dbo.#RETHHDS', 'U') is not null
drop table #RETHHDS
create table #RETHHDS
(BUC_CD varchar(12),
RET_BKD_HH_COUNT int)
select #Table = concat('dbo.', #Table)
set #sql =
'insert into #RETHHDS
select BUC_CD, count(distinct SK_HH_ID) as RET_BKD_HH_COUNT from ' + #Table +
' group by BUC_CD'
execute SP_EXECUTESQL #sql;
if object_id('tempdb.dbo.#HISTORICAL_TABLE', 'U') is null
begin
select #USA_BRANCHCATALOGUE.TR, #USA_BRANCHCATALOGUE.NAME, #RETHHDS.RET_BKD_HH_COUNT into #HISTORICAL_TABLE
from #USA_BRANCHCATALOGUE
left join #RETHHDS
on #USA_BRANCHCATALOGUE.TR = #RETHHDS.BUC_CD
end
else
begin
insert into #HISTORICAL_TABLE
select #USA_BRANCHCATALOGUE.TR, #USA_BRANCHCATALOGUE.NAME, #RETHHDS.RET_BKD_HH_COUNT
from #USA_BRANCHCATALOGUE
left join #RETHHDS
on #USA_BRANCHCATALOGUE.TR = #RETHHDS.BUC_CD
end
select * from #HISTORICAL_TABLE
end
select #MonthCount = #MonthCount - 1
end
As I loop through, the #HISTORICAL_TABLE does not grow with additional rightmost columns. Instead, it remains with columns TR, NAME, and RET_BKD_HH_COUNT.
Bonus points if there's an easy way to append the corresponding month to the RET_BKD_HH_COUNT field. However, when I do the following,
set #sql =
'create table #RETHHDS
(BUC_CD varchar(12),
RET_BKD_HH_COUNT_' + #Month + ' int)'
execute SP_EXECUTESQL #sql;
I receive the following message:
Invalid object name '#RETHHDS'.

Related

Replicate a table record with a new id value using dynamic SQL

I have an application with a requirement that the user can "copy" a record. This will duplicate the record in the table, and the associated records in any child tables.
I will use a trigger to execute a stored procedure to do the copy. The issue I am facing is that I want to increment the ID field for the copied record, which is also the FK in the child tables. The ID field is not a standard format, so using an increment won't work. In order to make this future-proof, I was going to use dynamic SQL to pull the columns for each table so that I don't need to modify the code if I add a new field to one of the tables. The client's system admin can also add columns to the table via the GUI, but they have no access to the SQL backend so they would need to contact us to modify the code (not ideal).
Example:
Declare #ColumnNames varchar(2000)
Declare #BLDGCODE char(4)
set #BLDGCODE = '001'
select #ColumnNames = COALESCE(#ColumnNames + ', ', '') + COLUMN_NAME
from
INFORMATION_SCHEMA.COLUMNS
where
TABLE_NAME='FMB0'
Declare #DynSqlStatement varchar(max);
set #DynSqlStatement = 'Insert into dbo.FMB0('+ #ColumnNames + ')
select * from dbo.FMB0 where BLDGCODE= ' + cast(#BLDGCODE as char(4));
print(#DynSqlStatement);
This solves the issue for a new column being added to one of the tables. However, how can I increment the ID (BLDGCODE in this example). Is my only solution to script out the columns by name so I can increment the ID, or is there a function I am overlooking?
Hopefully this made sense. I am an intermediate SQL user at best, so forgive the naivete if there's an obvious solution.
UPDATE
So I've decided to use #temp tables to hold the record that was changed, modify the id there, and then insert back into the main table from the #temp table. This is working pretty well, with one exception. I get the following error:
The column "FLOORID_" cannot be modified because it is either a computed column or is the result of a UNION operator.
Below is my stored procedure. I've investigated a STUFF approach, but not sure where to insert that code. Using STUFF with calculated column. I am now back to thinking I need to call out the columns specifically for the one table with the computed column, and if we add a new field, I just need to modify this stored procedure. Anyone have any other ideas?
ALTER PROCEDURE [dbo].[LDAC_BLDGCOPY]
#BLDGCODE CHAR(4)
AS
BEGIN
SET NOCOUNT ON;
--COPY BUILDING RECORD
BEGIN
DECLARE #ColumnNamesB0 VARCHAR(2000);
SELECT *
INTO #TEMPB0
FROM FMB0
WHERE BLDGCODE = #BLDGCODE;
UPDATE #TEMPB0
SET BLDGCODE = CONVERT(CHAR(4), (CAST((#BLDGCODE) AS INT) +
100)),
auto_key = dbo.GetAutoKey(),
BLDGCOPY = 0;
SELECT #ColumnNamesB0 = COALESCE(#ColumnNamesB0+', ',
'')+COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'FMB0';
DECLARE #DynSqlStatementB0 VARCHAR(MAX);
SET #DynSqlStatementB0 = 'Insert into dbo.FMB0('+#ColumnNamesB0+')
select * from #TEMPB0';
EXEC (#DynSqlStatementB0);
END;
--COPY FLOOR RECORDS
BEGIN
DECLARE #ColumnNamesL0 VARCHAR(2000);
--DECLARE #Val INT =
RTRIM(CONVERT(CHAR(10),CAST(LEFT(#BL_KEY,LEN(RTRIM(#BL_KEY))-2)+1 as
INT)))+'01
SELECT *
INTO #TEMPL0
FROM FML0
WHERE BLDGCODE = #BLDGCODE;
UPDATE #TEMPL0
SET BLDGCODE = CONVERT(CHAR(4), (CAST((#BLDGCODE) AS INT) +
100)),
auto_key = dbo.GetAutoKey();
UPDATE #TEMPL0
SET FLOORID_ = auto_key;
SELECT #ColumnNamesL0 = COALESCE(#ColumnNamesL0+', ',
'')+COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'FML0';
DECLARE #DynSqlStatementL0 VARCHAR(MAX);
SET #DynSqlStatementL0 = 'Insert into dbo.FML0('+#ColumnNamesL0+')
select * from #TEMPL0';
EXEC (#DynSqlStatementL0);
END;
--COPY ROOM RECORDS
BEGIN
DECLARE #ColumnNamesA0 VARCHAR(2000);
--DECLARE #Val INT =
RTRIM(CONVERT(CHAR(10),CAST(LEFT(#BL_KEY,LEN(RTRIM(#BL_KEY))-2)+1 as
INT)))+'01
SELECT *
INTO #TEMPA0
FROM FMA0
WHERE BLDGCODE = #BLDGCODE;
UPDATE #TEMPA0
SET
BLDGCODE = CONVERT(CHAR(4), (CAST((#BLDGCODE) AS INT) +
100)),
auto_key = dbo.GetAutoKey(),
FLOORID = #TEMPL0.FLOORID_
FROM #TEMPA0
INNER JOIN #TEMPL0 ON CONVERT(CHAR(4), (CAST((#BLDGCODE) AS
INT) + 100)) = #TEMPL0.BLDGCODE;
SELECT #ColumnNamesA0 = COALESCE(#ColumnNamesA0+', ',
'')+COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'FMA0';
DECLARE #DynSqlStatementA0 VARCHAR(MAX);
SET #DynSqlStatementA0 = 'Insert into dbo.FMA0('+#ColumnNamesA0+')
select * from #TEMPA0';
EXEC (#DynSqlStatementA0);
DROP TABLE #TEMPB0;
DROP TABLE #TEMPL0;
DROP TABLE #TEMPA0;
END;
END;

Show a few columns in a stored procedure based on a parameter

I have a parameter in my stored procedure called internal. If "internal" = yes then I want to display an additional 2 columns in my results. If it's no I don't want to display these columns.
I can do a case statement and then I can set the column to be empty but the column name will still be returned in the results.
My questions are:
Is there a way not to return these 2 columns in the results at all?
Can I do it in one case statement and not a separate case statement for each column?
Thank you
No, CASE is a function, and can only return a single value.
And According to your comment:-
The issue with 2 select statements are that it's a major complicated
select statement and I really don't want to have to have the whole
select statement twice.
so you can use the next approach for avoid duplicate code:-
Create procedure proc_name (#internal char(3), .... others)
as
BEGIN
declare #AddationalColumns varchar(100)
set #AddationalColumns = ''
if #internal = 'Yes'
set #AddationalColumns = ',addtionalCol1 , addtionalCol2'
exec ('Select
col1,
col2,
col3'
+ #AddationalColumns +
'From
tableName
Where ....
' )
END
Try IF Condition
IF(internal = 'yes')
BEGIN
SELECT (Columns) FROM Table1
END
ELSE
BEGIN
SELECT (Columns With additional 2 columns) FROM Table1
END
You can do something like this solution. It allows you to keep only one copy of code if it's so important but you will have to deal with dynamic SQL.
CREATE TABLE tab (col1 INT, col2 INT, col3 INT);
GO
DECLARE #internal BIT = 0, #sql NVARCHAR(MAX)
SET #sql = N'SELECT col1 ' + (SELECT CASE #internal WHEN 1 THEN N', col2, col3 ' ELSE N'' END) + N'FROM tab'
EXEC sp_executesql #sql
GO
DROP TABLE tab
GO
Another option is to create a 'wrapper' proc. Keep your current one untouched.
Create a new proc which executes this (pseudo code):
create proc schema.wrapperprocname (same #params as current proc)
as
begin
Create table #output (column list & datatypes from current proc);
insert into #output
exec schema.currentproc (#params)
if #internal = 'Yes'
select * from #output
else
select columnlist without the extra 2 columns from #output
end
This way the complex select statement remains encapsulated in the original proc.
Your only overhead is keeping the #output table & select lists in in this proc in sync with any changes to the original proc.
IMO it's also easier to understand/debug/tune than dynamic sql.

UPSERT into sql server from an Excel File

I have a SP that runs everynight to Insert and Update the content of a table based on an excel file (Excel 2010 on Windows Server 20008 R2). Below is my SP and the image represents my table's structure and the excel file format. I just need to double check my SP with you guys to make sure I am doing this correctly and if I am on the right track. The excel file includes 3 columns both Cust_Num and Cust_Seq are primary since there would never be a case that same combination of Cust_Num and Cust_Seq exist for a customer name. For example, for Cust_Num = 1 and Cust_Num=0 there will never be another of same combination of Cust_Num being 1 and Cust_Num being 0. However the name will usually repeat in the spreadsheet. So, would you guys please let me know if the SP is correct or not? (in the SP first the Insert statement runs and then the Update Statement):
**First The Insert runs in the SP
INSERT INTO Database.dbo.Routing_CustAddress
SELECT a.[Cust Num],a.[Cust Seq],a.[Name]
FROM OPENROWSET('Microsoft.ACE.OLEDB.12.0',
'Excel 8.0;HDR=YES;Database=C:\Data\custaddr.xls;',
'SELECT*
FROM [List_Frame_1$]') a Left join Routing_CustAddress b
on a.[Cust Num] = b.Cust_Num and a.[Cust Seq] = b.Cust_Seq where b.Cust_Num is null
***Then the Update Runs in the SP
UPDATE SPCustAddress
SET SPCustAddress.Name = CustAddress.Name
FROM ArPd_App.dbo.Routing_CustAddress SPCustAddress
INNER JOIN OPENROWSET('Microsoft.ACE.OLEDB.12.0',
'Excel 8.0;HDR=YES;Database=C:\Data\custaddr.xls;',
'SELECT *
FROM [List_Frame_1$]')CustAddress
ON SPCustAddress.Cust_Num = CustAddress.[Cust Num]
AND SPCustAddress.Cust_Seq = CustAddress.[Cust Seq]
Right here is some code I havent tested it so I'll leave it for you but it shold work
Create the stagging table first.
CREATE TABLE dbo.Routing_CustAddress_Stagging
(
Cust_Name NVARCHAR(80),
Cust_Seq NVARCHAR(80),
Name NVARCHAR(MAX)
)
GO
Then create the following Stored Procedure. It will take the FilePath and Sheet name as parameter and does the whole lot for you.
1) TRUNCATE the stagging table.
2) Upload data into stagging table from provided Excel file, and sheet.
3) and finnaly does the UPSERT operation in two separate statements.
CREATE PROCEDURE usp_Data_Upload_Via_File
#FilePath NVARCHAR(MAX),
#SheetName NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
IF (#FilePath IS NULL OR #SheetName IS NULL)
BEGIN
RAISERROR('Please Provide valid File Path and SheetName',16,1)
RETURN;
END
-- Truncate the stagging table first
TRUNCATE TABLE dbo.Routing_CustAddress_Stagging;
-- Load Data from Excel sheet
DECLARE #Sql NVARCHAR(MAX);
SET #Sql = N' INSERT INTO dbo.Routing_CustAddress_Stagging ([Cust Num],[Cust Seq],[Name]) ' +
N' SELECT [Cust Num],[Cust Seq],[Name] ' +
N' FROM OPENROWSET(''Microsoft.ACE.OLEDB.12.0'', ' +
N' ''Excel 8.0;HDR=YES;Database='+ #FilePath + ';'' ,' +
N' ''SELECT* FROM ['+ #SheetName +']'')'
EXECUTE sp_executesql #Sql
-- Now the UPSERT statement.
UPDATE T
SET T.Name = ST.NAME
FROM dbo.Routing_CustAddress T INNER JOIN dbo.Routing_CustAddress_Stagging ST
ON T.Cust_Name = ST.Cust_Name AND T.Cust_Seq = ST.Cust_Seq
-- Now the Insert Statement
INSERT INTO dbo.Routing_CustAddress
SELECT ST.[Cust Num],ST.[Cust Seq],ST.[Name]
FROM dbo.Routing_CustAddress_Stagging ST LEFT JOIN dbo.Routing_CustAddress T
ON T.Cust_Name = ST.Cust_Name AND T.Cust_Seq = ST.Cust_Seq
WHERE T.Cust_Name IS NULL OR T.Cust_Seq IS NULL
END

SQL variable to hold list of integers

I'm trying to debug someone else's SQL reports and have placed the underlying reports query into a query windows of SQL 2012.
One of the parameters the report asks for is a list of integers. This is achieved on the report through a multi-select drop down box. The report's underlying query uses this integer list in the where clause e.g.
select *
from TabA
where TabA.ID in (#listOfIDs)
I don't want to modify the query I'm debugging but I can't figure out how to create a variable on the SQL Server that can hold this type of data to test it.
e.g.
declare #listOfIDs int
set listOfIDs = 1,2,3,4
There is no datatype that can hold a list of integers, so how can I run the report query on my SQL Server with the same values as the report?
Table variable
declare #listOfIDs table (id int);
insert #listOfIDs(id) values(1),(2),(3);
select *
from TabA
where TabA.ID in (select id from #listOfIDs)
or
declare #listOfIDs varchar(1000);
SET #listOfIDs = ',1,2,3,'; --in this solution need put coma on begin and end
select *
from TabA
where charindex(',' + CAST(TabA.ID as nvarchar(20)) + ',', #listOfIDs) > 0
Assuming the variable is something akin to:
CREATE TYPE [dbo].[IntList] AS TABLE(
[Value] [int] NOT NULL
)
And the Stored Procedure is using it in this form:
ALTER Procedure [dbo].[GetFooByIds]
#Ids [IntList] ReadOnly
As
You can create the IntList and call the procedure like so:
Declare #IDs IntList;
Insert Into #IDs Select Id From dbo.{TableThatHasIds}
Where Id In (111, 222, 333, 444)
Exec [dbo].[GetFooByIds] #IDs
Or if you are providing the IntList yourself
DECLARE #listOfIDs dbo.IntList
INSERT INTO #listofIDs VALUES (1),(35),(118);
You are right, there is no datatype in SQL-Server which can hold a list of integers. But what you can do is store a list of integers as a string.
DECLARE #listOfIDs varchar(8000);
SET #listOfIDs = '1,2,3,4';
You can then split the string into separate integer values and put them into a table. Your procedure might already do this.
You can also use a dynamic query to achieve the same outcome:
DECLARE #SQL nvarchar(8000);
SET #SQL = 'SELECT * FROM TabA WHERE TabA.ID IN (' + #listOfIDs + ')';
EXECUTE (#SQL);
Note: I haven't done any sanitation on this query, please be aware that it's vulnerable to SQL injection. Clean as required.
For SQL Server 2016+ and Azure SQL Database, the STRING_SPLIT function was added that would be a perfect solution for this problem. Here is the documentation:
https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql
Here is an example:
/*List of ids in a comma delimited string
Note: the ') WAITFOR DELAY ''00:00:02''' is a way to verify that your script
doesn't allow for SQL injection*/
DECLARE #listOfIds VARCHAR(MAX) = '1,3,a,10.1,) WAITFOR DELAY ''00:00:02''';
--Make sure the temp table was dropped before trying to create it
IF OBJECT_ID('tempdb..#MyTable') IS NOT NULL DROP TABLE #MyTable;
--Create example reference table
CREATE TABLE #MyTable
([Id] INT NOT NULL);
--Populate the reference table
DECLARE #i INT = 1;
WHILE(#i <= 10)
BEGIN
INSERT INTO #MyTable
SELECT #i;
SET #i = #i + 1;
END
/*Find all the values
Note: I silently ignore the values that are not integers*/
SELECT t.[Id]
FROM #MyTable as t
INNER JOIN
(SELECT value as [Id]
FROM STRING_SPLIT(#listOfIds, ',')
WHERE ISNUMERIC(value) = 1 /*Make sure it is numeric*/
AND ROUND(value,0) = value /*Make sure it is an integer*/) as ids
ON t.[Id] = ids.[Id];
--Clean-up
DROP TABLE #MyTable;
The result of the query is 1,3
In the end i came to the conclusion that without modifying how the query works i could not store the values in variables. I used SQL profiler to catch the values and then hard coded them into the query to see how it worked. There were 18 of these integer arrays and some had over 30 elements in them.
I think that there is a need for MS/SQL to introduce some aditional datatypes into the language. Arrays are quite common and i don't see why you couldn't use them in a stored proc.
There is a new function in SQL called string_split if you are using list of string.
Ref Link STRING_SPLIT (Transact-SQL)
DECLARE #tags NVARCHAR(400) = 'clothing,road,,touring,bike'
SELECT value
FROM STRING_SPLIT(#tags, ',')
WHERE RTRIM(value) <> '';
you can pass this query with in as follows:
SELECT *
FROM [dbo].[yourTable]
WHERE (strval IN (SELECT value FROM STRING_SPLIT(#tags, ',') WHERE RTRIM(value) <> ''))
I use this :
1-Declare a temp table variable in the script your building:
DECLARE #ShiftPeriodList TABLE(id INT NOT NULL);
2-Allocate to temp table:
IF (SOME CONDITION)
BEGIN
INSERT INTO #ShiftPeriodList SELECT ShiftId FROM [hr].[tbl_WorkShift]
END
IF (SOME CONDITION2)
BEGIN
INSERT INTO #ShiftPeriodList
SELECT ws.ShiftId
FROM [hr].[tbl_WorkShift] ws
WHERE ws.WorkShift = 'Weekend(VSD)' OR ws.WorkShift = 'Weekend(SDL)'
END
3-Reference the table when you need it in a WHERE statement :
INSERT INTO SomeTable WHERE ShiftPeriod IN (SELECT * FROM #ShiftPeriodList)
You can't do it like this, but you can execute the entire query storing it in a variable.
For example:
DECLARE #listOfIDs NVARCHAR(MAX) =
'1,2,3'
DECLARE #query NVARCHAR(MAX) =
'Select *
From TabA
Where TabA.ID in (' + #listOfIDs + ')'
Exec (#query)

What are the different ways to replace a cursor?

I'd like to know your experience(s) with replacing SQL Server cursors in existing code, or how you took a problem that a procedural guy would use a cursor to solve, and did it set-based.
What was the problem the cursor was used to solve? How did you replace the cursor?
try to never loop, work on sets of data.
you can insert, update, delete multiple rows at one time. here in an example insert of multiple rows:
INSERT INTO YourTable
(col1, col2, col3, col4)
SELECT
cola, colb+Colz, colc, #X
FROM ....
LEFT OUTER JOIN ...
WHERE...
When looking at a loop see what it done inside it. If it is just inserts/deletes/updates, re-write to use single commands. If there are IFs, see if those can be CASE statements or WHERE conditions on inserts/deletes/updates. If so, remove the loop and use set commands.
I've taken loops and replaced them with the set based commands and reduced the execution time from minutes to a few seconds. I have taken procedures with many nested loops and procedure calls and kept the loops (was impossible to only use inserts/deletes/updates), but I removed the cursor, and have seen less locking/blocking and massive performance boosts as well. Here are two looping methods that are better than cursor loops...
if you have to loop, over a set do something like this:
--this looks up each row for every iteration
DECLARE #msg VARCHAR(250)
DECLARE #hostname sysname
--first select of currsor free loop
SELECT #hostname= min(RTRIM(hostname))
FROM master.dbo.sysprocesses (NOLOCK)
WHERE hostname <> ''
WHILE #hostname is not null
BEGIN
set #msg='exec master.dbo.xp_cmdshell "net send '
+ RTRIM(#hostname) + ' '
+ 'testing "'
print #msg
--EXEC (#msg)
--next select of cursor free loop
SELECT #hostname= min(RTRIM(hostname))
FROM master.dbo.sysprocesses (NOLOCK)
WHERE hostname <> ''
and hostname > #hostname
END
if you have a reasonable set of items (not 100,000) to loop over you can do this:
--this will capture each Key to loop over
DECLARE #msg VARCHAR(250)
DECLARE #From int
DECLARE #To int
CREATE TABLE #Rows
(
RowID int not null primary key identity(1,1)
,hostname varchar(100)
)
INSERT INTO #Rows
SELECT DISTINCT hostname
FROM master.dbo.sysprocesses (NOLOCK)
WHERE hostname <> ''
SELECT #From=0,#To=##ROWCOUNT
WHILE #From<#To
BEGIN
SET #From=#From+1
SELECT #msg='exec master.dbo.xp_cmdshell "net send '
+ RTRIM(hostname) + ' '
+ 'testing "'
FROM #Rows WHERE RowID=#From
print #msg
--EXEC (#msg)
END
I've replaced some cursors with WHILE loops.
DECLARE #SomeTable TABLE
(
ID int IDENTITY (1, 1) PRIMARY KEY NOT NULL,
SomeNumber int,
SomeText varchar
)
DECLARE #theCount int
DECLARE #theMax int
DECLARE #theNumber int
DECLARE #theText varchar
INSERT INTO #SomeTable (SomeNumber, SomeText)
SELECT Number, Text
FROM PrimaryTable
SET #theCount = 1
SELECT #theMax = COUNT(ID) FROM #SomeTable
WHILE (#theCount <= #theMax)
BEGIN
SET #theNumber = 0
SET #theText = ''
SELECT #theNumber = IsNull(Number, 0), #theText = IsNull(Text, 'nothing')
FROM #SomeTable
WHERE ID = #theCount
-- Do something.
PRINT 'This is ' + #theText + ' from record ' + CAST(#theNumber AS varchar) + '.'
SET #theCount = #theCount + 1
END
PRINT 'Done'
Well, often an app dev used to procedural programming will - out of habit - try to do everything procedurally, even in SQL.
Most often, a SELECT with the right paramters might do - or maybe you're dealing with an UPDATE statement.
The point really is: you need to begin to think in set operations and tell your RDBMS what you want done - not how to do it step by step.
It's hard to give a single, "right" answer to this..... you'd almost have to show it with a concrete example.
Marc
I wrote some code that calculated running totals for financial data related to a given year. In each quarter, I had to add the value for the current quarter to the running total while handling NULLs appropriately so that the running total for the previous quarter carried over when the value for the current quarter was NULL.
Originally, I did this using a cursor and from a functional standpoint this met the business requirement. From a technical standpoint, it turned out to be a show-stopper because as the amount of data increased the code took exponentially longer. The solution was to replace the cursor with a correlated sub-query which met the functional requirements and eliminated any performance issues.
Hope this helps,
Bill
While (see what I did there) the question is asked and answered, I wanted to add my response just in case it would help anyone else. Like many others, I'm converting over to while statements, but I wanted to leverage the existing primary key in my data set rather rather than counting up to the size of the result set. I came up with this pattern, which has been working well. I'm open to other others and improvements. Thanks!
declare #PrimaryKey int
/* get first record for looping */
select #PrimaryKey = min(PrimaryKey) from Table
/* it will be null when all records have iterated */
while (#PrimaryKey is not null)
begin
/* do stuff */
/* iterate to next key */
select #PrimaryKey = min(PrimaryKey) from Table where PrimaryKey > #PrimaryKey
end

Resources