From: Cindy Lee on
On Jun 20, 1:46 am, Jeroen Mostert <jmost...(a)xs4all.nl> wrote:
> On 2010-06-19 22:14, Cindy Lee wrote:> Yeah, I'm using forward only cursors.  Problem is I need to know if
> > it's the last row before i fetch.  I'm splitting money by percentages
> > and I round up on the first few rows, and the the last row gets what
> > every money is left over.  Otherwise there will be an extra penny.
> > I can either check if the amount left is less than the amount by
> > percentage, or check to see if it's the last row and then just take
> > the amount left.
> > I'm subtracting from the total amount everytime someone gets a
> > percentage.
> > I think it would be easier to just check if it's the last row rather
> > than if the amount left is less than the percentage amount.
>
> You can always complicate the logic by processing row N after fetching row N
> + 1. The last row is then processed after all the others.
>
>    FETCH NEXT FROM c INTO @a1, @b1, @c1, @d1, @e1;
>    WHILE @@FETCH_STATUS = 0 BEGIN
>      FETCH NEXT FROM c INTO @a2, @b2, @c2, @d2, @e2;
>      IF @@FETCH_STATUS <> 0 BREAK;
>      -- Process a1, b1, c1, d1, e1
>      SELECT @a1 = @a2, @b1 = @b2, @c1 = @c2, @d1 = @d2, @e1 = @e2;
>    END
>    -- Process a1, b1, c1, d1, e1 as the last row -- if there were no rows at
> all, the values will be all NULL
>
> Erland's solution with ROW_NUMBER() is preferable from a maintainability
> point of view, though -- my approach is harder to initially understand and
> duplicates code, both of which is bad. Also, a forward ROW_NUMBER() gives
> you an easy way of telling whether you're on "the first few rows".
>
> Because ROW_NUMBER() with a different ORDER BY than the main ORDER BY clause
> requires an additional sort step, the solution with an additional FETCH may
> perform better, but this should not be the overriding concern -- especially
> not since you're already using cursors, which will probably dwarf everything
> else. I'm just offering it as a possible alternative.
>
> Apropos that: judicious use of ROW_NUMBER() and CTEs/subqueries may solve
> your problem without the use of cursors at all, which is possibly better
> (possibly, because cursors can actually be a faster solution for problems
> like these). For example, you can calculate the total amount of money you're
> going to be allocating to rows and then add the leftover to the last row:
>
>    WITH x AS (
>      SELECT
>         money * percentage AS share,
>         CASE WHEN ROW_NUMBER() OVER (ORDER BY a DESC) = 1 THEN 1 ELSE 0 END AS last_row
>      FROM t
>      WHERE ...
>    )
>    SELECT share + CASE last_row WHEN 1 THEN totalmoney - (SELECT SUM(share)
> FROM x) ELSE 0 END
>    FROM x
>    ORDER BY a
>
> The problem here is that the last row determination and total sum require
> extra passes through the data. Cursors may or may not be faster here. The
> query does have the benefit of expressing the logic succinctly, which
> improves maintainability.
>
> --
> J.

Thanks for the responses. I think I like fetch the next one and keep
the old results. Only thing is it will create a few extra variables
which probably doesn't matter to me. That looks like the faster one,
I'm guessing. I'm using forward read only cursors and my query looks
a little like below, but a little more complicated. The main concern
here is speed, the table has millions of rows. I'm acutally opening
up 2 cursors. Thoughts?


Declare cursor C for
select billNumber amount from bills where customerId=50

FETCH NEXT FROM C into @billNumber, @amount
@amountLeft=(a)amount
WHILE @@FETCH_STATUS <> 0 BEGIN

Declare cursor BillSplitCursor for
select workerId, percentage from billsSplit where
billNumber=(a)billNumber
FETCH NEXT FROM BillSplitCursor into @workerId, @percentage
WHILE @@FETCH_STATUS <> 0 BEGIN
--IF LAST ROW
@workerAmount=(a)amountLeft
--else
@workerAmount = @percentage*@amount
@amountleft=(a)amountleft-@workerAmount


--Insert into a temp table or select to output
select @billNumber, @workerId, @workAmount


FETCH NEXT FROM BillSplitCursor into @workerId, @percentage

END


FETCH NEXT FROM C into @billNumber, @amount

END
From: Tom Cooper on
Comment inline.

"Erland Sommarskog" <esquel(a)sommarskog.se> wrote in message
news:Xns9D9CE68E328B0Yazorman(a)127.0.0.1...
> Cindy Lee (danparker276(a)yahoo.com) writes:
>> Yeah, I'm using forward only cursors. Problem is I need to know if
>> it's the last row before i fetch. I'm splitting money by percentages
>> and I round up on the first few rows, and the the last row gets what
>> every money is left over. Otherwise there will be an extra penny.
>> I can either check if the amount left is less than the amount by
>> percentage, or check to see if it's the last row and then just take
>> the amount left.
>> I'm subtracting from the total amount everytime someone gets a
>> percentage.
>> I think it would be easier to just check if it's the last row rather
>> than if the amount left is less than the percentage amount.
>>
>> So there is no way to tell what the total count will be?
>
> You could save @@cursor_rows into a variable, and then decrement it,
> but better is to add this column to your query:
>
> row_number() OVER(ORDER BY ... )
>
> The columns where are the same as you order on in your ORDER BY clause,
> but the order should be the reverse. That is, if the cursor has this
> ORDER BY clause:
>
> ORDER BY col1, col2 DESC, col3
>
> Your OVER clause needs to be:
>
> row_number() OVER(col1 DESC, col2 ASC, col3 DESC)
>
> When you get 1 in the varible that you populate from this column,
> you are on the last row in the cursor.

If you use this technique, you must be sure your order by cannot generate
ties. That is, in Erland's example no two rows have the same col1, same
col2, and same col3. If you are not guarenteed this, then you are not
guarenteed that you are on the last row when the row_number() column = 1.
Of course, if your cursor query has this problem you could fix it by adding
the primary key to the ORDER BY clause and the row_number() expression. So,
if PKcol was your primary key, you could do
ORDER BY col1, col2 DESC, col3, PKcol
and the row_number expression as
row_number() OVER(ORDER BY col1 DESC, col2 ASC, col3 DESC, PKcol ASC)

Tom


>
>
> --
> Erland Sommarskog, SQL Server MVP, esquel(a)sommarskog.se
>
> Links for SQL Server Books Online:
> SQL 2008: http://msdn.microsoft.com/en-us/sqlserver/cc514207.aspx
> SQL 2005: http://msdn.microsoft.com/en-us/sqlserver/bb895970.aspx
> SQL 2000:
> http://www.microsoft.com/sql/prodinfo/previousversions/books.mspx
>

From: Erland Sommarskog on
Tom Cooper (tomcooper(a)comcast.net) writes:
> If you use this technique, you must be sure your order by cannot generate
> ties. That is, in Erland's example no two rows have the same col1, same
> col2, and same col3. If you are not guarenteed this, then you are not
> guarenteed that you are on the last row when the row_number() column = 1.
> Of course, if your cursor query has this problem you could fix it by
> adding the primary key to the ORDER BY clause and the row_number()
> expression.
> So,
> if PKcol was your primary key, you could do
> ORDER BY col1, col2 DESC, col3, PKcol
> and the row_number expression as
> row_number() OVER(ORDER BY col1 DESC, col2 ASC, col3 DESC, PKcol ASC)

Good point. Yes, you must ORDER BY on something which is unique for
this to work.

In most cases you probably want a unique ORDER BY condition anyway, since
else you will not be guaranteed a deterministic result set.


--
Erland Sommarskog, SQL Server MVP, esquel(a)sommarskog.se

Links for SQL Server Books Online:
SQL 2008: http://msdn.microsoft.com/en-us/sqlserver/cc514207.aspx
SQL 2005: http://msdn.microsoft.com/en-us/sqlserver/bb895970.aspx
SQL 2000: http://www.microsoft.com/sql/prodinfo/previousversions/books.mspx

From: Erland Sommarskog on
Cindy Lee (danparker276(a)yahoo.com) writes:
> Thanks for the responses. I think I like fetch the next one and keep
> the old results. Only thing is it will create a few extra variables
> which probably doesn't matter to me. That looks like the faster one,
> I'm guessing. I'm using forward read only cursors and my query looks
> a little like below, but a little more complicated. The main concern
> here is speed, the table has millions of rows. I'm acutally opening
> up 2 cursors. Thoughts?

Ouch!

My reflection is that if there is a good set-based solution, performance
may improve drastically. But given your vague description so far, it may
be one of these problems where cursors after all are faster. I may also
be a problem where set-based iteration is faster.

If you can find the time, please post:

o CREATE TABLE statement(s) for the table(s) involved, preferably
simplified only to the pertinent columns.
o INSERT statements with sample data, enough to show the gist of
the problem, I guess that this is less than 10 rows.
o The desired result given the sample.
o The version of SQL Server you are using.

We might be able to find a better solution for you.

--
Erland Sommarskog, SQL Server MVP, esquel(a)sommarskog.se

Links for SQL Server Books Online:
SQL 2008: http://msdn.microsoft.com/en-us/sqlserver/cc514207.aspx
SQL 2005: http://msdn.microsoft.com/en-us/sqlserver/bb895970.aspx
SQL 2000: http://www.microsoft.com/sql/prodinfo/previousversions/books.mspx

From: Cindy Lee on
On Jun 21, 1:57 pm, Erland Sommarskog <esq...(a)sommarskog.se> wrote:
> Cindy Lee (danparker...(a)yahoo.com) writes:
> > Thanks for the responses.  I think I like fetch the next one and keep
> > the old results. Only thing is it will create a few extra variables
> > which probably doesn't matter to me.  That looks like the faster one,
> > I'm guessing.  I'm using forward read only cursors and my query looks
> > a little like below, but a little more complicated.  The main concern
> > here is speed, the table has millions of rows.  I'm acutally opening
> > up 2 cursors.  Thoughts?
>
> Ouch!
>
> My reflection is that if there is a good set-based solution, performance
> may improve drastically. But given your vague description so far, it may
> be one of these problems where cursors after all are faster. I may also
> be a problem where set-based iteration is faster.
>
> If you can find the time, please post:
>
> o   CREATE TABLE statement(s) for the table(s) involved, preferably
>     simplified only to the pertinent columns.
> o   INSERT statements with sample data, enough to show the gist of
>     the problem, I guess that this is less than 10 rows.
> o   The desired result given the sample.
> o   The version of SQL Server you are using.
>
> We might be able to find a better solution for you.
>
> --
> Erland Sommarskog, SQL Server MVP, esq...(a)sommarskog.se
>
> Links for SQL Server Books Online:
> SQL 2008:http://msdn.microsoft.com/en-us/sqlserver/cc514207.aspx
> SQL 2005:http://msdn.microsoft.com/en-us/sqlserver/bb895970.aspx
> SQL 2000:http://www.microsoft.com/sql/prodinfo/previousversions/books.mspx

Thanks so much for the responses. I think I'll probably go with
cursors, but here is some more information below if you feel like
looking at it. I'm working with a very old legacy database derived
from rpg programming. The database isn't normalized and it uses
char(6) for ids. I can't change the database tables.

Also, each bill can be split on either the client or the matter,
matter takes priority. So if one split check fails, you have to do
another split check.

There are a few clients that are still on sql server 2000, it's best
if it covers 2000-2008, but if there were a huge difference, I could
have 2 different ones. Most of the columns are char(6) for Ids
(Clients and Matters) and decimal(11,2) for the hours and amount.

Oh the, top 10 was just for debugging, it will return 1000s. This
query will take time (which is ok). I think from the information
below I you'll say I should use cursors.



--it's a very large dynamic select statement for TestCursor joining a
few different tables

OPEN TestCursor

FETCH NEXT FROM TestCursor INTO @gdtrtp,@clientId, @clientIdSub,
@matterId, @matterIdSub, @invoice,

@gdpsdt,@gdtrdt,@gdbldt,@amount,@hours,@gdtkdb,@tmsort,@tlnam,@blsort,@blnam,
@rlsort,@rlnam,@olsort,@olnam,@mttype,@gdseq#,@gdType

WHILE @@FETCH_STATUS = 0
BEGIN

DECLARE @timekeeper char(6),
@roundedAmt decimal(11,2),@amountLeft decimal(11,2),
@perc decimal(11,2), @rowsLeft int

DECLARE SplitCursorMatter CURSOR FORWARD_ONLY STATIC LOCAL FOR

select aspcnt as perc, astkpr, ROUND((@amount * aspcnt),2,0) as
RoundedAmt
from SplitsTable
where asclid=(a)clientId and asclsb=(a)clientIdSub and asmtid=(a)matterId
and asmtsb=(a)matterIdSub
where asclid=(a)clientId and asmtid=(a)matterId
OPEN SplitCursorMatter


FETCH NEXT FROM SplitCursorMatter INTO @perc, @timekeeper,
@roundedAmt
IF @@FETCH_STATUS <> 0
BEGIN
--if no rows for matter then check if it could be split on a client
level

DECLARE SplitCursorClient CURSOR FORWARD_ONLY STATIC LOCAL FOR
select aspcnt as perc, astkpr, ROUND((@amount * aspcnt),2,0) as
RoundedAmt
from SplitsTable
where asclid=(a)clientId and asclsb=(a)clientIdSub

FETCH NEXT FROM SplitCursorClient INTO @perc, @timekeeper,
@roundedAmt
IF @@FETCH_STATUS <> 0
BEGIN
select 'call insert'
END
ELSE

BEGIN
SET @amountLeft=(a)amount
SET @rowsleft=@@cursor_rows

WHILE @@FETCH_STATUS = 0
BEGIN
--migh have to see who gets the larger amount later
IF @rowsLeft=1
BEGIN
SET @roundedAmt= @amountLeft
END
ELSE
BEGIN
--not going to add logic, if the 2nd to last row could be off
also
--it will probably never happen
SET @amountLeft=(a)amountLeft - @roundedAmt

END

select @amount as DebugTotal,@rowsLeft as rowsLeft,
@matterId as matterId, @clientId as CLIENTID,
@roundedAmt as RoundedAmt,
@timekeeper as timeKeeper


SET @rowsLeft=(a)rowsLeft-1
FETCH NEXT FROM SplitCursorClient INTO
@perc,@timekeeper,@roundedAmt
END

END

CLOSE SplitCursorMatter

DEALLOCATE SplitCursorMatter


END
ELSE
BEGIN
SET @amountLeft=(a)amount
SET @rowsleft=@@cursor_rows

WHILE @@FETCH_STATUS = 0
BEGIN
IF @rowsLeft=1
BEGIN
SET @roundedAmt= @amountLeft
END
ELSE
BEGIN
--not going to add logic, if the 2nd to last row could be off
also
--it will probably never happen
SET @amountLeft=(a)amountLeft - @roundedAmt

END


select @amount as DebugTotal,@rowsLeft as rowsLeft,
@matterId as MATTERID, @clientId as clientId,
@roundedAmt as RoundedAmt,
@timekeeper as timeKeeper




--insert into the table here
--or pass back cus this will be a stored procedure


SET @rowsLeft=(a)rowsLeft-1
FETCH NEXT FROM SplitCursorMatter INTO
@perc,@timekeeper,@roundedAmt
END
END

CLOSE SplitCursorMatter

DEALLOCATE SplitCursorMatter



RETURN