CHECK Constraint on Group function? - sql

I need an SQL constraint (using SQLDeveloper) to check that for a specific account_id, only ONE or NO regular_id exists, such as the data attached, cell containing '6' being what should not be allowed, even though it is a different value.
AccountID RegularID OpenID
1 5 null
1 null 10
1 null 11
1 6 <-- Forbidden

Best way is with a trigger
Create trigger trig_NoSingleRegId
On MyTable For Insert, Update
As
if Exists(Select * From MyTable t
Where AccountId In (Select AcountId From inserted)
Group By AccountId
Having Count(Distinct regularId) > 1)
Begin
RollBack Transaction
Raiserror('Cannot have more than one RegularId per AccountId', 16, 1)
End
Note: The Where clause is for performance only, to limit trigger to only those accountIds inserted or updated by the triggering update or insert.
or you can also can use join to accomplish same restriction.
Create trigger trig_NoSingleRegId
On MyTable For Insert, Update
As
if Exists(Select * From MyTable t
join inserted I
on i.AccountId = t.AccountId
Group By t.AccountId
Having Count(Distinct t.regularId) > 1)
Begin
RollBack Transaction
Raiserror('Cannot have more than one RegularId per AccountId', 16, 1)
End

Related

Why merge-delete-trigger causes ORA-30926: unable to get a stable set of rows in the source tables?

Prepare the schema to reproduce the issue (on db<>fiddle):
create table t (id int, val int, modified timestamp default systimestamp)
/
create or replace trigger trigg_on_t
before update on t for each row enable
begin
:new.modified := systimestamp;
end;
/
insert into t (id, val) values (1, 1);
commit;
The error ocures in a merge stataement with delete clause and a trigger defined on the target table:
merge into t
using (
select 1 id, 10 val, 1 flag from dual
) s on (t.id = s.id)
when matched then
update set t.val=s.val
delete where s.flag=1
ORA-30926: unable to get a stable set of rows in the source tables
30926, 00000, "unable to get a stable set of rows in the source tables"
// *Cause: A stable set of rows could not be got because of large dml
// activity or a non-deterministic where clause.
// *Action: Remove any non-deterministic where clauses and reissue the dml.
What goes wrong here? Or rather, where is non-deterministic Where-clause?
Explicitly setting the timestamp value or even disabling the trigger will work:
merge into t
using (
select 1 id, 1 val, 1 flag from dual
) s on (t.id = s.id)
when matched then
update set t.val=s.val, modified=systimestamp
delete where s.flag=1
/
1 row merged.
rollback;
alter trigger trigg_on_t disable;
merge into t
using (
select 1 id, 1 val, 1 flag from dual
) s on (t.id = s.id)
when matched then
update set t.val=s.val
delete where s.flag=1
/
1 row merged.
It seems that the problem is when you are trying to update and delete the same record in the same time. It is a strange error message I admit. You asked where is non-deterministic Where-clause - there is no such clause and that is the problem. Lets say you have 3 records in your taable:
insert into t (id, val) values (1, 1);
insert into t (id, val) values (2, 2);
insert into t (id, val) values (3, 3);
commit;
--
-- ID VAL MODIFIED
-- 1 1 30-JUN-22 06.55.45.683595000
-- 2 2 30-JUN-22 06.55.45.690624000
-- 3 3 30-JUN-22 06.55.45.693622000
Now, your command is:
merge into t
using (
select 1 id, 10 val, 1 flag from dual
) s on (t.id = s.id)
when matched then
update set t.val=s.val
delete where s.flag=1
-- Result is SQL Error: ORA-30926
Update should be done on record with id=1 and delete should be done with s.flag=1 which means s.id=1 and therefore the record to delete is id=1. The same record.
Now lets see this:
merge into t
using (
select 1 id, 10 val, 1 flag from dual union all
select 2 id, 20 val, 2 flag from dual union all
select 3 id, 30 val, 3 flag from dual
) s on (t.id = s.id)
when matched then
update set t.val=s.val
delete where s.flag=1
-- Result is SQL Error: ORA-30926
... above is trying to do the same as in your question but on all three records
But if you change the where clause of delete statement and add the where clause to update statement to be deterministic there will be no error messages
merge into t
using (
select 1 id, 10 val, 1 flag from dual union all
select 2 id, 20 val, 2 flag from dual union all
select 3 id, 30 val, 3 flag from dual
) s on (t.id = s.id)
when matched then
update set t.val=s.val where s.flag = 1
delete where s.flag != 1
Finaly, lets go back to your command. There are some data from dual merged to the record with id=1. Update should be done over that record and delete command should have deterministic where clause that excludes deletion of the record that is being updated. That deterministic where clause shoud be like here:
merge into t
using (
select 1 id, 10 val, 1 flag from dual
) s on (t.id = s.id)
when matched then
update set t.val=s.val
delete where s.flag != 1
-- 1 rows merged
The problem disapears if you disable the before update trigger. If it is enabled then the trigger demands this determinism. And if you have an active after update trigger there is no problem because after update has been done there is no conflict and the record will be deleted.
CONCLUSION:
In this case when there is a table with BEFORE UPDATE trigger enabled when you try to update and delete the record simultaneously using merge there are two transactions that should be imposed. As the update transaction is interrupted by a db trigger changing the record data the second transaction (delete) gets the flag that the record that should be deleted is changed by another transaction and demands for determinism.
If before update trigger do something else (updating some other table or record) and not change the record itself then the command as it is does not raise the error.
You can try to change the trigger to update some value in any other table and run your command as it is ---> there will be no error.

Trigger for UPDATE runs many time on batch Updates

All of my tables have a Trigger for CRUD operations.
here is a sample:
ALTER TRIGGER [dbo].[Cities_tr] ON [dbo].[Cities] AFTER INSERT, UPDATE
AS
BEGIN
DECLARE #operation CHAR(6)
SET #operation = CASE WHEN EXISTS (SELECT * FROM inserted) AND EXISTS (SELECT * FROM deleted)
THEN 'Update'
WHEN EXISTS (SELECT * FROM inserted)
THEN 'Insert'
WHEN EXISTS(SELECT * FROM deleted)
THEN 'Delete'
ELSE NULL
END
IF #operation = 'Insert'
INSERT INTO history ([dt],[tname],[cuser] ,[id],op)
SELECT GETDATE(),'Cities', i.ldu, i.CityId,#operation
FROM inserted i
set nocount on
IF #operation = 'Update'
INSERT INTO history ([dt],[tname],[cuser] ,[id],op)
SELECT GETDATE(),'Cities', i.ldu, i.CityId,#operation
FROM deleted d, inserted i
END
If I update one row, everything works fine and trigger inserts one row in history.
For example
update top(1) cities set f=1
But if more than one row updated, updatedrow^2 rows will be inserted.
For example 9 for 3 rows 100 for 10 rows...
What is wrong with my trigger and how could I solve it?
The problem with your code is that you are cross joining inserted and deleted. On a multi-rows update, both contain many rows, which the cartesian product multiplies.
It looks like you actually want to log the "new" rows (either inserted or updated). If so, you don’t want to select from deleted. Also, the conditional logic can be moved within a single query, which allows to simplify your code as follows:
ALTER TRIGGER dbo.Cities_tr
ON dbo.Cities
AFTER INSERT, UPDATE
AS
BEGIN
INSERT INTO history (dt, tname, cuser, id, op)
SELECT
getdate(),
'Cities',
ldu,
cityId,
case when exists (select 1 from deleted) then 'Update' else 'Insert' end
FROM inserted;
END
On the other hand, if you want to log both the "old" and "new" rows (which is not what your code does, even on a single-row update), then you want to union all two queries that select from inserted and deleted.
You are cross joining inserted and deleted. Normally, they would be joined using the table's primary key, which is presumably CityId:
INSERT INTO history ([dt], [tname], [cuser] , [id], op)
SELECT GETDATE(), 'Cities', i.ldu, i.CityId, #operation
FROM deleted d JOIN
inserted i
ON d.CityId = i.CityId;
In this case, deleted is not being used, so it does not even need to be included in the query.
You could implement the entire trigger as a single query in the table using LEFT JOIN:
INSERT INTO history ([dt], [tname], [cuser] , [id], op)
SELECT GETDATE(), 'Cities', i.ldu, i.CityId,
(CASE WHEN d.CityId IS NOT NULL THEN 'Update' ELSE 'Insert' END)
FROM inserted i LEFT JOIN
deleted d
ON d.CityId = i.CityId;

How to pass multiple arguments to delete operation on trigger?

estado numFactura
a 1
a 2
c 3
c 4
create trigger deleteFactura
on factura
INSTEAD OF DELETE
as
declare #estado varchar
set #estado= (select estado from deleted);
begin
if #estado='c'
THROW 51000, 'La factura esta cerrada, no se puede eliminar', 1
else
delete from factura
where numFactura=(select numFactura from deleted)
end
go
Goal: delete registers with estado ='c' and leave the ones with estado='a' be
I need it to work with more than one value.
If I try to delete two values, I get this error:
Msg 512, Level 16, State 1, Procedure deleteFactura, Line 6 [Batch Start Line 9]
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
How can I make it accept multiple values?
When writing triggers always keep in mind that it may be processing several records.
The records that are being inserted, deleted, or updated, are available in tables INSERTED and DELETED. i.e. They appear to be tables, but they only exist within your trigger.
create table dbo.Invoices ( Status varchar(1), InvoiceNum int )
go
create trigger DeleteInvoice on Invoices
instead of delete
as
delete from Invoices where InvoiceNum in ( select InvoiceNum from deleted ) and Status='a'
go
insert into Invoices ( Status, InvoiceNum ) values
( 'a', 1 ), ('a', 2 ), ('c', 3 ), ('c', 4 )
select * from Invoices
delete from Invoices
select * from Invoices
You want to JOIN with the deleted collection to select just the rows you want to delete, and add the appropriate WHERE clause... something like this:
CREATE TRIGGER DeleteInvoice ON Invoices
INSTEAD OF DELETE
AS
BEGIN
DELETE inv
FROM Invoices inv
INNER JOIN deleted d ON inv.InvoiceNum = d.InvoiceNum
WHERE inv.Status = 'a'
END

Update Table From Select

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.

Select rows and Update same rows for locking?

I need to write a procedure that will allow me to select x amount of rows and at the same time update those rows so the calling application will know those records are locked and in use. I have a column in the table named "locked". The next time the procedure is called it will only pull the next x amount of records that do not have the "locked" column checked. I have read a little about the OUTPUT method for SQL server, but not sure that is what I want to do.
As you suggested, you can use the OUTPUT clause effectively:
Live demo: https://data.stackexchange.com/stackoverflow/query/8058/so3319842
UPDATE #tbl
SET locked = 1
OUTPUT INSERTED.*
WHERE id IN (
SELECT TOP 1 id
FROM #tbl
WHERE locked = 0
ORDER BY id
)​
Also see this article:
http://www.sqlmag.com/article/tsql3/more-top-troubles-using-top-with-insert-update-and-delete.aspx
Vote for Cade Roux's answer, using OUTPUT:
UPDATE #tbl
SET locked = 1
OUTPUT INSERTED.*
WHERE id IN (SELECT TOP 1 id
FROM #tbl
WHERE locked = 0
ORDER BY id)​
Previously:
This is one of the few times I can think of using a temp table:
ALTER PROCEDURE temp_table_test
AS
BEGIN
SELECT TOP 5000 *
INTO #temp_test
FROM your_table
WHERE locked != 1
ORDER BY ?
UPDATE your_table
SET locked = 1
WHERE id IN (SELECT id FROM #temp_test)
SELECT *
FROM #temp_test
IF EXISTS (SELECT NULL
FROM tempdb.dbo.sysobjects
WHERE ID = OBJECT_ID(N'tempdb..#temp_test'))
BEGIN
DROP TABLE #temp_test
END
END
This:
Fetches the rows you want, stuffs them into a local temp table
Uses the temp table to update the rows to be "locked"
SELECTs from the temp table to give you your resultset output
Drops the temp table because they live for the session