4

From this answer: Is there a way to loop through a table variable in TSQL without using a cursor?

I'm using the method

WHILE EXISTS(SELECT * FROM #Temp)

The problem is that it's outputting multiple tables, if possible I'd like to output as a single table.

Declare @Id int

WHILE EXISTS(SELECT * FROM #Temp)
Begin

    Select Top 1 @Id = Id From #Temp

    --Do some processing here

    Delete #Temp Where Id = @Id

End

So right now it outputs this:

x  y
-- --
1  a

x  y
-- --
1  b

But I'd like it to output this:

x  y
-- --
1  a
2  b

What I'm trying to achieve, I have this in a field:

1234,1432,1235

I have a process that splits the field into records(it works with sql server 2000):

DECLARE @String VARCHAR(100)
    SELECT @String = str FROM  field --with the 1234,1432,1235

    SELECT SUBSTRING(',' + @String + ',', Number + 1,
    CHARINDEX(',', ',' + @String + ',', Number + 1) - Number -1)AS str
    INTO #temp
    FROM master..spt_values
    WHERE Type = 'P'
    AND Number <= LEN(',' + @String + ',') - 1
    AND SUBSTRING(',' + @String + ',', Number, 1) = ','
    GO

So now, #temp has:

str
---
1234
1432
1235

So I need to go through each record to query the information I need.

And I'd like it to output something like this:

str   name   age
---   ----   ---
1234  Bob    23
1432  Jay    41
1235  Tim    12

The current While loop outputs it like this, which I don't want:

str   name   age
---   ----   ---
1234  Bob    23

str   name   age
---   ----   ---
1432  Jay    41

str   name   age
---   ----   ---
1235  Tim    12

Final Working Result:

SET NOCOUNT ON;

DECLARE @String VARCHAR(1000);
SELECT @String = Tnn FROM (SELECT 
 CO.USER_2 AS Tnn
FROM 
    [VMFG].[dbo].[CUSTOMER_ORDER] AS CO 
    LEFT JOIN DBO.Tnn_Header AS Tnn ON Tnn.TnnNumber = CO.USER_2 AND Tnn.StatusID = '5' WHERE CO.ID = 'ORDERID') AS Place --with the 1234,1432,1235

DECLARE @Id nvarchar(50),
        @Discount nvarchar(50), 
        @Spin nvarchar(50), 
        @Commission_Hmm nvarchar(50), 
        @Commission nvarchar(50), 
        @TnnID nvarchar(50);

DECLARE @Output TABLE (
TnnNumber nvarchar(50),
        Discount nvarchar(50), 
        Spin nvarchar(50), 
        Commission_Hmm nvarchar(50), 
        Commission nvarchar(50), 
        TnnID nvarchar(50));

DECLARE crs CURSOR STATIC LOCAL READ_ONLY FORWARD_ONLY
FOR  SELECT SUBSTRING(',' + @String + ',', Number + 1,
     CHARINDEX(',', ',' + @String + ',', Number + 1) - Number -1) AS [ID]
     FROM master..spt_values
     WHERE Type = 'P'
     AND Number <= LEN(',' + @String + ',') - 1
     AND SUBSTRING(',' + @String + ',', Number, 1) = ',';

OPEN crs;

FETCH NEXT
FROM  crs
INTO  @Id;

WHILE (@@FETCH_STATUS = 0)
BEGIN
    -- do some processing..
SELECT 
@Id = TH.TnnNumber,
@Discount = CASE WHEN COUNT(DISTINCT TL.DiscountCodeID) > 1 THEN 'Varies, View Tnn' ELSE CAST(MAX(DC.Value) AS VARCHAR(60)) END,
@Spin = CASE WHEN TS.SpinID > 4 THEN 'Has Specifics, View Tnn' ELSE TS.Value END,
@Commission_Hmm = CASE WHEN COUNT(DISTINCT TL.Commission_Hmm) > 1 THEN 'Varies, View Tnn' ELSE CAST(MAX( ISNULL(str(TL.Commission_Hmm,12),'Default Comm')) AS VARCHAR(60)) END,
@Commission = CASE WHEN COUNT(DISTINCT TL.Commission) > 1 THEN 'Varies, View Tnn' ELSE CAST(MAX(ISNULL(str(TL.Commission,12),'Default Comm')) AS VARCHAR(60)) END,
@TnnID = TL.TnnID 

FROM DBO.Tnn_Header AS TH
LEFT JOIN DBO.Tnn_LINE AS TL ON TH.TnnID = TL.TnnID
LEFT JOIN DBO.Tnn_Spin AS TS ON TH.SpinID = TS.SpinID
LEFT JOIN DBO.Tnn_DiscountCode AS DC ON TL.DiscountCodeID = DC.DiscountCodeID 

WHERE TnnNumber = @id

GROUP BY 
TH.TnnNumber,
TS.SpinID,
TS.Value,
TL.TnnID
-- end do some processing..
    INSERT INTO @Output (TnnNumber, Discount, Spin, Commission_Hmm, Commission, TnnID)
    VALUES (@Id, @Discount, @Spin, @Commission_Hmm, @Commission, @TnnID);

    FETCH NEXT
    FROM  crs
    INTO  @Id;
END;

CLOSE crs;
DEALLOCATE crs;

SELECT TnnNumber, Discount, Spin, Commission_Hmm, Commission, TnnID
FROM   @Output;
General Grievance
  • 4,555
  • 31
  • 31
  • 45
sojim2
  • 1,245
  • 2
  • 15
  • 38

2 Answers2

4

You are wasting your time and energy following such bad advice. If you absolutely must (extra emphasis on the must) take a row-by-row approach (CURSOR or WHILE loop), then you are better off with a CURSOR. It is a built-in construct that is more efficient, and less error-prone. You just need to use the right options, such as making it STATIC, LOCAL, READ_ONLY, and FORWARD_ONLY. You don't need STATIC if the cursor query is only hitting temporary tables and/or table variables.

People will argue with this and say that "you must avoid cursors at all cost!", but they haven't done the tests to see that such a popular notion is really just a myth. And if they have done tests that appear to confirm it, then they haven't set the appropriate options, mostly STATIC, which dumps the result of the cursor query into a temp table. Without this option, fetching new rows will re-check the base tables to make sure that they still exist, and that is where the performance hit is (the I/O plus the locking). And that is also why you typically don't need the STATIC option when querying only temporary tables and/or table variables. What do I mean by "re-checking"? Just look at the documentation for @@FETCH_STATUS. The return values don't just cover "success" (0) and "no more rows" (-1): there is a return value, (-2), that means "The row fetched is missing".

SET NOCOUNT ON;
DECLARE @Id INT,
        @Name sysname,
        @Type VARCHAR(5);

--  the Table Variable replaces #Temp2 in the original query
DECLARE @Output TABLE (Id INT NOT NULL, Name sysname, [Type] VARCHAR(5));

-- the CURSOR replaces #Temp in the original query
DECLARE crs CURSOR STATIC LOCAL READ_ONLY FORWARD_ONLY
FOR  SELECT [object_id], name, [type]
     FROM   sys.objects -- dbo.sysobjects for SQL 2000 -- ATable in the original query
    ORDER BY [object_id] ASC;

OPEN crs;

FETCH NEXT
FROM  crs
INTO  @Id, @Name, @Type;

WHILE (@@FETCH_STATUS = 0)
BEGIN
    INSERT INTO @Output (Id, Name, [Type])
    VALUES (@Id, @Name, @Type);

    -- do some processing..

    FETCH NEXT -- replaces the DELETE and re-SELECT in the original query
    FROM  crs
    INTO  @Id, @Name, @Type;
END;

CLOSE crs;
DEALLOCATE crs;

SELECT Id, Name, [Type]
FROM   @Output;

UPDATE

Given the iteration is being done over a query that splits a CSV of INTs, the resulting query would look similar to the following:

SET NOCOUNT ON;

DECLARE @String VARCHAR(1000);
SELECT @String = str FROM [Table]; --with the 1234,1432,1235

DECLARE @Id INT,
        @Name NVARCHAR(50),
        @Age  TINYINT;

DECLARE @Output TABLE (Id INT NOT NULL, Name NVARCHAR(50), Age TINYINT);

DECLARE crs CURSOR STATIC LOCAL READ_ONLY FORWARD_ONLY
FOR  SELECT SUBSTRING(',' + @String + ',', Number + 1,
     CHARINDEX(',', ',' + @String + ',', Number + 1) - Number -1) AS [ID]
     FROM master..spt_values
     WHERE Type = 'P'
     AND Number <= LEN(',' + @String + ',') - 1
     AND SUBSTRING(',' + @String + ',', Number, 1) = ',';

OPEN crs;

FETCH NEXT
FROM  crs
INTO  @Id;

WHILE (@@FETCH_STATUS = 0)
BEGIN
    -- do some processing..
    -- Logic to set value of @Name
    -- Logic to set value of @Age

    INSERT INTO @Output (Id, Name, Age)
    VALUES (@Id, @Name, @Age);

    FETCH NEXT
    FROM  crs
    INTO  @Id;
END;

CLOSE crs;
DEALLOCATE crs;

SELECT Id, Name, Age
FROM   @Output;
Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
  • Thank you for the explanation, I'll give this one a try too, hopefully it'll work with sql server 2000 – sojim2 Feb 18 '16 at 02:55
  • where would i put the temp table? – sojim2 Feb 18 '16 at 03:07
  • @sojim2 I don't see why it wouldn't work in SQL Server 2000. It has been a while since I used that version, but I remember doing this back then. About the temp table, are you referring to `#Temp`? If so, then you don't need it at all in this approach. Technically, the system-created / hidden table created to hold the results of the cursor query due to the `STATIC` option, is the equivalent of `#Temp`. If it helps, I updated the example code in my answer to note where each piece fits from the original query. I just wanted to provide a working example. – Solomon Rutzky Feb 18 '16 at 03:28
  • But #temp has the data I need to process? Or am I not understanding something? I just tried the code with just the processing code (no #temp table with the data i need to process) and it's giving some errors such as "invalid column name 'object_id' also sys.objects may not work with sql server 2000 – sojim2 Feb 18 '16 at 04:10
  • @sojim2 I just updated with the correct system table for SQL Server 2000 (i.e. `dbo.sysobjects`). Regarding `#Temp`, where did you get the data to populate `#Temp` in the first place? And why did you create `#Temp` in the first place? If it was to accomplish the suggestion from that other post, then that is part of what is wasteful. Whatever query was used to populate `#Temp` is the query that is used in the CURSOR definition (after the `FOR`). – Solomon Rutzky Feb 18 '16 at 04:14
  • So what I have is this value in a field(without quotes) "1234,1432,1235" I then have a process that split the commas and put each of the 3 value into a #temp table (#temp has 3 records now).. then I use the #temp in the loop process – sojim2 Feb 18 '16 at 04:22
  • I use the #temp in the while loop to go through each record and process it – sojim2 Feb 18 '16 at 05:00
  • @sojim2 What do you mean by "a process"? It's a query or TVF, right? If so, just put that in for the cursor query. Or, if it really must be a temp table, just replace the cursor query with `SELECT field from #Temp`, but I really think you most likely don't need that step. It would help if you were less vague about this stuff, and put more detail in the Question. – Solomon Rutzky Feb 18 '16 at 05:06
  • I added more detail in the question. Let me know if you need more clarification. It starts at "**What I'm trying to achieve**" – sojim2 Feb 18 '16 at 05:40
  • @sojim2 Thanks, I see that info. 1) As I said before, just replace the query I have in my example code in the CURSOR with your `SELECT SUBSTRING()..` query, and of course, remove the `INTO #temp`. of course, that leaves yet another question: where do `name` and `age` come from? Is there a reason why you are not simply JOINing the `#Temp` table with the table containing the `name` and `age` fields? – Solomon Rutzky Feb 18 '16 at 06:03
  • the name and age is an over-simplification, there are many dependencies from 3 left joins and 4 cases to get the data from the "str" column.. and it has to do this for each of the values in field containing "1234,1432,1235" .. this field may contain X number of comma delimited values – sojim2 Feb 18 '16 at 06:57
  • So I did what you said and it's in the "**Current Result:**" at the bottom of the question. I ran it with the process (joins cases etc) at the do process here and it says incorrect syntax near the keyword `FROM` which points to `From master..spt_values` – sojim2 Feb 18 '16 at 07:35
  • @sojim2 No, you did not do as I instructed. I said to replace the SELECT query in the CURSOR (starts after the `FOR`) with your `SELECT SUBSTRING()...` query, and remove the `INTO #Temp`. You just took part of your query and put it at what appears to be the end of the loop. And regarding the complexity of getting `name` and `age`, I am still not convinced that it can't be done in a set-based manner, but again, you aren't providing enough info to figure that part out. Still, I added an UPDATE section to the answer with what I have been suggesting. Just curious: is this a class assignment? – Solomon Rutzky Feb 18 '16 at 15:44
  • Definitely not a class assignment, I'm creating an order proofing page and need to pull up some discount information for an order. We're on an old SQL Server 2000 (I don't think classes would be on this system :) thank god!) I've updated the "**Current Result:**" btw with the full processing code.. only variables changed – sojim2 Feb 18 '16 at 18:15
  • I've also put the errors I'm currently getting with this query towards the end with `**` – sojim2 Feb 18 '16 at 18:26
  • @sojim2 Good point about a class not using an old version of SQL Server. But I have to ask, are you new to programming and/or SQL Server? You are missing things that are _very_ basic. And being new to this is not a bad thing, but I don't have any more time to devote to this and it seems like you need more than just the answer. You aren't setting the `@String` variable (you undid the code that was setting it), you changed the variables (good) but aren't setting them, nor have you updated the table variable definition. You need to walk-through the code and figure out how the pieces fit together. – Solomon Rutzky Feb 18 '16 at 18:45
  • I'm happy to say it's working now! Thank you for all your help.. it's so much faster! And yes, I'm quite new to programming and sql.. just need a little guidance and I can get to it.. I followed every word you said. (Code is updated now) – sojim2 Feb 18 '16 at 19:40
  • @sojim2 Yer quite welcome. Glad it is working and hopefully you are learning several new concepts here. Now that I see your query, I still think you could do away with the loop entirely by removing the CURSOR and WHILE constructs, going putting the `INTO #Temp` back into your splitter query, and then simply add an INNER JOIN in your main query to `#Temp on #temp.id = TH.TnnNumber`. then you wouldn't have the loop or table variable. – Solomon Rutzky Feb 18 '16 at 19:51
  • I'm attempting to do the innerjoin; however, the innerjoin keeps on skipping the last row for some reason as stated here: http://stackoverflow.com/questions/35755501/query-shows-1-less-than-whats-available-in-the-temp-table – sojim2 Mar 02 '16 at 18:43
1

your query has syntax error but I tried below query and worked fine

-- this is only to populate my data table
Select object_id Id, name Into #Temp From sys.tables

select * into #temp2 from #Temp where 1=2

Declare @Id int

WHILE EXISTS(SELECT * FROM #Temp)
Begin
    Select Top 1 @Id = Id
    From #Temp
    ORDER BY Id -- this order is important

    -- use insert...into, NOT select...into
    insert into #temp2 
    select * 
    from #Temp
    where Id = @Id

    Delete #Temp Where Id = @Id
End

BTW, you can not have SELECT...INTO inside a loop, as the 2nd iteration will raise error. You need to create #temp2, out side the loop and use INSERT...INTO instead of SELECT...INTO

FLICKER
  • 6,439
  • 4
  • 45
  • 75
  • Missing select * from #temp2 outside the WHILE? – nobody Feb 18 '16 at 01:01
  • @inquisitive_mind, no that part is only to populate my sample data table. I think the missing part is the ORDER BY. I added some comments, please take a look – FLICKER Feb 18 '16 at 01:02
  • if this is the solution, pleas mark as answer. thanks – FLICKER Feb 18 '16 at 01:06
  • Just a thought. @FLICKER You could also just do an empty select to create the `#temp2` table i.e. `SELECT * FROM #Temp INTO #temp2 WHERE 1 = 2` That way you don't have to know the schema of `#Temp` before you create the `#temp2` table – Kidiskidvogingogin Feb 18 '16 at 01:09
  • @Kidiskidvogingogin, right, that is a way to create a temp table from existing one. I edit my post – FLICKER Feb 18 '16 at 01:15
  • @FLICKER noted, I'll have to check back in a few hours then I'll mark it if it answers the question. Thanks. (I'm forced to go home from work now) – sojim2 Feb 18 '16 at 01:17
  • @FLICKER so I just pasted this in SSMS and it's showing "sys.tables" as green.. I'm assuming sql server 2000 doesn't support this. – sojim2 Feb 18 '16 at 04:16
  • use sysobjects instead of sys.tables. that is not important because it supposed to populate my sample table (#Temp). you can ignore that just use you own table – FLICKER Feb 18 '16 at 04:36
  • @FLICKER Thanks for your help but with the help from srutzky I didn't need any temp tables at all and the query is so fast.. I've updated my original post with the final answer. – sojim2 Feb 18 '16 at 19:42
  • @sojim2, you're welcome. I'm glad you found the right solution :) – FLICKER Feb 18 '16 at 21:40