How do I Index this View Properly? - sql-server

I have a table similar to this:
CREATE TABLE dbo.SomeTable (Work_ID VARCHAR(9));
I need to be able to run the following query and unfortunately can not change the data type of the Work_ID column:
SELECT Work_ID
FROM dbo.SomeTable
WHERE WorkID >= 100 AND WorkID <=200
This of course will give me an implicite conversion and cause a table scan (several million rows).
My thought was to put the following indexed view on it.
CREATE VIEW [dbo].[vw_Work_ID]
WITH SCHEMABINDING AS
SELECT CAST(q.Work_ID as INT) as Work_ID
FROM dbo.SomeTable q
GO
CREATE UNIQUE CLUSTERED INDEX [cl_vw_Work_ID] ON [dbo].[vw_Work_ID]
(
[Work_ID] ASC
)
GO
When I now run
SELECT Work_ID FROM dbo.vw_Work_ID WHERE WorkID >= 100 AND WorkID <=200
``
I still get IMPLICIT CONVERSION and a table scan. Any solutions?

Use TRY_CAST instead of CAST to avoid conversion errors. The resultant value will be NULL for invalid integer values. Also, add a NOEXPAND hint so to use the view index:
CREATE TABLE dbo.SomeTable (Work_ID VARCHAR(9));
GO
CREATE VIEW [dbo].[vw_Work_ID]
WITH SCHEMABINDING AS
SELECT TRY_CAST(q.Work_ID as INT) as Work_ID
FROM dbo.SomeTable q;
GO
CREATE UNIQUE CLUSTERED INDEX [cl_vw_Work_ID] ON [dbo].[vw_Work_ID]
(
[Work_ID] ASC
);
GO
INSERT INTO dbo.SomeTable VALUES('111');
INSERT INTO dbo.SomeTable VALUES('xxx');
GO
SELECT *
FROM [dbo].[vw_Work_ID] WITH(NOEXPAND)
WHERE Work_ID = 0;
GO

We can index on the column using the same WHERE conditions as we want to use in the query. When we check with XML Stastics on we see that the query has been run on the index and has not done a table scan.
Please see the dbFiddle link for confirmation of the query plan.
SET STATISTICS XML ON;
CREATE TABLE dbo.SomeTable (Work_ID VARCHAR(9));
SELECT * FROM dbo.SomeTable
WHERE Work_ID >= '100' AND Work_ID <='200';
The query plan shows a full table scan
create index [cl_vw_Work_ID]
on [dbo].[SomeTable](Work_ID)
WHERE Work_ID >= '100' AND Work_ID <='200';
SELECT * FROM dbo.SomeTable
WHERE Work_ID >= '100' AND Work_ID <='200';
The query plan shows an index scan and no table scan
db<>fiddle here
UPDATE
following the comment that the values 100 and 200 are not fixed I have tried creating a virtual column casting the Work_ID to int and created an index on it. The query plan is showing a full table scan, even when I insert 4000 rows.
ALTER TABLE dbo.SomeTable
ADD num_work_id AS CAST(Work_ID AS INT)
;
create index [cl_vw_Work_ID]
on [dbo].[SomeTable](num_work_id)
;
SELECT * FROM dbo.SomeTable
WHERE num_work_id >= '100' AND num_work_id <='200';
Work_ID | num_work_id
:------ | ----------:
db<>fiddle here
with 4000 rows db<>fiddle here

Related

Efficient query to filter for list of values across columns/distinct rows

SQL Server version is 2016+/Azure SqlDb (flexible if additive, compatible with both, forward-compatible).
Use case is API users sending a single-column list of values to filter some target table. The target table has 2-n columns whose values are unique across rows (maybe columns, doesn't matter) for the table/range being queried. (So far n <= 5, but that's a detail/not guaranteed.)
Here's a good-enough sample table DDL:
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'SomeTable')
BEGIN
CREATE TABLE dbo.SomeTable (
ID int IDENTITY(1, 1) not null PRIMARY KEY CLUSTERED
, NaturalKey1 nvarchar(10) not null UNIQUE NONCLUSTERED
, NaturalKey2 nvarchar(10) not null UNIQUE NONCLUSTERED
, NaturalKey3 nvarchar(10) not null UNIQUE NONCLUSTERED
);
END
IF NOT EXISTS (SELECT 1 FROM dbo.SomeTable)
BEGIN
INSERT INTO dbo.SomeTable VALUES
('A', 'AA', 'ZZZZZ')
,('B', 'B', 'YYYYY')
,('C', 'CC', 'XXX')
,('D', 'DDD', 'WWWWW')
,('E', 'EEEE', 'V')
,('F', 'FF', 'UUUUUUUUU')
,('G', 'GGGGGGGG', 's')
-- lots more
;
END
SELECT * FROM dbo.SomeTable;
-- DROP TABLE dbo.SomeTable;
Assumptions are that all NaturalKey columns are of same type (probably nvarchar); filtering happens db-side; and in as few steps as possible, ideally one execution, in a stored procedure. Parameter will be string list or TVP, doesn't matter really. Result will include all data in any row of SomeTable that matches any value on any column. Target table is of unknown size.
Here's an example parameter for our pal above:
DECLARE #filterValues nvarchar(1000) = 'DDD,XXX,E,HH,ok,whatever,YYYYY';
SELECT * FROM string_split(#filterValues, ',');
I know a couple ways to do this, and can imagine several more, so it's not that kind of stuck. I'll bet someone knows a better trick than either of the two I'll illustrate.
Approach 1 Build a temp table updated for existence and join on it (concise and nice to audit, that's about it for pros)
DECLARE #filterValues nvarchar(1000) = 'DDD,XXX,E,HH,ok,whatever,YYYYY';
SELECT value AS InValue, CONVERT(int, null) AS IDMatch
INTO #filters
FROM string_split(#filterValues, ',');
UPDATE f
SET f.IDMatch = st.ID
FROM #filters AS f
INNER JOIN dbo.SomeTable AS st ON f.InValue IN (st.NaturalKey1, st.NaturalKey2, st.NaturalKey3);
SELECT * FROM #filters; -- Audit
SELECT st.* FROM #filters AS f INNER JOIN dbo.SomeTable AS st ON f.IDMatch = st.ID;
IF OBJECT_ID('tempdb..#filters') IS NOT NULL DROP TABLE #filters;
Approach 2 Unpivot SomeTable (I like the nifty cross apply trick) and just join (at scale there be ogres methinks)
SELECT
st.*
FROM
dbo.SomeTable AS st
CROSS APPLY (VALUES (st.NaturalKey1)
, (st.NaturalKey2)
, (st.NaturalKey3)
) AS nk(Val)
INNER JOIN #filters AS f ON nk.Val = f.InValue;
IF OBJECT_ID('tempdb..#filters') IS NOT NULL DROP TABLE #filters;
Is there a question in our future
Works is better than doesn't work, but looking for better/more efficient/more scalable methods from the T-SQL gurus. Unknown dimensions in columns and rows, response time is an SLA, filter size may or may not be capped. Bonus points if this ports neatly from SomeTable to SomeTableVersionN. (No dynamic SQL in an API.)
Could be dupe question, couldn't find it, pointing that out is just fine thank you.

SQL Server: check constraint if relationship exists, else insert

I am trying to create a constraint to validate if a relation exists
I have tried to create a procedure and then use it in check constraint. Apparently that does not seem to work.
These are my tables:
STOCKITEMS table:
StockItemId INT
StockItemName VARCHAR
ColorId INT
COLOR table:
ColorId INT
ColorName VARCHAR
This is my stored procedure:
CREATE PROCEDURE USP_ValidateColor
(#Color NVARCHAR(50))
AS
IF NOT EXISTS(SELECT ColorName FROM WareHouse.Colors WHERE ColorName = #Color)
BEGIN
DECLARE #Id INT
SET #Id = (SELECT TOP(1) ColorId + 1 FROM Warehouse.Colors
ORDER BY ColorId DESC)
INSERT INTO Warehouse.Colors
VALUES (#Id, #Color)
PRINT 'Does not exist';
END;
ELSE
PRINT 'Exists';
So if a user insert into the table stock items, I want a check that checks if the colorId already exists in the color table
If it does not, then insert that colorname into colors and. I was thinking about using a constraint check with my procedure, but can't fix the query.
Don't use an SP to check a constraint, use a foreign key:
CREATE TABLE Colour (ColourID int PRIMARY KEY, --This should really have a name
ColourName varchar(20));
CREATE TABLE StockItem (StockItemID int PRIMARY KEY, --This should really have a name too
StockItemName varchar(20),
ColourID int);
ALTER TABLE dbo.StockItem ADD CONSTRAINT Colour_FK FOREIGN KEY (ColourID) REFERENCES dbo.Colour(ColourID);
Then, if you try to insert something into the StockItem table, it'll fail unless the colour exists:
INSERT INTO dbo.Colour (ColourID,
ColourName)
VALUES (1,'Green'),(2,'Blue');
GO
INSERT INTO dbo.StockItem (StockItemID,
StockItemName,
ColourID)
VALUES(1,'Paint',1); --works
GO
INSERT INTO dbo.StockItem (StockItemID,
StockItemName,
ColourID)
VALUES (1,'Wood Panels',3); --fails
GO
--clean up
DROP TABLE dbo.StockItem;
DROP TABLE dbo.Colour;
For checking, use a UNIQUE check constraint. If you want to insert a color only if it doesn't exist, use INSERT .. FROM .. WHERE to check for existence and insert in the same query.
The only "trick" is that FROM needs a table. This can be fixed using a table value constructor to create tables out of the values to insert. If the stored procedure accepts a table-valued parameter, there's no problem.
This example uses a LEFT JOIN to insert non-matching values :
declare #colors table (Color nvarchar(10) UNIQUE)
insert into #colors VALUES ('green')
select * from #colors;
insert into #Colors (Color)
select new.Color
from (VALUES ('red'),
('green')) new(Color)
left outer join #Colors old on old.Color=new.Color
where old.Color is NULL
-- (1 row affected)
insert into #Colors (Color)
select new.Color
from (VALUES ('red'),
('green')) new(Color)
left outer join #Colors old on old.Color=new.Color
where old.Color is NULL
-- (0 rows affected)
select * from #colors;
-- green
-- red
The same using a subquery:
insert into #Colors (Color)
select new.Color
from
(VALUES ('red'),
('green')) new(Color)
where not exists (select 1
from #colors
where color=new.Color);
By using the UNIQUE constraint we ensure that duplicate entries can't be inserted

SQL Server constraint on row count

I want a database level block on adding more than 3 instances of an id into a column in a mapping table. I do not want to use a trigger or add any non-computed column to the table. I tried an indexed view but I can't use HAVING or CAST in the query as the two non-valid examples below show. Any ideas?
CREATE VIEW VW WITH SCHEMABINDING AS
SELECT col1, CAST(COUNT_BIG(*)+252 AS TINYINT) a
FROM tbl1 GROUP BY col1
CREATE VIEW VW WITH SCHEMABINDING AS
SELECT col1, COUNT_BIG(*), CAST(256 AS TINYINT) a
FROM tbl1 GROUP BY col1 HAVING COUNT(*)>3
you can create a function that have an id as parameters and count how many id there are in the table
ALTER FUNCTION [dbo].[test](#id integer)
RETURNS int
AS
BEGIN
DECLARE #retval int
SELECT #retval = COUNT(*) FROM table where id = #id
RETURN #retval
END
then you can add a check constraint in the table that check if id <= 3
the test for check constraint will be
dbo.test(id) <= 3
this check is for add rows and update.

Is there anyway to reset the identity of a Table Variable?

Say I have a table variable:
DECLARE #MyTableVar TABLE (ID INT IDENTITY(1,1), SomeData NVARCHAR(300))
After I have inserted 250 rows, I need to "Start Over" with the table. I do this:
DELETE FROM #MyTableVar
Is there anything I can do to the table variable so that this:
insert into #MyTableVar Values("TestData")
select * from #MyTableVar
will return this:
_______________________________
| ID | SomeData |
|___________|_________________|
| | |
| 1 | TestData |
|___________|_________________|
instead of this:
_______________________________
| ID | SomeData |
|___________|_________________|
| | |
| 251 | TestData |
|___________|_________________|
Instead relying on an Identity, why not use the new ranking functions such as Row_Number
Insert #MyTableVar( Id, Value )
Select Row_Number() Over ( Order By Value )
, Value
From SomeOtherTable
Instead of re-seeding the IDENTITY, why not just delete from the #table variable, then use ROW_NUMBER() against the input? e.g. instead of the lazy
SELECT * FROM #MyTableVar;
...use...
SELECT ID = ROW_NUMBER() OVER (ORDER BY ID), SomeData FROM #MyTableVar;
Now you don't need to care what the seed is, whether it starts at 1, whether there are any gaps, etc.
unfortunately there is no function to reseed identity column in table variable, I know this question is very old, but in case other people encountered the same problem, I would like to share my method to solve this problem.
/* declare another table variable with same structure and perform select insert*/
DECLARE #MyTableVar1 TABLE (ID INT IDENTITY(1,1), SomeData NVARCHAR(300))
insert into #MyTableVar1
select someData from #MyTableVar
However, If you want to perform dynamic reseeding inside a loop, I would suggest using a table object
You can't reseed the identity value on a Table Variable but you can do the same thing with a Temp Table:
CREATE TABLE #TAB(ID INT IDENTITY,VALUE VARCHAR(10))
DECLARE #RESEED INT = 32
DBCC CHECKIDENT(#TAB,RESEED,#RESEED)
INSERT INTO #TAB
SELECT 'TEST'
SELECT * FROM #TAB
Since you are re-using your table, if I got it right, how about you do not initialize your counters to 1 and instead use this as an example?
DECLARE #ctr INT
IF #ctr IS NULL or #ctr <= 0 --this part is to control #ctr value on loops
SET #ctr = 1
ELSE
SELECT #ctr = MIN(id) FROM #tbl
This way, you are not restarting your loop to 1 nor is there a need for you to truncate the table.
Is it possible to have another int column on your table variable and update that column with modulo after the insert is finished?
declare #Mytablevar table
(
id int identity(1,1)
,id1 int
somedata nvarchar(300)
)
-- insert your data as you would. After insert is finished, do the following:
update #mytablevar set id1 = case when id > 250 then id % 250 else id end
I tried it on net but i am not able to get any solution on reset identity for table variable.
If you are able to use temp table #MyTableVar instead of table #MyTableVar variable then it is possible to reset identity value
DBCC CHECKIDENT('TableName', RESEED, NewValue)
DBCC CHECKIDENT(#MyTableVar, RESEED, 0)
Newvalue must be one less than the newIdentiyValue
NewValue= NewIdentity-1;
If you still want to learn more you can refer my blog
http://tryconcepts.blogspot.in/2012/08/reset-identity-column-to-new-id.html
I just had this idea and it works!!! :
declare #TableVariable table (
IdentityColumn int identity(1,1),
SomeOtherValue int,
DesiredResult int
)
declare #FirstIdentityValueEachTimeYouLoadDataToTable int
declare #Count int
set #Count = 1
while #Count <= 5
begin
delete #TableVariable
insert into #TableVariable (SomeOtherValue) select 45
insert into #TableVariable (SomeOtherValue) select 90
insert into #TableVariable (SomeOtherValue) select 2
select #FirstIdentityValueEachTimeYouLoadDataToTable = min(IdentityColumn) from #TableVariable
Update #TableVariable set DesiredResult = IdentityColumn - #FirstIdentityValueEachTimeYouLoadDataToTable + 1
select * from #TableVariable
set #Count = #Count + 1
end
Can you use temporary table?
This is a sample how to do this with a temp table.
CREATE TABLE #MyTableVar (ID INT IDENTITY(1,1), SomeData NVARCHAR(300))
insert #MyTableVar(SomeData) values ('test1'), ('test2')
---doesn't work
DELETE FROM #MyTableVar
insert #MyTableVar(SomeData) values ('test3'), ('test4')
select * from #MyTableVar
--resets the identity
truncate table #MyTableVar
insert #MyTableVar(SomeData) values ('test3'), ('test4')
select * from #MyTableVar
Regards
Piotr
you should truncate your table instead of deleting all rows from it.
but note that truncate will not work for some tables, (listed from MSDN):
You cannot use TRUNCATE TABLE on tables that:
Are referenced by a FOREIGN KEY constraint. (You can truncate a table that has a foreign key that references itself.)
Participate in an indexed view.
Are published by using transactional replication or merge replication.
and added myself:
You cannot truncate a table variable.
syntac of truncate is:
TRUNCATE TABLE
[ { database_name .[ schema_name ] . | schema_name . } ]
table_name
[ ; ]
EDIT: I didn't notice that you are questioning about table variables.
as far as I know there is no way to reset an identity column in a table variable. you can use a temp table instead.
DELETE FROM does not reset identity.
TRUNCATE does.
If you are using SQL Server then use this DBCC CHECKIDENT('Customer', RESEED, 0) Where Customer is a table name. When you insert records into table after this command your primery key column value will be start from 1 again.
Read this
http://codedotnets.blogspot.in/2012/09/how-to-reset-identity-in-sql-server.html

Split table and insert with identity link

I have 3 tables similar to the sctructure below
CREATE TABLE [dbo].[EmpBasic](
[EmpID] [int] IDENTITY(1,1) NOT NULL Primary Key,
[Name] [varchar](50),
[Address] [varchar](50)
)
CREATE TABLE [dbo].[EmpProject](
[EmpID] [int] NOT NULL primary key, // referencing column with EmpBasic
[EmpProject] [varchar](50) )
CREATE TABLE [dbo].[EmpFull_Temp](
[ObjectID] [int] IDENTITY(1,1) NOT NULL Primary Key,
[T1Name] [varchar](50) ,
[T1Address] [varchar](50) ,
[T1EmpProject] [varchar](50)
)
The EmpFull_Temp table has the records with a dummy object ID column... I want to populate the first 2 tables with the records in this table... But with EmpID as a reference between the first 2 tables.
I tried this in a stored procedure...
Create Table #IDSS (EmpID bigint, objID bigint)
Insert into EmpBasic
output Inserted.EmpID, EmpFull_Temp.ObjectID
into #IDSS
Select T1Name, T1Address from EmpFull_Temp
Where ObjectID < 106
Insert into EmpProject
Select A.EmpID, B.T1EmpProject from #IDSS as A, EmpFull_Temp as B
Where A.ObjID = B.ObjectID
But it says.. The multi-part identifier "EmpFull_Temp.ObjectID" could not be bound.
Could you please help me in achieving this...
Edit : There is no guarantee that [Name]+[Address] would be unique across [EmpBasic] Table
With your EmpProject join table, you probably don't want the primary key constraint on only the EmpID column
DECLARE #Count int
DECLARE #NextEmpID int
DECLARE #StartObjectID int
DECLARE #EndObjectID int
-- range of IDs to transfer (inclusive)
SET #StartObjectID = 1
SET #EndObjectID = 105
BEGIN TRAN
-- lock tables so IDENT_CURRENT is valid
SELECT #Count = COUNT(*) FROM [EmpBasic] WITH (TABLOCKX, HOLDLOCK)
SELECT #Count = COUNT(*) FROM [EmpProject] WITH (TABLOCKX, HOLDLOCK)
SELECT #NextEmpID = IDENT_CURRENT('EmpBasic')
SET IDENTITY_INSERT [EmpBasic] ON
INSERT [EmpBasic] ([EmpID], [Name], [Address])
SELECT #NextEmpID + ROW_NUMBER() OVER(ORDER BY ObjectID), [T1Name], [T1Address]
FROM [EmpFull_Temp]
WHERE [ObjectID] BETWEEN #StartObjectID AND #EndObjectID
SET IDENTITY_INSERT [EmpBasic] OFF
INSERT [EmpProject]([EmpID], [EmpProject])
SELECT #NextEmpID + ROW_NUMBER() OVER(ORDER BY ObjectID), [T1EmpProject]
FROM [EmpFull_Temp]
WHERE [ObjectID] BETWEEN #StartObjectID AND #EndObjectID
COMMIT TRAN
The solution to this problem depends on whether the "parent" table (i.e. the one with the IDENTITY column) has a natural key (i.e. one or more fields which, when combined, are guaranteed to be unique, other than the surrogate primary key).
For example, in this case, is the combinaton of Name and Address aways going to be unique?
If the answer is yes then you can simply insert into EmpBasic without bothering to output and store the generated IDs. You can then insert into EmpProject joining back on to EmpBasic using the natural key (e.g. name and address) to fnd the correct EmpID.
Insert into EmpBasic
Select T1Name, T1Address from EmpFull_Temp
Where ObjectID < 106
Insert into EmpProject
Select A.EmpID, B.T1EmpProject from EmpBasic as A, EmpFull_Temp as B
Where A.Name = B.Name And A.Address = B.Address
If the answer is no then there is no easy solution I know of - in SQL Server 2005 (I've no idea if this is any different in 2008), it's not possible to OUTPUT values that are not inserted. I've got around this issue in the past by using one of the other fields (e.g. Name) to temporarily store the original ID (in this case, ObjectID), use that to join when inserting the child records as described above and then gone back to update the parent records o remove/replace the temporary values. It's not nice but I've not found a better way.
Insert into EmpBasic
Select cast(ObjectID as varchar(50)) as name, T1Address from EmpFull_Temp
Where ObjectID < 106
Insert into EmpProject
Select A.EmpID, B.T1EmpProject from EmpBasic as A, EmpFull_Temp as B
Where A.Name = cast(B.ObjectID as varchar(50))
Update EmpBasic
Set Name = B.T1Name
from EmpBasic as A, EmpFull_Temp as B
Where A.Name = cast(B.ObjectID as varchar(50))
Please note: I've not tested the sample SQL given above but I hope it gives you an idea of how you might approach this.
Add an ObjectID column to the EmpBasic table to facilitate the data transfer then drop it when you're done. I'm assuming this is a one-time operation, I don't recommend adding and dropping a column if this is on-going
I have used the Stack Exchange Data Explorer to investigate alternative solutions. The only one with promise at the moment is shown here. It is effectively #ScotHauder's answer, except using a temporary table that has the ObjectID column and using IDENTITY_INSERT to move the generated EmpId values into EmpBasic.
If you have to do this multiple times you need to get the EmpBasic_Temp EmpId IDENTITY starting value to be Max(EmpBasic.EmpID)+1.

Resources