One SQL statement that updates two tables - sql

I have a SQL script that returns one unique row. I want to update one column in that row and a counter in a different table
I model a kind of a work item queue and when one element is selected from the queue, its state gets set to "executing" but I also want to keep track of how many items of a certain priority have been selected.
Following is the script without the table that is used for counting
IF OBJECT_ID(N'[Queue]', N'U') IS NOT NULL
DROP TABLE [Queue]
CREATE TABLE [Queue](
[ID] int IDENTITY(1,1) NOT NULL,
[Priority] int NOT NULL default 3,
[State] int NOT NULL default 0,
[Command] nvarchar(max),
[Queued] datetime2 NOT NULL default GetDate(),
[Assigned] datetime2 NULL
)
INSERT INTO [Queue] ([Command], [Priority], [Queued]) VALUES
('systeminfo', 1, DATEADD(MILLISECOND, -40, GETDATE())),
('systeminfo', 2, DATEADD(MILLISECOND, -30, GETDATE())),
('systeminfo', 1, DATEADD(MILLISECOND, -20, GETDATE())),
('systeminfo', 3, DATEADD(MILLISECOND, -20, GETDATE()))
GO
IF OBJECT_ID(N'Dequeue', N'P') IS NOT NULL
DROP PROCEDURE Dequeue
GO
CREATE PROCEDURE Dequeue
AS
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
UPDATE selected
SET [State] = 1, Assigned = GETDATE()
OUTPUT inserted.*
FROM (
SELECT TOP(1) *
FROM [Queue]
WHERE [State] = 0
ORDER BY [Priority] ASC, [Queued] ASC
) selected
The update statement in the stored procedure now should also do something like
UPDATE [Counter]
SET [Counter].[Count] = [Counter].[Count] + 1
WHERE [Counter][Priority] = selected.[Priority]
Do I have to temporarily store the selected row in a Temp Table or variable, or is there a more elegant way?

You can use option with variable of type table
DECLARE #Priority TABLE (Priority int)
UPDATE selected
SET [State] = 1, Assigned = GETDATE()
OUTPUT inserted.Priority INTO #Priority(Priority)
FROM (
SELECT TOP(1) *
FROM [Queue]
WHERE [State] = 0
ORDER BY [Priority] ASC, [Queued] ASC
) selected
UPDATE c
SET c.[Count] = ISNULL(c.[Count], 0) + 1
FROM [Counter] c JOIN #Priority p ON c.[Priority] = p.[Priority]

Have you tried adding a trigger to the Queue table thad runs your update on the Counter table?

Related

Database design with Loan and LoanLines

I have not been able to come up with a viable solution for days.
I am developing a system to maintain items and lending out these items.
Loan contains IEnumerable<LoanLine> which each points at an Item:
So far so good.
The tricky part comes to light when each item can't be lent out in the same period. And that period is defined by LoanLine.PickedUp ?? Loan.DateFrom > LoanLine.Returned ?? Loan.DateTo. This means that if LoanLine.PickedUp is null, then Loan.DateFrom should be used to compare, and if LoanLine.Returned is null, then Loan.DateTo should be used.
An item can be picked up and returned outside the loans boundaries. So these scenarioes can occur:
It should also be possible to "go back", ie. set LoanLine.Returned to null, in which case Loan.DateTo is used to compare again. The same goes with LoanLine.PickedUp.
It should also be possible to update both Loan.DateFrom and Loan.DateTo, with the berforementioned constraints still in effect. That means that if an update to Loan results in one of the lines, with either DateTime set to null, is overlapping, then the constraint shall throw an error.
This is the create-script:
create table loan
(
id int primary key identity(1, 1),
datefrom date not null,
dateto date not null,
employee_id int references employee(id) not null,
recipient_id int references employee(id) null,
note nvarchar(max) not null,
constraint c_loan_chkdates check (datefrom <= dateto)
);
create table loanlineitem
(
id int primary key identity(1, 1),
loan_id int references loan(id) on delete cascade not null,
item_id int references item(id) not null,
pickedup datetime null,
returned datetime null,
constraint uq_loanlineitem unique (loan_id, item_id),
constraint c_loanlineitem_chkdates check (returned is null or pickedup <= returned)
);
And this is the constraint:
create function checkLoanLineItem(#itemId int, #loanId int, #pickedup datetime, #returned datetime)
returns bit
as
begin
declare #result bit = 0;
declare #from date = #pickedup;
declare #to date = #returned;
--If either #from or #to is null, fill the ones with null from loan-table
if (isnull(#from, #to) is null)
begin
select #from = isnull(#from, datefrom),
#to = isnull(#to, dateadd(d, 1, dateto))
from loan
where id = #loanId;
end
if not exists (select top 1 lli.id from loanlineitem lli
inner join loan l on lli.loan_id = l.id
where l.id <> #loanId
and lli.item_id = #itemId
and ((isnull(lli.pickedup, l.datefrom) >= #from and isnull(lli.pickedup, l.datefrom) < #to)
--When comparing datetime with date, the date's time is 00:00:00
--so one day is added to account for this
or (isnull(lli.returned, dateadd(d, 1, l.dateto)) >= #from and isnull(lli.returned, dateadd(d, 1, l.dateto)) < #to))
)
begin
set #result = 1;
end
return #result;
end;
go;
alter table loanlineitem
add constraint c_loanlineitem_checkoverlap check (dbo.checkLoanLineItem(item_id, loan_id, pickedup, returned) = 1)
go;
I could make a similar constraint on Loan-table but then I would have similar code two places, which I would prefer to avoid, if possible.
So what I'm asking is; should I rethink my schema to accomplish this, or is it possible with some constraints which I'm not familiar with?
For this we will need two things:
A way to track the status of an item with respect to a loan
Only allow one active loan at a point in time
The first item can be addressed through the data model (see below) but the second will require any changes to the database MUST occur through stored procedures and those stored procedures will have to contain logic to keep the database in a consistent state. Otherwise you'll have a real mess on your hands (or rely on triggers, which is another headache).
We'll track the physical state of the item through an item status based on a timestamp, and, if desired, reservations through another mechanism based on a future date.
This query will return the current status and loan of all items, as well as the next reservation. From this you can also determine which items are past due.
SELECT
Item.ItemId
,ItemStatus.UpdateDtm
,ItemStatus.StatusCd
,ItemStatus.LoanNumber
,Loan.StartDt
,Loan.EndDt
,Reservation.StartDt
,Reservation.EndDt
FROM
Item Item
LEFT JOIN
LoanItemStatus ItemStatus
ON ItemStatus.ItemId = Item.ItemId
AND ItemStatus.UpdateDtm =
(
SELECT
MAX(UpdateDtm)
FROM
LoanItemStatus
WHERE
ItemId = Item.ItemId
)
LEFT JOIN
Loan Loan
ON Loan.LoanNumber = ItemStatus.LoanNumber
LEFT JOIN
ItemReservation Reservation
ON Reservation.ItemId = Item.ItemId
AND Reservation.StartDt =
(
SELECT
MIN(StartDt)
FROM
ItemReservation
WHERE
ItemId = Item.ItemId
AND StartDt >= GetDate()
)
It will probably make sense to harden this logic into a view.
To see if an item is reserved during a given timeframe:
SELECT
Item.ItemId
,CASE
WHEN COALESCE(PriorReservation.EndDt,GETDATE()) <= #ReservationStartDt AND #ReservationEndDt <= COALESCE(NextReservation.StartDt,'9999-12-31') THEN 'Y'
ELSE 'N'
END AS ReservationAvailableInd
FROM
Item Item
LEFT JOIN
ItemReservation PriorReservation
ON PriorReservation.ItemId = Item.ItemId
AND PriorReservation.StartDt =
(
SELECT
MAX(StartDt)
FROM
ItemReservation
WHERE
ItemId = Item.ItemId
AND StartDt <= #ReservationStartDt
)
LEFT JOIN
ItemReservation NextReservation
ON NextReservation.ItemId = Item.ItemId
AND NextReservation.StartDt =
(
SELECT
MIN(StartDt)
FROM
ItemReservation
WHERE
ItemId = Item.ItemId
AND StartDt > #ReservationStartDt
)
So you'll need to roll all of this into your stored procedures so:
When an item is loaned, it is available for the time period specified
When the loan date range is changed it does not conflict with the existing items or future reservations
When new reservations are made they do not conflict with existing procedures reservations
State transitions make sense (Not loaned/Returned -> Awaiting pickup -> Picked Up -> Returned/Lost)
You cannot delete loans with items that have been picked up or items that have been picked up
Alright, I found the solution, although it isn't the most elegant or DRY one.
First a view with occupations (thanks bbaird for the suggestion, which made it easier to figure out the logic):
create view vw_loanlineitem_occupations
as
select lli.id, loan_id, item_id, isnull(lli.pickedup, l.datefrom) as [from], isnull(lli.returned, dateadd(d, 1, l.dateto)) as [to] from loanlineitem lli inner join loan l on lli.loan_id = l.id
then a general check overlap udf:
create function udf_isOverlapping(#span1Start datetime, #span1End datetime, #span2Start datetime, #span2End datetime)
returns bit
as
begin
return iif((#span1Start <= #span2End and #span1End >= #span2Start), 1, 0);
end
then a udf and constraint on loan:
create function udf_isLoanValid(#loanId int, #dateFrom date, #dateTo date)
returns bit
as
begin
declare #result bit = 0;
--When type 'date' is compared to 'datetime' the time-part is 00:00:00, so add one day
set #dateTo = dateadd(d, 1, #dateTo)
if not exists (
select top 1 lli.id from loanlineitem lli
inner join loan l on lli.loan_id = l.id
--Only check items that are in this loan
where lli.item_id in (select item_id from loanlineitem where loan_id = #loanId)
--Check if this span is overlapping with other lines/loans
--When type 'date' is compared to 'datetime' the time-part is 00:00:00, so add one day
and (dbo.udf_isOverlapping(
#dateFrom,
#dateTo,
isnull(lli.pickedup, iif(l.id = #loanId, #dateFrom, l.datefrom)),
isnull(lli.returned, iif(l.id = #loanId, #dateTo, dateadd(d, 1, l.dateto)))
) = 1
)
)
begin
set #result = 1
end
return #result;
end;
go;
alter table loan
add constraint c_loan_datecheck check (dbo.udf_isLoanValid(id, dateFrom, dateTo) = 1);
and a separate constraint on loanlineitem, which unfortunately repeats some of the code from loan's constraint:
create function udf_isLineValid(#itemId int, #loanId int, #pickedup datetime, #returned datetime)
returns bit
as
begin
declare #result bit = 0;
declare #from date = #pickedup;
declare #to date = #returned;
--If either #from or #to is null, fill the ones with null from loan-table
if (#from is null or #to is null)
begin
select #from = isnull(#from, datefrom),
#to = isnull(#to, dateadd(d, 1, dateto))
from loan
where id = #loanId;
end
--If no lines with overlap exists, this line is valid, so set result to 1
if not exists (
select top 1 id from vw_loanlineitem_occupations
where item_id = #itemId
and loan_id <> #loanId
and dbo.udf_isOverlapping(#from, #to, [from], [to]) = 1
)
begin
set #result = 1;
end
return #result;
end;
go;
alter table loanlineitem
add constraint c_loanlineitem_checkoverlap check (dbo.udf_isLineValid(item_id, loan_id, pickedup, returned) = 1)
It works, which is the most important part. I am not sure about how performance is, but data integrity is more important.

Stored procedures multiple filters - all filters in one procedure or separate into own procedures per filter

I have a stored procedure where I pass parameter with type of filter, and second parameter with value of filter. It can be game type, user type etc.
I want to filter data based on different type. If it is game type it should filter by game_name column(whatever it is passed as parameter), if user type by user type name.
I am wondering from perspective of design, is it better to put multiple case statements in one stored procedure or create each stored procedure for different filter type, which in the end I would end with 5-6 different stored procedures with same core sql(select statement).
Example of procedure:
ALTER PROCEDURE [dbo].[Reports_UserStatsDaily] #network VARCHAR(9) = NULL,
#playerAddress VARCHAR(42) = NULL,
#year INT = NULL,
#month INT = NULL,
#from VARCHAR(15) = null,
#to VARCHAR(15) = null
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET nocount ON;
-- Insert statements for procedure here
SELECT [playeraddress],
[network],
[rounds],
[sessions],
[handle],
[hold],
Datefromparts([year], [month], [day]) AS [Date]
FROM [dbo].[userstatsdaily]
WHERE ( #network IS NULL
OR ( network = Upper(#network) ) )
AND ( #playerAddress IS NULL
OR ( playeraddress = Upper(#playerAddress) ) )
AND ( #year IS NULL
OR [year] = #year )
AND ( #month IS NULL
OR [month] = #month )
AND ( #from IS NULL
OR ( Datefromparts([year], [month], [day]) BETWEEN
Cast(#from AS DATETIME2) AND Cast(#to AS DATETIME2)
)
)
AND (len(Handle) > 9 or len(Hold) > 9)
ORDER BY [year] ASC,
[month] ASC,
[day] ASC
END
Problem here is, more filters I put, I put more optional parameters, and append in the end in WHERE clause. How to achieve separation of concerns in stored procedures?

store timestamp of an action in a table

I have a query here :
alter proc spbadge
#WeekNumber nvarchar(255)
as
begin
update pointsbadge
set
badge1 =(case when WeeklyHoursLogged >= '40' then 1 else 0 end),
badge2 = (case when WeeklyHoursLogged<'40' then 1 else 0 end),
badge3 = (case WHEN WeeklyHoursLogged >= 50 and WeeklyHoursLogged < 55 THEN '1' else 0 end),
badge4 = (case WHEN WeeklyHoursLogged >= 55 and WeeklyHoursLogged < 60 THEN '1' else 0 end),
badge5 = (case WHEN WeeklyHoursLogged >= 60 THEN '1' else 0 end);
end
Now,after running the above query with the conditions,I have this updated "pointsBadge" table -
Scenario :
Here you will see 5 badges (badge1 till badge5). When any employee is rewarded a badge it becomes 1, else 0. For example: Brinda wins all the badges, whereas lyka wins only badge1.
Problem : I want to have an another table "employee_badge" - where whenever any employee is rewarded a badge ,i.e whenever her value is 1,in this "employee_badge" table ,the employeeid,badgeid and the time gets recorded.
For instance like this,
I am storing the image and ID of the badges in a different table like this :
How do you guys think this can be achieved.can I modify the above stored procedure or you suggest a better efficient solution for this .I believe I need to use an update trigger here.But ,how will I use it on the above stored procedure.
Please check out with the following code, if it satisfies your requirement: you can adjust query according to your table structure and data
--Badge Point Information
CREATE TABLE pointsBadge(EmployeeName VARCHAR(50), EmployeeID INT, badge1 INT, badge2 INT, badge3 INT, badge4 INT, badge5 INT)
INSERT INTO pointsBadge VALUES
('Lyka', 1122, 1, 1, 0, 0, 0),
('Brinda', 2233, 0, 0, 0, 0, 0),
('Anil', 34, 1, 1, 0, 0, 1)
--New table to store badge data employee wise with current timestamp
CREATE TABLE employee_badge(id INT IDENTITY(1,1), employee_id INT, badge_id INT, earned_on DATETIME)
--Badge Information Table
CREATE TABLE #badges(BadgeId INT IDENTITY(1, 1), BadgeName VARCHAR(50))
INSERT INTO #badges VALUES
('Badge1'),
('Badge2'),
('Badge3'),
('Badge4'),
('Badge5'),
('Badge6')
--Trigger to insert data into employee_badge table with updated data
IF OBJECT_ID('[dbo].[TRGUpdate_badge_info]', 'TR') IS NOT NULL
DROP TRIGGER dbo.TRGUpdate_badge_info
GO
CREATE TRIGGER dbo.TRGUpdate_badge_info ON dbo.pointsBadge
FOR UPDATE
AS
INSERT INTO employee_badge
SELECT d.EmployeeID,
b.BadgeId,
GETDATE()
FROM pointsBadge p
INNER JOIN DELETED d ON p.EmployeeID = d.EmployeeID
INNER JOIN #badges b ON b.BadgeName = CASE WHEN (p.badge1 <> d.badge1) THEN 'Badge1'
WHEN (p.badge2 <> d.badge2) THEN 'Badge2'
WHEN (p.badge3 <> d.badge3) THEN 'Badge3'
WHEN (p.badge4 <> d.badge4) THEN 'Badge4'
WHEN (p.badge5 <> d.badge5) THEN 'Badge5'
END
GO
UPDATE pointsBadge SET badge5 = 2 WHERE employeeID = 1122
SELECT * FROM employee_badge

how to perform sorting and filtering in stored procedure with performance optimization?

I want to perform sorting and filtering in my stored procedure.
My create table for Holiday table:
CREATE TABLE [dbo].[Holiday](
[HolidaysId] [int] IDENTITY(1,1) NOT NULL,
[HolidayDate] [date] NULL,
[HolidayDiscription] [nvarchar](500) NULL,
[Name] [nvarchar](max) NULL,
CONSTRAINT [PK_Holiday] PRIMARY KEY CLUSTERED
(
[HolidaysId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
My filtering criteria would be as:
Starts With
Is Equal to
Not Equal to.
Note:Please ignore HolidayId in filter comparision.
My Table:Holiday
HolidaysId int,Name nvarchar(500),HolidayDate date.
Sample Input:
HolidayId Name Date
1 abc 1/1/2015
2 pqr 1/2/2015
3 xyz 1/3/2015
Output:
Case 1:Starts with(This is just for name column only.likewise i want to do for HolidayDate column too)
Input:ab(filtering parameter)
Query:where Name like '%ab%' order by Name(when sort column name is passed as parameter in stored procedure for column to sort(for eg:Name))
output:1,abc,1/1/2015
Case 2:Is Equal to(Same as above)
Input:prr(filtering parameter)
output:2,pqr,1/2/2015
Case 3:Not Equal to(Same as above)
Input:bbb(filtering parameter)
output:All Records
This is my stored procedure so far:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[sp_PagedItems]
(
#Page int,
#RecsPerPage int
)
AS
-- We don't want to return the # of rows inserted
-- into our temporary table, so turn NOCOUNT ON
SET NOCOUNT ON
--Create a temporary table
CREATE TABLE #TempItems
(
ID int,
Name varchar(50),
HolidayDate date
)
-- Insert the rows from tblItems into the temp. table
INSERT INTO #TempItems (ID, Name,HolidayDate)
SELECT HolidaysId,HolidayDiscription,HolidayDate FROM holiday
-- Find out the first and last record we want
DECLARE #FirstRec int, #LastRec int
SELECT #FirstRec = (#Page - 1) * #RecsPerPage
SELECT #LastRec = (#Page * #RecsPerPage + 1)
-- Now, return the set of paged records, plus, an indiciation of we
-- have more records or not!
SELECT *,
MoreRecords =
(
SELECT COUNT(*)
FROM #TempItems TI
WHERE TI.ID >= #LastRec
)
FROM #TempItems
WHERE ID > #FirstRec AND ID < #LastRec
-- Turn NOCOUNT back OFF
SET NOCOUNT OFF
Now there are 4 things i would send to my stored procedure are:
Page no
PageSize(number of records to retrive)
Sorting Column Name(Name Or HolidayDate)
My filter Column name(Name of Holidaydate) and Operator like StartWith or Equal to or not equal to.(ColumnName and Operator)
Can anybody help me to perform sorting and filtering and if any performance optimization related changes is there then please please do suggest me.
I've not tested this, but something like this you can use as starter and do modifications to make it stable:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[sp_PagedItems]
(
#ID int = NULL,
#Name varchar(50) = NULL,
#HolidayDate date = NULL,
#SortCol varchar(20) = '',
#Page int=1,
#RecsPerPage int=10 -- default size, you can change it or apply while executing the SP
)
AS
BEGIN
-- We don't want to return the # of rows inserted
-- into our temporary table, so turn NOCOUNT ON
SET NOCOUNT ON
--Create a temporary table
CREATE TABLE #TempItems
(
ID int,
Name varchar(50),
HolidayDate date
)
-- Insert the rows from tblItems into the temp. table
INSERT INTO #TempItems (ID, Name,HolidayDate)
SELECT HolidaysId, HolidayDiscription, HolidayDate
FROM holiday
-- Find out the first and last record we want
DECLARE #FirstRec int, #LastRec int
SELECT #FirstRec = (#Page - 1) * #RecsPerPage
SELECT #LastRec = (#Page * #RecsPerPage + 1)
-- Now, return the set of paged records, plus, an indiciation of we
-- have more records or not!
; WITH CTE_Results
AS (
SELECT ROW_NUMBER() OVER (ORDER BY
CASE WHEN #SortCol = 'ID_Asc' THEN ID
END ASC,
CASE WHEN #SortCol = 'ID_Desc' THEN ID
END DESC,
CASE WHEN #SortCol = 'Name_Asc' THEN Name
END ASC,
CASE WHEN #SortCol = 'Name_Desc' THEN Name
END DESC,
CASE WHEN #SortCol = 'HolidayDate_Asc' THEN HolidayDate
END ASC,
CASE WHEN #SortCol = 'HolidayDate_Desc' THEN HolidayDate
END DESC
) AS ROWNUM,
ID,
Name,
HolidayDate
FROM #TempItems
WHERE
(#ID IS NULL OR ID = #ID)
AND (#Name IS NULL OR Name LIKE '%' + #Name + '%')
AND (#HolidayDate IS NULL OR HolidayDate = #HolidayDate)
)
SELECT
ID,
Name,
HolidayDate
FROM CTE_Results
WHERE
ROWNUM > #FirstRec
AND ROWNUM < #LastRec
ORDER BY ROWNUM ASC
-- Turn NOCOUNT back OFF
SET NOCOUNT OFF
END
GO
You can check the blog posts I've written on:
Creating Stored Procedures with Dynamic Search (filter)
Creating Stored Procedures with Dynamic Search & Paging (Pagination)
Creating Stored Procedure with Dynamic Search, Paging and Sorting
You can also use the FETCH-OFFSET clause for Pagination if you are on SQL 2012 or more, link.
This is how i have done and i am getting expected output but still i want to take improvement suggestion from all of you if there is any.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[HolidayFetchList]
#pageno int,
#pagesize int,
#sortorder varchar(10),
#sortcolumn varchar(100),
#filter varchar(max),
#count int OUTPUT
AS
BEGIN
declare #Start int=(#pageno)*#pagesize;
declare #End int=#Start+#pagesize;
SET NOCOUNT ON;
DECLARE #tblHoliday AS TABLE
(HolidaysId int,HolidayDate date,HolidayDiscription nvarchar(500),HolidayName nvarchar(max),RN int)
declare #sql varchar(max)= '
select HolidaysId,HolidayDate,HolidayDiscription,HolidayDiscription as HolidayName,ROW_NUMBER() OVER
(ORDER BY '+#sortcolumn + ' '+#sortorder+' ) AS RN from Holiday
WHERE 1=1 '+#filter
print #sql
INSERT INTO #tblHoliday
exec (#sql)
select #count=COUNT(*) from #tblHoliday
print #count
select * from #tblHoliday where RN>#Start and RN<=#End order by RN
END
Please do give me any suggestion if you have any.
It's NOT recommended to use #temp tables because you can affect the RAM on your server. But, bad news :(, you should NOT use the exec command either... now you are susceptible to SQL Injection in your application. So, I think there are at least two options: 1) Using views (include flag values), table valued-functions and other components; 2) Filtering inside the WHERE statement as shown below:
SELECT * FROM Holiday
WHERE
CASE WHEN #paramStartDate Is Not Null THEN HolidayDate ELSE '' END
>= CASE WHEN #paramStartDate Is Not Null THEN #paramStartDate ELSE '' END
AND
CASE WHEN #paramEndDate Is Not Null THEN HolidayDate ELSE '' END
<= CASE WHEN #paramEndDate Is Not Null THEN #paramEndDate ELSE '' END
AND
CASE WHEN #paramName Is Not Null THEN [Name] ELSE '' END
LIKE CASE WHEN #paramName Is Not Null THEN '%' + #paramName + '%' ELSE '' END
You should keep in mind that this method can increment the time of process. If so, you have the possibility of creating several stored procedures, one for HolidayDate search, another one for Name search and another one that combines filters. Your application must be able to decide which one to use depending on the input parameters.
For pagination (ad-hoc reports) I would use OFFSET and FETCH. Use some advantage of T-SQL, then you won't need temporary tables and any of that mess.

Microsoft SQL Server: Generate a sequence number, per day

I'm tasked to create an increasing sequence number per day for a project. Multiple processes (theoretically on multiple machines) need to generate this. It ends up as
[date]_[number]
like
20101215_00000001
20101215_00000002
...
20101216_00000001
20101216_00000002
...
Since I'm using an SQL Server (2008) in this project anyway, I tried to do this with T-SQL/SQL magic. This is where I am right now:
I created a table containing the sequence number like this:
CREATE TABLE [dbo].[SequenceTable](
[SequenceId] [bigint] IDENTITY(1,1) NOT NULL,
[SequenceDate] [date] NOT NULL,
[SequenceNumber] [int] NULL
) ON [PRIMARY]
My naive solution so far is a trigger, after insert, that sets the SequenceNumber:
CREATE TRIGGER [dbo].[GenerateMessageId]
ON [dbo].[SequenceTable]
AFTER INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- The ID of the record we just inserted
DECLARE #InsertedId bigint;
SET #InsertedId = (SELECT SequenceId FROM Inserted)
-- The next SequenceNumber that we're adding to the new record
DECLARE #SequenceNumber int;
SET #SequenceNumber = (
SELECT SequenceNumber FROM
(
SELECT SequenceId, ROW_NUMBER() OVER(PARTITION BY SequenceDate ORDER BY SequenceDate ASC) AS SequenceNumber
FROM SequenceTable
) tmp
WHERE SequenceId = #InsertedId
)
-- Update the record and set the SequenceNumber
UPDATE
SequenceTable
SET
SequenceTable.SequenceNumber = ''+#SequenceNumber
FROM
SequenceTable
INNER JOIN
inserted ON SequenceTable.SequenceId = inserted.SequenceId
END
As I said, that's rather naive, and keeps a full day of rows just for a single number that I never need again anyway: I do an insert, get the generated sequence number and ignore the table afterwards. No need to store them on my side, I just need to generate them once. In addition I'm pretty sure this isn't going to scale well, gradually getting slower the more rows the table contains (i.e. I don't want to fall into that "worked on my dev machine with 10.000 rows only" trap).
I guess the current way was more me looking at SQL with some creativity, but the result seems to be - erm - less useful. More clever ideas?
Forget about that SequenceTable. You should just create two columns on your final table: a datetime and a identity. And if you really need them to be combined, just add a computed column.
I guess it would be something like that:
CREATE TABLE [dbo].[SomeTable] (
[SequenceId] [bigint] IDENTITY(1,1) NOT NULL,
[SequenceDate] [date] NOT NULL,
[SequenceNumber] AS (CAST(SequenceDate AS VARCHAR(10)) + '_' + RIGHT('0000000000' + CAST(SequenceID AS VARCHAR(10)), 10)) PERSISTED
) ON [PRIMARY]
That way will scale - you are not creating any kind of intermediary or temporary data.
Edit I still think that the answer above is the best solution. BUT there is another option: computed columns can reference functions...
So do this:
CREATE FUNCTION dbo.GetNextSequence (
#sequenceDate DATE,
#sequenceId BIGINT
) RETURNS VARCHAR(17)
AS
BEGIN
DECLARE #date VARCHAR(8)
SET #date = CONVERT(VARCHAR, #sequenceDate, 112)
DECLARE #number BIGINT
SELECT
#number = COALESCE(MAX(aux.SequenceId) - MIN(aux.SequenceId) + 2, 1)
FROM
SomeTable aux
WHERE
aux.SequenceDate = #sequenceDate
AND aux.SequenceId < #sequenceId
DECLARE #result VARCHAR(17)
SET #result = #date + '_' + RIGHT('00000000' + CAST(#number AS VARCHAR(8)), 8)
RETURN #result
END
GO
CREATE TABLE [dbo].[SomeTable] (
[SequenceId] [bigint] IDENTITY(1,1) NOT NULL,
[SequenceDate] [date] NOT NULL,
[SequenceNumber] AS (dbo.GetNextSequence(SequenceDate, SequenceId))
) ON [PRIMARY]
GO
INSERT INTO SomeTable(SequenceDate) values ('2010-12-14')
INSERT INTO SomeTable(SequenceDate) values ('2010-12-15')
INSERT INTO SomeTable(SequenceDate) values ('2010-12-15')
INSERT INTO SomeTable(SequenceDate) values ('2010-12-15')
GO
SELECT * FROM SomeTable
GO
SequenceId SequenceDate SequenceNumber
-------------------- ------------ -----------------
1 2010-12-14 20101214_00000001
2 2010-12-15 20101215_00000001
3 2010-12-15 20101215_00000002
4 2010-12-15 20101215_00000003
(4 row(s) affected)
It's ugly, but works, right? :-) No temporary table whatsoever, no views, no triggers, and it will have a decent performance (with at least an index over SequenceId and SequenceDate, of course). And you can remove records (since and identity is being used for the resulting computed field).
If you can create the actual table with a different name, and perform all of your other operations through a view, then it might fit the bill. It does also require that no transaction is ever deleted (so you'd need to add appropriate trigger/permission on the view/table to prevent that):
create table dbo.TFake (
T1ID int IDENTITY(1,1) not null,
T1Date datetime not null,
Val1 varchar(20) not null,
constraint PK_T1ID PRIMARY KEY (T1ID)
)
go
create view dbo.T
with schemabinding
as
select
T1Date,
CONVERT(char(8),T1Date,112) + '_' + RIGHT('00000000' + CONVERT(varchar(8),ROW_NUMBER() OVER (PARTITION BY CONVERT(char(8),T1Date,112) ORDER BY T1ID)),8) as T_ID,
Val1
from
dbo.TFake
go
insert into T(T1Date,Val1)
select '20101201','ABC' union all
select '20101201','DEF' union all
select '20101202','GHI'
go
select * from T
Result:
T1Date T_ID Val1
2010-12-01 00:00:00.000 20101201_00000001 ABC
2010-12-01 00:00:00.000 20101201_00000002 DEF
2010-12-02 00:00:00.000 20101202_00000001 GHI
You can, of course, also hide the date column from the view and make it default to CURRENT_TIMESTAMP.
You could do something like
CREATE TABLE SequenceTableStorage (
SequenceId bigint identity not null,
SequenceDate date NOT NULL,
OtherCol int NOT NULL,
)
CREATE VIEW SequenceTable AS
SELECT x.SequenceDate, (CAST(SequenceDate AS VARCHAR(10)) + '_' + RIGHT('0000000000' + CAST(SequenceID - (SELECT min(SequenceId) + 1 FROM SequenceTableStorage y WHERE y.SequenceDate = x.SequenceDate) AS VARCHAR(10)), 10)) AS SequenceNumber, OtherCol
FROM SequenceTableStorage x
If you create an index on the SequenceDate and SequenceId, I don't think the performance will be too bad.
Edit:
The code above might miss some sequence numbers, for example if a transaction inserts a row and then rolls back (the identity value will then be lost in space).
This can be fixed with this view, whose performance might or might not be good enough.
CREATE VIEW SequenceTable AS
SELECT SequenceDate, (CAST(SequenceDate AS VARCHAR(10)) + '_' + RIGHT('0000000000' + row_number() OVER(PARTITION BY SequenceDate ORDER BY SequenceId)
FROM SequenceTableStorage
My guess is that it will be good enough until you start getting millions of sequence numbers per day.
I tried this way to create session codes for user logging and its working;
CREATE FUNCTION [dbo].[GetSessionSeqCode]()
RETURNS VARCHAR(15)
AS
BEGIN
DECLARE #Count INT;
DECLARE #SeqNo VARCHAR(15)
SELECT #Count = ISNULL(COUNT(SessionCode),0)
FROM UserSessionLog
WHERE SUBSTRING(SessionCode,0,9) = CONVERT(VARCHAR(8), GETDATE(), 112)
SET #SeqNo = CONVERT(VARCHAR(8), GETDATE(), 112) +'-' + FORMAT(#Count+1,'D3');
RETURN #SeqNo
END
generated codes are:
'20170822-001'
,'20170822-002'
,'20170822-003'
If you don't mind the numbers not starting at one you could use DATEDIFF(dd, 0, GETDATE()) which is the number of days since 1-1-1900. That will increment every day.