May 4, 2009 at 7:57 pm
RBarryYoung (5/4/2009)
I would like to ask Bruce and Samuel if they could describe what a minimally acceptable non-cursor solution would have to be. I am not asking you to tell me a solution, just what such a solution should look like: what would its characteristics or attributes have to be? By minimally acceptable, I mean, what features would it have to have for you to agree to use it instead of Cursors (or While loops)?
[font="Verdana"]For me? It would have to be something that showed some benefit.
Replacing a form of looping over a set and calling a stored procedure per row with dynamic SQL that calls the same stored procedure per row isn't really addressing the issue. For me, the benefit would be in replacing the stored procedure with one that could accept a set of data, so you call it the once. The overhead would be in the procedure calls, not in the loop, so why bother eliminating the loop?
That's where the whole thinking outside of the box approach can help. As it happens, you've already covered better solutions (I believe) to the classic reason for looping over a set and calling a stored procedure per row, that reason being to send an email from SQL Server. From memory, those solutions are:
1. Build a distribution list, and send one email to the list (if the emails are identical).
2. Move the solution outside of the database and use something like SQL Server Notification/Reporting Services to receive the set of data and convert it into individualised emails.
3. Create a custom application, again outside of SQL Server, that deals with the emails.
Aside from interfacing SQL Server with external processes, I can't think of a good reason why a database process should require a row-by-row approach. So as I had mentioned earlier, I believe that a stored procedure interface that works with individual rows is a design issue more than anything else. If you are forced to use such an interface, there is no "good" answer to that, so why not use a loop?
I personally still wouldn't bother with a cursor unless the input data set has no primary key, in which case once again it is a design issue. I've already shown examples of how easy it is to code a loop without a cursor.
[/font]
May 4, 2009 at 9:29 pm
Thanks, Bruce
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
May 4, 2009 at 10:53 pm
thank you barry..
the source code about the sql is:
DECLARE curMultipal CURSOR LOCAL FOR
SELECT T.ORDNUM,T.ITMNUM,K.CLIMOD,T.CLIMAT,COUNT(T.SERNUM) AS QTY FROM A T INNER JOIN B K ON T.CLIMAT=K.CLIMAT WHERE T.SERNUM LIKE 'TMP%' GROUP BY T.ORDNUM,T.ITMNUM,K.CLIMOD,T.CLIMAT ORDER BY T.ORDNUM,T.CLIMAT
OPEN curMultipal
FETCH NEXT FROM curMultipal INTO @ORDNUM,@ITMNUM,@CLIMOD,@CLIMAT,@QTY
WHILE @@FETCH_STATUS -1
BEGIN
EXEC ChooseMul @ORDNUM,@ITMNUM,@CLIMAT,@CLIMOD,@QTY,@MulOrSgl OUTPUT --Get the result, 1, 0
IF @MulOrSgl='1'
BEGIN
SELECT @CnInBox=CTRLN1 FROM CTRL WHERE CTRLID='MULTIPALQTY' AND CTRLNM=@CLIMOD AND FLGMAT=LEFT(@CLIMAT,1)----CADE ADD FLAG:M OR P
SET @BOXQTY=@QTY/@CnInBox
SET @IniBoxQTY=1
WHILE @IniBoxQTY<=@BOXQTY
BEGIN
SELECT @BOXID=CTRLN1 FROM CTRL WHERE CTRLID='FULFILL' AND CTRLNM='BOXID'
UPDATE CTRL SET CTRLN1=CTRLN1+1 WHERE CTRLID='FULFILL' AND CTRLNM='BOXID'
SET @BOXID='BTMP'+@BOXID
SET @SQLSTMT='UPDATE A SET REFNM5=''' + @BOXID + ''' WHERE SERNUM IN (SELECT TOP '+ str(@CnInBox) + ' SERNUM FROM A WHERE ORDNUM=''' + @ORDNUM +''' AND ITMNUM=''' + @ITMNUM + ''' AND CLIMAT=''' + @CLIMAT + ''' AND REFNM5='''')'
EXEC(@SQLSTMT)
IF @@ERROR!=0
GOTO NeedRollBack
SET @IniBoxQTY=@IniBoxQTY+1
END
END
FETCH NEXT FROM curMultipal INTO @ORDNUM,@ITMNUM,@CLIMOD,@CLIMAT,@QTY
END
DEALLOCATE curMultipal
UPDATE A SET REFNM5=SERNUM WHERE REFNM5=''
ChooseMul:
ALTER PROCEDURE [dbo].[ChooseMul]
@ORDNUM AS VARCHAR(20),
@ITMNUM AS VARCHAR(10),
@CLIMAT AS VARCHAR(20),
@CLIMOD AS VARCHAR(10),
@QTY AS INT,
@pResult AS CHAR(1) OUTPUT
AS
/* DEFAULT IS Sig*/
SET @pResult='0'
DECLARE @CnInBox AS INT
SELECT @CnInBox=CTRLN1 FROM CTRL WHERE CTRLID='MULTIPALQTY' AND CTRLNM=@CLIMOD AND FLGMAT=LEFT(@CLIMAT,1) -- ADD FLAG:M OR P BY CADE
IF @QTY<@CnInBox
BEGIN
RETURN
END
IF NOT EXISTS (SELECT * FROM MLPN WHERE CLIMAT=@CLIMAT)
BEGIN
RETURN
END
SET @pResult='1'
RETURN
Thank you agagin...
May 5, 2009 at 7:02 am
RBarryYoung (5/4/2009)
Therefore, I would like to ask Bruce and Samuel if they could describe what a minimally acceptable non-cursor solution would have to be.
It should offer performance benefits over the cursor method
It should be an entirely T-SQL method
The reason for the latter is to keep the solution within the scope of the articles which is:
how to use the new and old features of Transact-SQL to both create and convert SQL routines that are faster, smaller, cleaner, clearer and more supportable without the use of Cursors or While loops.
Distributions lists and SSIS (and other external programs) are what I would consider "normal" tools for performing large emailing operations (or anything else which needs to talk to another system) which the database engine shouldn't be tasked with.
But some people clearly do use the dbmail procedure that ships with SQL Server for more than the occasional system raised email
May 5, 2009 at 8:20 am
Samuel Vella (5/5/2009)
RBarryYoung (5/4/2009)
Therefore, I would like to ask Bruce and Samuel if they could describe what a minimally acceptable non-cursor solution would have to be.It should offer performance benefits over the cursor method
It should be an entirely T-SQL method
The reason for the latter is to keep the solution within the scope of the articles which is:
how to use the new and old features of Transact-SQL to both create and convert SQL routines that are faster, smaller, cleaner, clearer and more supportable without the use of Cursors or While loops.
Distributions lists and SSIS (and other external programs) are what I would consider "normal" tools for performing large emailing operations (or anything else which needs to talk to another system) which the database engine shouldn't be tasked with.
But some people clearly do use the dbmail procedure that ships with SQL Server for more than the occasional system raised email
Thanks Sam. Although I do not entirely agree (because dbmail itself is not "an entirely T-SQL method"), I do appreciate you taking the time to provide your perspective.
Thanks Again,
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
May 5, 2009 at 9:44 am
Cade.Bu (5/4/2009)
thank you barry..the source code about the sql is: ...
Cade: Just want to confirm: are you on SQL Server 2005? If not, then what version are you on?
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
May 5, 2009 at 10:15 am
Cade:
One thing that I just noticed, in the dynamic SQL statement that you construct in the inner loop:
SET @SQLSTMT='UPDATE A SET REFNM5=''' + @BOXID
+ ''' WHERE SERNUM IN (SELECT TOP '+ str(@CnInBox) + ' SERNUM
FROM A WHERE ORDNUM=''' + @ORDNUM
+''' AND ITMNUM=''' + @ITMNUM
+ ''' AND CLIMAT=''' + @CLIMAT
+ ''' AND REFNM5='''')'
EXEC(@SQLSTMT)
you have a "SELECT TOP" but no "ORDER BY" clause. Without that the rows actually returned through the TOP clause are non-deterministic. Is that really what you want or is there some order that should be applied?
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
May 5, 2009 at 11:43 am
That code is quite a rat's warren, without table structure, testdata and proper formatting.
But first let us get rid of the procedure call, once and for all.
SELECT data.*, ctrl.ctrln1
FROM
( SELECT t.ordnum, t.itmnum, k.climod, t.climat, COUNT(t.sernum) AS qty
FROM A T INNER JOIN B K ON t.climat = k.climat
WHERE t.sernum LIKE 'TMP%'
GROUP BY t.ordnum, t.itmnum, k.climod, t.climat
) data /* derived table */
JOIN ctrl ON ctrl.ctrlnm = data.climod AND ctrl.flgmat = LEFT(t.climat, 1)
/* replaces IF @QTY ctrl.ctrln1
A beginning, but long ways to go yet. Now you can start eliminating the WHILE LOOP, and forget about the following first lines within.
EXEC ChooseMul @ORDNUM, @ITMNUM, @CLIMAT, @CLIMOD, @QTY, @MulOrSgl OUTPUT
--Get the result, 1, 0
IF @MulOrSgl='1' BEGIN
SELECT @CnInBox=CTRLN1
FROM CTRL
WHERE CTRLID = 'MULTIPALQTY' AND CTRLNM =@CLIMOD AND FLGMAT = LEFT(@CLIMAT,1)
----CADE ADD FLAG:M OR P
Hope this helps, but I haven't got the slightest idea what the code must do :blink:
Perhaps you can give us some clarification?
May 5, 2009 at 6:37 pm
Thanks for everyone's response.
Cade: Just want to confirm: are you on SQL Server 2005? If not, then what version are you on?
------------
HI Barry, the sql server servion is 2008.
One thing that I just noticed, in the dynamic SQL statement that you construct in the inner loop:
SET @SQLSTMT='UPDATE A SET REFNM5=''' + @BOXID
+ ''' WHERE SERNUM IN (SELECT TOP '+ str(@CnInBox) + ' SERNUM
FROM A WHERE ORDNUM=''' + @ORDNUM
+''' AND ITMNUM=''' + @ITMNUM
+ ''' AND CLIMAT=''' + @CLIMAT
+ ''' AND REFNM5='''')'
EXEC(@SQLSTMT)
you have a "SELECT TOP" but no "ORDER BY" clause. Without that the rows actually returned through the TOP clause are non-deterministic. Is that really what you want or is there some order that should be applied?
---
i just need to update the top quantity rows to the same @BOXID, no matter about the order sequence.
--===--
DECLARE curMultipal CURSOR LOCAL FOR SELECT T.ORDNUM,T.ITMNUM,K.CLIMOD,T.CLIMAT,COUNT(T.SERNUM) AS QTY FROM A T INNER JOIN KNMT K ON T.CLIMAT=K.CLIMAT WHERE T.SERNUM LIKE 'TMP%' GROUP BY T.ORDNUM,T.ITMNUM,K.CLIMOD,T.CLIMAT ORDER BY T.ORDNUM,T.CLIMAT
OPEN curMultipal
FETCH NEXT FROM curMultipal INTO @ORDNUM,@ITMNUM,@CLIMOD,@CLIMAT,@QTY
WHILE @@FETCH_STATUS -1
BEGIN
EXEC ChooseMul @ORDNUM,@ITMNUM,@CLIMAT,@CLIMOD,@QTY,@MulOrSgl OUTPUT
----check whether this group[@ORDNUM,@ITMNUM,@CLIMOD,@CLIMAT,@QTY] fit the condition
if return 1, will run the following sql.
May 5, 2009 at 7:59 pm
Cade.Bu (5/5/2009)
you have a "SELECT TOP" but no "ORDER BY" clause. Without that the rows actually returned through the TOP clause are non-deterministic. Is that really what you want or is there some order that should be applied?
---
i just need to update the top quantity rows to the same @BOXID, no matter about the order sequence.
No, Cade, this issue is not what order the "top quantity rows" are returned, the issue is that you are not selecting the "top quantity rows" at all because you are not ordering them by any quantity. Without that TOP just picks the first rows it finds randomly (well technically, non-deterministically). I can go ahead and write it with this flaw incorporated in the solution, but be aware that it means that you may get completely different results, for no apparent reason, from time to time (the same is true of the current routine).
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
May 5, 2009 at 8:21 pm
Thank you berry..
Let's take a example:
CREATE TABLE A
(
ORDNUM VARCHAR(20),
SERNUM VARCHAR(20),
ORDQTY INT,
SBOXID VARCHAR(20)
)
GO
CREATE TABLE CTRL
(
CTRLID VARCHAR(15),
CTRLN1 INT,
CTRLNM VARCHAR(10)
)
GO
DECLARE @N INT=1;
DECLARE @N2 INT;
WHILE @N<=5000
BEGIN
SET @N2=@N/100+1
INSERT INTO A VALUES(@N2,@N,10,'')
SET @N+=1
END
GO
INSERT INTO CTRL VALUES('MULTIPALQTY',5,'')
INSERT INTO CTRL VALUES('FULFILL',10000,'BOXID')
GO
CREATE PROC ChooseMul
@ORDNUM VARCHAR(20),
@PRESULT CHAR(1) OUTPUT
AS
BEGIN
SET @PRESULT=1
END
GO
DECLARE @ORDNUM VARCHAR(20);
DECLARE @BOXID VARCHAR(30);
DECLARE @QTY AS INT;
DECLARE @CnInBox AS INT;
DECLARE @IniBoxQTY AS INT;
DECLARE @BOXQTY AS INT;
DECLARE @PRESULT CHAR(1);
DECLARE TESTCURSOR CURSOR LOCAL FOR SELECT ORDNUM,SUM(ORDQTY) AS QTY FROM A GROUP BY ORDNUM
OPEN TESTCURSOR
FETCH NEXT FROM TESTCURSOR INTO @ORDNUM,@QTY
WHILE @@FETCH_STATUS -1
BEGIN
EXEC ChooseMul @ORDNUM,@PRESULT OUTPUT
IF @PRESULT='1'
BEGIN
SELECT @CnInBox=CTRLN1 FROM CTRL WHERE CTRLID='MULTIPALQTY'
SET @BOXQTY=@QTY/@CnInBox
SET @IniBoxQTY=1
WHILE @IniBoxQTY<=@BOXQTY
BEGIN
SELECT @BOXID=CTRLN1 FROM CTRL WHERE CTRLID='FULFILL' AND CTRLNM='BOXID'
UPDATE CTRL SET CTRLN1=CTRLN1+1 WHERE CTRLID='FULFILL' AND CTRLNM='BOXID'
SET @BOXID='BTMP'+@BOXID
UPDATE TOP (@CnInBox) A SET SBOXID=@BOXID WHERE ORDNUM=@ORDNUM AND SBOXID=''
SET @IniBoxQTY+=1
END
END
FETCH NEXT FROM TESTCURSOR INTO @ORDNUM,@QTY
END
--========--
take 1m and 8s in my nb(cpu:AMD T64x2 1.9G ,RAM:2G)
i think the cursor in the circulation consume much more time...
May 5, 2009 at 11:42 pm
Cade:
Not sure what this is showing us?
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
May 6, 2009 at 9:29 am
Jeff Moden (5/4/2009)
Manie Verster (5/4/2009)
gserdijn (5/1/2009)
Manie Verster (4/30/2009) Here is a cursor that I would like you to convert to set for me please. If you can do this I will accept the fact that since SQL Server 2005 cursors are no longer necessary.
Does this work?
DECLARE @MaxLen INT
SELECT @MaxLen = MAX(LEN(firstname + ' ' + lastname )) FROM AdventureWorks.Sales.Customer
SET ROWCOUNT @MaxLen;
SELECT IDENTITY(int,1,1) AS id INTO #Tally FROM sysobjects a;
SET ROWCOUNT 0;
SELECT
SUBSTRING(firstname + ' ' + lastname , id,1) search,
ASCII(SUBSTRING(firstname + ' ' + lastname , id,1)),
firstname + ' ' + lastname [searchstr] FROM AdventureWorks.Sales.Customer CROSS JOIN #Tally tally
WHERE NOT ASCII(SUBSTRING(firstname + ' ' + lastname , id,1)) IS NULL
ORDER BY searchstr, id
DROP TABLE #Tally;
(Luckily you didn't mention performance)
Friend, you taught me something today. With a few minor changes in your query I ran it and got the results that I wanted. Your query returned duplicates so I added DISTINCT and fixed the ORDER BY to accommodate the DISTINCT. I also used Barry's code from his article to check the performance and it's not to shabby.
cpumslogrdselapsed
1562343548
Thanks for the lesson. Barry, get this man to help you!
Funny.... AdventureWorks.Sales.Customer has no first or last name columns in my Server... and it's a brand new installation with sp3. In other words, the following fails on my box...
DECLARE @MaxLen INT
SELECT @MaxLen = MAX(LEN(firstname + ' ' + lastname )) FROM AdventureWorks.Sales.Customer
You sure that's the correct table?
Maybe I have an old version but doesn't matter, use any table or any string fields. This works famously. I wonder where he got the tally table idea from....?:hehe::hehe:;-)
:-PManie Verster
Developer
Johannesburg
South Africa
I can do all things through Christ who strengthens me. - Holy Bible
I am a man of fixed and unbending principles, the first of which is to be flexible at all times. - Everett Mckinley Dirkson (Well, I am trying. - Manie Verster)
May 6, 2009 at 10:09 am
OK, Cade, here is the No Cursor, No While Loops solution that I came up with to your original question. I cannot be sure however because I do not have anything to test it against nor even the table definitions. The later definitions & samples that you posted were not at all compatible with your original posting, so I cannot use them.
Anway, here it is:
BEGIN TRY --Drop the Temp table if it already exists
DROP Table #ATemp
END TRY
BEGIN CATCH
--suppress any error
END CATCH
--====== Preserve the current conditions state
SELECT T.ORDNUM,T.ITMNUM,K.CLIMOD,T.CLIMAT,COUNT(T.SERNUM) AS QTY
Into #ATemp
FROM A T
INNER JOIN B K ON T.CLIMAT=K.CLIMAT
WHERE T.SERNUM LIKE 'TMP%'
GROUP BY T.ORDNUM,T.ITMNUM,K.CLIMOD,T.CLIMAT
ORDER BY T.ORDNUM,T.CLIMAT
Alter table #ATemp Add Constraint PK_ATemp Primary Key( ORDNUM, ITMNUM, CLIMOD, CLIMAT )
--==
;WITH cteMultipal as (
Select ta.*, c.*
, ta.QTY/CTRLN1 as BoxQty
From #ATemp ta
Join CTRL c ON c.CTRLNM = ta.CLIMOD
And c.FLGMAT = LEFT(ta.CLIMAT,1) -- ADD FLAG:M OR P BY CADE
And ta.QTY < c.CTRLN1
Where c.CTRLID='MULTIPALQTY'
And NOT EXISTS (SELECT * FROM MLPN m WHERE m.CLIMAT=ta.CLIMAT)
), cteBoxID as (
Select 'BMTP'+CTRLN1 as BoxID
FROM CTRL
WHERE CTRLID='FULFILL'
AND CTRLNM='BOXID'
), cteAwhere as (
Select Top(Select MAX(CTRLN1) From cteMultipal) *
From A
Join cteMultipal c ON a.ORDNUM=c.ORDNUM
AND a.ITMNUM=c.ITMNUM
AND a.CLIMAT=c.CLIMAT
AND a.REFNM5=''
)
UPDATE cteAwhere
Set REFNM5=(Select BoxID From cteBoxID)
;WITH cteMultipal as (
Select ta.*, c.*
, ta.QTY/CTRLN1 as BoxQty
From #ATemp ta
Join CTRL c ON c.CTRLNM = ta.CLIMOD
And c.FLGMAT = LEFT(ta.CLIMAT,1) -- ADD FLAG:M OR P BY CADE
And ta.QTY < c.CTRLN1
Where c.CTRLID='MULTIPALQTY'
And NOT EXISTS (SELECT * FROM MLPN m WHERE m.CLIMAT=ta.CLIMAT)
)
UPDATE cteMultipal
SET CTRLN1=CTRLN1+(Select BoxQty From cteCnInBox)
WHERE CTRLID='FULFILL'
AND CTRLNM='BOXID'
AND 1 = (Select MulOrSql From cteChoseMul)
UPDATE A
SET REFNM5=SERNUM
WHERE REFNM5=''
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
May 6, 2009 at 7:28 pm
Manie Verster (5/6/2009)
Maybe I have an old version but doesn't matter, use any table or any string fields. This works famously. I wonder where he got the tally table idea from....?:hehe::hehe:;-)
I can only hope that gserdijn got it from me. 😛
But, here's another problem that we've warned many a poster about that another whole article could be written about. You basically said that you wanted to replace a given cursor and that's actually a problem because what the cursor did was actually the wrong thing to do according to the description that followed that. All the cursor did was split the names into a huge column of single character rows that you would have to go looking through to find your problem. Imagine if you had a million names.
The real problem is as you described it, not as you coded it. You really just want to find any name that has a bad character in it and what that character is. Your cursor method took 22 seconds on my machine... gserdijn's method, as well written as it was, took 11 seconds on my machine and left you with the same problem as the cursor... a huge column of letters to review one way or another.
And this isn't directed at just you, Mannie... it's for everyone to think about. The reason why someone wrote a cursor to begin with is because they were having problems with some logic. When you rewrite a cursor to be set based, don't make the same error in logic. Look at what the cursor does and make sure the logic is sound. If it's not, don't just convert it to set based... you may not get the full benefit of performance because the logic was bad to begin with. Re-engineer the code.
So, what to do about your cursor? Re-engineer it to do what it should have to begin with... find ONLY bad names and ONLY bad characters. Like this (my Tally table is in a DB called "Util" and I pointed to the Person.Contact table because that's what was available on my machine)...
[font="Courier New"] USE AdventureWorks
;WITH cteNames AS
(
SELECT FirstName + ' ' + LastName AS FullName
FROM AdventureWorks.Person.Contact
WHERE FirstName + ' ' + LastName LIKE '%[^- .''A-Z]%'
)
SELECT n.FullName,
SUBSTRING(n.FullName,t.N,1) AS BadChar,
t.n AS BadCharPosition,
ASCII(SUBSTRING(n.FullName,t.N,1)) AS BadCharNum
FROM cteNames n
CROSS JOIN Util.dbo.Tally t
WHERE t.N <= LEN(n.FullName)
AND SUBSTRING(n.FullName,t.N,1) LIKE '[^- .''A-Z]'[/font]
That little chunk of computational heaven runs in less than 4/10ths of a second AND does everything you want instead of just creating a list of characters. And (get this), it's not even optimized, yet. (Not kidding... some of the wizards on this site will take the extra step and find a way to warp the splitter logic and get it down to 1/10th of a second). Still it's more than 55 times faster than the original cursor and more than 27 times faster than the original logic converted to set based.
Like I said, if you're going to take the time to replace a cursor (and that's not long... took me longer to format the code for this forum than to write it and test it), take the time to make sure that the original logic isn't flawed while you're at it. I've seen it where not only was the cursor replaced, but the logic change also made it so that hundreds of other lines of other "support" code suddenly became totally unnecessary.
--Jeff Moden
Change is inevitable... Change for the better is not.
Viewing 15 posts - 166 through 180 (of 316 total)
You must be logged in to reply to this topic. Login to reply