SQL Server Trigger Error Message - sql-server

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

Related

Insert While Loop SQL Server - Fast at first, grinds to a halt

I have been tasked to replicate a large amount of data being entered into our application database to see how it would perform.
I have produced the following while loop,
DECLARE #DATABASESID uniqueidentifier
SET #DATABASESID = (SELECT SID FROM mcm_system)
DECLARE #START_MIN_PROCESS INT;
SET #START_MIN_PROCESS = 200000;
WHILE #START_MIN_PROCESS > 1
BEGIN
BEGIN TRAN
INSERT INTO READING (ID, SID, POINT_DETAIL_ID, TAKEN, VALUE, STATUS, POINT_DATA_ID, WHO, MACHINE_STATE, RESTORED_ID, JOTTING, TAG, VALUEA, VALUEB, VALUEC)
SELECT
NEWID(), #DATABASESID, Point_Detail.ID,
DATEADD(MINUTE, -#START_MIN_PROCESS, GETDATE()), ABS(CHECKSUM(NewId())) % 60,
'N', '00000000-0000-0000-0000-000000000000',
#USERID, '', '00000000-0000-0000-0000-000000000000',
'', 'N', 0, 0, 0
FROM
POINT_DETAIL
WHERE
TYPE = 0
PRINT DATEADD(MINUTE, -#START_MIN_PROCESS, GETDATE())
SET #START_MIN_PROCESS = #START_MIN_PROCESS - 15;
COMMIT
END
The point_detail table referenced has 9000 records and the loop is designed to insert a record with a random value, and a date in the past for every point_detail record. The date is then incremented 15 minutes into the future and the loop runs again. I use the Print to keep track of progress as I am trying to replicate a years worth of data.
It runs fine at first and eventually grinds to a halt. Is there any way I can optimize my statement or another alternative method?

How to design bar chart on SSRS

I want to create reports like below picture's report on SSRS.
Yellow parts mean SET_PHASE,
Green parts mean PROD_PHASE
And my query result like this:
I want to show for per line, all order and I want to show for per order, SETUP and PRODUCTION depends on duratıon time.
SET_PHASE's duration time is SET_DURATION,
PROD_PHASE's duration time is PROD_DURATION
I hope so my query is clear :) Could you help me about issue?
Answer:
Hello Alan,
Current situation I have just these data:
PROD100059335 SETUP PRODUCTION 1 14 LINE 4
PROD100058991 SETUP PRODUCTION 1 5 LINE 6
PROD100059259 SETUP PRODUCTION 2 24 LINE 4
PROD100059188 SETUP PRODUCTION 1 3 LINE 2
PROD100059248 SETUP PRODUCTION 1 15 LINE 2
PROD100059055 SETUP PRODUCTION 2 23 LINE 2
PROD100058754 SETUP PRODUCTION 5 18 LINE 6
And If I use your query I just show "PROD100058754", "PROD100059259", "PROD100059055" these order. I don't understand why other data lost.
until "DECLARE #n TABLE(n int)" part I can show other data. but after that I can not show.
And I applied procedure on SSRS my report shows like this:
I couldn't do correctly and I don't know how can I fix them:(
for example "PROD100059259" this order normally has setup phase but on the report I don't have yellow field.
Do you have any suggestions for me?
OK, here is an attempt to give you what you want but there are a few caveats:
The durations are scaled and no operation can take less than 1 time slot so the setup vs production duration is only approximate
I haven't found a good way of labelling each bar so I've used tooltips
First the code... I've added lots of comments so hopefully you can follow it thru, it's based on your sample data.
NOTE: I've update the table as it now seems like you are using integer durations rather than the 00:00 format from your first example.
-- CREATE A TEST TABLE AND POPULATE IT
DECLARE #data TABLE(STR_ORDER_ID varchar(20), SET_DURATION varchar(10), PROD_DURATION varchar(10), Set_decimal int, Prod_Decimal int, Line varchar(10))
INSERT INTO #data
VALUES
('PROD100059335', NULL, NULL, 1, 14, 'LINE 4'),
('PROD100058991', NULL, NULL,1, 5, 'LINE 6'),
('PROD100059259', NULL, NULL,2, 24, 'LINE 4'),
('PROD100059188', NULL, NULL,1, 3, 'LINE 2'),
('PROD100059248', NULL, NULL,1, 15, 'LINE 2'),
('PROD100059055', NULL, NULL,2, 23, 'LINE 2'),
('PROD100058754', NULL, NULL,5, 18, 'LINE 6')
DECLARE #Gap int = 2 -- determines how many columns we use to separate each order
-- ASSUME durations are in hours/minutes or minutes/seconds and convert them to decimal minutes or decimal seconds respectively
-- COMMENTED THIS AS WE NO LONGER NEED IT. No longer required as durations are now integer values.
--UPDATE d
-- SET
-- Set_decimal = (CAST(LEFT(d.SET_DURATION, len(d.SET_DURATION)-3) AS INT) * 60) + CAST(RIGHT(d.SET_DURATION, 2) AS INT) ,
-- Prod_Decimal = (CAST(LEFT(d.PROD_DURATION, len(d.PROD_DURATION)-3) AS INT) * 60) + CAST(RIGHT(d.PROD_DURATION, 2) AS INT)
--FROM #data d
-- CREATE A NORMALISED TABLE, this will just help to make the next steps simpler
DECLARE #normData TABLE(RowId INT IDENTITY (1,1), Line varchar(10), STR_ORDER_ID varchar(20), OperationOrder int, Operation varchar(10), Duration int)
INSERT INTO #normData (Line, STR_ORDER_ID, OperationOrder, Operation, Duration)
SELECT * FROM (
SELECT Line, STR_ORDER_ID, 1 as OperationOrder , 'SET' as Operation , Set_decimal FROM #data
UNION
SELECT Line, STR_ORDER_ID, 2 , 'PROD' , Prod_decimal FROM #data
UNION
SELECT Line, STR_ORDER_ID, 3 , 'GAP' , #Gap FROM #data ) u -- this adds dummy data that will act as gaps in hte timeline. Change 5 to whatever value suits you best
ORDER BY Line, STR_ORDER_ID, OperationOrder
-- find the largest line running total duration per line and scale it to fit to 240 (so we dont go over 256 column limit in SSRS)
DECLARE #MaxDur INT = (SELECT MAX(rt) FROM (
select *
, SUM(Duration) OVER(PARTITION BY Line ORDER BY Line, STR_ORDER_ID, OperationOrder) AS Rt
from #normData) mRt)
-- Now scale the values back so they fit but don't let any value become less than 1
IF #MaxDur > 240
BEGIN
UPDATE nd
SET Duration = CASE WHEN nd.Duration / (#MaxDur/240) <1 THEN 1 ELSE nd.Duration / (#MaxDur/240) END
FROM #normData nd
END
/* check what we have so far by uncommenting this bit
select *
, SUM(Duration) OVER(PARTITION BY Line ORDER BY Line, STR_ORDER_ID, OperationOrder) AS Rt
from #normData
--*/
-- ================================================================ --
-- At this point you 'may' have enough data to plot a bar chart. == --
-- ================================================================ --
-- CREATE A SIMPLE NUMBERS TABLE, we'll need this to act as our time series
DECLARE #n TABLE(n int)
DECLARE #i int = 0
DECLARE #t int = #MaxDur --(SELECT max(Duration) +5 FROM #normData) -- simple loop counter target set to slightly bigger than our highest duration
WHILE #i<#t
BEGIN
INSERT INTO #n SELECT #i
SET #i = #i +1
END
-- Join our numbers table to our real data
-- This will give us at least 1 row per time slot and associated activity during that time slot.
-- We can plot this driectly as a matrix.
SELECT *
FROM #n n
LEFT JOIN (
-- Sub queries below give use a runnintg total, we then join this back to itself to get the previous
-- running total and this will give us the 'time range' for each operation.
SELECT
a.*
, ISNULL(b.Rt,0)+1 AS TimeStart
, a.RT AS TimeEnd
FROM
(SELECT *
, SUM(Duration) OVER(PARTITION BY Line ORDER BY Line, STR_ORDER_ID, OperationOrder) AS Rt
from #normData
) a
LEFT JOIN
(SELECT *
, SUM(Duration) OVER(PARTITION BY Line ORDER BY Line, STR_ORDER_ID, OperationOrder) AS Rt
from #normData
) b
ON a.RowId = b.RowId + 1 and a.Line = b.Line
) d
ON n.n between d.TimeStart and d.TimeEnd
ORDER BY Line, STR_ORDER_ID, OperationOrder, n, TimeStart, TimeEnd
You can use the code above in your dataset.
The report design:
The report is very simple. It's a matrix with a single row group based on Line and a single column group based on n which is our time slot number.
I've added a blank row to act as a spacer between the 'bars'.
The expression of the cell background is
=SWITCH(
Fields!OperationOrder.Value = 1, "Yellow",
Fields!OperationOrder.Value = 2, "Green",
Fields!OperationOrder.Value = 3, Nothing,
True, Nothing
)
There is also a tooltip which displays STR_ORDER_ID and the operation name.
You get the following output.

Microsoft SQL Server: trigger to check that the sum of a field grouped by another field does not exceed a particular value?

I have a table where a person can log a number of hours on a day:
|__person__|__day__|__hours__|
| 1 | 1 | 4 |
|___2______|__1____|___2_____|
...
I want to create a trigger that doesn't allow the sum of hours to be greater than a specific value, for example 24, for a single person on a specific day.
Multiple rows can be inserted on multiple days simultaneously, and the trigger should then check that each day still has a valid number of hours for each person.
I have tried reading the documentation and similar questions here, but haven't been able to solve this, and have very little experience with SQL Server.
Any help would be appreciated!
You would need a user defined function for this and create a contratins using that user defined functions something like this...
User-Defined Function
CREATE FUNCTION dbo.get_TotalHoursRemaining (
#PersonID INT
, #Day INT
)
RETURNS INT
AS
BEGIN
Declare #Hours INT
SELECT #Hours = ISNULL(SUM([HOURS]), 0)
FROM Test_Table
WHERE Person = #PersonID
AND [DAY] = #Day
GROUP BY Person , [DAY]
SET #Hours = 24 - #Hours;
RETURN #Hours;
END
Constraint
ALTER TABLE Test_Table
ADD CONSTRAINT chk_hours_remaining
CHECK (((dbo.get_TotalHoursRemaining(Person , [Day])) >= 0))
You are looking for constraint,trigger is a overkill in this case.
This is based on assuming ,hours will be logged only once per day against each id
create table dbo.test
(
id int,
hrs int
);
ALTER TABLE dbo.test
ADD CONSTRAINT CHK_hrs CHECK (hrs <= 24);
You also can add constraint to restict least hours:
ALTER TABLE dbo.test
ADD CONSTRAINT CHK_hrs CHECK (hrs > 0 AND hrs <= 24);
insert into dbo.test
select 1,25
You also can have a trigger if each per logs hours mutiple times a day and you want the sum to be < 24
create table dbo.test
(
id int,
hrs int,
dayy datetime
);
create trigger trg_test
on dbo.test
after insert
as
begin
if exists(select id, sum(hrs)
from inserted i
join test t on i.id = t.id
group by id, dayy
having sum(hrs) > 24)
---sum of hours per day exceeded...some thig like that--you can even insert to log table '
insert into logtable
select id, sum(hrs)
from inserted i
join test t on i.id = t.id
group by id, dayy
having sum(hrs) > 24
end
rollback
end
If you create a check constraint on that column, you are able to prevent any value greater than 24 to be inserted. Basically what that does is: when you run an insert statement and the hour sum turns out bigger than 24, you violate the constraint and it kills the transaction immediately.
Check this link to get you started: http://www.w3schools.com/sql/sql_check.asp
Hope this helps :)

I need to update age in SQL Server 2008 according to DOB

I have a table in SQL Server 2008, which has DOB (e.g. 1992-03-15) and in same table, I have an Age column, which is right now Null. I need to update the Age according to the DOB. I have both columns (AGE and DOB) in the same table. I need script which does my job to update Age according to DOB
And other one is in same table, I have Arrival Month (e.g. 8) and Arrival year (e.g. 2011), according to that I need to update another column (Time in country). Say let's say according to example (08(MM), 2011(YYYY)), should update (TimeInCountry) - 4.2 something like that. Which should deduct from current date and time has mentioned into month and year
Do let me know if you need anything else.
Not sure what is the data type your age column is
You can do something like below
Update TableName
Set Age = DATEDIFF(yy, DOB, getdate())
if you using decimal
Age = DATEDIFF(hour,DOB,GETDATE())/8766.0
I believe creating a trigger bill be use full, if you adding new rows in future
For yopur first Problem,
UPDATE TABLE_NAME SET AGE=DATEDIFF(hour,DOB_COLUMN,GETDATE())/8766.0
If you want in Round ,
UPDATE TABLE_NAME SET
AGE= CONVERT(int,ROUND(DATEDIFF(hour,DOB_COLUMN,GETDATE())/8766.0,0))
And Not Sure what you really want to do in the Second Problem,but my guess you can try something like..
Update Table_Name set
TimeInCountry=cast(len(Arrival_year) as varchar(4))+'.'+cast(len(Arrival_Month) as varchar(2))
I have implemented User Defined function
Here it is which may help to someone.
ALTER FUNCTION [dbo].[TimeInCountry]
(
#ArrivalMonth varchar(10),
#ArrivalYear Varchar(10)
) RETURNS VARCHAR(10)
BEGIN
Declare #Ageyear int
Declare #Agemonth int
Declare #Final varchar(10)
Declare #CurrentMonth int
Declare #Currentyear int
Set #CurrentMonth = (Select DatePart(mm, GetDate()))
Set #Currentyear = (Select DatePart(yyyy,GetDate()))
Select #AgeYear = #Currentyear - #ArrivalYear
Select #AgeMonth = #CurrentMonth - #ArrivalMonth
if (#AgeMonth < 0)
BEGIN
Set #AgeYear = #AgeYear - 1
Set #AgeMonth = #AgeMonth + 12
END
Select #Final = (Select Cast(#AgeYear as Varchar(max)) +'.'+ Cast(#AgeMonth as varchar(max)))
Return #Final
---And finally call this function where to update.
--To Check
Select [DBName].TimeInCountry (8,2013)
--- and Finally updating.
Update [DBName].[dbo].[TableName] Set TimeInCountry = dbo.TimeInCountry (ArrivalMonth,ArrivalYear) from [DBName].[dbo].[TableName]
Thanks again everyone.

Selecting rows partially matching rows in another table

I have a table for the actions.
The table has several slots for the same time on the same day. Same action can't be booked for the same time twice. I'm trying to come up with the way to list all the IDs for an action 'A', such as every available time is listed only once, even if there are both slots available, but if 'A' is book for some time already and another slot for this time is empty, that slot wouldn't be showing.
And it comes to me that I don't know T-SQL that good.
I overcame this by selecting all the rows where 'A' is booked, selecting all distinct (date, time start and time end) which are not booked and doing check whether 'A' is already booked for this time. But all this checking is done on the software level, and those multiple requests to the server and looping in the program to perform the same job as one LIKELY SIMPLE sql request don't look very efficient to me.
If there a way to do something like:
SELECT ID FROM mytable
WHERE Action IS NULL AND (date, time_start, time_end **'ALL TOGETHER IN ONE ROW'**)
NOT IN (SELECT date, time_start, time_end FROM mytable
WHERE Action = 'A')
HAVING 'THOSE THREE BEING DISTINCT'
By other words can I select rows which partially match other rows? It would be simple if I had only one column to compare, but there are three.
In SQL Server we generally use WHILE instead of FOR. I believe what you're trying to do could be fulfilled as follows if you want to loop through the table (ideally your ID field would be the PRIMARY KEY as well). This is just inserting it into a temp table for now, but potentially it should give you the results you want:
-- DECLARE and set counters
DECLARE #curr INT, #prev INT, #max INT
SELECT #curr = 0, #prev = 0, #max = MAX(ID) FROM myTable
-- Make a simple temp table
CREATE TABLE #temp (ID INT)
-- Start looping
WHILE (#curr < #max)
BEGIN
-- Set our counter for the next row
SELECT #curr = MIN(ID) FROM myTable WHERE ID > #prev
-- Populate temp table with a self-join to compare slots
-- Slot must match on date + time but NOT have equal SLOT value
-- Will only INSERT if we meet our criteria i.e. neither slot booked
INSERT INTO #temp
SELECT DISTINCT A.ID
FROM myTable A
JOIN myTable B ON B.[Date] = A.[date] AND B.time_start = A.time_start AND B.time_end = A.time_end
WHERE A.[Action] IS NULL -- Indicates NO booking
AND B.[Action] IS NULL -- Indicates NO booking
AND A.SLOT <> B.SLOT
AND A.ID = #curr
-- Update our counter
SET #prev = #curr
END
-- Get all our records
SELECT * FROM #temp
-- Remove the sleeping dog ;)
DROP TABLE #temp
There is a little bit of redundancy here because it checks ALL rows, even if a condition has been found in the first row of that time slot, but you can tweak it from here if you need to.
You should really avoid using field names like "Date" and "Action" because these are reserved words in SQL.
You question is a bit unclear, but I think this will point you in a productive direction. SQL is designed to perform operation on sets of rows, not to loop through processing one row at a time. The following code will correlate your data into one row for each pair of slots at each date/time. You can use a CASE expression, as shown, to add a column that indicates the status of the row, and you can then add a WHERE clause, not shown, to perform any additional filtering.
-- Sample data.
declare #Samples as Table ( SampleId Int, Slot Int, EventDate Date, StartTime Time(0), EndTime Time(0), Action VarChar(10) );
insert into #Samples ( SampleId, Slot, EventDate, StartTime, EndTime, Action ) values
( 200, 1, '20150501', '00:00:00', '00:30:00', NULL ),
( 201, 2, '20150501', '00:00:00', '00:30:00', NULL ),
( 202, 1, '20150501', '00:30:00', '01:00:00', 'A' ),
( 203, 2, '20150501', '00:30:00', '01:00:00', NULL ),
( 204, 1, '20150501', '01:00:00', '01:30:00', NULL ),
( 205, 2, '20150501', '01:00:00', '01:30:00', 'A' ),
( 206, 1, '20150501', '01:30:00', '02:00:00', 'B' ),
( 207, 2, '20150501', '01:30:00', '02:00:00', 'B' );
select * from #Samples;
-- Data correleated for each date/time.
select Slot1.EventDate, Slot1.StartTime, Slot1.EndTime,
Slot1.Action as Action1, Slot2.Action as Action2,
Coalesce( Slot1.Action, Slot2.Action ) as SummaryAction,
case when Slot1.Action = Slot2.Action then 'ERROR!' else 'Okay.' end as Status
from #Samples as Slot1 inner join
#Samples as Slot2 on Slot2.EventDate = Slot1.EventDate and Slot2.StartTime = Slot1.StartTime and
Slot1.Slot = 1 and Slot2.Slot = 2;

Resources