Stored Procedure to build a result set from a tree structure? - sql

I need to write a stored procedure that will take in a string to search a tree like structure and perform a recursive result set. First, here is the table:
CREATE TABLE [dbo].[WorkAreas] (
[Id] uniqueidentifier DEFAULT newid() NOT NULL,
[Name] nvarchar(max) COLLATE Latin1_General_CI_AS NULL,
[ParentWorkAreaId] uniqueidentifier NULL,
CONSTRAINT [PK__WorkArea__3214EC073FD07829] PRIMARY KEY CLUSTERED ([Id]),
CONSTRAINT [WorkArea_ParentWorkArea] FOREIGN KEY ([ParentWorkAreaId])
REFERENCES [dbo].[WorkAreas] ([Id])
ON UPDATE NO ACTION
ON DELETE NO ACTION,
)
I'd Like the stored procedure to output the results like this:
Work Area 1 - Child Of Work Area 1 - Child Child Of Work Area 1
So if this were real data it may look like this:
Top Floor - Room 7 - Left Wall
Top Floor - Room 9 - Ceiling
the stored procedure would take in a parameter: #SearchTerm varchar(255)
The search term would look at the results and perform a "contains" query.
So if we passed in "Room 9" the result should come up with the Room 9 example, or if just the word "Room" was passed in, we would see both results.
I am not sure how to construct the SP to recursively build the results.

Cade Roux's comment lead me to what I needed. Here is what I ended up with:
;WITH ProjectWorkAreas (EntityId ,ParentIDs,DisplayText)
AS
(
SELECT Id,CAST(Id AS VARCHAR(1000)) ,CAST(Name AS VARCHAR(1000))
FROM WorkAreas
WHERE ParentWorkAreaId IS NULL And ProjectId = #projectId
UNION ALL
SELECT Id, CAST( ParentIDs+','+ CAST(Id AS VARCHAR(100))
AS VARCHAR(1000)),CAST( DisplayText + ' - ' + Name AS VARCHAR(1000))
FROM WorkAreas AS ChildAreas
INNER JOIN ProjectWorkAreas
ON ChildAreas.ParentWorkAreaId = ProjectWorkAreas.EntityId
)
SELECT * FROM ProjectWorkAreas Where DisplayText like '%' + #searchTerm + '%'
I added the ProjectId to the mix

You would do something like this:
select w1.name, w2.name, w3.name
from workareas w1
inner join workareas w2
on w1.parentworkareaid = w2.id
inner join workareas w3
on w2.parentworkareaid = w3.id
where contains(w3.name, #yourSearchString)

Related

Check if a temp table exists when I only know part of the name?

I have a function for checking if certain tables exist in my database, using part of the table name as a key to match (my table naming conventions include unique table name prefixes). It uses a select statement as below, where #TablePrefix is a parameter to the function and contains the first few characters of the table name:
DECLARE #R bit;
SELECT #R = COUNT(X.X)
FROM (
SELECT TOP(1) 1 X FROM sys.tables WHERE [name] LIKE #TablePrefix + '%'
) AS X;
RETURN #R;
My question is, how can I extend this function to work for #temp tables too?
I have tried checking the first char of the name for # then using the same logic to select from tempdb.sys.tables, but this seems to have a fatal flaw - it returns a positive result when any temp table exists with a matching name, even if not created by the current session - and even if created by SPs in a different database. There does not seem to be any straightforward way to narrow the selection down to only those temp tables that exist in the context of the current session.
I cannot use the other method that seems universally to be suggested for checking temp tables - IF OBJECT('tempdb..#temp1') IS NOT NULL - because that requires me to know the full name of the table, not just a prefix.
create table #abc(id bit);
create table #abc_(id bit);
create table #def__(id bit);
create table #xyz___________(id bit);
go
select distinct (left(t.name, n.r)) as tblname
from tempdb.sys.tables as t with(nolock)
cross join (select top(116) row_number() over(order by(select null)) as r from sys.all_objects with(nolock)) as n
where t.name like '#%'
and object_id('tempdb..'+left(t.name, n.r)) is not null;
drop table #abc;
drop table #abc_;
drop table #def__;
drop table #xyz___________;
Try something like this:
DECLARE #TablePrefix VARCHAR(50) = '#temp';
DECLARE #R BIT, #pre VARCHAR(50) = #TablePrefix + '%';
SELECT #R = CASE LEFT ( #pre, 1 )
WHEN '#' THEN (
SELECT CASE WHEN EXISTS ( SELECT * FROM tempdb.sys.tables WHERE [name] LIKE #pre ) THEN 1
ELSE 0
END )
ELSE (
SELECT CASE WHEN EXISTS ( SELECT * FROM sys.tables WHERE [name] LIKE #pre ) THEN 1
ELSE 0
END )
END;
SELECT #R AS TableExists;

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.

Create a view based on column metadata

Let's assume two tables:
TableA holds various data measurements from a variety of stations.
TableB holds metadata, about the columns used in TableA.
TableA has:
stationID int not null, pk
entryDate datetime not null, pk
waterTemp float null,
waterLevel float null ...etc
TableB has:
id int not null, pk, autoincrement
colname varchar(50),
unit varchar(50) ....etc
So for example, one line of data from tableA reads:
1 | 2013-01-01 00:00 | 2.4 | 3.5
two lines from tableB read:
1| waterTemp | celcius
2| waterLevel | meters
This is a simplified example. In truth, tableA might hold close to 20 different data columns, and table b has close to 10 metadata columns.
I am trying to design a view which will output the results like this:
StationID | entryDate | water temperature | water level |
1 | 2013-01-01 00:00 | 2.4 celcius | 3.5 meters |
So two questions:
Other than specifying subselects from TableB (..."where
colname='XXX'") for each column, which seems horribly insufficient
(not to mention...manual :P ), is there a way to get the result I
mentioned earlier with automatic match on colname?
I have a hunch
that this might be bad design on the database. Is it so? If yes,
what would be a more optimal design? (Bear in mind the complexity of
the data structure I mentioned earlier)
dynamic SQL with PIVOT is the answer. though it is dirty in terms of debugging or say for some new developer to understand the code but it will give you the result you expected.
check the below query.
in this we need to prepare two things dynamically. one is list columns in the result set and second is list of values will appear in PIVOT query. notice in the result i do not have NULL values for Column3, Column5 and Column6.
SET NOCOUNT ON
IF OBJECT_ID('TableA','u') IS NOT NULL
DROP TABLE TableA
GO
CREATE TABLE TableA
(
stationID int not null IDENTITY (1,1)
,entryDate datetime not null
,waterTemp float null
,waterLevel float NULL
,Column3 INT NULL
,Column4 BIGINT NULL
,Column5 FLOAT NULL
,Column6 FLOAT NULL
)
GO
IF OBJECT_ID('TableB','u') IS NOT NULL
DROP TABLE TableB
GO
CREATE TABLE TableB
(
id int not null IDENTITY(1,1)
,colname varchar(50) NOT NULL
,unit varchar(50) NOT NULL
)
INSERT INTO TableA( entryDate ,waterTemp ,waterLevel,Column4)
SELECT '2013-01-01',2.4,3.5,101
INSERT INTO TableB( colname, unit )
SELECT 'WaterTemp','celcius'
UNION ALL SELECT 'waterLevel','meters'
UNION ALL SELECT 'Column3','unit3'
UNION ALL SELECT 'Column4','unit4'
UNION ALL SELECT 'Column5','unit5'
UNION ALL SELECT 'Column6','unit6'
DECLARE #pvtInColumnList NVARCHAR(4000)=''
,#SelectColumnist NVARCHAR(4000)=''
, #SQL nvarchar(MAX)=''
----getting the list of Columnnames will be used in PIVOT query list
SELECT #pvtInColumnList = CASE WHEN #pvtInColumnList=N'' THEN N'' ELSE #pvtInColumnList + N',' END
+ N'['+ colname + N']'
FROM TableB
--PRINT #pvtInColumnList
----lt and rt are table aliases used in subsequent join.
SELECT #SelectColumnist= CASE WHEN #SelectColumnist = N'' THEN N'' ELSE #SelectColumnist + N',' END
+ N'CAST(lt.'+sc.name + N' AS Nvarchar(MAX)) + SPACE(2) + rt.' + sc.name + N' AS ' + sc.name
FROM sys.objects so
JOIN sys.columns sc
ON so.object_id=sc.object_id AND so.name='TableA' AND so.type='u'
JOIN TableB tbl
ON tbl.colname=sc.name
JOIN sys.types st
ON st.system_type_id=sc.system_type_id
ORDER BY sc.name
IF #SelectColumnist <> '' SET #SelectColumnist = N','+#SelectColumnist
--PRINT #SelectColumnist
----preparing the final SQL to be executed
SELECT #SQL = N'
SELECT
--this is a fixed column list
lt.stationID
,lt.entryDate
'
--dynamic column list
+ #SelectColumnist +N'
FROM TableA lt,
(
SELECT * FROM
(
SELECT colname,unit
FROM TableB
)p
PIVOT
( MAX(p.unit) FOR p.colname IN ( '+ #pvtInColumnList +N' ) )q
)rt
'
PRINT #SQL
EXECUTE sp_executesql #SQL
here is the result
ANSWER to your Second Question.
the design above is not even giving performance nor flexibility. if user wants to add new Metadata (Column and Unit) that can not be done w/o changing table definition of TableA.
if we are OK with writing Dynamic SQL to give user Flexibility we can redesign the TableA as below. there is nothing to change in TableB. I would convert it in to Key-value pair table. notice that StationID is not any more IDENTITY. instead for given StationID there will be N number of row where N is the number of column supplying the Values for that StationID. with this design, tomorrow if user adds new Column and Unit in TableB it will add just new Row in TableA. no table definition change required.
SET NOCOUNT ON
IF OBJECT_ID('TableA_New','u') IS NOT NULL
DROP TABLE TableA_New
GO
CREATE TABLE TableA_New
(
rowID INT NOT NULL IDENTITY (1,1)
,stationID int not null
,entryDate datetime not null
,ColumnID INT
,Columnvalue NVARCHAR(MAX)
)
GO
IF OBJECT_ID('TableB_New','u') IS NOT NULL
DROP TABLE TableB_New
GO
CREATE TABLE TableB_New
(
id int not null IDENTITY(1,1)
,colname varchar(50) NOT NULL
,unit varchar(50) NOT NULL
)
GO
INSERT INTO TableB_New(colname,unit)
SELECT 'WaterTemp','celcius'
UNION ALL SELECT 'waterLevel','meters'
UNION ALL SELECT 'Column3','unit3'
UNION ALL SELECT 'Column4','unit4'
UNION ALL SELECT 'Column5','unit5'
UNION ALL SELECT 'Column6','unit6'
INSERT INTO TableA_New (stationID,entrydate,ColumnID,Columnvalue)
SELECT 1,'2013-01-01',1,2.4
UNION ALL SELECT 1,'2013-01-01',2,3.5
UNION ALL SELECT 1,'2013-01-01',4,101
UNION ALL SELECT 2,'2012-01-01',1,3.6
UNION ALL SELECT 2,'2012-01-01',2,9.9
UNION ALL SELECT 2,'2012-01-01',4,104
SELECT * FROM TableA_New
SELECT * FROM TableB_New
SELECT *
FROM
(
SELECT lt.stationID,lt.entryDate,rt.Colname,lt.Columnvalue + SPACE(3) + rt.Unit AS ColValue
FROM TableA_New lt
JOIN TableB_new rt
ON lt.ColumnID=rt.ID
)t1
PIVOT
(MAX(ColValue) FOR Colname IN ([WaterTemp],[waterLevel],[Column1],[Column2],[Column4],[Column5],[Column6]))pvt
see the result below.
I would design this database like the following:
A table MEASUREMENT_DATAPOINT that contains the measured data points. It would have the columns ID, measurement_id, value, unit, name.
One entry would be 1, 1, 2.4, 'celcius', 'water temperature'.
A table MEASUREMENTS that contains the data of the measurement itself. Columns: ID, station_ID, entry_date.
You might want to look into the MS-SQL function called PIVOT/UNPIVOT
http://technet.microsoft.com/en-us/library/ms177410(v=sql.105).aspx
you can take column names and have them in rows or vice versa using this command.
Once you have the column name in the column itself you can join that column from tableA to tableB. Then unpivot to get your data back the way you want it. (caveat I may be swapping the use of pivot and unpivot :))
Word to the wise though, if you are working with large tables, pivot is not the fastest of operations.
I think you would have to flip it to a row per metric. Looking at your design above:
1 | 2013-01-01 00:00 | 2.4 | 3.5
How do I know what row in table b that applies to?
I would try something like this:
Table B:
Metric_Key | Metric
1 | WaterLevel in Meters
2 | Temp in Celcius
...
Table A:
StationID | entrydate | Metric_Key | Value
1 2013-01-01 00:00 1 2.4

Find manager in a comma-separated list

I have a Projects table with ID and Responsible manager. The Responsible manager columns has values as John,Jim for Project 1 and Jim,Julie for Project 2.
But if I pass Jim to my stored procedure I should get 2 projects (1,2). This returns no rows because the column is John,Jim but SQL Server is looking for ='Jim':
select distinct ID,Manager from Projects where Manager=#Manager
WHERE ',' + Manager + ',' LIKE '%,Jim,%'
Or I suppose to match your actual code:
WHERE ',' + Manager + ',' LIKE '%,' + #Manager + ',%'
Note that your design is extremely flawed. There is no reason you should be storing names in this table at all, never mind a comma-separated list of any data points. These facts are important on their own, so treat them that way!
CREATE TABLE dbo.Managers
(
ManagerID INT PRIMARY KEY,
Name NVARCHAR(64) NOT NULL UNIQUE, ...
);
CREATE TABLE dbo.Projects
(
ProjectID INT PRIMARY KEY,
Name NVARCHAR(64) NOT NULL UNIQUE, ...
);
CREATE TABLE dbo.ProjectManagers
(
ProjectID INT NOT NULL FOREIGN KEY REFERENCES dbo.Projects(ProjectID),
ManagerID INT NOT NULL FOREIGN KEY REFERENCES dbo.Managers(ManagerID)
);
Now to set up the sample data you mentioned:
INSERT dbo.Managers(ManagerID, Name)
VALUES(1,N'John'),(2,N'Jim'),(3,N'Julie');
INSERT dbo.Projects(ProjectID, Name)
VALUES(1,N'Project 1'),(2,N'Project 2');
INSERT dbo.ProjectManagers(ProjectID,ManagerID)
VALUES(1,1),(1,2),(2,2),(2,3);
Now to find all the projects Jim is managing:
DECLARE #Manager NVARCHAR(32) = N'Jim';
SELECT p.ProjectID, p.Name
FROM dbo.Projects AS p
INNER JOIN dbo.ProjectManagers AS pm
ON p.ProjectID = pm.ProjectID
INNER JOIN dbo.Managers AS m
ON pm.ManagerID = m.ManagerID
WHERE m.name = #Manager;
Or you can even manually short circuit a bit:
DECLARE #Manager NVARCHAR(32) = N'Jim';
DECLARE #ManagerID INT;
SELECT #ManagerID = ManagerID
FROM dbo.Managers
WHERE Name = #Manager;
SELECT p.ProjectID, p.Name
FROM dbo.Projects AS p
INNER JOIN dbo.ProjectManagers AS pm
ON p.ProjectID = pm.ProjectID
WHERE pm.ManagerID = #ManagerID;
Or even more:
DECLARE #Manager NVARCHAR(32) = N'Jim';
DECLARE #ManagerID INT;
SELECT #ManagerID = ManagerID
FROM dbo.Managers
WHERE Name = #Manager;
SELECT ProjectID, Name
FROM dbo.Projects AS p
WHERE EXISTS
(
SELECT 1
FROM dbo.ProjectManagers AS pm
WHERE pm.ProjectID = p.ProjectID
AND pm.ManagerID = #ManagerID
);
As an aside, I really, really, really hope the DISTINCT in your original query is unnecessary. Do you really have more than one project with the same name and ID?
In a WHERE clasue a = operator looks for an exact match. You can use a LIKE with wildcards for a partial match.
where Manager LIKE '%Jim%'
You may try the following:
SELECT DISTINCT
ID,
Manager
FROM
Projects
WHERE
(
(Manager LIKE #Manager + ',*') OR
(Manager LIKE '*,' + #Manager) OR
(Manager = #Manager)
)
That should cover both names and surnames, while still searching for literal values. Performance can be a problem however, depending on table
please tried with below query
select distinct ID,Manager from Projects where replace('#$#' + Manager + '#$#', ',', '#$#') like '%Jim%'

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.