Integer pattern searching using effective index - sql-server

How do I effectively use the index on an INT/BIGINT column for pattern-matching queries.
Sample data
SET NOCOUNT ON;
IF OBJECT_ID('tempdb..#TempIDTable') IS NOT NULL
DROP TABLE #TempIDTable;
CREATE TABLE #TempIDTable
(
ID BIGINT PRIMARY KEY CLUSTERED
);
-- Pattern ending with 123
INSERT INTO #TempIDTable(ID) VALUES(1123);
INSERT INTO #TempIDTable(ID) VALUES(2123);
INSERT INTO #TempIDTable(ID) VALUES(3123);
-- Pattern ending with 223
INSERT INTO #TempIDTable(ID) VALUES(7223);
INSERT INTO #TempIDTable(ID) VALUES(8223);
INSERT INTO #TempIDTable(ID) VALUES(9223);
-- Pattern ending with 323
INSERT INTO #TempIDTable(ID) VALUES(52323);
INSERT INTO #TempIDTable(ID) VALUES(53323);
INSERT INTO #TempIDTable(ID) VALUES(5323);
-- Selecting all rows with pattern ending in 123
DECLARE #Pattern INT = 123;
SELECT
T.ID, Pattern = (T.ID % 1000)
FROM
#TempIDTable T
WHERE
(T.ID % 1000) = #Pattern;
SET #Pattern = 223;
SELECT
T.ID, Pattern = (T.ID % 1000)
FROM
#TempIDTable T
WHERE
(T.ID % 1000) = #Pattern;
IF OBJECT_ID('tempdb..#TempIDTable') IS NOT NULL
DROP TABLE #TempIDTable;
GO
Basically, given a pattern (an INT input), I have to find the rows matching ID column. How do I create an INDEX to be used effectively by the query? Is there any other way of rewriting the query to effectively use the index?
Currently, the Execution Plan shows a scan:
If it helps, this is on SQL Server 2017 on Windows

Related

Issue with delete statement in merge statement

I am working with merge into statement
I have a table that looks like this (first I insert like this) using below query:
5
2
5
3
5
5
5
6
table type :
CREATE TYPE [dbo].[userid] AS TABLE(
[userid] [bigint] NULL
)
GO
Now I want the below output :
5
2
5
3
5
6
I write the below query like this:
--use test
declare #sid varchar(100) = '5'
declare #uid as userid
insert into #uid(userid) values(2)
insert into #uid(userid) values(3)
--insert into #uid(userid) values(5) // I remove this line
insert into #uid(userid) values(6)
MERGE INTO dbo.test_master AS dest
USING #uid AS sou ON
dest.sid = #sid
AND
sou.userid = dest.testid
WHEN MATCHED THEN UPDATE SET
dest.testid = sou.userid
WHEN NOT MATCHED THEN
INSERT( sid, testid )
VALUES( #sid, sou.userid )
--WHEN NOT MATCHED BY SOURCE
-- THEN
-- DELETE
;
I am trying to achieve this output
5
2
5
3
5
6
I am using delete keyword, see my SQL query, but it is deleting the all records from the table. I try, but can't work it out.
You need to pre-filter the destination table, otherwise all rows, even ones that have a different sid will be deleted. You can pre-filter with a CTE or a view.
I note that the WHEN MATCHED clause makes no sense, as the only column being updated is the join column, which obviously matches anyway.
declare #sid varchar(100) = '5';
declare #uid as userid;
insert into #uid (userid) values
(2),
(3),
-- (5), -- I remove this line
(6);
WITH dest AS (
SELECT *
FROM dbo.test_master dest
WHERE dest.sid = #sid
)
MERGE INTO dest
USING #uid AS sou ON
sou.userid = dest.testid
-- WHEN MATCHED THEN UPDATE SET
-- dest.testid = sou.userid -- doesn't make sense
WHEN NOT MATCHED THEN
INSERT( sid, testid )
VALUES( #sid, sou.userid )
WHEN NOT MATCHED BY SOURCE THEN
DELETE
;
db<>fiddle

Capture IDENTITY column value during insert and use as value for another column in same transaction

While performing an insert to a table which has an IDENTITY column, is it possible to use the IDENTITY value as the value for another column, in the same transaction?
For example:
DECLARE #TestTable TABLE
(
PrimaryId INT NOT NULL IDENTITY(1, 1),
SecondaryId INT NOT NULL
);
INSERT INTO #TestTable (SecondaryId)
SELECT
SCOPE_IDENTITY() + 1; -- set SecondaryId = PrimaryId + 1
SELECT * FROM #TestTable;
Expected:
| PrimaryId | SecondaryId |
+-----------+-------------+
| 1 | 2 |
I thought I might be able to achieve this with the SCOPE_IDENTITYor ##IDENTITY system functions, but unfortunately this does not work, as it is NULL at the time the transaction is executed.
Cannot insert the value NULL into column 'SecondaryId', table '#TestTable'; column does not allow nulls. INSERT fails.
I know I could use a computed column for this example, but I'm curious if what I'm trying to do is even possible.
Could you change your approach and use a SEQUENCE instead of an IDENTITY column?
CREATE SEQUENCE TestSequence
START WITH 1
INCREMENT BY 1 ;
GO
CREATE TABLE TestTable (PrimaryId INT NOT NULL DEFAULT NEXT VALUE FOR TestSequence, SecondaryId INT NOT NULL);
GO
INSERT INTO TestTable (
SecondaryId
)
SELECT NEXT VALUE FOR TestSequence + 1; -- set SecondaryId = PrimaryId + 1;
GO 3
SELECT * FROM TestTable;
GO
DROP TABLE TestTable;
DROP SEQUENCE TestSequence;
I would go with a trigger, this should also work for multi row inserts, You will need to remove the not null for SecondaryID, not sure if that's acceptable.
create trigger trg_TestTable
on dbo.TestTable
after insert
AS
BEGIN
update TestTable
set SecondaryId = i.PrimaryId
from inserted i
join TestTable a
on i.PrimaryId = a.PrimaryId;
END
GO
One thing you could do is use the OUTPUT INSERTED option of the INSERT COMMAND to capture the IDENTITY.
In this example the IDENTITY field is ScheduleID.
CREATE PROCEDURE dbo.spScheduleInsert
( #CustomerID int,
#ItemID int,
#StartDate Date,
#TimeIn DateTime,
#TimeOut DateTime,
#ReturnIdentityValue int OUTPUT )
AS
BEGIN
DECLARE #TempScheduleIdentity table ( TempScheduleID int )
INSERT INTO Schedule ( CustomerID,ItemID,StartDate,TimeIn,TimeOut )
OUTPUT INSERTED.ScheduleID into #TempScheduleIdentity
VALUES (#CustomerID,#ItemID,#StartDate,#TimeIn,#TimeOut)
SELECT #ReturnIdentityValue = (SELECT TempScheduleID FROM #TempScheduleIdentity)
END
Once you have the #ReturnIdentityValue...you could then update the records other field with the value.

check constraints multiple options

i have a table with a column i want to apply constraints too but it has two variables.
Table is called: MyOrderTable
Column is called : Orderref
example data would be:
OrderRef
80
75
110
110
80
110
What i want to do is create a constraint where if the value of OrderRef is below 100 it cannot be a duplicate number to any exisiting value in that column but if it is above 100 it can be a duplicate - can this be done - assume i will need a function to do this?
tried the following but it did not work:
ALTER TABLE MYORDERTABLE
ADD CONSTRAINT TAS_CK
CHECK (
MYORDERTABLE_CHECK(ORDERREF) = 1)
CREATE FUNCTION MYORDERTABLE_CHECK (
#ORDERREF INT
)
RETURNS TINYINT
AS
BEGIN
DECLARE #RESULT TINYINT
IF EXISTS(SELECT * FROM MYORDERTABLE WHERE ORDERREF < 100)
SET #RESULT= 1
ELSE
SET #RESULT= 0
RETURN #RESULT
END
i was still allowed to enter data under 100 for the orderref column
The CHECK fires up AFTER the row has been inserted into the table, not
before.
here in comments noticed about that
So you should use instead of insert trigger
use tempdb
go
create table t (
id int not null identity(1,1),
orderref int not null
)
go
create trigger tr_t on t instead of insert
as
begin
if exists(
select 1
from inserted i
join t on i.orderref < 100 and i.orderref = t.orderref
)
begin
raiserror('orderref < 100 already exists', 16, 1)
return
end
insert t(orderref)
select orderref from inserted
end
go
insert t (orderref) values (1)
insert t (orderref) values (2)
insert t (orderref) values (100)
insert t (orderref) values (100)
insert t (orderref) values (5)
insert t (orderref) values (5) -- error here
select * from t

Insert two columns from different tables

I need to write an insert query to insert some rows into a table using data from different tables. I have:
A variable #ID which contains ID of the new inserted row in a table. (1)
A table (2) contains some IDs
A table (3) define the relation between the two above tables.
Now I need to insert for each ID in (2) a new row in the table (3).
So if #ID=2 and IDs = 1, 2, 3, 4, 5, 6, I want to insert the following rows in Table (3):
table1_ID table2_ID
--------- ---------
1 1
1 2
1 3
1 4
1 5
Assuming that the value 6 was not meant to be discarded (if it was, please explain the logic). Also assuming that you just want every row in table 2 inserted into table 2.
INSERT dbo.Table3(table1_ID, table2_ID)
SELECT #ID, ID
FROM dbo.Table2
-- WHERE ID <> 6???
;
If the list of IDs is really a CSV, then:
First create a Split function (several alternatives described here, except they output strings instead of integers), e.g.
CREATE FUNCTION dbo.SplitInts
(
#List VARCHAR(MAX),
#Delimiter VARCHAR(255)
)
RETURNS #t TABLE(Item INT)
AS
BEGIN
INSERT #t(Item) SELECT CONVERT(INT, SUBSTRING(#List, Number,
CHARINDEX(#Delimiter, #List + #Delimiter, Number) - Number))
FROM (SELECT ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_objects) AS n(Number)
WHERE Number <= CONVERT(INT, LEN(#List))
AND SUBSTRING(#Delimiter + #List, Number, 1) = #Delimiter;
RETURN;
END
GO
Now your stored procedure can simply say:
CREATE PROCEDURE dbo.whatever
#ID INT,
#IDs VARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
INSERT dbo.Table3(table1_ID, table2_ID)
SELECT #ID, Item
FROM dbo.SplitInts(#IDs, ',');
END
GO
However if you are on SQL Server 2008 or above, and this list of comma-separated IDs is coming from your application (e.g. from a DataTable or other set), you could use a Table-Valued Parameter and avoid the need for splitting.
CREATE TYPE dbo.SetOfIntegers
( Number INT );
GO
CREATE PROCEDURE dbo.whatever
#ID INT,
#IDs dbo.SetOfIntegers READONLY
AS
BEGIN
SET NOCOUNT ON;
INSERT dbo.Table3(table1_ID, table2_ID)
SELECT #ID, Item
FROM #IDs;
END
GO
Then you have to change the C# code to pass your DataTable in as SqlDbType.Structured instead of passing your CSV in as a string. More info here:
http://www.sqlperformance.com/2012/08/t-sql-queries/splitting-strings-now-with-less-t-sql

Using merge..output to get mapping between source.id and target.id

Very simplified, I have two tables Source and Target.
declare #Source table (SourceID int identity(1,2), SourceName varchar(50))
declare #Target table (TargetID int identity(2,2), TargetName varchar(50))
insert into #Source values ('Row 1'), ('Row 2')
I would like to move all rows from #Source to #Target and know the TargetID for each SourceID because there are also the tables SourceChild and TargetChild that needs to be copied as well and I need to add the new TargetID into TargetChild.TargetID FK column.
There are a couple of solutions to this.
Use a while loop or cursors to insert one row (RBAR) to Target at a time and use scope_identity() to fill the FK of TargetChild.
Add a temp column to #Target and insert SourceID. You can then join that column to fetch the TargetID for the FK in TargetChild.
SET IDENTITY_INSERT OFF for #Target and handle assigning new values yourself. You get a range that you then use in TargetChild.TargetID.
I'm not all that fond of any of them. The one I used so far is cursors.
What I would really like to do is to use the output clause of the insert statement.
insert into #Target(TargetName)
output inserted.TargetID, S.SourceID
select SourceName
from #Source as S
But it is not possible
The multi-part identifier "S.SourceID" could not be bound.
But it is possible with a merge.
merge #Target as T
using #Source as S
on 0=1
when not matched then
insert (TargetName) values (SourceName)
output inserted.TargetID, S.SourceID;
Result
TargetID SourceID
----------- -----------
2 1
4 3
I want to know if you have used this? If you have any thoughts about the solution or see any problems with it? It works fine in simple scenarios but perhaps something ugly could happen when the query plan get really complicated due to a complicated source query. Worst scenario would be that the TargetID/SourceID pairs actually isn't a match.
MSDN has this to say about the from_table_name of the output clause.
Is a column prefix that specifies a table included in the FROM clause of a DELETE, UPDATE, or MERGE statement that is used to specify the rows to update or delete.
For some reason they don't say "rows to insert, update or delete" only "rows to update or delete".
Any thoughts are welcome and totally different solutions to the original problem is much appreciated.
In my opinion this is a great use of MERGE and output. I've used in several scenarios and haven't experienced any oddities to date.
For example, here is test setup that clones a Folder and all Files (identity) within it into a newly created Folder (guid).
DECLARE #FolderIndex TABLE (FolderId UNIQUEIDENTIFIER PRIMARY KEY, FolderName varchar(25));
INSERT INTO #FolderIndex
(FolderId, FolderName)
VALUES(newid(), 'OriginalFolder');
DECLARE #FileIndex TABLE (FileId int identity(1,1) PRIMARY KEY, FileName varchar(10));
INSERT INTO #FileIndex
(FileName)
VALUES('test.txt');
DECLARE #FileFolder TABLE (FolderId UNIQUEIDENTIFIER, FileId int, PRIMARY KEY(FolderId, FileId));
INSERT INTO #FileFolder
(FolderId, FileId)
SELECT FolderId,
FileId
FROM #FolderIndex
CROSS JOIN #FileIndex; -- just to illustrate
DECLARE #sFolder TABLE (FromFolderId UNIQUEIDENTIFIER, ToFolderId UNIQUEIDENTIFIER);
DECLARE #sFile TABLE (FromFileId int, ToFileId int);
-- copy Folder Structure
MERGE #FolderIndex fi
USING ( SELECT 1 [Dummy],
FolderId,
FolderName
FROM #FolderIndex [fi]
WHERE FolderName = 'OriginalFolder'
) d ON d.Dummy = 0
WHEN NOT MATCHED
THEN INSERT
(FolderId, FolderName)
VALUES (newid(), 'copy_'+FolderName)
OUTPUT d.FolderId,
INSERTED.FolderId
INTO #sFolder (FromFolderId, toFolderId);
-- copy File structure
MERGE #FileIndex fi
USING ( SELECT 1 [Dummy],
fi.FileId,
fi.[FileName]
FROM #FileIndex fi
INNER
JOIN #FileFolder fm ON
fi.FileId = fm.FileId
INNER
JOIN #FolderIndex fo ON
fm.FolderId = fo.FolderId
WHERE fo.FolderName = 'OriginalFolder'
) d ON d.Dummy = 0
WHEN NOT MATCHED
THEN INSERT ([FileName])
VALUES ([FileName])
OUTPUT d.FileId,
INSERTED.FileId
INTO #sFile (FromFileId, toFileId);
-- link new files to Folders
INSERT INTO #FileFolder (FileId, FolderId)
SELECT sfi.toFileId, sfo.toFolderId
FROM #FileFolder fm
INNER
JOIN #sFile sfi ON
fm.FileId = sfi.FromFileId
INNER
JOIN #sFolder sfo ON
fm.FolderId = sfo.FromFolderId
-- return
SELECT *
FROM #FileIndex fi
JOIN #FileFolder ff ON
fi.FileId = ff.FileId
JOIN #FolderIndex fo ON
ff.FolderId = fo.FolderId
I would like to add another example to add to #Nathan's example, as I found it somewhat confusing.
Mine uses real tables for the most part, and not temp tables.
I also got my inspiration from here: another example
-- Copy the FormSectionInstance
DECLARE #FormSectionInstanceTable TABLE(OldFormSectionInstanceId INT, NewFormSectionInstanceId INT)
;MERGE INTO [dbo].[FormSectionInstance]
USING
(
SELECT
fsi.FormSectionInstanceId [OldFormSectionInstanceId]
, #NewFormHeaderId [NewFormHeaderId]
, fsi.FormSectionId
, fsi.IsClone
, #UserId [NewCreatedByUserId]
, GETDATE() NewCreatedDate
, #UserId [NewUpdatedByUserId]
, GETDATE() NewUpdatedDate
FROM [dbo].[FormSectionInstance] fsi
WHERE fsi.[FormHeaderId] = #FormHeaderId
) tblSource ON 1=0 -- use always false condition
WHEN NOT MATCHED
THEN INSERT
( [FormHeaderId], FormSectionId, IsClone, CreatedByUserId, CreatedDate, UpdatedByUserId, UpdatedDate)
VALUES( [NewFormHeaderId], FormSectionId, IsClone, NewCreatedByUserId, NewCreatedDate, NewUpdatedByUserId, NewUpdatedDate)
OUTPUT tblSource.[OldFormSectionInstanceId], INSERTED.FormSectionInstanceId
INTO #FormSectionInstanceTable(OldFormSectionInstanceId, NewFormSectionInstanceId);
-- Copy the FormDetail
INSERT INTO [dbo].[FormDetail]
(FormHeaderId, FormFieldId, FormSectionInstanceId, IsOther, Value, CreatedByUserId, CreatedDate, UpdatedByUserId, UpdatedDate)
SELECT
#NewFormHeaderId, FormFieldId, fsit.NewFormSectionInstanceId, IsOther, Value, #UserId, CreatedDate, #UserId, UpdatedDate
FROM [dbo].[FormDetail] fd
INNER JOIN #FormSectionInstanceTable fsit ON fsit.OldFormSectionInstanceId = fd.FormSectionInstanceId
WHERE [FormHeaderId] = #FormHeaderId
Here's a solution that doesn't use MERGE (which I've had problems with many times I try to avoid if possible). It relies on two memory tables (you could use temp tables if you want) with IDENTITY columns that get matched, and importantly, using ORDER BY when doing the INSERT, and WHERE conditions that match between the two INSERTs... the first one holds the source IDs and the second one holds the target IDs.
-- Setup... We have a table that we need to know the old IDs and new IDs after copying.
-- We want to copy all of DocID=1
DECLARE #newDocID int = 99;
DECLARE #tbl table (RuleID int PRIMARY KEY NOT NULL IDENTITY(1, 1), DocID int, Val varchar(100));
INSERT INTO #tbl (DocID, Val) VALUES (1, 'RuleA-2'), (1, 'RuleA-1'), (2, 'RuleB-1'), (2, 'RuleB-2'), (3, 'RuleC-1'), (1, 'RuleA-3')
-- Create a break in IDENTITY values.. just to simulate more realistic data
INSERT INTO #tbl (Val) VALUES ('DeleteMe'), ('DeleteMe');
DELETE FROM #tbl WHERE Val = 'DeleteMe';
INSERT INTO #tbl (DocID, Val) VALUES (6, 'RuleE'), (7, 'RuleF');
SELECT * FROM #tbl t;
-- Declare TWO temp tables each with an IDENTITY - one will hold the RuleID of the items we are copying, other will hold the RuleID that we create
DECLARE #input table (RID int IDENTITY(1, 1), SourceRuleID int NOT NULL, Val varchar(100));
DECLARE #output table (RID int IDENTITY(1,1), TargetRuleID int NOT NULL, Val varchar(100));
-- Capture the IDs of the rows we will be copying by inserting them into the #input table
-- Important - we must specify the sort order - best thing is to use the IDENTITY of the source table (t.RuleID) that we are copying
INSERT INTO #input (SourceRuleID, Val) SELECT t.RuleID, t.Val FROM #tbl t WHERE t.DocID = 1 ORDER BY t.RuleID;
-- Copy the rows, and use the OUTPUT clause to capture the IDs of the inserted rows.
-- Important - we must use the same WHERE and ORDER BY clauses as above
INSERT INTO #tbl (DocID, Val)
OUTPUT Inserted.RuleID, Inserted.Val INTO #output(TargetRuleID, Val)
SELECT #newDocID, t.Val FROM #tbl t
WHERE t.DocID = 1
ORDER BY t.RuleID;
-- Now #input and #output should have the same # of rows, and the order of both inserts was the same, so the IDENTITY columns (RID) can be matched
-- Use this as the map from old-to-new when you are copying sub-table rows
-- Technically, #input and #output don't even need the 'Val' columns, just RID and RuleID - they were included here to prove that the rules matched
SELECT i.*, o.* FROM #output o
INNER JOIN #input i ON i.RID = o.RID
-- Confirm the matching worked
SELECT * FROM #tbl t

Resources