Create a function for generating random number in SQL Server trigger - sql

I have to create a function in a SQL Server trigger for generating random numbers after insert. I want to update the column with that generated random number please help what I have missed in my code.
If you know other ways please suggest a way to complete my task.
This my SQL Server trigger:
ALTER TRIGGER [dbo].[trgEnquiryMaster]
ON [dbo].[enquiry_master]
AFTER INSERT
AS
declare #EnquiryId int;
declare #ReferenceNo varchar(50);
declare #GenReferenceNo NVARCHAR(MAX);
select #EnquiryId = i.enquiry_id from inserted i;
select #ReferenceNo = i.reference_no from inserted i;
BEGIN
SET #GenReferenceNo = 'CREATE FUNCTION functionRandom (#Reference VARCHAR(MAX) )
RETURNS VARCHAR(MAX)
As
Begin
DECLARE #r varchar(8);
SELECT #r = coalesce(#r, '') + n
FROM (SELECT top 8
CHAR(number) n FROM
master..spt_values
WHERE type = P AND
(number between ascii(0) and ascii(9)
or number between ascii(A) and ascii(Z)
or number between ascii(a) and ascii(z))
ORDER BY newid()) a
RETURNS #r
END
'
EXEC(#GenReferenceNo)
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON
-- update statements for trigger here
UPDATE enquiry_master
SET reference_no ='updated'
WHERE enquiry_id = #EnquiryId
END

To generate random numbers, just call CRYPT_GEN_RANDOM which was introduced in SQL Server 2008:
SELECT CRYPT_GEN_RANDOM(5) AS [Hex],
CONVERT(VARCHAR(20), CRYPT_GEN_RANDOM(5), 2) AS [HexStringWithout0x],
CONVERT(VARCHAR(20), CRYPT_GEN_RANDOM(10)) AS [Translated-ASCII],
CONVERT(NVARCHAR(20), CRYPT_GEN_RANDOM(20)) AS [Translated-UCS2orUTF16]
returns:
Hex HexStringWithout0x Translated-ASCII Translated-UCS2orUTF16
0x4F7D9ABBC4 0ECF378A7A ¿"bü<ݱØï 붻槬㟰添䛺⯣왚꒣찭퓚
If you are ok with just 0 - 9 and A - F, then the CONVERT(VARCHAR(20), CRYPT_GEN_RANDOM(5), 2) is all you need.
Please see my answer on DBA.StackExchange on a similar question for more details:
Password generator function
The UPDATE statement shown in the "Update" section of that linked answer is what you want, just remove the WHERE condition and add the JOIN to the Inserted pseudo-table.
The query should look something like the following:
DECLARE #Length INT = 10;
UPDATE em
SET em.[reference_no] = rnd.RandomValue
FROM dbo.enquiry_master em
INNER JOIN Inserted ins
ON ins.enquiry_id = em.enquiry_id
CROSS APPLY dbo.GenerateReferenceNo(CRYPT_GEN_RANDOM((em.[enquiry_id] % 1) + #Length)) rnd;
And since the function is slightly different, here is how it should be in order to get both upper-case and lower-case letters:
CREATE FUNCTION dbo.GenerateReferenceNo(#RandomValue VARBINARY(20))
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN
WITH base(item) AS
(
SELECT NULL UNION ALL SELECT NULL UNION ALL SELECT NULL UNION ALL
SELECT NULL UNION ALL SELECT NULL UNION ALL SELECT NULL
), items(item) AS
(
SELECT NULL
FROM base b1
CROSS JOIN base b2
)
SELECT (
SELECT TOP (LEN(#RandomValue))
SUBSTRING('1234567890QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm',
(CONVERT(TINYINT, SUBSTRING(#RandomValue, 1, 1)) % 62) + 1,
1) AS [text()]
FROM items
FOR XML PATH('')
) AS [RandomReferenceNo];
GO
And please follow the usage shown above, passing in CRYPT_GEN_RANDOM((em.[enquiry_id] % 1) + #Length), not: CRYPT_GEN_RANDOM(#RefferenceNOLength).
Other notes:
#marc_s already explained the one-row vs multiple-rows flaw and how to fix that.
not only is a trigger not the place to create a new object (i.e. the function), that function wouldn't have worked anyway since the call to newid() (in the ORDER BY) is not allowed in a function.
You don't need to issue two separate SELECTs to set two different variables. You could do the following:
SELECT #EnquiryId = i.enquiry_id,
#ReferenceNo = i.reference_no
FROM TableName i;
Passing strings into a function requires quoting those strings inside of single-quotes: ASCII('A') instead of ASCII(A).
UPDATE
The full Trigger definition should be something like the following:
ALTER TRIGGER [dbo].[trgEnquiryMaster]
ON [dbo].[enquiry_master]
AFTER INSERT
AS
BEGIN
DECLARE #Length INT = 10;
UPDATE em
SET em.[reference_no] = rnd.RandomValue
FROM dbo.enquiry_master em
INNER JOIN Inserted ins
ON ins.enquiry_id = em.enquiry_id
CROSS APPLY dbo.GenerateReferenceNo(
CRYPT_GEN_RANDOM((em.[enquiry_id] % 1) + #Length)
) rnd;
END;

A trigger should be very nimble and quick - it is no place to do heavy and time-intensive processing, and definitely no place to create new database objects since (a) the trigger is executed in the context of the code causing it to fire, and (b) you cannot control when and how often the trigger is fired.
You need to
define and create your function to generate that random value during database setup - once, before any operations are executed on the database
rewrite your trigger to take into account that multiple rows could be inserted at once, and in that case, the Inserted table will contain multiple rows which all have to be handled.
So your trigger will look something like this (with several assumptions by me - e.g. that enquiry_id is the primary key on your table - you need this to establish the INNER JOIN between your data table and the Inserted pseudo table:
ALTER TRIGGER [dbo].[trgEnquiryMaster]
ON [dbo].[enquiry_master]
AFTER INSERT
AS
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON
-- update statements for trigger here
UPDATE enq
SET reference_no = dbo.GenerateRandomValue(.....)
FROM enquiry_master enq
INNER JOIN inserted i ON enq.enquiry_id = i.enquiry_id

Related

Enumerate the multiple rows in a multi-update Trigger

I have something like the table below:
CREATE TABLE updates (
id INT PRIMARY KEY IDENTITY (1, 1),
name VARCHAR (50) NOT NULL,
updated DATETIME
);
And I'm updating it like so:
INSERT INTO updates (name, updated)
VALUES
('fred', '2020-11-11),
('fred', '2020-11-11'),
...
('bert', '2020-11-11');
I need to write an after update Trigger and enumerate all the name(s) that were added and add each one to another table but can't work out how enumerate each one.
EDIT: - thanks to those who pointed me in the right direction, I know very little SQL.
What I need to do is something like this
foreach name in inserted
look it up in another table and
retrieve a count of the updates a 'name' has done
add 1 to the count
and update it back into the other table
I can't get to my laptop at the moment, but presumably I can do something like:
BEGIN
SET #count = (SELECT UCount from OTHERTAB WHERE name = ins.name)
SET #count = #count + 1
UPDATE OTHERTAB SET UCount = #count WHERE name = ins.name
SELECT ins.name
FROM inserted ins;
END
and that would work for each name in the update?
Obviously I'll have to read up on set based SQL processing.
Thanks all for the help and pointers.
Based on your edits you would do something like the following... set based is a mindset, so you don't need to compute the count in advance (in fact you can't). It's not clear whether you are counting in the same table or another table - but I'm sure you can work it out.
Points:
Use the Inserted table to determine what rows to update
Use a sub-query to calculate the new value if its a second table, taking into account the possibility of null
If you are really using the same table, then this should work
BEGIN
UPDATE OTHERTAB SET
UCount = COALESCE(UCount,0) + 1
WHERE [name] in (
SELECT I.[name]
FROM Inserted I
);
END;
If however you are using a second table then this should work:
BEGIN
UPDATE OTHERTAB SET
UCount = COALESCE((SELECT UCount+1 from OTHERTAB T2 WHERE T2.[name] = OTHERTAB.[name]),0)
WHERE [name] in (
SELECT I.[name]
FROM Inserted I
);
END;
Using inserted and set-based approach(no need for loop):
CREATE TRIGGER trg
ON updates
AFTER INSERT
AS
BEGIN
INSERT INTO tab2(name)
SELECT name
FROM inserted;
END

In SQL Server 2008 R2, is there a way to create a custom auto increment identity field without using IDENTITY(1,1)?

I would like to be able to pull the custom key value from a table, but would also like it to perform like SQL Server's IDENTITY(1,1) column on inserts.
The custom key is for another application and will need to be used by different functions so the value will need to be pulled from a table and available for other areas.
Here are some if my attempts:
Tried a trigger on the table works well on single inserts, failed on using SQL insert (forgetting the fact that a triggers are not per row but by batch)
ALTER TRIGGER [sales].[trg_NextInvoiceDocNo]
ON [sales].[Invoice]
AFTER INSERT
AS
BEGIN
DECLARE #ResultVar VARCHAR(25)
DECLARE #Key VARCHAR(25)
EXEC [dbo].[usp_GetNextKeyCounterChar]
#tcForTbl = 'docNbr', #tcForGrp = 'docNbr', #NewKey = #ResultVar OUTPUT
UPDATE sales.InvoiceRET
SET DocNbr = #ResultVar
FROM sales.InvoiceRET
JOIN inserted ON inserted.id = sales.InvoiceRET.id;
END;
Thought about a scalar function, but functions cannot exec stored procedures or update statements in order to set the new key value in the lookup table.
Thanks
You can use ROW_NUMBER() depending on the type of concurrency you are dealing with. Here is some sample data and a demo you can run locally.
-- Sample table
USE tempdb
GO
IF OBJECT_ID('dbo.sometable','U') IS NOT NULL DROP TABLE dbo.sometable;
GO
CREATE TABLE dbo.sometable
(
SomeId INT NULL,
Col1 INT NOT NULL
);
GO
-- Stored Proc to insert data
CREATE PROC dbo.InsertProc #output BIT AS
BEGIN -- Your proc starts here
INSERT dbo.sometable(Col1)
SELECT datasource.[value]
FROM (VALUES(CHECKSUM(NEWID())%100)) AS datasource([value]) -- simulating data from somewhere
CROSS APPLY (VALUES(1),(1),(1)) AS x(x);
WITH
id(MaxId) AS (SELECT ISNULL(MAX(t.SomeId),0) FROM dbo.sometable AS t),
xx AS
(
SELECT s.SomeId, RN = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))+id.MaxId, s.Col1, id.MaxId
FROM id AS id
CROSS JOIN dbo.sometable AS s
WHERE s.SomeId IS NULL
)
UPDATE xx SET xx.SomeId = xx.RN;
IF #output = 1
SELECT t.* FROM dbo.sometable AS t;
END
GO
Each time I run: EXEC dbo.InsertProc 1; it returns 3 more rows with the correct ID col. Each time I execute it, it adds more rows and auto-increments as needed.
SomeId Col1
-------- ------
1 62
2 73
3 -17

Explain syntax: select #au_id = au_id from #mytemp (Equals sign in SELECT clause) [duplicate]

What are the differences between the SET and SELECT statements when assigning variables in T-SQL?
Quote, which summarizes from this article:
SET is the ANSI standard for variable assignment, SELECT is not.
SET can only assign one variable at a time, SELECT can make multiple assignments at once.
If assigning from a query, SET can only assign a scalar value. If the query returns multiple values/rows then SET will raise an error. SELECT will assign one of the values to the variable and hide the fact that multiple values were returned (so you'd likely never know why something was going wrong elsewhere - have fun troubleshooting that one)
When assigning from a query if there is no value returned then SET will assign NULL, where SELECT will not make the assignment at all (so the variable will not be changed from its previous value)
As far as speed differences - there are no direct differences between SET and SELECT. However SELECT's ability to make multiple assignments in one shot does give it a slight speed advantage over SET.
I believe SET is ANSI standard whereas the SELECT is not. Also note the different behavior of SET vs. SELECT in the example below when a value is not found.
declare #var varchar(20)
set #var = 'Joe'
set #var = (select name from master.sys.tables where name = 'qwerty')
select #var /* #var is now NULL */
set #var = 'Joe'
select #var = name from master.sys.tables where name = 'qwerty'
select #var /* #var is still equal to 'Joe' */
When writing queries, this difference should be kept in mind :
DECLARE #A INT = 2
SELECT #A = TBL.A
FROM ( SELECT 1 A ) TBL
WHERE 1 = 2
SELECT #A
/* #A is 2*/
---------------------------------------------------------------
DECLARE #A INT = 2
SET #A = (
SELECT TBL.A
FROM ( SELECT 1 A) TBL
WHERE 1 = 2
)
SELECT #A
/* #A is null*/
Aside from the one being ANSI and speed etc., there is a very important difference that always matters to me; more than ANSI and speed. The number of bugs I have fixed due to this important overlook is large. I look for this during code reviews all the time.
-- Arrange
create table Employee (EmployeeId int);
insert into dbo.Employee values (1);
insert into dbo.Employee values (2);
insert into dbo.Employee values (3);
-- Act
declare #employeeId int;
select #employeeId = e.EmployeeId from dbo.Employee e;
-- Assert
-- This will print 3, the last EmployeeId from the query (an arbitrary value)
-- Almost always, this is not what the developer was intending.
print #employeeId;
Almost always, that is not what the developer is intending. In the above, the query is straight forward but I have seen queries that are quite complex and figuring out whether it will return a single value or not, is not trivial. The query is often more complex than this and by chance it has been returning single value. During developer testing all is fine. But this is like a ticking bomb and will cause issues when the query returns multiple results. Why? Because it will simply assign the last value to the variable.
Now let's try the same thing with SET:
-- Act
set #employeeId = (select e.EmployeeId from dbo.Employee e);
You will receive an error:
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
That is amazing and very important because why would you want to assign some trivial "last item in result" to the #employeeId. With select you will never get any error and you will spend minutes, hours debugging.
Perhaps, you are looking for a single Id and SET will force you to fix your query. Thus you may do something like:
-- Act
-- Notice the where clause
set #employeeId = (select e.EmployeeId from dbo.Employee e where e.EmployeeId = 1);
print #employeeId;
Cleanup
drop table Employee;
In conclusion, use:
SET: When you want to assign a single value to a variable and your variable is for a single value.
SELECT: When you want to assign multiple values to a variable. The variable may be a table, temp table or table variable etc.
Surround everything in select with ().
Make sure you are only returning 1 item
eg
ET #sql_update = (select left(#sql_update, len(#sql_update)-1))
SET #Telephone2 = (SELECT REPLACE(LTRIM(REPLACE(#Telephone2, '0', ' ')), ' ', '0'))

How to delete all rows without deleting the last returned row in SQL Server?

I want to delete all the rows from a SELECT without deleting the last returned row by using a trigger when a delete query is executed.
This trigger doesn't work so any help is greatly appreciated.
CREATE TRIGGER TR_StergereOfertaSpeciala
ON OferteSpeciale
INSTEAD OF DELETE
AS
DECLARE #nr INTEGER;
IF (EXISTS(SELECT * FROM DELETED))
BEGIN
SET #nr = (SELECT COUNT(*) FROM DELETED);
DELETE FROM (
SELECT TOP(#nr - 1)* FROM OferteSpeciale
INNER JOIN DELETED ON OferteSpeciale.codP = Deleted.codP
AND OferteSpeciale.codM = Deleted.codM
AND OferteSpeciale.dela = Deleted.dela)
END
Here is an example of getting your concept to work properly:
CREATE TRIGGER TR_StergereOfertaSpeciala
ON OferteSpeciale
INSTEAD OF DELETE
AS BEGIN
DECLARE #nr INT
SET #nr = (SELECT COUNT(*) FROM DELETED)
IF (#nr > 1) BEGIN
DELETE o
FROM OferteSpeciale AS o
INNER JOIN (SELECT TOP (#nr - 1) * FROM DELETED /* ORDER BY ??? */) AS d
ON o.codP = d.codP
AND o.codM = d.codM
AND o.dela = d.dela
END
END
Note the syntax for a delete with a join. Also note that we're arbitrarily choosing the 1 row to keep. I would suggest, as #RBarryYoung has mentioned, specifically ordering the set by something to know which row we are keeping.
Another way of doing this which could avoid the somewhat dynamic TOP clause (clever, BTW) would be to specifically exclude the record you want to keep using NOT EXISTS/IN
Also, you probably want to avoid trigger recursion and nested triggers in this case.

Why my T-SQL (WHILE) does not work?

In my code, I need to test whether specified column is null and the most close to 0 as possible (it can holds numbers from 0 to 50) so I have tried the code below.
It should start from 0 and for each value test the query. When #Results gets null, it should return. However, it does not work. Still prints 0.
declare #hold int
declare #Result int
set #hold0
set #Result=0
WHILE (#Result!=null)
BEGIN
select #Result=(SELECT Hold from Numbers WHERE Name='Test' AND Hold=#hold)
set #hold=#hold+1
END
print #hold
First, you can't test equality of NULL. NULL means an unknown value, so you don't know whether or not it does (or does not) equal any specific value. Instead of #Result!=NULL use #result IS NOT NULL
Second, don't use this kind of sequential processing in SQL if you can at all help it. SQL is made to handle sets, not process things sequentially. You could do all of this work with one simple SQL command and it will most likely run faster anyway:
SELECT
MIN(hold) + 1
FROM
Numbers N1
WHERE
N1.name = 'Test' AND
NOT EXISTS
(
SELECT
*
FROM
Numbers N2
WHERE
N2.name = 'Test' AND
N2.hold = N1.hold + 1
)
The query above basically tells the SQL Server, "Give me the smallest hold value plus 1 (MIN(hold) + 1) in the table Numbers where the name is test (name = 'Test') and where the row with name of 'Test' and hold of one more that that does not exist (the whole "NOT EXISTS" part)". In the case of the following rows:
Name Hold
-------- ----
Test 1
Test 2
NotTest 3
Test 20
SQL Server finds all of the rows with name of "Test" (1, 2, 20) then finds which ones don't have a row with name = Test and hold = hold + 1. For 1 there is a row with Test, 2 that exists. For Test, 2 there is no Test, 3 so it's still in the potential results. For Test, 20 there is no Test, 21 so that leaves us with:
Name Hold
-------- ----
Test 2
Test 20
Now SQL Server looks for MIN(hold) and gets 2 then it adds 1, so you get 3.
SQL Server may not perform the operations exactly as I described. The SQL statement tells SQL Server what you're looking for, but not how to get it. SQL Server has the freedom to use whatever method it determines is the most efficient for getting the answer.
The key is to always think in terms of sets and how do those sets get put together (through JOINs), filtered (through WHERE conditions or ON conditions within a join, and when necessary, grouped and aggregated (MIN, MAX, AVG, etc.).
have you tried
WHILE (#Result is not null)
BEGIN
select #Result=(SELECT Hold from Numbers WHERE Name='Test' AND Hold=#hold)
set #hold=#hold+1
END
Here's a more advanced version of Tom H.'s query:
SELECT MIN(N1.hold) + 1
FROM Numbers N1
LEFT OUTER JOIN Numbers N2
ON N2.Name = N1.Name AND N2.hold = N1.hold + 1
WHERE N1.name = 'Test' AND N2.name IS NULL
It's not as intuitive if you're not familiar with SQL, but it uses identical logic. For those who are more familiar with SQL, it makes the relationship between N1 and N2 easier to see. It may also be easier for the query optimizer to handle, depending on your DBMS.
Try this:
declare #hold int
declare #Result int
set #hold=0
set #Result=0
declare #max int
SELECT #max=MAX(Hold) FROM Numbers
WHILE (#hold <= #max)
BEGIN
select #Result=(SELECT Hold from Numbers WHERE Name='Test' AND Hold=#hold)
set #hold=#hold+1
END
print #hold
While is tricky in T-SQL - you can use this for (foreach) looping through (temp) tables too - with:
-- Foreach with T-SQL while
DECLARE #tempTable TABLE (rownum int IDENTITY (1, 1) Primary key NOT NULL, Number int)
declare #RowCnt int
declare #MaxRows int
select #RowCnt = 1
select #MaxRows=count(*) from #tempTable
declare #number int
while #RowCnt <= #MaxRows
begin
-- Number from given RowNumber
SELECT #number=Number FROM #tempTable where rownum = #RowCnt
-- next row
Select #RowCnt = #RowCnt + 1
end