I have a Customers table, a Transactions table, and a Payments table. The Transactions table represents charges to a customer, and the Payments table represents credits to a customer. (Both tables have foreign keys to the Customers table.)
A customer's balance is calculated using Customers.StartingBalance, plus the sum of all that customer's charges in the Transactions table, minus the sum of all that customer's payments in the Payments table.
Now I want to implement an archive feature that deletes all transactions and payments prior to a given date, and then updates Customers.StartingBalance so that the final balance (calculated as described in the previous paragraph) remains the same.
Here's what I have so far:
ALTER PROCEDURE [dbo].[ArchiveData] #ArchiveDateTime DATETIME
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #CustomerBalance TABLE
(
CustomerId INT,
Amount BIGINT
);
BEGIN TRANSACTION;
-- Archive transactions
DELETE Transactions WITH (TABLOCK)
OUTPUT deleted.CustomerId, deleted.TotalAmount INTO #CustomerBalance
WHERE [TimeStamp] < #ArchiveDateTime;
IF EXISTS (SELECT 1 FROM #CustomerBalance)
BEGIN
UPDATE Customers SET StartingBalance = StartingBalance +
(SELECT SUM(Amount) FROM #CustomerBalance cb WHERE Id = cb.CustomerId)
END;
DELETE FROM #CustomerBalance
-- Archive payments
DELETE Payments WITH (TABLOCK)
OUTPUT deleted.CustomerId, deleted.Amount INTO #CustomerBalance
WHERE [Date] < #ArchiveDateTime;
IF EXISTS (SELECT 1 FROM #CustomerBalance)
BEGIN
UPDATE Customers SET StartingBalance = StartingBalance -
(SELECT SUM(Amount) FROM #CustomerBalance cb WHERE Id = cb.CustomerId)
END;
-- Probably not needed
DELETE FROM #CustomerBalance
COMMIT TRANSACTION;
END
Since SQL is not my core competency, I'd like to get feedback on this. Does it seem "correct"? Does it seem optimal? Also, I'm not sure about clauses like the following.
UPDATE Customers SET StartingBalance = StartingBalance -
(SELECT SUM(Amount) FROM #CustomerBalance cb WHERE Id = cb.CustomerId)
What does this do where #CustomerBalance contains no rows for the customer?
What does this do where #CustomerBalance contains multiple rows for the customer?
Thanks for any suggestions.
About your last question
-- i would add the name of the outer query table in the inner one
UPDATE Customers
SET StartingBalance = StartingBalance - (SELECT SUM(Amount)
FROM #CustomerBalance cb
WHERE Customers.Id = cb.CustomerId);
As you are using an aggregation function, the inner query will give you the sum of all the rows found for the client. And 0 if no row is found.
Related
I have the following tables in my database:
The first table is named Amount, second Product, third Purchase.
And I should to create the trigger on insert to amount table. For example, I'll insert the following values: 4, 1, 10, where 4 is id_purchase, 1 is id_product and 4 is amount of this products. And trigger should subtract this amount from Amount_On_Stock. In my example, it should be: was 48, became 38.
Here's the code of my trigger:
CREATE TRIGGER AmountInsert ON Amount
AFTER INSERT
AS
BEGIN
UPDATE Product
SET Amount_On_Stock = (
SELECT
Amount_On_Stock
FROM Product
WHERE ID_Product = (
SELECT
MAX(ID_Product)
FROM Purchase
WHERE ID_Purchase = (
SELECT
MAX(ID_Purchase)
FROM Purchase
)
)
)-(
SELECT
Amount
FROM AMOUNT
WHERE ID_Product = (
SELECT
MAX(ID_Product)
FROM Purchase
WHERE ID_Purchase = (
SELECT
MAX(ID_Purchase)
FROM Purchase
)
)
)
END
But when I try to create this trigger I have the following error:
The aggregate expression cannot be used in the WHERE clause unless it
is contained in a subquery of the HAVING clause or in the select list,
and the column being aggregated is not an external reference.
So, how can I solve this problem?
Your trigger looks nothing like a SQL Server trigger. I would expect your trigger to look more like this:
CREATE TRIGGER AmountInsert ON Amount AFTER INSERT
AS
BEGIN
UPDATE p
SET Amount_On_Stock = p.Amount_On_Stock - i.amount
FROM Product p JOIN
inserted i
ON p.ID_Product = i.ID_Product;
END;
However, this will not do the right thing if you have multiple inserts on the same product at the same time. To handle that you need aggregation:
CREATE TRIGGER AmountInsert ON Amount AFTER INSERT
AS
BEGIN
UPDATE p
SET Amount_On_Stock = p.Amount_On_Stock - i.amount
FROM Product p JOIN
(SELECT i.ID_Product, SUM(i.amount) as amount
FROM inserted i
GROUP BY i.ID_Product
) i
ON p.ID_Product = i.ID_Product;
END;
I created a procedure from querying other tables including transaction tbl to settle all transaction records with a reference number and date automatically stamped on it.
What I try to do is my settle_transaction procedure needs to generate my query from the selected statement and insert them into the settlement table. Meanwhile, I also need to update the ref_num and processed date as a "stamp" to the transaction table so that I don't have duplicated settlement when calling the procedure again. Otherwise, I don't know how to stop showing the same settlement data twice
Here is the procedure to output a settlement tbl and structure similar below:
BEGIN
for r_client in
(
select clientid,
client_name, sum(transaction) total_amount
from transaction_tbl tran join terminal_tbl term
on tran.terminalid = term.terminalid join client_tbl c on c.clientid = term.clientid
where refnr is null
)
loop
v_refnr := get_refnr;
insert into settlement_tbl
(
Ref_Num,
Total,
CLIENTID,
TITLE,
processeddate
)
values (v_refnr, total_amount, clientid,
name,sysdate);
update_refnr(v_refnr, sysdate)
end loop;
END
Output:
| reference_num | total amount | client id | client name | processed_date |
|---------------|--------------|-----------|-------------|----------------|
When I execute the above procedure, it populates all the result from the select query. However, if I execute again, it will duplicate the same result especially the total amount.
I'm seeking a solution to put another procedure/function inside this settlement procedure to prevents duplicate records from the selected query in this procedure.
I use the ref. no# and process_date to update the existing reference num and date to the transaction tbl show below.
| transaction_num | transaction amount | reference_num | processed_date |
|-----------------|--------------------|---------------|----------------|
Here is the attempted code I put inside the settlement procedure but still shows duplicated records and can not update to the transaction tbl.
procedure update_refnr(
p_refnr in number,
p_processeddate in date
)
is
begin
UPDATE TRANSACTION t
SET t.refnr = p_refnr
WHERE EXISTS (SELECT p_processeddate
FROM terminal_tbl
WHERE t.TERMINALID= term.TERMINALID
AND t.processeddate = p_processeddate
AND t.refnr IS NULL);
--exception handling below
end update_refnr;
I also tried other SQL reference but cannot compile.
Ideally, I don't have duplicated records in my settlement tbl when I retrieve each record from my stored procedure.
You want to insert new data into your table only when it doesn't already exist. As others have said, you can use MERGE to do that:
BEGIN
for r_client in (select clientid,
client_name,
sum(transaction) total_amount
from transaction_tbl tran
join terminal_tbl term
on tran.terminalid = term.terminalid
join client_tbl c
on c.clientid = term.clientid
where refnr is null)
loop
v_refnr := get_refnr;
MERGE INTO settlement_tbl s
USING (SELECT v_refnr AS REF_NUM,
total_amount AS TOTAL,
clientid AS CLIENTID,
name AS TITLE,
SYSDATE AS PROCESSEDDATE
FROM DUAL) d
ON (s.REF_NUM = d.REF_NUM)
WHEN NOT MATCHED THEN
INSERT (Ref_Num, Total, CLIENTID, TITLE, processeddate)
VALUES (d.REF_NUM, d.TOTAL, d.CLIENTID, d.TITLE, d.PROCESSEDDATE);
update_refnr(v_refnr, sysdate);
END LOOP;
END;
WHEN NOT MATCHED inserts new data when v_refnr does not already exist in your table.
Best of luck.
I am using ms-sql server. I have table which I want to update from select statement. For example the table which I want to update is Table_A with 2 rows in it. The update statement from which I want to update Table_A return 10 rows. So I want to update Table_A 10 times. The problem is that Table_A is updated 2 times(the count of rows in Table_A).
Example:
CREATE TABLE #tmp
(
AccountID INT,
Inflow DECIMAL(10,2)
)
DECLARE #n INT = 0
WHILE (#n <10 )
BEGIN
INSERT INTO #tmp SELECT 2, 10
SET #n += 1
END
UPDATE dbo.Table_A
SET Balance += sss.Inflow
FROM ( SELECT t.AccountID ,
t.Inflow
FROM #tmp AS t
) AS sss
WHERE dbo.tAccount.AccountID = sss.AccountID;
-- Updates only 2 times
-- What I expected here is Table_A to be updated as many times as the count of the select statement which is 10, based on the insert before.
Your expectation is wrong. Admittedly, the documentation buries this idea:
The example runs without error, but each SalesYTD value is updated
with only one sale, regardless of how many sales actually occurred on
that day. This is because a single UPDATE statement never updates the
same row two times.
The documentation continues with the solution:
In the situation in which more than one sale for a specified
salesperson can occur on the same day, all the sales for each sales
person must be aggregated together within the UPDATE statement, as
shown in the following example:
So, simply aggregate before doing the join:
UPDATE dbo.Table_A
SET Balance += sss.Inflow
FROM (SELECT t.AccountID, SUM(t.Inflow) as Inflow
FROM #tmp t
GROUP BY t.AccountId
) sss
WHERE dbo.tAccount.AccountID = sss.AccountID;
Note you can also write this as:
UPDATE a
SET Balance += sss.Inflow
FROM dbo.Table_A a JOIN
(SELECT t.AccountID, SUM(t.Inflow) as Inflow
FROM #tmp t
GROUP BY t.AccountId
) sss
ON a.AccountID = sss.AccountID;
This makes the JOIN more explicit.
I am new to triggers and am having a bit of a problem. I am trying to create a trigger to add the new inserted value of 90 in table sales to the the total sales for id 100 in salesYTD in table internetServices. It seems to do the calculation correctly but it is only suppose to affect the row with the id of 100. Sadly it seems to be changing it for every salesYTD.
CREATE TRIGGER InsertTrigger
ON Sales
AFTER INSERT As
UPDATE InternetServices
SET SalesYTD = (SELECT SUM(Amount)
FROM Sales
WHERE ServiceID = 100)
WHERE ServiceID = 100;
GO
Print 'Master table Before Insert'
Select * From InternetServices
Print 'After Insert'
INSERT INTO Sales VALUES( 11, '2012-11-14' , 90 , 100 );
Select * From InternetServices
Not sure if I gave enough information this is my first time posting a SQL question. Please don't rate down just let me know and I will update it. Thank you.
You need to use INSERTED table. so its add the inserted amount(in table sales) to existing amount in
column SalesYTD of table InternetServices .
CREATE TRIGGER InsertTrigger
ON Sales
AFTER INSERT AS
BEGIN
UPDATE ITS
SET SalesYTD = ITS.Amount + I.Amount
FROM InternetServices ITS
JOIN INSERTED I ON ITS.ID = I.ID
END
GO
You need a where clause for the update, not just in the subquery:
UPDATE InternetServices
SET SalesYTD = (SELECT SUM(Amount)
FROM Sales
WHERE ServiceID = 100
)
WHERE ServiceID = 100;
I am trying to create an update trigger that checks the quantity of a product in stock over how much someone is ordering and displays a message if not enough of that product is in stock.
It's letting me create the trigger but when testing, it displays an error "Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=,...."
I'm not really understanding how it's returning more than one value as I have it searching for the particular row with the product ID that matches the inserted value first. I then have it comparing the UnitsInStock from the inserted value.
Here's what I have so far:
CREATE TRIGGER tr_check_qty
ON OrderDetails
FOR UPDATE
AS
DECLARE #ProductID int,
#Quantity int
SELECT #ProductID = ProductID,
#Quantity = Quantity
FROM inserted
WHERE #ProductID = ( SELECT ProductID FROM Products )
IF
#Quantity > ( SELECT UnitsInStock FROM Products )
BEGIN
PRINT 'Not enough product in stock'
ROLLBACK TRANSACTION
END
I think you want something like:
CREATE TRIGGER tr_check_qty
ON OrderDetails
FOR UPDATE
AS
IF EXISTS (
SELECT *
FROM
Products p
inner join
inserted i
on p.ProductID = i.ProductID
WHERE i.Quantity > p.UnitsInStock)
BEGIN
PRINT 'Not enough product in stock'
ROLLBACK TRANSACTION
END
However, I'm a bit mystified on why this is inside an update trigger, as compared to an insert trigger.
SELECT UnitsInStock FROM Products as well as SELECT ProductID FROM Products may return a whole column, not a single value. You should specify some restriction there, like WHERE id = #someId.
You have more the one products right?
Then this line:
WHERE #ProductID = ( SELECT ProductID FROM Products )
And this line:
#Quantity > ( SELECT UnitsInStock FROM Products )
will return many rows.
Note as well that if you insert more then one row. The inserted table will have more then one row as well
You might want have to do something like this:
IF EXISTS
(
SELECT
NULL
FROM
inserted
WHERE EXISTS
(
SELECT
NULL
FROM
Product
WHERE
Product.Quantity>inserted.Quantity
)
)
BEGIN
PRINT 'Not enough product in stock'
ROLLBACK TRANSACTION
END