SQL Server : stored procedure or function - sql-server

I have a table with 2 columns (A as bool and B as text), these columns can be:
both are null
if A is False, then B should be null
if A is True, then B should be not null
There are rules. I want to create a stored procedure or function to check these rules when row adding or updating (via trigger). What is better, stored procedure or function? If function, which type? In general, which variant is the best (return boolean or other way etc)?

I think you're after a CHECK Constraint.
Example:
ALTER TABLE Xxx
ADD CONSTRAINT chk_Xxx
CHECK ( (A IS NULL AND B IS NULL)
OR (A = 0 AND B IS NULL)
OR (A = 1 AND B IS NOT NULL)
) ;

I would use a CHECK CONSTRAINT wired up to a UDF.
Here is a silly example verifying that if you insert a Person, their age will be greater than 17.
if NOT exists (select * from sysobjects
where id = object_id('dbo.udfOlderThan17Check') and sysstat & 0xf = 0)
BEGIN
print 'Creating the stubbed version of dbo.udfOlderThan17Check'
EXEC ( 'CREATE FUNCTION dbo.udfOlderThan17Check ( #j as smallint ) RETURNS bit AS BEGIN RETURN 0 END')
END
GO
ALTER FUNCTION dbo.udfOlderThan17Check ( #Age smallint )
RETURNS bit AS
BEGIN
declare #exists int
select #exists = 0
if ( #Age IS NULL )
BEGIN
select #exists = 1 -- NULL VALUES SHOULD NOT BLOW UP THE CONSTRAINT CHECK
END
if ( #exists = 0 )
BEGIN
if #Age > 17
begin
select #exists = 1
end
END
return #exists
END
GO
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'[dbo].[Person]') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
BEGIN
DROP TABLE [dbo].[Person]
END
GO
CREATE TABLE [dbo].[Person]
(
PersonUUID [UNIQUEIDENTIFIER] NOT NULL DEFAULT NEWSEQUENTIALID()
, Age smallint not null
)
GO
ALTER TABLE dbo.Person ADD CONSTRAINT PK_Person
PRIMARY KEY NONCLUSTERED (PersonUUID)
GO
ALTER TABLE dbo.Person
ADD CONSTRAINT [CK_Person_AgeValue] CHECK ([dbo].[udfOlderThan17Check]( [Age] ) != 0)
GO
Here are some "tests":
INSERT INTO dbo.Person (Age) values (33)
INSERT INTO dbo.Person (Age) values (16)
INSERT INTO dbo.Person (Age) select 333 UNION select 58
INSERT INTO dbo.Person (Age) select 444 UNION select 4
select * from dbo.Person

Related

Stored procedure in that needs to return unique values in a custom format but seems to return duplicates

I have a stored procedure in Microsoft SQL Server that should return unique values based on a custom format: SSSSTT99999 where SSSS and TT is based on a parameter and 99999 is a unique sequence based on the values of SSSS and TT. I need to store the last sequence based on SSSS and TT on table so I can retrieve the next sequence the next time. The problem with this code is that in a multi-user environment, at least two simultaneous calls may generate the same value. How can I make sure that each call to this stored procedure gets a unique value?
CREATE PROCEDURE GenRef
#TT nvarchar(30),
#SSSS nvarchar(50)
AS
declare #curseq as integer
set #curseq=(select sequence from DocSequence where
docsequence.TT=#TT and
DocSequence.SSSS=#SSSS)
if #curseq is null
begin
set #curseq=1
insert docsequence (id,TT,SSSS,sequence) values
(newid(),#TT,#SSSS,1)
end
else
begin
update DocSequence set Sequence=#curseq+1 where
docsequence.TT=#TT and
DocSequence.SSSS=#SSSS
end
declare #curtr varchar(30)
set #curtr=RIGHT('0000' + #SSSS,4)
+ #TT
+ RIGHT('00000' + #curseq,5)
select #curtr
GO
updated code with transactions:
ALTER PROCEDURE [dbo].[GenTRNum]
#TRType nvarchar(50),
#BranchCode nvarchar(50)
AS
declare #curseq as integer
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
begin transaction
if not exists (select top 1 sequence from DocSequence where
docsequence.DocType=#trtype and
DocSequence.BranchCode=#BranchCode)
begin
insert docsequence (id,doctype,sequence,branchcode) values
(newid(),#trtype,1,#BranchCode)
end
else
begin
update DocSequence set Sequence=sequence+1 where
docsequence.DocType=#trtype and
DocSequence.BranchCode=#BranchCode
end
commit
set #curseq=(select top 1 sequence from DocSequence where
docsequence.DocType=#trtype and
DocSequence.BranchCode=#BranchCode)
declare #curtr varchar(30)
set #curtr=RIGHT('0000' + #BranchCode,4)
+ #TRType
+ RIGHT('00000' + convert(varchar(5),#curseq),5)
select #curtr
You can handle this on application level by using threading assuming you have single application Server.
Suppose you have method GetUniqueVlaue Which Executes this SP.
What you should do is use threading. that method use database transactions with readcommited. Now for example if two users have made the call to GetUniqueVlaue method at exactly 2019-08-30 10:59:38.173 time your application will make threads and Each thread will try to open transaction. only one will open that transaction on that SP and other will go on wait.
Here is how I would solve this task:
Table structure, unique indexes are important
--DROP TABLE IF EXISTS dbo.DocSequence;
CREATE TABLE dbo.DocSequence (
RowID INT NOT NULL IDENTITY(1,1)
CONSTRAINT PK_DocSequence PRIMARY KEY CLUSTERED,
BranchCode CHAR(4) NOT NULL,
DocType CHAR(2) NOT NULL,
SequenceID INT NOT NULL
CONSTRAINT DF_DocSequence_SequenceID DEFAULT(1)
CONSTRAINT CH_DocSequence_SequenceID CHECK (SequenceID BETWEEN 1 AND 999999),
)
CREATE UNIQUE INDEX UQ_DocSequence_BranchCode_DocType
ON dbo.DocSequence (BranchCode,DocType) INCLUDE(SequenceID);
GO
Procedure:
CREATE OR ALTER PROCEDURE dbo.GenTRNum
#BranchCode VARCHAR(4),
#DocType VARCHAR(2),
--
#curseq INT = NULL OUTPUT,
#curtr VARCHAR(30) = NULL OUTPUT
AS
SELECT #curseq = NULL,
#curtr = NULL,
#BranchCode = RIGHT(CONCAT('0000',#BranchCode),4),
#DocType = RIGHT(CONCAT('00',#DocType),2)
-- Atomic operation, no transaction needed
UPDATE dbo.DocSequence
SET #curseq = SequenceID += 1
WHERE DocType = #DocType
AND BranchCode = #BranchCode;
IF #curseq IS NULL -- Not found, create new one
BEGIN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRAN
INSERT dbo.docsequence (doctype,branchcode)
SELECT #DocType, #BranchCode
WHERE NOT EXISTS(SELECT 1 FROM dbo.DocSequence WHERE DocType = #DocType AND BranchCode = #BranchCode)
IF ##ROWCOUNT = 1
BEGIN
COMMIT;
SET #curseq = 1
END
ELSE
BEGIN
ROLLBACK;
UPDATE dbo.DocSequence
SET #curseq = SequenceID += 1
WHERE DocType = #DocType
AND BranchCode = #BranchCode;
END
END
SET #curtr = #BranchCode + #DocType + RIGHT(CONCAT('00000',#curseq),5)
RETURN
GO
I did some tests to make sure is it works as described. You can use it if you need
-- Log table just for test
-- DROP TABLE IF EXISTS dbo.GenTRNumLog;
CREATE TABLE dbo.GenTRNumLog(
RowID INT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED,
SPID SMALLINT NOT NULL,
Cycle INT NULL,
dt DATETIME NULL,
sq INT NULL,
tr VARCHAR(30) NULL,
DurationMS INT NULL
)
This script should be opened in several separate MS SQL Management Studio windows and run they almost simultaneously
-- Competitive insertion test, run it in 5 threads simultaneously
SET NOCOUNT ON
DECLARE
#dt DATETIME,
#start DATETIME,
#DurationMS INT,
#Cycle INT,
#BC VARCHAR(4),
#DC VARCHAR(2),
#SQ INT,
#TR VARCHAR(30);
SELECT #Cycle = 0,
#start = GETDATE();
WHILE DATEADD(SECOND, 60, #start) > GETDATE() -- one minute test, new #DocType every second
BEGIN
SET #dt = GETDATE();
SELECT #BC = FORMAT(#dt,'HHmm'), -- Hours + Minuts as #BranchCode
#DC = FORMAT(#dt,'ss'), -- seconds as #DocType
#Cycle += 1
EXEC dbo.GenTRNum #BranchCode = #BC, #DocType = #Dc, #curseq = #SQ OUTPUT, #curtr = #TR OUTPUT
SET #DurationMS = DATEDIFF(ms, #dt, GETDATE());
INSERT INTO dbo.GenTRNumLog (SPID, Cycle , dt, sq, tr, DurationMS)
SELECT SPID = ##SPID, Cycle = #Cycle, dt = #dt, sq = #SQ, tr = #TR, DurationMS = #DurationMS
END
/*
Check test results
SELECT *
FROM dbo.DocSequence
SELECT sq = MAX(sq), DurationMS = MAX(DurationMS)
FROM dbo.GenTRNumLog
SELECT * FROM dbo.GenTRNumLog
ORDER BY tr
*/

MSSQL - How to not allow multiple users to view same set of records

e.g.
in table I have following records
a
b
c
d
e
f
g
h
I want to retrieved records by page
say 4 records per page
user x picks
a
b
c
d
Now user y should not pick any of the above
e
f
g
h
user x processes a record say record b
now he should see
a
e
c
d
and user y should see
f
g
h
i
how can I accomplish this, is there any built in way in mssql?
UPDATE
Here's what I have accomplished so far using auxilary table
http://sqlfiddle.com/#!18/e96f1/1
AllocateRecords2 2, 5, 1
GO
AllocateRecords2 2, 5, 2
both the queries are returning same set of results
I think you can use an auxiliary table UserData
CREATE TABLE Data(
ID int NOT NULL IDENTITY PRIMARY KEY,
Value varchar(1) NOT NULL
)
INSERT Data(Value)VALUES
('a'),
('b'),
('c'),
('d'),
('e'),
('f'),
('g'),
('h')
-- auxiliary table
CREATE TABLE UserData(
UserID int NOT NULL,
DataID int NOT NULL,
PRIMARY KEY(UserID,DataID),
FOREIGN KEY(DataID) REFERENCES Data(ID)
)
GO
And fill this table using the following procedure
CREATE PROC AddDataToUserData
#UserID int
AS
INSERT UserData(DataID,UserID)
SELECT TOP 4 ID,#UserID
FROM Data d
WHERE ID NOT IN(SELECT DataID FROM UserData)
ORDER BY ID
GO
Execute procedure for each other users
--TRUNCATE TABLE UserData
EXEC AddDataToUserData 1
EXEC AddDataToUserData 2
EXEC AddDataToUserData 3
...
Select data for a specific user
SELECT d.*
FROM Data d
JOIN UserData u ON u.DataID=d.ID
WHERE u.UserID=1
SELECT d.*
FROM Data d
JOIN UserData u ON u.DataID=d.ID
WHERE u.UserID=2
You can also create procedure for it
CREATE PROC GetDataForUser
#UserID int
AS
SELECT d.*
FROM Data d
JOIN UserData u ON u.DataID=d.ID
WHERE u.UserID=#UserID
GO
And then use it
EXEC GetDataForUser 1
EXEC GetDataForUser 2
Hope I understood your question correctly. But if I'm wrong you may use it as an idea.
I've added one column PageNumber into AllocatedRecords
/*
DROP TABLE Alphabets
DROP TABLE AllocatedRecords
GO
*/
CREATE TABLE Alphabets
(
ID INT IDENTITY(1,1) PRIMARY KEY,
Record varchar(1)
)
GO
CREATE TABLE [dbo].[AllocatedRecords](
[ID] [bigint] IDENTITY(1,1) NOT NULL primary key,
[ReferenceID] [int] NULL,
[IsProcessed] [bit] NULL,
[AllocatedToUser] [int] NULL,
[AllocatedDate] [datetime] NULL,
[ProcessedDate] [datetime] NULL,
PageNumber int -- new column
)
GO
INSERT Alphabets VALUES('a'),('b'),('c'),('d'),('e'),('f'),('g'),('h'),('i'),('j'),('k'),('l'),('m'),('n'),('o'),('p'),('q'),('r'),('s'),('t'),('u'),('v'),('w'),('x'),('y'),('z')
GO
And changed your procedure
DROP PROC AllocateRecords2
GO
CREATE PROC AllocateRecords2
(
#UserID INT,
#PageSize INT,
#PageNumber INT
)
AS
BEGIN
DECLARE #Today DATETIME
SET #Today = GETDATE()
--deallocated expired items
--TRUNCATE TABLE AllocatedRecords
DELETE AllocatedRecords
WHERE IsProcessed = 0 AND
(
(DATEDIFF(minute, #Today, AllocatedDate) > 5)
OR (AllocatedToUser = #UserID AND PageNumber <> #PageNumber)
)
DECLARE
#Draw INT = 10,
#PoolSize INT = #PageSize,
#CurrentRecords INT = (SELECT Count(*) from AllocatedRecords WHERE AllocatedToUser = #UserID AND IsProcessed = 0)
IF #CurrentRecords = 0
BEGIN
SET #Draw = #PoolSize
END
ELSE IF #CurrentRecords < #PoolSize
BEGIN
SET #Draw = #PoolSize - #CurrentRecords
END
ELSE IF #CurrentRecords >= #PoolSize
BEGIN
SET #Draw = 0
END
IF #Draw>0
BEGIN
INSERT AllocatedRecords(ReferenceID,IsProcessed,AllocatedToUser,AllocatedDate,ProcessedDate,PageNumber)
SELECT ID, 0, #UserID, GETDATE(), NULL, #PageNumber
FROM Alphabets
WHERE ID NOT IN (SELECT ReferenceID FROM AllocatedRecords)
ORDER BY ID
OFFSET (#PageNumber - 1) * #PageSize ROWS
FETCH NEXT #Draw ROWS ONLY
END
SELECT x.ID, x.Record
FROM AllocatedRecords A
JOIN Alphabets x ON A.ReferenceID = x.ID
WHERE AllocatedToUser = #UserID
AND IsProcessed = 0
SELECT COUNT(*) as TotalRecords
FROM AllocatedRecords
WHERE AllocatedToUser = #UserID
AND IsProcessed = 0
END
GO
Test
TRUNCATE TABLE AllocatedRecords
GO
-- user 2
EXEC AllocateRecords2 2, 5, 1
EXEC AllocateRecords2 2, 5, 2
EXEC AllocateRecords2 2, 5, 2 -- the same page
EXEC AllocateRecords2 1, 5, 1 -- user 1
EXEC AllocateRecords2 3, 5, 1 -- user 3

How to get rid of timeout error when using mssql hierarchy check constrint

i am creating sql hirercky table
Here is my code;
The Constraint Function Code
alter Function Accounts.Types_Sub_Check_fn (#ID uniqueidentifier, #Sub Uniqueidentifier) returns int
begin
--declare #id uniqueidentifier = '8c7d4151-246c-476c-adf6-964ca9afdd3c' declare #sub uniqueidentifier = '47c2b6da-25fc-4921-adfa-b1f635bddde6'
declare #a int
declare #b int =(iif(#ID=#SUB,2,0))
;with cte(id, lvl) as
(
select f.sub,
1
from Accounts.Types as f
where f.id = #id
union all
select f.sub,
lvl + 1
from Accounts.Types as f
inner join cte as c
on f.id = c.id
)
select #a = (select count (*)
from cte
where id =#sub) + #b
option (maxrecursion 0)
return #a
end
go
The Table code
create Table Accounts.Types
(
ID uniqueidentifier not null CONSTRAINT DF_Accounts_Types_ID DEFAULT newid() CONSTRAINT PK_Accounts_Types_ID PRIMARY KEY NONCLUSTERED (ID) ,
Name varchar(200) not null CONSTRAINT UQ_Accounts_Types_NAME UNIQUE (NAME),
Sub uniqueidentifier CONSTRAINT FK_Accounts_Types_Sub Foreign key references Accounts.Types ,
Ctype uniqueidentifier CONSTRAINT FK_Accounts_Types_Ctype Foreign key references Accounts.Types ,
insert_time datetime not null CONSTRAINT DF_Accounts_Types_Insert_Time DEFAULT getdate() ,
insert_user uniqueidentifier CONSTRAINT DF_Accounts_Types_Insert_User DEFAULT'9EC66F53-9233-4A6C-8933-F8417D2BB5A9' ,
ts timestamp,
INDEX IX_Accounts_Types_NAME#ASC CLUSTERED (Name ASC),
Constraint Check_Accounts_Types_Sub check (Accounts.Types_Sub_Check_fn(ID,Sub)<=1)
)
go
This function will give 2 as result if trying to insert itseft as parent (in sub column)
it will give 1 if its already a child, which trying to insert as its parent
The Check constraint is created to check if the the parent (sub column) for any id should not be its child or grand child,
and itself cannot be its parent
When i try to insert a data which does not match the check constraint, it stuck, and give a timeout error,
eg:
insert into Accounts.Types (ID, Name, Sub)
values ('607936b9-6f95-4989-8ebe-87a08807f43e','LLL','607936b9-6f95-4989-8ebe-87a08807f43e')
this will give timeout
can anyone help me out, i need to get rid of time out error; get the constraint error only
Easy question - when will your recursion end when your ID and Sub are the same values and you don't limit maxrecursion or lvl? Never. It'll never end.
values ('607936b9-6f95-4989-8ebe-87a08807f43e','LLL','607936b9-6f95-4989-8ebe-87a08807f43e')
You have to remove rows where ID = Sub or add maxrecursion or add level limit or normalize your table.
alter Function Accounts.Types_Sub_Check_fn (#ID uniqueidentifier, #Sub Uniqueidentifier) returns int
begin
--declare #id uniqueidentifier = '00279c6b-df00-4144-810d-571fdb1c5109' declare #sub uniqueidentifier = 'bc887e7b-36d2-4ece-8ec1-720dc81a9de4'
declare #a int = 0
declare #b int =(iif(#ID=#SUB,2,0))
if #ID <> #sub
begin
;with cte(id, lvl) as
(
select f.Sub ,
1
from Accounts.Types as f
where f.id = #sub
union all
select iif(f.Sub = #sub, Null, f.sub),
lvl + 1
from Accounts.Types as f
inner join cte as c
on f.id = c.id
)
select #a = (select count (*)
from cte
where id =#id)
option (maxrecursion 0);
end
-- select #a + #b
return #a + #b
end
go

SQL need to insert into table which only consists of a primary key but no auto increment

I need to insert a value into a table which only consists of one column, that is, the primary key.
Furthermore, NULL is not allowed, Identity is set to FALSE and both Identity Seed and Identity Increment are set to 0.
I try to insert with INSERT INTO table(id) VALUES (null) which obviously does not work. INSERT INTO table(id) default values also does not work.
How can I fill this column with the correctly incremented ID?
Implementing Identity or Sequence would be the best solution, but if you really cannot alter the schema the alternative is to lock the table in a transaction, create the new value, unlock the table. Note this can have performance consequences.
create table dbo.ids ( id int primary key clustered );
GO
insert dbo.ids values ( 1 ), ( 2 ), ( 3 ), ( 4 ) ;
GO
declare #newid int;
begin transaction
set #newid = ( select top( 1 ) id from dbo.ids with ( tablockx, holdlock ) order by id desc ) + 1 ;
insert into dbo.ids values ( #newid );
select #newid;
commit
GO 20
You can use while function in that insert
declare #id int
select #id = max(id) from table
while #id <= (... put here max nuber of your id you want to insert)
begin
insert into table values (#id)
set #id = #id+1 end
end
This can be a solution too.
declare #newid integer
begin tran
select #newid = isnull(max(id), 0) + 1 from table with (xlock,holdlock)
insert into table values(#newid)
select #newid
commit tran

##ROWCOUNT returning 1 when no UPDATE made

I have some SQL within a stored procedure where I am updating a table based on another SELECT statement from a temp table (code below).
SET NOCOUNT ON
DECLARE #RowCount int
UPDATE TABLEX SET
TRA = ISNULL (ir.DcTra, DCBASIC.TRA),
TRD = ISNULL(CAST(NULLIF(REPLACE(ir.DcTRD, '-', ''), '') AS datetime), DCBASIC.TRD),
LSINC = ISNULL(ir.DcLsInc, DCBASIC.LSINC),
REVSWOVR = ISNULL(ir.DcRevswovr, DCBASIC.REVSWOVR) FROM #TempData ir WHERE TABLEX.MEMBNO = ir.IntMembNo
SET #RowCount = ##ROWCOUNT
The #RowCount variable is being set to 1.
The SELECT of the #TempData table returns no rows and no rows in the TABLEX table are updated (or even exist) with the MembNo (I have added SELECT statements within the sp to debug and they confirm this)
Why is #RowCount being set to 1?
Here is an explanation:
Statements that make a simple assignment always set the ##ROWCOUNT value to 1.
More information you can find here:
##ROWCOUNT
My example:
CREATE DATABASE FirstDB
GO
USE FirstDB;
GO
CREATE TABLE Person (
personId INT IDENTITY PRIMARY KEY,
firstName varchar(20) ,
lastName varchar(20) ,
age int
)
INSERT INTO dbo.Person (firstName, lastName, age)
VALUES ('Nick', 'Smith', 30),
('Jack', 'South', 25),
('Garry', 'Perth', 20)
CREATE TABLE PersonAge (
personAgeId INT IDENTITY PRIMARY KEY ,
personId INT ,
newAge varchar(10)
)
INSERT INTO dbo.PersonAge(personId, newAge)
VALUES (1, 60),
(2, 65),
(3, 70)
ALTER TABLE dbo.PersonAge
ADD CONSTRAINT FK_PersonAgePerson FOREIGN KEY (personId)
REFERENCES dbo.Person (personId)
And then example of query:
USE FirstDB;
GO
SET NOCOUNT ON;
DECLARE #row int;
UPDATE Person
SET age = 40
FROM dbo.Person as p join dbo.PersonAge as p1
ON p.personId = p1.personId
WHERE p.age = 60
SET #row = ##ROWCOUNT
SELECT #row
I create an UPDATE query where none of rows will be affected.
At the end #row consist 0 value.
Here is another example, using INSERT and DELETE--
DECLARE #deletedRows INT = 0;
SELECT #deletedRows = ##ROWCOUNT; --no previous DML statement
SELECT #deletedRows; --##ROWCOUNT = 1 for a simple assignment
GO
DROP TABLE IF EXISTS #Test;
GO
CREATE TABLE #Test (ID INT IDENTITY, CurrentDate DATETIME DEFAULT GETDATE());
GO
INSERT #Test DEFAULT VALUES; --INSERT a single row
DECLARE #deletedRows INT = ##ROWCOUNT; --##ROWCOUNT = 1
SELECT #deletedRows;
GO
DELETE FROM #Test WHERE 1=2; --no rows deleted
DECLARE #deletedRows INT = ##ROWCOUNT; --##ROWCOUNT = 0
SELECT #deletedRows;
GO
DELETE TOP (1) t FROM #Test t WHERE 1=1; --1 row deleted
DECLARE #deletedRows INT = ##ROWCOUNT; --##ROWCOUNT = 1
SELECT #deletedRows;
GO
DELETE TOP (1) t FROM #Test t WHERE 1=1; --no rows left to delete
DECLARE #deletedRows INT = ##ROWCOUNT; --##ROWCOUNT = 0
SELECT #deletedRows;
GO

Resources