Microsoft SQL Server: Generate a sequence number, per day - sql

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.

Related

SQL Server: auto-generated custom format sequence number

I am working with Microsoft SQL Server 2014. In our requirement, custom formatted sequence number is include.
The sequence number format is CAT-YYYY-MM-NNNNNN. Sample data:
CAT-2016-10-000001
CAT-2016-10-000002
.
.
.
CAT-2016-10-999999
I don't want to use GUID or any other and I want to work with a procedure or function.
So, I am trying with this:
CREATE TABLE [category]
(
[id] int NOT NULL UNIQUE IDENTITY,
[category_no] nvarchar(20) NOT NULL,
[category_name] nvarchar(50) NOT NULL,
PRIMARY KEY ([id])
);
CREATE FUNCTION generate_category_no()
RETURNS CHAR(20)
AS
BEGIN
DECLARE #category_no CHAR(20)
SET #category_no = (SELECT MAX(category_no) FROM category)
IF #category_no IS NULL
SET #category_no = 'CAT-' + YEAR(getDate()) + '-' + MONTH(getDate()) + '-000001'
DECLARE #no int
SET #no = RIGHT(#category_no,6) + 1
RETURN 'CAT-' + YEAR(getDate()) + '-' + MONTH(getDate()) + '-' + right('00000' + CONVERT(VARCHAR(10),#no),6)
END
GO
ALTER TABLE category DROP COLUMN category_no;
ALTER TABLE category ADD category_no AS dbo.generate_category_no();
INSERT INTO category (category_name)
VALUES ('BMW'), ('JAGUAR');
When I run the above SQL in step-by-step, it is OK. It shown no error. But when I run the following command:
SELECT * FROM category;
it shows the following error:
Msg 217, Level 16, State 1, Line 1
Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32).
I don't know how to solve this one. And even I don't know my function has worked or not. I referenced from internet for this function.
ADDED
I need to reset sequence no for every month. Eg. for next month, no should be as follow:
CAT-2016-11-000001
Please, enlighten me. Thanks in advance!
Modify your function as below
ALTER TABLE category DROP COLUMN category_no;
alter FUNCTION dbo.generate_category_no( #id int)
RETURNS CHAR(20)
AS
BEGIN
RETURN 'CAT-' + cast(YEAR(getDate()) as varchar(10)) + '-' + cast(MONTH(getDate()) as varchar(10))+ '-' + right('00000' + CONVERT(VARCHAR(10),#id),6)
END
ALTER TABLE category ADD category_no AS dbo.generate_category_no(id);
INSERT INTO category
(category_name)
VALUES
('BMW13'),
('JAGUAR');
SELECT * FROM category will give the below result.
1 BMW CAT-2016-10-000001
2 JAGUAR CAT-2016-10-000002
3 BMW1 CAT-2016-10-000003
4 BMW13 CAT-2016-10-000004
Try this:
To initialize your new field:
ALTER TABLE category DROP COLUMN category_no;
ALTER TABLE category ADD category_no CHAR(20)
UPDATE category set category_no = dbo.generate_category_no()
For other insert:
CREATE TRIGGER [dbo].[category_i]
ON [dbo].[category]
AFTER INSERT
AS BEGIN
UPDATE category
SET category_no = dbo.generate_category_no()
FROM inserted
WHERE category.pk = inserted.pk
END
But you can try to use SEQUENCE feature, available on Sql Server by 2012 version
About SEQUENCE you can see here
Biggest flaw in your function is it will not work for Batch Insert's
Since you have ID auto generated, here is a easier way to do this
category_no AS Concat('CAT-', Year(Getdate()), '-', Month(Getdate()), '-', RIGHT(Concat('00000', id), 6))
Demo
CREATE TABLE #seq
(
id INT IDENTITY(1, 1),
name VARCHAR(10),
category_no AS Concat('CAT-', Year(Getdate()), '-', Month(Getdate()), '-', RIGHT(Concat('00000', id), 6))
)
INSERT INTO #seq
(name)
VALUES ('val')
Result :
id name category_no
-- ---- -----------
1 val CAT-2016-10-000001
Finally, I solved the problem. My Function look like as follow:
CREATE FUNCTION generate_category_no()
RETURNS CHAR(20)
AS
BEGIN
DECLARE #category_no CHAR(20)
SET #category_no = (SELECT MAX(category_no) FROM category WHERE category_no LIKE CONCAT('CAT-', YEAR(getDate()), '-', MONTH(getDate()), '-%'))
IF #category_no is null SET #category_no = CONCAT('CAT-', YEAR(getDate()), '-', MONTH(getDate()), '-000000')
DECLARE #no INT
SET #no = RIGHT(#category_no,6) + 1
RETURN CONCAT('CAT-', YEAR(getDate()), '-', MONTH(getDate()), '-', RIGHT('00000' + CONVERT(VARCHAR(10),#no),6))
END
GO
And I insert data as follow:
INSERT INTO category (category_no, category_name) VALUES (dbo.generate_category_no(),'BMW');
INSERT INTO category (category_no, category_name) VALUES (dbo.generate_category_no(),'JAGUAR');
One things is that We can call function from INSERT query.
So, when I run the following sql:
SELECT * FROM category;
It give the result as shown in below.
+---+--------------------+--------------+
|id |category_no |category_name |
+---+--------------------+--------------+
| 1 |CAT-2016-10-000001 | BMW |
| 2 |CAT-2016-10-000002 | JAGUAR |
+---+--------------------+--------------+
Thanks everybody for helping me. Thanks!!!

Formatted sequence

I need to make a SQL Server sequence formatted like this VT-000/2015,
where the zero's is the bill number which increments every record, starting from one in same year which is 2015
e.g:
VT-001/2014,
VT-002/2014,
VT-003/2014,
VT-004/2014,
VT-001/2015,
VT-002/2015
How can I make this?
Materializing the row number in the table is problematic. I think a view is the right tool for this job.
Here is one way to do this by creating a view and using ROW_NUMBER():
CREATE TABLE PRODUCT (PRODUCT_ID int,YEAR INT);
CREATE VIEW dbo.VIEW_PRODUCT AS
SELECT
PRODUCT_ID,
[YEAR],
ROW_NUMBER() OVER(PARTITION BY [YEAR] ORDER by [YEAR]) AS RowNum,
'VT-' + RIGHT(replicate('0', 3)
+ cast(ROW_NUMBER() OVER(PARTITION BY [YEAR] ORDER by [YEAR]) AS VARCHAR(10)), 3)
+ '/' + cast(YEAR AS VARCHAR(4)) as SERIAL
FROM PRODUCT;
INSERT INTO PRODUCT VALUES
(1000,2014),
(2000,2014),
(3000,2015),
(4000,2015),
(5000,2015);
SQL Fiddle Demo
If you're on SQL Server 2012, you can use sequences and a computed column - like this:
Step 1: define your sequence
CREATE SEQUENCE dbo.YearlyNumbers
START WITH 1
INCREMENT BY 1 ;
GO
You can reset that sequence back to 1 every January 1 of a new year, to get new numbers starting at 1, for the new year
Step 2: define your table, include a computed column that concatenates together the various bits and pieces:
CREATE TABLE dbo.Product
(
ProductID INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
ProductDate DATE,
-- default constraint gets a new value from the sequence on every insert
SequenceValue INT
CONSTRAINT DF_Product_SeqValue DEFAULT(NEXT VALUE FOR YearlyNumbers),
-- computed column puts together all bits to create the formatted ID you're looking for
FormattedID AS 'VT-' + RIGHT('0000' + CAST(SequenceValue AS VARCHAR(4)), 4) +
'/' + CAST(YEAR(ProductDate) AS VARCHAR(4)) PERSISTED
)
Now, when you insert values into your table:
INSERT INTO dbo.Product (ProductDate)
VALUES ('20150115'), ('20150331'), ('20150222'), ('20150414'), ('20150526')
the INSERT will fetch values from the sequence, and the computed column shows the formatted ID as requested:
SELECT * FROM dbo.Product
Consider your table is like this:
CREATE TABLE PRODUCT ([SERIAL] NVARCHAR(30), [YEAR] INT, [COUNTER] INT)
Here is the procedure you can use:
CREATE PROCEDURE PRODUCT_INSERT
#YEAR INT
AS
BEGIN
DECLARE #COUNTER INT
SELECT #COUNTER = ISNULL(MAX([COUNTER]), 0) + 1 FROM PRODUCT
WHERE [YEAR] = #YEAR
INSERT INTO PRODUCT
VALUES ('VT-' + REPLACE(STR(#COUNTER, 3), SPACE(1), '0') + '/' + CONVERT(nvarchar(4),#YEAR), #YEAR, #COUNTER)
END

What is the best way to join between two table which have coma seperated columns

Table1
ID Name Tags
----------------------------------
1 Customer1 Tag1,Tag5,Tag4
2 Customer2 Tag2,Tag6,Tag4,Tag11
3 Customer5 Tag6,Tag5,Tag10
and Table2
ID Name Tags
----------------------------------
1 Product1 Tag1,Tag10,Tag6
2 Product2 Tag2,Tag1,Tag5
3 Product5 Tag1,Tag2,Tag3
what is the best way to join Table1 and Table2 with Tags column?
It should look at the tags column which coma seperated on table 2 for each coma seperated tag on the tags column in the table 1
Note: Tables are not full-text indexed.
The best way is not to have comma separated values in a column. Just use normalized data and you won't have trouble with querying like this - each column is supposed to only have one value.
Without this, there's no way to use any indices, really. Even a full-text index behaves quite different from what you might thing, and they are inherently clunky to use - they're designed for searching for text, not meaningful data. In the end, you will not get much better than something like
where (Col like 'txt,%' or Col like '%,txt' or Col like '%,txt,%')
Using a xml column might be another alternative, though it's still quite a bit silly. It would allow you to treat the values as a collection at least, though.
I don't think there will ever be an easy and efficient solution to this. As Luaan pointed out, it is a very bad idea to store data like this : you lose most of the power of SQL when you squeeze what should be individual units of data into a single cell.
But you can manage this at the slight cost of creating two user-defined functions. First, use this brilliant recursive technique to split the strings into individual rows based on your delimiter :
CREATE FUNCTION dbo.TestSplit (#sep char(1), #s varchar(512))
RETURNS table
AS
RETURN (
WITH Pieces(pn, start, stop) AS (
SELECT 1, 1, CHARINDEX(#sep, #s)
UNION ALL
SELECT pn + 1, stop + 1, CHARINDEX(#sep, #s, stop + 1)
FROM Pieces
WHERE stop > 0
)
SELECT pn AS SplitIndex,
SUBSTRING(#s, start, CASE WHEN stop > 0 THEN stop-start ELSE 512 END) AS SplitPart
FROM Pieces
)
Then, make a function that takes two strings and counts the matches :
CREATE FUNCTION dbo.MatchTags (#a varchar(512), #b varchar(512))
RETURNS INT
AS
BEGIN
RETURN
(SELECT COUNT(*)
FROM dbo.TestSplit(',', #a) a
INNER JOIN dbo.TestSplit(',', #b) b
ON a.SplitPart = b.SplitPart)
END
And that's it, here is a test roll with table variables :
DECLARE #A TABLE (Name VARCHAR(20), Tags VARCHAR(100))
DECLARE #B TABLE (Name VARCHAR(20), Tags VARCHAR(100))
INSERT INTO #A ( Name, Tags )
VALUES
( 'Customer1','Tag1,Tag5,Tag4'),
( 'Customer2','Tag2,Tag6,Tag4,Tag11'),
( 'Customer5','Tag6,Tag5,Tag10')
INSERT INTO #B ( Name, Tags )
VALUES
( 'Product1','Tag1,Tag10,Tag6'),
( 'Product2','Tag2,Tag1,Tag5'),
( 'Product5','Tag1,Tag2,Tag3')
SELECT * FROM #A a
INNER JOIN #B b ON dbo.MatchTags(a.Tags, b.Tags) > 0
I developed a solution as follows:
CREATE TABLE [dbo].[Table1](
Id int not null,
Name nvarchar(250) not null,
Tag nvarchar(250) null,
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Table2](
Id int not null,
Name nvarchar(250) not null,
Tag nvarchar(250) null,
) ON [PRIMARY]
GO
get sample data for Table1, it will insert 28000 records
INSERT INTO Table1
SELECT CustomerID,CompanyName, (FirstName + ',' + LastName)
FROM AdventureWorks.SalesLT.Customer
GO 3
sample data for Table2.. i need same tags for Table2
declare #tag1 nvarchar(50) = 'Donna,Carreras'
declare #tag2 nvarchar(50) = 'Johnny,Caprio'
get sample data for Table2, it will insert 9735 records
INSERT INTO Table2
SELECT ProductID,Name, (case when(right(ProductID,1)>=5) then #tag1 else #tag2 end)
FROM AdventureWorks.SalesLT.Product
GO 3
My Solution
create TABLE #dt (
Id int IDENTITY(1,1) PRIMARY KEY,
Tag nvarchar(250) NOT NULL
);
I've create temp table and i will fill with Distinct Tag-s in Table1
insert into #dt(Tag)
SELECT distinct Tag
FROM Table1
Now i need to vertical table for tags
create TABLE #Tags ( Tag nvarchar(250) NOT NULL );
Now i'am fill #Tags table with While, you can use Cursor but while is faster
declare #Rows int = 1
declare #Tag nvarchar(1024)
declare #Id int = 0
WHILE #Rows>0
BEGIN
Select Top 1 #Tag=Tag,#Id=Id from #dt where Id>#Id
set #Rows =##RowCount
if #Rows>0
begin
insert into #Tags(Tag) SELECT Data FROM dbo.StringToTable(#Tag, ',')
end
END
last step : join Table2 with #Tags
select distinct t.*
from Table2 t
inner join #Tags on (',' + t.Tag + ',') like ('%,' + #Tags.Tag + ',%')
Table rowcount= 28000 Table2 rowcount=9735 select is less than 2 second
I use this kind of solution with paths of trees. First put a comma at the very begin and at the very end of the string. Than you can call
Where col1 like '%,' || col2 || ',%'
Some database index the column also for the like(postgres do it partially), therefore is also efficient. I don't know sqlserver.

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.

t-sql string unique ID (Northwind database)

I've been trying to get this right for some time now with no use.
I have a table in mssql database and I want to insert new row using stored procedure
CREATE TABLE "Customers" (
"CustomerID" NCHAR(5) NOT NULL,
"CompanyName" NVARCHAR(40) NOT NULL,
"ContactName" NVARCHAR(30) NULL,
"ContactTitle" NVARCHAR(30) NULL,
"Address" NVARCHAR(60) NULL,
"City" NVARCHAR(15) NULL,
"Region" NVARCHAR(15) NULL,
"PostalCode" NVARCHAR(10) NULL,
"Country" NVARCHAR(15) NULL,
"Phone" NVARCHAR(24) NULL,
"Fax" NVARCHAR(24) NULL,
PRIMARY KEY ("CustomerID")
);
The problem is CustomerID field which contains unique string for each record (ALFKI, BERGS, BERGS, etc.)
I want to make a stored procedure which will insert a row with new data and create an unique CustomerID. Build in functions are out of a question as I need the string to be 5 chars long.
I have a procedure which generates 5 chars ID as follows
begin
declare #chars char(26) = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
declare #i int = 0
declare #id varchar(max) = ''
while #i < 5
begin
set #id = #id + substring(#chars, cast(ceiling(rand() * 26) as int), 1)
set #i = #i + 1
end
Select (cast(#id as nvarchar(400)))
end
And the one that I tried to make work with no use. It is supposed to select an unique id (set #id = 'ANATR' is there on purpose to make it go into the loop
begin
declare #randID varchar(5) = ''
declare #selectID varchar(20) = ''
declare #chars char(26) = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
declare #i int = 0
declare #id varchar(10) = ''
while #i < 5
begin
set #id = #id + substring(#chars, cast(ceiling(rand() * 26) as int), 1)
set #i = #i + 1
end
select #id
set #id = 'ANATR'
SET #selectID = (SELECT CustomerID FROM CUSTOMERS WHERE CustomerID = #id)
while #selectID <> 'NULL'
begin
set #id = ''
while #i < 5
begin
set #id = #id + substring(#chars, cast(ceiling(rand() * 26) as int), 1)
set #i = #i + 1
end
SET #selectID = (SELECT CustomerID FROM CUSTOMERS WHERE CustomerID = #id)
SELECT #id
end
end
Here is the insert procedure I have at the moment
CREATE PROCEDURE [dbo].[InsertCustomers]
(
#CustomerID nchar(5),
#CompanyName nvarchar(40),
#ContactName nvarchar(30) = NULL,
#ContactTitle nvarchar(30) = NULL,
#Address nvarchar(60) = NULL,
#City nvarchar(15) = NULL,
#Region nvarchar(15) = NULL,
#PostalCode nvarchar(10) = NULL,
#Country nvarchar(15) = NULL,
#Phone nvarchar(24) = NULL,
#Fax nvarchar(24) = NULL
)
AS
SET NOCOUNT OFF;
INSERT INTO [dbo].[Customers] ([CustomerID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax]) VALUES (#CustomerID, #CompanyName, #ContactName, #ContactTitle, #Address, #City, #Region, #PostalCode, #Country, #Phone, #Fax);
The main problem here is that the incremental cost of detecting collisions from the generated string, and try again, increases as you generate more and more strings (since you have to read all of those strings to make sure you didn't generate a duplicate). At the same time, the odds of hitting a duplicate goes up, meaning the bigger the table gets, the slower this process will get.
Why do you need to generate the unique string at runtime? Build them all in advance. This article and this post are about random numbers, but the basic concept is the same. You build up a set of unique strings and pull one off the stack when you need one. Your chance of collisions stays constant at 0% throughout the lifetime of the application (provided you build up a stack of enough unique values). Pay for the cost of collisions up front, in your own setup, instead of incrementally over time (and at the cost of a user waiting for those attempts to finally yield a unique number).
This will generate 100,000 unique 5-character strings, at the low, one-time cost of about 1 second (on my machine):
;WITH
a(a) AS
(
SELECT TOP (26) number + 65 FROM master..spt_values
WHERE type = N'P' ORDER BY number
),
b(a) AS
(
SELECT TOP (10) a FROM a ORDER BY NEWID()
)
SELECT DISTINCT CHAR(b.a) + CHAR(c.a) + CHAR(d.a) + CHAR(e.a) + CHAR(f.a)
FROM b, b AS c, b AS d, b AS e, b AS f;
That's not enough? You can generate about 1.12 million unique values by changing TOP (10) to TOP (20). This took 18 seconds. Still not enough? TOP (24) will give you just under 8 million in about 2 minutes. It will get exponentially more expensive as you generate more strings, because that DISTINCT has to do the same duplicate checking you want to do every single time you add a customer.
So, create a table:
CREATE TABLE dbo.StringStack
(
ID INT IDENTITY(1,1) PRIMARY KEY,
String CHAR(5) NOT NULL UNIQUE
);
Insert that set:
;WITH
a(a) AS
(
SELECT TOP (26) number + 65 FROM master..spt_values
WHERE type = N'P' ORDER BY number
),
b(a) AS
(
SELECT TOP (10) a FROM a ORDER BY NEWID()
)
INSERT dbo.StringStack(String)
SELECT DISTINCT CHAR(b.a) + CHAR(c.a) + CHAR(d.a) + CHAR(e.a) + CHAR(f.a)
FROM b, b AS c, b AS d, b AS e, b AS f;
And then just create a procedure that pops one off the stack when you need it:
CREATE PROCEDURE dbo.AddCustomer
#CustomerName VARCHAR(64) /* , other params */
AS
BEGIN
SET NOCOUNT ON;
DELETE TOP (1) dbo.StringStack
OUTPUT deleted.String, #CustomerName /* , other params */
INTO dbo.Customers(CustomerID, CustomerName /*, ...other columns... */);
END
GO
No silly looping, no needing to check if the CustomerID you generated just exists, etc. The only additional thing you'll want to build is some type of check that notifies you when you're getting low.
As an aside, these are terrible identifiers for a CustomerID. What is wrong with a sequential surrogate key, like an IDENTITY column? How is a 5-digit random string with all this effort involved, any better than a unique number the system can generate for you much more easily?
Muhammed Ali 's answer works, but will prove rather ressource intensive (especially when there aren't many combinations of 5 letters left to use) : your function uses the random generator, and it will take it a while to find a combination that isn't used, especially since it has a very limited memory of its previous results.
This means it will try, and might give you something of the sort (exaggerating a bit) : BAGER the first time, then ANSWE the second time, then again BAGER the third time. You see you will lose a good amount of time with the generator giving you the same answer over and over again (especially over 12M possible combinations).
If you are looking for a fixed length ID (since you use NCHAR(5), I guess that's a good assumption), I would rather look into building a table that contains all the possible combinations, and pick one value of this table every time you need one. You would delete it once it got used, or mark it as used (which I would prefer, for reuseability reasons).
This leads to my final comment (which I cannot put as comment 'cause I don't have enough reputation) : why not use the IDENTITY function provided by MS-SQL ? This provides a much better handling of the Primary key generation...
I believe you can do something like this to make sure you all get a unique id
begin
declare #chars char(26) = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
declare #i int = 0
declare #id varchar(max) = ''
while (1=1)
begin
set #id = #id + substring(#chars, cast(ceiling(rand() * 26) as int), 1)
set #i = #i + 1
IF (NOT EXISTS(SELECT * FROM Customers WHERE CustomerID = #id) AND LEN(#id) = 5)
BREAK
ELSE
CONTINUE
end
Select (cast(#id as nvarchar(400)))
end
Set the while condition to be always true and break out of while loop only when both of your requirements are TRUE i.e Length of new ID is 5 and it does not exist in the customers table already.