SQL Server 2012 Sequence Object - sql

Here is my sample table, the primary key is a composite key of Akey+Bkey
Akey Bkey ItemSequence
---- ---- ------------
1 1 1
1 5 2
1 7 3
2 7 1
3 2 1
3 3 2
Akey is generated from a SQL 2012 Sequence object ASequence. In most cases I insert one row at a time and when necessary I call NEXT VALUE FOR ASequence. However I need to do an insert from a statement like:
SELECT DENSE_RANK() OVER ( ORDER BY Something) as AKey,
Bkey, Sequence
FROM TABLEB
The OVER clause of the NEXT VALUE does not work this way as I need to be able to insert records as a SET but only increment the Sequence once per DENSE_RANK set.
So we have the ALTER SEQUENCE command and with this I am able to set the sequence to what I want. The caveat to this is that it must be a constant and will not accept a variable. My workaround to this was:
DECLARE #startingID INT
DECLARE #sql VARCHAR(MAX)
DECLARE #newSeed INT
SET #startingID = NEXT VALUE FOR ASequence
INSERT TABLEA
SELECT DENSE_RANK() OVER ( ORDER BY Something) + #startingID as AKey,
Bkey, Sequence
FROM TABLEB
SELECT #newSeed = MAX(Akey) FROM TABLEA
SET #sql = ‘ALTER SEQUENCE ASEQUENCE RESTART WITH ‘ + cast(#newSeed+1 as varchar(10))
EXEC(#sql)
Seems terrible to have DML statements in Dynamic SQL like this. Is there a better way to do this?

This should do it:
INSERT TABLEA
SELECT NEXT VALUE FOR ASequence OVER(ORDER BY Something) as AKey,
Bkey, Seq
FROM TABLEB

Or, how about this:
CREATE TABLEA
(
GroupID INT,
AKey INT,
BKey INT,
ItemSequence INT,
CONSTRAINT PK_TABLEA PRIMARY KEY CLUSTERED
(
GroupID,
AKey,
BKey
)
)
DECLARE #GroupID INT
SET #GroupID = NEXT VALUE FOR ASequence
INSERT TABLEA
SELECT #GroupID, DENSE_RANK() OVER ( ORDER BY Something) as AKey,
Bkey, Sequence
FROM TABLEB
and if you need the value of AKey as it is in your example, you can do GroupID+AKey here.

Related

Know identity before insert

I want to copy rows from the table within the table itself. But before inserting I need to modify a varchar column appending the value of identity column to it.
My table structure is:
secID docID secName secType secBor
1 5 sec-1 G 9
2 5 sec-2 H 12
3 5 sec-3 G 12
4 7 sec-4 G 12
5 7 sec-5 H 9
If I want to copy data of say docID 5, currently this runs through a loop one row at a time.
I can write my query as
insert into tableA (docID, secName, secType, secBor)
select 8, secName, secType, secBor from tableA where docID = 5
But how can I set value of secName before hand so that it becomes sec-<value of secID column>?
Don't try to guess the value of identity column. In your case you could simply create a computed column secName AS CONCAT('sec-', secID). There is no further need to update that column.
DB Fiddle
It is also possible to create an AFTER INSERT trigger to update the column.
Since SQL Server does not have GENERATED ALWAYS AS ('Sec - ' + id) the only simple option I see is to use a trigger.
Adding to my comment something like:
insert into tableA (docID, secName, secType, secBor)
select
ROW_NUMBER() OVER (ORDER BY DocID),
'Sec -'+ ROW_NUMBER() OVER (ORDER BY DocID),
secType, secBor
from tableA
where docID = 5
In SQL Server 2012 and later, you can achieve this by using the new sequence object.
CREATE SEQUENCE TableAIdentitySeqeunce
START WITH 1
INCREMENT BY 1 ;
GO
create table TableA
(
secId int default (NEXT VALUE FOR TableAIdentitySeqeunce) not null primary key,
varcharCol nvarchar(50)
)
declare #nextId int;
select #nextId = NEXT VALUE FOR TableAIdentitySeqeunce
insert TableA (secId, varcharCol)
values (#nextId, N'Data #' + cast(#nextId as nvarchar(50)))

Left join with nearest value without duplicates

I want to achieve in MS SQL something like below, using 2 tables and through join instead of iteration.
From table A, I want each row to identify from table B which in the list is their nearest value, and when value has been selected, that value cannot re-used. Please help if you've done something like this before. Thank you in advance! #SOreadyToAsk
Below is a set-based solution using CTEs and windowing functions.
The ranked_matches CTE assigns a closest match rank for each row in TableA along with a closest match rank for each row in TableB, using the index value as a tie breaker.
The best_matches CTE returns rows from ranked_matches that have the best rank (rank value 1) for both rankings.
Finally, the outer query uses a LEFT JOIN from TableA to the to the best_matches CTE to include the TableA rows that were not assigned a best match due to the closes match being already assigned.
Note that this does not return a match for the index 3 TableA row indicated in your sample results. The closes match for this row is TableB index 3, a difference of 83. However, that TableB row is a closer match to the TableA index 2 row, a difference of 14 so it was already assigned. Please clarify you question if this isn't what you want. I think this technique can be tweaked accordingly.
CREATE TABLE dbo.TableA(
[index] int NOT NULL
CONSTRAINT PK_TableA PRIMARY KEY
, value int
);
CREATE TABLE dbo.TableB(
[index] int NOT NULL
CONSTRAINT PK_TableB PRIMARY KEY
, value int
);
INSERT INTO dbo.TableA
( [index], value )
VALUES ( 1, 123 ),
( 2, 245 ),
( 3, 342 ),
( 4, 456 ),
( 5, 608 );
INSERT INTO dbo.TableB
( [index], value )
VALUES ( 1, 152 ),
( 2, 159 ),
( 3, 259 );
WITH
ranked_matches AS (
SELECT
a.[index] AS a_index
, a.value AS a_value
, b.[index] b_index
, b.value AS b_value
, RANK() OVER(PARTITION BY a.[index] ORDER BY ABS(a.Value - b.value), b.[index]) AS a_match_rank
, RANK() OVER(PARTITION BY b.[index] ORDER BY ABS(a.Value - b.value), a.[index]) AS b_match_rank
FROM dbo.TableA AS a
CROSS JOIN dbo.TableB AS b
)
, best_matches AS (
SELECT
a_index
, a_value
, b_index
, b_value
FROM ranked_matches
WHERE
a_match_rank = 1
AND b_match_rank= 1
)
SELECT
TableA.[index] AS a_index
, TableA.value AS a_value
, best_matches.b_index
, best_matches.b_value
FROM dbo.TableA
LEFT JOIN best_matches ON
best_matches.a_index = TableA.[index]
ORDER BY
TableA.[index];
EDIT:
Although this method uses CTEs, recursion is not used and is therefore not limited to 32K recursions. There may be room for improvement here from a performance perspective, though.
I don't think it is possible without a cursor.
Even if it is possible to do it without a cursor, it would definitely require self-joins, maybe more than once. As a result performance is likely to be poor, likely worse than straight-forward cursor. And it is likely that it would be hard to understand the logic and later maintain this code. Sometimes cursors are useful.
The main difficulty is this part of the question:
when value has been selected, that value cannot re-used.
There was a similar question just few days ago.
The logic is straight-forward. Cursor loops through all rows of table A and with each iteration adds one row to the temporary destination table. To determine the value to add I use EXCEPT operator that takes all values from the table B and removes from them all values that have been used before. My solution assumes that there are no duplicates in value in table B. EXCEPT operator removes duplicates. If values in table B are not unique, then temporary table would hold unique indexB instead of valueB, but main logic remains the same.
Here is SQL Fiddle.
Sample data
DECLARE #TA TABLE (idx int, value int);
INSERT INTO #TA (idx, value) VALUES
(1, 123),
(2, 245),
(3, 342),
(4, 456),
(5, 608);
DECLARE #TB TABLE (idx int, value int);
INSERT INTO #TB (idx, value) VALUES
(1, 152),
(2, 159),
(3, 259);
Main query inserts result into temporary table #TDst. It is possible to write that INSERT without using explicit variable #CurrValueB, but it looks a bit cleaner with variable.
DECLARE #TDst TABLE (idx int, valueA int, valueB int);
DECLARE #CurrIdx int;
DECLARE #CurrValueA int;
DECLARE #CurrValueB int;
DECLARE #iFS int;
DECLARE #VarCursor CURSOR;
SET #VarCursor = CURSOR FAST_FORWARD
FOR
SELECT idx, value
FROM #TA
ORDER BY idx;
OPEN #VarCursor;
FETCH NEXT FROM #VarCursor INTO #CurrIdx, #CurrValueA;
SET #iFS = ##FETCH_STATUS;
WHILE #iFS = 0
BEGIN
SET #CurrValueB =
(
SELECT TOP(1) Diff.valueB
FROM
(
SELECT B.value AS valueB
FROM #TB AS B
EXCEPT -- remove values that have been selected before
SELECT Dst.valueB
FROM #TDst AS Dst
) AS Diff
ORDER BY ABS(Diff.valueB - #CurrValueA)
);
INSERT INTO #TDst (idx, valueA, valueB)
VALUES (#CurrIdx, #CurrValueA, #CurrValueB);
FETCH NEXT FROM #VarCursor INTO #CurrIdx, #CurrValueA;
SET #iFS = ##FETCH_STATUS;
END;
CLOSE #VarCursor;
DEALLOCATE #VarCursor;
SELECT * FROM #TDst ORDER BY idx;
Result
idx valueA valueB
1 123 152
2 245 259
3 342 159
4 456 NULL
5 608 NULL
It would help to have the following indexes:
TableA - (idx) include (value), because we SELECT idx, value ORDER BY idx;
TableB - (value) unique, Temp destination table - (valueB) unique filtered NOT NULL, to help EXCEPT. So, it may be better to have a temporary #table for result (or permanent table) instead of table variable, because table variables can't have indexes.
Another possible method would be to delete a row from table B (from original or from a copy) as its value is inserted into result. In this method we can avoid performing EXCEPT again and again and it could be faster overall, especially if it is OK to leave table B empty in the end. Still, I don't see how to avoid cursor and processing individual rows in sequence.
SQL Fiddle
DECLARE #TDst TABLE (idx int, valueA int, valueB int);
DECLARE #CurrIdx int;
DECLARE #CurrValueA int;
DECLARE #iFS int;
DECLARE #VarCursor CURSOR;
SET #VarCursor = CURSOR FAST_FORWARD
FOR
SELECT idx, value
FROM #TA
ORDER BY idx;
OPEN #VarCursor;
FETCH NEXT FROM #VarCursor INTO #CurrIdx, #CurrValueA;
SET #iFS = ##FETCH_STATUS;
WHILE #iFS = 0
BEGIN
WITH
CTE
AS
(
SELECT TOP(1) B.idx, B.value
FROM #TB AS B
ORDER BY ABS(B.value - #CurrValueA)
)
DELETE FROM CTE
OUTPUT #CurrIdx, #CurrValueA, deleted.value INTO #TDst;
FETCH NEXT FROM #VarCursor INTO #CurrIdx, #CurrValueA;
SET #iFS = ##FETCH_STATUS;
END;
CLOSE #VarCursor;
DEALLOCATE #VarCursor;
SELECT
A.idx
,A.value AS valueA
,Dst.valueB
FROM
#TA AS A
LEFT JOIN #TDst AS Dst ON Dst.idx = A.idx
ORDER BY idx;
I highly believe THIS IS NOT A GOOD PRACTICE because I am bypassing the policy SQL made for itself that functions with side-effects (INSERT,UPDATE,DELETE) is a NO, but due to the fact that I want solve this without resulting to iteration options, I came up with this and gave me better view of things now.
create table tablea
(
num INT,
val MONEY
)
create table tableb
(
num INT,
val MONEY
)
I created a hard-table temp which I shall drop from time-to-time.
if((select 1 from sys.tables where name = 'temp_tableb') is not null) begin drop table temp_tableb end
select * into temp_tableb from tableb
I created a function that executes xp_cmdshell (this is where the side-effect bypassing happens)
CREATE FUNCTION [dbo].[GetNearestMatch]
(
#ParamValue MONEY
)
RETURNS MONEY
AS
BEGIN
DECLARE #ReturnNum MONEY
, #ID INT
SELECT TOP 1
#ID = num
, #ReturnNum = val
FROM temp_tableb ORDER BY ABS(val - #ParamValue)
DECLARE #SQL varchar(500)
SELECT #SQL = 'osql -S' + ##servername + ' -E -q "delete from test..temp_tableb where num = ' + CONVERT(NVARCHAR(150),#ID) + ' "'
EXEC master..xp_cmdshell #SQL
RETURN #ReturnNum
END
and my usage in my query simply looks like this.
-- initialize temp
if((select 1 from sys.tables where name = 'temp_tableb') is not null) begin drop table temp_tableb end
select * into temp_tableb from tableb
-- query nearest match
select
*
, dbo.GetNearestMatch(a.val) AS [NearestValue]
from tablea a
and gave me this..

Conversion failed when converting the varchar value to data type int while incrementing

Was wondering if you can correct my syntax.
update TestTable
set test_person = CAST(test_person as Varchar = 'TEST' (50)) + 1
Im trying to update all columns in a table and increment it by 1 (hence the cast since the column is a string).
T-SQL
DECLARE #counter INT = 0
UPDATE TestTable
SET test_person = 'Test' + CAST(#counter AS VARCHAR(16)), #counter = #counter + 1
Something I've done in the past to do incrementing values in a string column is something like this:
http://sqlfiddle.com/#!6/7f0f5/2
create table testTable (id int primary key identity(1,1), test_person varchar(100))
insert into testTable (test_person)
select 'bob loblaw'
union all select 'Buster Bluth'
union all select 'Jason Bateman'
union all select 'gob'
union all select 'Lucille #2'
update testTable
set test_person = 'test ' + convert(varchar(30), id)
select * from testTable
results:
ID TEST_PERSON
1 test 1
2 test 2
3 test 3
4 test 4
5 test 5
note this works when your table has an identity primary key. You did not give your entire table schema, but if it does not have this identity primary key you could do something similar by creating a temp/variable table to hold your data that does have an int primary key to do the same thing.
Another option would be to use a loop/cursor to do your population... but that's kind of a pain to write.
Another option would be to use ROW_NUMBER which is sql server (not sure about others). Without the full table schema i can only give an example of how to accomplish:
seen in fiddle: http://sqlfiddle.com/#!6/7f0f5/5
update testTable
set test_person = 'test ' + convert(varchar(30), rowNum)
from testTable
inner join (
-- create an incrementing number for each row
select row_number() over(order by test_person) as rowNum, test_person
from testTable
) rows on testTable.test_person = rows.test_person
select * from testTable

optimizing SQL query with multiple keys

I have a table in a database with a primary key, and a 'second' key as well. This second key can have the same value occur more than once in the table, but often i only want to return the most recent row for that second key. I have an existing query that works below, but I feel like it's very ugly and there should be a simpler way to do this instead of creating a table variable, going through a loop, and inserting 1 row into the table variable on each pass through the loop. Am i making this too hard?
declare #RowCnt int
declare #MaxRows int
declare #secondID as uniqueidentifier
DECLARE #retList TABLE(
firstGUID uniqueidentifier,
secondGUID uniqueidentifier,
name nvarchar(50),
DateCreated datetime
)
select #RowCnt = 1
declare #Import table (rownum int IDENTITY (1, 1) Primary key NOT NULL , secondGUID uniqueidentifier)
insert into #Import (secondGUID) SELECT DISTINCT dbo.TestTable.secondGUID FROM dbo.TestTable
select #MaxRows=count(*) from #Import
while #RowCnt <= #MaxRows
begin
select #secondID=secondGUID from #Import where rownum = #RowCnt
INSERT INTO #retList
SELECT TOP (1) firstGUID,secondGUID,name,datecreated
FROM dbo.TestTable
WHERE dbo.TestTable.secondGUID = #secondID
ORDER BY DateCreated Desc
Set #RowCnt = #RowCnt + 1
END
select * from #retList
EDIT:
for example, imagine the table has these values
firstGUID secondGUID Name DateCreated
EAD50999-E9B1-43F0-9FA6-615405FA5A9A 6163B6ED-6AF4-494E-ACE6-184F4804847B Test1 2014-04-11 15:12:36.303
A9645486-1021-4E98-92AC-1205CC3FB9D3 6163B6ED-6AF4-494E-ACE6-184F4804847B Test2 2014-04-10 15:21:46.087
DEE375BB-BFAF-44BE-AC64-06D7702E2ACB 3BD0A2F0-4E44-43B9-BD24-003B518609C7 Test3
2014-04-11 15:22:37.097
I only want the Test1 and Test3 rows to be returned.
You could use SQLServer's analytical functions:
select firstGUID, secondGUID, name, datecreated
from (select t.*,
rank() over (partition by secondGUID order by datecreated desc) r
from TestTable t) ilv
where r=1
I'm not 100% sure I understand what you're asking, but it sounds like you want to select only the rows containing the max DateCreated. The normal way of doing that is to join with a subselect that uses a group by clause, eg.:
select tt.*
from TestTable tt
join (
select firstguid, max(DateCreated) as maxdate
from TestTable
group by firstguid
) gtmp on tt.firstguid = gtmp.firstguid and tt.dateCreated = gtmp.maxdate

Generating unique values

There is a column 'Keys' in a table. With data Like this
Key Name
1 aaa
2 sads
null asd
null asd
Now I need to replace null values with 3 and 4. How can I do that.
Depending on which version of SQL Server you're working with, you'd use TOP or SET ROWCOUNT to limit an UPDATE to a single row. Something like this will work:
select top 1 * from sysobjects
WHILE ##ROWCOUNT > 0
BEGIN
UPDATE TOP 1 Keys SET Key = (SELECT MAX(Key) from Keys)+1 WHERE Key is null
END
But isn't ideal. I can't think of another way of addressing the duplicate rows - unless this is a one-off task, in which case temporarily adding an IDENTITY column would work.
If you can put the data (temporarily) into a separate table you can use ROW_COUNT():
declare #Keys table ([Key] int not null, [Name] varchar(50) null);
insert into #Keys ([Key], [Name])
select [Key], [Name]
from [Keys]
where [Key] is not null
union all
select [Key] = row_number() over (order by [Name]) +
(select max([Key]) from [Keys]),
[Name]
from [Keys]
where [Key] is null;
truncate table [Keys];
insert into [Keys] select * from #Keys;