4

I am stuck on how to loop over a rowset and save in a variable.

Mind you, this may be pseudo-code because SQL is not my specialty.

 @all_customers = select CustNum from [crrsql].[dbo].[Customer];
 some loop(@all_customers as user)
 //I need to find out what the Acct_balance field is and either subtract or add to bring all the balances to 0
    @balance = select Acct_balance from [crrsql].[dbo].[Customer] where CustNum = user;
    if @balance > 0 
      update [crrsql].[dbo].[Customer] set Acct_balance = 0;
      INSERT INTO [crrsql].[dbo].[AR_Transactions] (cashier_ID, CustNum, Balance) VALUES (100199, user, @balance); 
    else
      update [crrsql].[dbo].[Customer] set Acct_balance = 0;
      INSERT INTO [crrsql].[dbo].[AR_Transactions] (cashier_ID, CustNum, Balance) VALUES (100199, user, "-" + @balance); 
    end
 end loop

As you can see I am looping through the customers and within that loop I need to get the current balance and set it to zero, but first I need to find out if it's a positive or negative number to be able to figure out if the insert per user in the AR_Transactions table needs to be a positive or negative number. Could you help with the missing pieces?

ErikE
  • 48,881
  • 23
  • 151
  • 196
Matt Elhotiby
  • 43,028
  • 85
  • 218
  • 321
  • Your code there looks like it's going to always put a positive value in AR_Transactions and there is nothing to indicate debits vs. credits. – Tom H May 23 '11 at 19:08
  • 1
    Converting to a string with `"-" + @balance` is awful! The way you make a number negative is `-@balance`. Furthermore, you'd always want the negated amount, with no conditionals, to make a negative balance have a positive transaction and vice versa. – ErikE May 23 '11 at 21:02
  • This can be done in ONE statement. Please see my answer. – ErikE May 23 '11 at 21:15

6 Answers6

3

You should be able to do this in a couple of statements without the use of a cursor or other procedural code. Just make sure that it's all in one transaction:

BEGIN TRANSACTION

INSERT INTO crrsql.dbo.AR_Transactions (
    cashier_id,
    cust_num,
    balance,
    transaction_date)
SELECT
    100199,
    cust_num,
    -acct_balance,
    DATEADD(MINUTE, -30, current_date)
FROM crrsql.dbo.Customers
WHERE acct_balance <> 0

UPDATE crrsql.dbo.Customers SET acct_balance = 0 WHERE acct_balance <> 0

COMMIT TRANSACTION

Of course, add proper error handling and be sure to test this first.

Also, I slightly altered some of your table and column names. I don't want to get into which specific naming conventions are better than others, but at least be consistent. If you're going to use underscores, use them. If you're going to use camel-back notation instead then use that, but don't mix them. The same goes for plural vs. singular table names.

Tom H
  • 46,766
  • 14
  • 87
  • 128
  • hey @Tom ...thanks for this code but when does this chunk of code ceck to see what the balance is to update it to a positive or negative – Matt Elhotiby May 23 '11 at 19:19
  • Add ABS() around acct_balance – Cade Roux May 23 '11 at 19:19
  • Are you always inserting a positive value? If so, then ABS() as Cade recommends would do that. I posted a comment to your question because I thought that might be a coding mistake, which was also why I left this as-is for now. – Tom H May 23 '11 at 19:22
  • Hey @Tom thanks again but i need to enter either a positive or a negative number in there ...so for real numbers sake if a customer has -200 in the acct_balance field in the customer table then i need to add an entry or 200 to bring the value to 0. Kind of like a line item to 0 it out. And vice verca for if the customer has 200 in the in the acct_balance field in the customer table then i need to add an entry or -200 to zero that out as well – Matt Elhotiby May 23 '11 at 19:31
  • @Tamer, if you just need to reverse the amount you can use the unary minus operator. -acct_balance or -1 * acct_balance - in both cases this will give you a reversed amount. – Cade Roux May 23 '11 at 19:44
  • I've changed the answer appropriately given a clearer understanding of your requirements. I also added in a WHERE clause so that there are no unnecessary row inserts or updates. – Tom H May 23 '11 at 19:48
  • two last questions.....i promise, ok what is the BEGIN TRANSACTION and COMMIT TRANSACTION keywords and how would i do INSERT INTO crrsql.dbo.AR_Transactions (cashier_id, cust_num, balance, (current_date -30minutes)) i just noticed that i need to also insert a datetime field minus 30 minutes – Matt Elhotiby May 23 '11 at 19:57
  • A transaction basically defines a single unit of work so that either the whole thing gets committed to the database or none of it does. I suggest that you read up on it a bit as it's a very important concept in database programming. I'll add a bit of code for the datetime column to my answer. – Tom H May 23 '11 at 20:01
2

For SQL 2005 and up:

UPDATE C
SET C.Acct_Balance = 0
OUTPUT 100199, Inserted.CustNum, -Deleted.Acct_Balance, DateAdd(Minute, -30, GetDate())
INTO crrsql.dbo.AR_Transactions (Cashier_ID, CustNum, Balance, Transaction_Date)
FROM crrsql.dbo.Customer C
WHERE C.Acct_Balance <> 0

Since there's been some confusion I will note that -Deleted.Acct_Balance is all you need to bring the balance to zero. It negates negative balances to insert a positive, and negates positive balances to insert a negative.

For SQL 2000 you'll need multiple statements.

ErikE
  • 48,881
  • 23
  • 151
  • 196
  • I am not following how this will work...i tried to cut and paste and i get Msg 207, Level 16, State 1, Line 2 Invalid column name 'Trans_Amount'. – Matt Elhotiby May 23 '11 at 21:54
  • @Tamer I may have the column names wrong. Please try to fix them. the OUTPUT clause needs the column names from your Customer table and the INTO clause needs the column names from your AR_Transactions table. If you really still can't make it work, please post the DDL script for your two tables and I'll provide you with the query. You ARE using SQL 2005 or higher, right? – ErikE May 23 '11 at 21:57
  • Ok i will try to fix them but what is the Inserted. and the -Deleted. ...i dont understand that part – Matt Elhotiby May 23 '11 at 22:36
  • They are meta-tables that contain the values of the rows involved in the update, `deleted` having the pre-update value (before the `SET`) and `inserted` having the post-update value (after the `SET`). See [OUTPUT Clause (Transact-SQL)](http://msdn.microsoft.com/en-us/library/ms177564.aspx). – ErikE May 23 '11 at 22:41
  • what about the top part UPDATE C SET C.Acct_Balance = 0 what is that doing – Matt Elhotiby May 23 '11 at 22:43
  • `C` is the alias I gave to the table `crrdql.dbo.Customer` (see it immediately after the table name in the `FROM` clause?). This means that I'm setting the `Acct_Balance = 0` in the `Customer` table. – ErikE May 23 '11 at 22:45
  • awesome...so basically i am not in front of the work computer anymore...i will be in the am so i will try this then..So let me make sure i get this straight this "OUTPUT 100199, Inserted.CustNum, -Deleted.Acct_Balance, DateAdd(Minute, -30, GetDate())" is for the fields in the Customers table and the " INTO crrsql.dbo.AR_Transactions (Cashier_ID, CustNum, Balance, Transaction_Date)" is for fields in the AR_Transactions table and this will enter an entry in the AR_Transactions for each customer?? – Matt Elhotiby May 23 '11 at 22:49
  • Yes, it's a combined update and insert all in one, using the OUTPUT clause. I will be here tomorrow. – ErikE May 23 '11 at 23:31
  • once again Erik you manage to produce code that twists my brain into knots! – DForck42 May 24 '11 at 15:24
  • @Tamer did you get it figured out? – ErikE May 26 '11 at 16:30
1

As a general rule, thinking you need to do row-based operations in SQL is a pretty good indication that you (or someone else) has framed the problem incorrectly: it is a code smell indicative of procedural rather set-based thinking.

The pseudocode you want is, I think, something along these lines:

-- Create a temporary table. A table starting with # is a temporary. It will be
-- automatically dropped when the session ends. If two sessions creates temp
-- tables with the same name, at the same time, they will still get one table each.
create table #work
(
  CustNum int ,
  Balance money ,
)

insert #work ( CustNum , Balance )
select CustNum , Balance
from Customer
where Balance != 0

begin transaction

insert dbo.AR_Transactions (cashier_ID, CustNum, Balance)
select cashier_ID = 100199 ,
       user       = CustNum ,
       adjustment = case sign(@balance) -- should always be +1 or -1
                    when  1 then @balance -- positive balance
                    when -1 then -@balace -- negative balance
                    end 

update Customer set balance = 0
from Customer c
join #work    w on w.CustNum = c.CustNum

commit transaction

-- Manual tidying up if the connection might be kept open.
drop table #work
Anders Abel
  • 67,989
  • 17
  • 150
  • 217
Nicholas Carey
  • 71,308
  • 16
  • 93
  • 135
  • In SQL Server, table names beginning with # are temporary tables and are created in tempdb. They are local to the session and exist for the duration of the session. If created in the body of a stored procedure, they are dropped when the stored procedure execution is terminated. – Nicholas Carey May 23 '11 at 20:19
  • @Tamer it creates a temporary table, which will be automatically dropped when the connection is disconnected or reset. See comments in the code. – Anders Abel May 23 '11 at 20:30
0

You are looking for a function that returns @balance if @balance>0, else -@balance.

ABS(@balance) would do that. (Unless you need to insert a string literal starting with a "-" but that seems odd - I'd assume the Balance column is a decimal type.)

I'm guessing at the business domain, but there probably is also a credit/debit column that you would need to set according to the sign of the transaction. In which case you may want something like:

INSERT INTO [crrsql].[dbo].[AR_Transactions] (cashier_ID, CustNum, Balance, CR_DR ) VALUES (100199, user, ABS(@balance), CASE WHEN @balance > 0 THEN 'CR' ELSE 'DR' END ); 
slothrop
  • 3,218
  • 1
  • 18
  • 11
0

Here is a direct translation of your original code into valid SQL Server syntax. I can't speak to the business rules surrounding what you are doing, but note that this approach avoids the use of cursors and uses the ABS() function to do away with your original if/else block.

declare @all_customers as table(
  customernumber int
);

/*
--you can insert dummy data for testing purposes like so:
insert into @all_customers
select 5, 1
union
select 2, 1
--*/


while (0 < (select count(*) from @all_customers)) begin
  declare @current_customer int = (select top 1 customernumber from @all_customers);

  declare @balance money = (select acct_balance from [crrsql].[dbo].[Customer] where CustNum = @current_customer);
  update [crrsql].[dbo].[Customer] set Acct_balance = 0;
  INSERT INTO [crrsql].[dbo].[AR_Transactions] (cashier_ID, CustNum, Balance) VALUES (100199, user, abs(@balance)); 

  delete @all_customers where customernumber = @current_customer;
end
JosephStyons
  • 57,317
  • 63
  • 160
  • 234
  • 1
    Why would you offer looping when a simple set-based solution is possible? Did you know that looping like you're doing actually performs **worse** than a cursor? – ErikE May 23 '11 at 21:20
-1

I would take a look at cursors, however IMO this is better done in the application logic than in SQL.

Dustin Laine
  • 37,935
  • 10
  • 86
  • 125
  • I dont have access to the application and the requirements ask for a SQL file to make the changes...i will look into cursors – Matt Elhotiby May 23 '11 at 18:53
  • @Tamer, no worries. Like to give my answer and any suggestions I see fit. Every situation is unique. – Dustin Laine May 23 '11 at 18:55
  • Why would you suggest cursors for something that can be done in ONE set-based query? – ErikE May 23 '11 at 21:14
  • Based on his acknowledgement of "psuedo" code it was not possible, IMO, to determine if he truly could do it set-based. I agree avoid them at all possible. – Dustin Laine May 23 '11 at 21:19