I have a Reservations table with the following columns
Reservation_ID
Res_TotalAmount - money
Res_StartDate - datetime
IsDeleted - bit column with default value - false
So when a user tries to delete his reservation I've created a trigger that instead of delete - he just updates the value of column IsDelete to true;
So far so good - but this tourist may owe some compensation to the firm, for example when he has cancelled his reservation 30 days to 20 days from the start_date of the reservation - he owes 30% of the Res_TotalAmount and so on
And here is my trigger
Create Trigger tr_TotalAMountUpdateAfterInsert on RESERVATIONS after Delete
As
Begin
Declare #period int
Declare #oldResAmount int
Declare #newAmount money
Declare #resID int
Select #resID = Reservation_ID from deleted
select #oldResAmount = Res_TotalAmount from deleted
Select #period= datediff (day,Res_StartDate,GETDATE()) from deleted
case
#period is between 30 and 20 then #newAmount=30%*#oldResAmount
#period is between 20 and 10 then #newAmount=50%*#oldResAmount
end
exec sp_NewReservationTotalAmount #newAmount #resID
End
GO
As I have to use both triggers and stored procedure you see that I call at the end of the trigger one stored procedure which just updates Res_TotalAmount column
Create proc sp_NewReservationTotalAmount(#newTotalAmount money, #resID)
As
declare #resID int
Update RESERVATIONS set Res_TotalAmount=#newTotalAmount where Reservation_ID=resID
GO
So my first problem is that it gives me incorrect syntax near case
And my second - I would appreciate suggestions how to make both the trigger and stored procedure better.
Your fundamental flaw is that you seem to expect the trigger to be fired once per row - this is NOT the case in SQL Server. Instead, the trigger fires once per statement, and the pseudo table Deleted might contain multiple rows.
Given that that table might contain multiple rows - which one do you expect will be selected here??
Select #resID = Reservation_ID from deleted
select #oldResAmount = Res_TotalAmount from deleted
Select #period= datediff (day,Res_StartDate,GETDATE()) from deleted
It's undefined - you might get the values from arbitrary rows in Deleted.
You need to rewrite your entire trigger with the knowledge the Deleted WILL contain multiple rows! You need to work with set-based operations - don't expect just a single row in Deleted !
Also: the CASE statement in T-SQL is just intended to return an atomic value - it's not a flow control statement like in other languages, and it cannot be used to execute code. So your CASE statement in your trigger is totally "lost" - it needs to be used in an assignment or something like that ....
1) Here is the correct syntax for the CASE statement. Note that:
I changed the order of your comparisons with the CASE statement; the smaller value has to come first.
I have included an "ELSE" case so you don't wind up with an undefined value when #period is not within your given ranges
SELECT #newAmount =
CASE
WHEN #period between 10 and 20 then 0.5 * #oldResAmount
WHEN #period between 20 and 30 THEN 0.3 * #oldResAmount
ELSE #oldResAmount
END
2) You are going to have an issue with this trigger if ever a delete statement affects more than one row. Your statements like "SELECT #resID = Reservation_ID from deleted;" will simply assign one value from the deleted table at random.
EDIT
Here is an example of a set-based approach to your problem that will still work when multiple rows are "deleted" within the transaction (example code only; not tested):
Create Trigger tr_TotalAMountUpdateAfterInsert on RESERVATIONS after Delete
As
Begin
UPDATE RESERVATIONS
SET Res_TotalAmount =
d.Res_TotalAmount * dbo.ufn_GetCancellationFactor(d.Res_StartDate)
FROM RESERVATIONS r
INNER JOIN deleted d ON r.Reservation_ID = d.Reservation_ID
End
GO
CREATE FUNCTION dbo.ufn_GetCancellationFactor (#scheduledDate DATETIME)
RETURNS FLOAT AS
BEGIN
DECLARE #cancellationFactor FLOAT;
DECLARE #period INT = DATEDIFF (DAY, #scheduledDate, GETDATE());
SELECT #cancellationFactor =
CASE
WHEN #period <= 10 THEN 1.0 -- they owe the full amount (100%)
WHEN #period BETWEEN 11 AND 20 THEN 0.5 -- they owe 50%
WHEN #period BETWEEN 21 AND 30 THEN 0.3 -- they owe 30%
ELSE 0 -- they owe nothing
END
RETURN #cancellationFactor;
END;
GO
About the case:
The syntax is wrong. Even if it worked (see below) you'd be missing the WHENs:
case
WHEN #period is between 30 and 20 then #newAmount=30%*#oldResAmount
WHEN #period is between 20 and 10 then #newAmount=50%*#oldResAmount
end
Yet, the case statement can not be used this way. In the context where you want it, you need to use if. It's not like the switch statement in C++/C# for example. You can only use it in queries like
SELECT
case
WHEN #period is between 30 and 20 then value1
WHEN #period is between 20 and 10 then value2
end
Having said the above: I didn't actually read all your code. But now that I've read some of it, it is really important to understand how triggers work in SQL Server, as mark_s says.
Related
Firstly, may I state that I'm aware of the ability to, e.g., create a new function, declare variables for rowcount1 and rowcount2, run a stored procedure that returns a subset of rows from a table, then determine the entire rowcount for that same table, assign it to the second variable and then 1 / 2 x 100....
However, is there a cleaner way to do this which doesn't result in numerous running of things like this stored procedure? Something like
select (count(*stored procedure name*) / select count(*) from table) x 100) as Percentage...
Sorry for the crap scenario!
EDIT: Someone has asked for more details. Ultimately, and to cut a very long story short, I wish to know what people would consider the quickest and most processor-concise method there would be to show the percentage of rows that are returned in the stored procedure, from ALL rows available in that table. Does that make more sense?
The code in the stored procedure is below:
SET #SQL = 'SELECT COUNT (DISTINCT c.ElementLabel), r.FirstName, r.LastName, c.LastReview,
CASE
WHEN c.LastReview < DateAdd(month, -1, GetDate()) THEN ''OUT of Date''
WHEN c.LastReview >= DateAdd(month, -1, GetDate()) THEN ''In Date''
WHEN c.LastReview is NULL THEN ''Not Yet Reviewed'' END as [Update Status]
FROM [Residents-'+#home_name+'] r
LEFT JOIN [CarePlans-'+#home_name+'] c ON r.PersonID = c.PersonID
WHERE r.Location = '''+#home_name+'''
AND CarePlanType = 0
GROUP BY r.LastName, r.FirstName, c.LastReview
HAVING COUNT(ELEMENTLABEL) >= 14
Thanks
Ant
I could not tell from your question if you are attempting to get the count and the result set in one query. If it is ok to execute the SP and separately calculate a table count then you could store the results of the stored procedure into a temp table.
CREATE TABLE #Results(ID INT, Value INT)
INSERT #Results EXEC myStoreProc #Parameter1, #Parameter2
SELECT
Result = ((SELECT COUNT(*) FROM #Results) / (select count(*) from table))* 100
I created a database with NBA player statistics just to practice SQL and SSRS. I am new to working with stored procedures, but I created the following procedure that should (I think) allow me to specify the team and number of minutes.
CREATE PROCEDURE extrapstats
--Declare variables for the team and the amount of minutes to use in --calculations
#team NCHAR OUTPUT,
#minutes DECIMAL OUTPUT
AS
BEGIN
SELECT p.Fname + ' ' + p.Lname AS Player_Name,
p.Position,
--Creates averages based on the number of minutes per game specified in #minutes
(SUM(plg.PTS)/SUM(plg.MP))*#minutes AS PTS,
(SUM(plg.TRB)/SUM(plg.MP))*#minutes AS TRB,
(SUM(plg.AST)/SUM(plg.MP))*#minutes AS AST,
(SUM(plg.BLK)/SUM(plg.MP))*#minutes AS BLK,
(SUM(plg.STL)/SUM(plg.MP))*#minutes AS STL,
(SUM(plg.TOV)/SUM(plg.MP))*#minutes AS TOV,
(SUM(plg.FT)/SUM(plg.MP))*#minutes AS FTs,
SUM(plg.FT)/SUM(plg.FTA) AS FT_Percentage,
(SUM(plg.FG)/SUM(plg.MP))*#minutes AS FGs,
SUM(FG)/SUM(FGA) as Field_Percentage,
(SUM(plg.[3P])/SUM(plg.MP))*#minutes AS Threes,
SUM([3P])/SUM([3PA]) AS Three_Point_Percentage
FROM PlayerGameLog plg
--Joins the Players and PlayerGameLog tables
INNER JOIN Players p
ON p.PlayerID = plg.PlayerID
AND TeamID = #team
GROUP BY p.Fname, p.Lname, p.Position, p.TeamID
ORDER BY PTS DESC
END;
I then tried to use the SP by executing the query below:
DECLARE #team NCHAR,
#minutes DECIMAL
EXECUTE extrapstats #team = 'OKC', #minutes = 35
SELECT *
When I do that, I encounter this message:
Msg 263, Level 16, State 1, Line 5
Must specify table to select from.
I've tried different variations of this, but nothing has worked. I thought the SP specified the tables from which to select the data.
Any ideas?
Declaring the stored procedure parameters with OUTPUT clause means the values will be returned by the stored procedure to the calling function. However you are using them as input parameters, please remove the OUTPUT clause from both input parameters and try.
Also remove the SELECT * in your execute statement, it is not required, the stored procedure will return the data as it has the select statement.
I need to update a table with a counter - its quite a simple setup.
The table will have no more than 10 rows. At present there is a PrimaryIndex on a 3-char string column (SiteCode) with a second column (NumVotes) holding a long number.
The idea is to increment NumVotes with thread safety and minimal locking.
I've managed to put toegther a stored proc that works but I have 2 questions that are beyond my expertise.
Can the stored proc I've put together be improved or is it adequate should 10, 20 or 30 updates occur simultaneously?
Is the string index going to be a problem.
SiteCode - varchar(3)
NumVotes - int (more than 1 vote can be added at once)
ALTER PROCEDURE dbo.ComparisonStats_Update
#SiteCode varchar(3),
#IncNumVotes int
AS
SET NOCOUNT ON
BEGIN
MERGE ComparisonStats WITH (HOLDLOCK) AS t
USING (SELECT #SiteCode AS SiteCode, #IncNumVotes AS IncNumVotes) as s
ON t.SiteCode = s.SiteCode
WHEN MATCHED THEN
UPDATE SET
NumNumVotes += IncNumVotes
WHEN NOT MATCHED BY TARGET THEN
INSERT (SiteCode, NumNumVotes)
VALUES (s.SiteCode, s.IncNumVotes);
END
This is the error I am getting:
Msg 512, Level 16, State 1, Procedure tr_UpdateFolio, Line 361
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
I've run up and down this code over and over for hours now. What am I not seeing? Any help would be appreciated. I've been working on this for a few days now, and this is pretty much the one thing I need working in order to get this project done.
-- 2. Write a trigger named tr_UpdateFolio that will be invoked when the Folio table 'status'
-- field ONLY (column update) is changed.
ALTER TRIGGER tr_UpdateFolio ON FOLIO--switch alter back to create
AFTER UPDATE
AS
IF UPDATE([Status])
BEGIN
DECLARE #Status char(1)
DECLARE #FolioID smallint
DECLARE #CheckinDate smalldatetime
DECLARE #Nights tinyint
DECLARE #CurrentDate smalldatetime = '7/27/2016 2:00:00 PM'
SELECT
#Status = i.Status,
#FolioID = i.FolioID,
#CheckinDate = i.CheckinDate,
#Nights = i.Nights
FROM
INSERTED i
-- If Folio status is updated to 'C' for Checkout, trigger two different Insert statements to
-- (1) INSERT in the Billing table, the amount for the total lodging cost as BillingCategoryID1
-- - (normally the FolioRate * number of nights stay, but you must also factor in any late checkout fees*).
-- *Checkout time is Noon on the checkout date. Guest is given a one hour
-- grace period to check out. After 1PM (but before 4PM), a 50% surcharge is added to the FolioRate. After 4PM, an
-- additional full night's FolioRate is applied. You can recycle code from A7 (part 5), but note it's not the exact same
-- function - we only need the late charge (if any).
IF #Status = 'C'
SET #Nights = 1
--#CurrentDate may need to switch back to getdate()
IF DATEDIFF(HOUR, #CheckinDate + #Nights, #CurrentDate) >= 16
SET #Nights = #Nights + 1
ELSE IF DATEDIFF(HOUR, #CheckinDate + #Nights, #CurrentDate) >= 13
SET #Nights = #Nights + .5
UPDATE FOLIO
SET Nights = #Nights
WHERE FolioID = #FolioID
INSERT INTO BILLING (FolioID, BillingCategoryID, BillingDescription, BillingAmount, BillingItemQty, BillingItemDate)
VALUES (25, 1, 'Room', dbo.GetRackRate(11, #CurrentDate) * #Nights, 1, #CurrentDate)
-- (2) The second INSERT statement in the same trigger will insert the Lodging Tax* - as a separate entry in the
-- Billing table for tax on lodging (BillingCategoryID2). *Use the dbo.GetRoomTaxRate function from A7 to determine
-- the Lodging Tax.
INSERT INTO BILLING (FolioID, BillingCategoryID, BillingDescription, BillingAmount, BillingItemQty, BillingItemDate)
VALUES (25, 2, 'Lodging Tax', dbo.GetRoomTaxRate(20), 1, #CurrentDate)
END
GO
-- 3. Write a trigger named tr_GenerateBill that will be invoked when an entry is INSERTED in to the Billing
-- table. If BillngCategoryID is 2 (for testing purposes only) then call the function dbo.ProduceBill (from A7).
ALTER TRIGGER tr_GenerateBill ON BILLING
AFTER INSERT
AS
BEGIN
DECLARE #FolioID smallint
DECLARE #BillingCategoryID smallint
SELECT #FolioID = i.FolioID, #BillingCategoryID = i.BillingCategoryID
FROM INSERTED i
IF #BillingCategoryID = 2
SELECT * FROM dbo.ProduceBill(#FolioID)
END
GO
dbo.producebill should be fine, but the error occurs when I try to run this block
-- 4A. Assume today is (July 27, 2016 at 2PM)*. Anita is due to check out today (from Part 1 above).
-- Write an Update Statement to change the status of her Folio to 'CheckedOut.
-- (Be careful to include a WHERE clause so ONLY here folio is updated).
-- Note: This should automatically invoke tr_UpdateFolio above (factoring in the late charge),
-- which automatically invokes tr_GenerateBill above, and calls dbo.ProduceBill , and produces a bill.
UPDATE FOLIO
SET [Status] = 'C'
WHERE ReservationID = 5020
I'm going nuts trying to figure this out. Thanks.
Let's start with the easier one (tr_GenerateBill). This one is a little odd for sure. You have a select statement in an insert trigger. This means that when you insert a row you are expecting it to return row(s). This is not the typical behavior of an insert but you can work with it.
If you insert 3 rows and only 2 of them have a BillingCategoryID of 2 what should the trigger do? What does that table valued function look like?
This is kind of a guess but you should be able to rewrite that entire trigger to something along these lines.
ALTER TRIGGER tr_GenerateBill ON BILLING
AFTER INSERT
AS
BEGIN
SELECT *
FROM inserted i
cross apply dbo.ProduceBill(i.FolioID) pb
where i.BillingCategoryID = 2
END
Your second trigger is more challenging. It is not totally clear what you are trying to do here but I think this is pretty close.
UPDATE f
set Nights = case when DATEDIFF(HOUR, #CheckinDate + i.Nights, #CurrentDate) >= 16 then 2 else 1 end --adding .5 to a tiny int is pointless. It will ALWAYS be the same value.
from inserted i
join Folio f on f.FolioID = i.FolioID
INSERT INTO BILLING (FolioID, BillingCategoryID, BillingDescription, BillingAmount, BillingItemQty, BillingItemDate)
Select i.FolioID
, 1
, 'Room'
, dbo.GetRackRate(11, #CurrentDate) * case when DATEDIFF(HOUR, #CheckinDate + i.Nights, #CurrentDate) >= 16 then 2 else 1 end, 1, #CurrentDate)
from inserted i
INSERT INTO BILLING (FolioID, BillingCategoryID, BillingDescription, BillingAmount, BillingItemQty, BillingItemDate)
select i.FolioID
, 2
, 'Lodging Tax'
, dbo.GetRoomTaxRate(20), 1, #CurrentDate)
from inserted i
I want to compare a number of values (up to ten) with a function that will return the smallest value of them.
My colleague wrote the function like:
set #smallest = null
if #smallest is null or #date0 < #smallest
begin
set #smallest = #date0
end
if #smallest is null or #date1 < #smallest
begin
set #smallest = #date1
end
... (repeating 10 times)
Beside of that the if statement could be written smarter (the null check can fall away after the first comparison) I was wondering if creating an in-memory indexed table and let the function return me the first value would be more efficient?
Is there any documentation that I could read for this?
creating an in-memory indexed table
There is no point having an index on 10 records. Create a derived table (will sit in memory) as shown below, then run MIN across the table:
select #smallest = MIN(Adate)
from (
select #date0 Adate union all
select #date1 union all
select #date2 union all
-- ....
select #date9) X