I have a table of names with Ids
Create table Names (
Id int,
Name nvarchar(500)
)
I'm trying to create a procedure that would select 1 name that matches the provided Id if Id is provided or select all names if no Id is provided
Create Procedure SelectNames
#Id int = null
AS
BEGIN
Select * From Names
Where IsNull(#Id, 0) = 0
Or Id = #Id
END
GO
But I get an error: 'Error: SR0015 : Microsoft.Rules.Data : Deterministic function call (ISNULL) might cause an unnecessary table scan.'
What does the 'unnecessary table scan' refer to in this instance?
And is there a better way to write the procedure?
The simplest way to remove the table scan is to create an index (probably unique) on your Id column. In general, one wouldn't expect a nullable Id value. With that index in place, finding a name by Id will not require scanning (or iterating through every row in) the table.
Regarding "better way to write the procedure" - once the nullability is removed, a simple SELECT without the WHERE should be fine.
Related
I would like to make Postgres choose the first next available id so that no error occurs in the following case:
CREATE TABLE test(
id serial PRIMARY KEY,
name varchar
);
Then:
INSERT INTO test VALUES (2,'dd');
INSERT INTO test (name) VALUES ('aa');
INSERT INTO test (name) VALUES ('bb');
This will give a constraint error since id is primary.
How can I tell Postgres to insert the record with the next free id?
Generally it's best to never overrule the default in a serial column. If you sometimes need to provide id values manually, replace the standard DEFAULT clause nextval('sequence_name') of the serial column with a custom function that omits existing values.
Based on this dummy table:
CREATE TABLE test (test_id serial PRIMARY KEY, test text);
Function:
CREATE OR REPLACE FUNCTION f_test_test_id_seq(OUT nextfree bigint) AS
$func$
BEGIN
LOOP
SELECT INTO nextfree val
FROM nextval('test_test_id_seq'::regclass) val -- use actual name of sequence
WHERE NOT EXISTS (SELECT 1 FROM test WHERE test_id = val);
EXIT WHEN FOUND;
END LOOP;
END
$func$ LANGUAGE plpgsql;
Alter default:
ALTER TABLE test ALTER COLUMN test_id SET DEFAULT f_test_test_id_seq();
It's not strictly a serial any more, but serial is only a convenience feature anyway:
Safely and cleanly rename tables that use serial primary key columns in Postgres?
And if you build this on top of a serial column the SEQUENCE is automatically "owned" by the table column, which is probably a good thing.
This is a slightly faster variant of:
Autoincrement, but omit existing values in the column
Table and sequence name are hard coded here. You could easily parametrize the sequence name (like in the linked answer) and even the table name - and test existence with a dynamic statement using EXECUTE. Would give you a generic function, but the call would be a bit more expensive.
CREATE OR REPLACE FUNCTION f_nextfree(_tbl regclass
, _col text
, _seq regclass
, OUT nextfree bigint) AS
$func$
BEGIN
LOOP
EXECUTE '
SELECT val FROM nextval($1) val WHERE NOT EXISTS (
SELECT 1 FROM ' || _tbl || ' WHERE ' || quote_ident(_col) || ' = val)'
INTO nextfree
USING _seq;
EXIT WHEN nextfree IS NOT NULL;
END LOOP;
END
$func$ LANGUAGE plpgsql;
ALTER TABLE test2 ALTER COLUMN test2_id
SET DEFAULT f_nextfree('test2', 'test2_id', 'test2_test2_id_seq');
SQL Fiddle.
I have a table with a unique column, "X".
I would like to pass a list to a stored procedure using a table value pair of X, and I would like back a list of all the X's that do not exist in the table.
What is the most efficient way to do this in SQL Server?
EDIT:
I've come up with this. Not sure how efficient it is:
CREATE TYPE StringList_TBLType AS TABLE (s NVARCHAR(255) NOT NULL PRIMARY KEY)
CREATE PROCEDURE [dbo].ReturnProductsNotExisting
(
#CatalogIdList StringList_TBLType READONLY
)
AS
BEGIN
SELECT s FROM #CatalogIdList
WHERE s NOT IN (SELECT CatalogId from Products)
END
I have a table that contains, for example, two fields that I want to make unique within the database. For example:
create table Subscriber (
ID int not null,
DataSetId int not null,
Email nvarchar(100) not null,
...
)
The ID column is the primary key and both DataSetId and Email are indexed.
What I want to be able to do is prevent the same Email and DataSetId combination appearing in the table or, to put it another way, the Email value must be unique for a given DataSetId.
I tried creating a unique index on the columns
CREATE UNIQUE NONCLUSTERED INDEX IX_Subscriber_Email
ON Subscriber (DataSetId, Email)
but I found that this had quite a significant impact on search times (when searching for an email address for example - there are 1.5 million rows in the table).
Is there a more efficient way of achieving this type of constraint?
but I found that this had quite a significant impact on search times
(when searching for an email address for example
The index you defined on (DataSetId, Email) cannot be used for searches based on email. If you would create an index with the Email field at the leftmost position, it could be used:
CREATE UNIQUE NONCLUSTERED INDEX IX_Subscriber_Email
ON Subscriber (Email, DataSetId);
This index would server both as a unique constraint enforcement and as a means to quickly search for an email. This index though cannot be used to quickly search for a specific DataSetId.
The gist of it if is that whenever you define a multikey index, it can be used only for searches in the order of the keys. An index on (A, B, C) can be used to seek values on column A, for searching values on both A and B or to search values on all three columns A, B and C. However it cannot be used to search values on B or on C alone.
I assume that only way to enter data into that table is through SPs, If that's the case you can implement some logic in your insert and update SPs to find if the values you are going to insert / update is already exists in that table or not.
Something like this
create proc spInsert
(
#DataSetId int,
#Email nvarchar(100)
)
as
begin
if exists (select * from tabaleName where DataSetId = #DataSetId and Email = #Email)
select -1 -- Duplicacy flag
else
begin
-- insert logic here
select 1 -- success flag
end
end
GO
create proc spUpdate
(
#ID int,
#DataSetId int,
#Email nvarchar(100)
)
as
begin
if exists
(select * from tabaleName where DataSetId = #DataSetId and Email = #Email and ID <> #ID)
select -1 -- Duplicacy flag
else
begin
-- insert logic here
select 1 -- success flag
end
end
GO
Long time reader, first time poster ;-)
I'm implementing a system based on an old system. The new system uses SQL Server 2008 and my problem comes when trying to insert new items in the main table. This will happen in two ways: It may be imported from the existing system (1) or may be created in the new system (2).
In case (1) the item already has an ID (int) which I would like to keep. In case (2) the ID will not be filled in and I'd like to generate an ID which is +1 of the maximum current value in the table. This should of course also work for inserts of mutiple rows.
As far as I can see, the solution will be to create a INSTEAD OF TRIGGER, but I can't quite figure out how this is done. Can anyone give me a hint or point me in the direction of how this can be done?
Chris
Following your request of using an INSTEAD OF trigger this SQL code can get you started.
CREATE TABLE dbo.SampleTable
(
ID INT,
SomeOtherValue VARCHAR(100) NULL
)
GO
CREATE TRIGGER dbo.TR_SampleTable_Insert
ON dbo.SampleTable
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
-- Inserting rows with IDs
INSERT INTO dbo.SampleTable (
ID,
SomeOtherValue)
SELECT
ID,
SomeOtherValue
FROM
Inserted
WHERE
ID IS NOT NULL
-- Now inserting rows without IDs
INSERT INTO dbo.SampleTable (
ID,
SomeOtherValue)
SELECT
(SELECT ISNULL(MAX(ID), 0) FROM dbo.SampleTable)
+ ROW_NUMBER() OVER(ORDER BY ID DESC),
SomeOtherValue
FROM
Inserted
WHERE
ID IS NULL
END
GO
INSERT INTO dbo.SampleTable
SELECT 1, 'First record with id'
UNION
SELECT NULL, 'First record without id'
UNION
SELECT 2, 'Second record with id'
UNION
SELECT NULL, 'Second record without id'
GO
SELECT * FROM dbo.SampleTable
GO
How about using a stored procedure to do your inserts, with the primary key as an optional parameter. In the stored procedure, you can set the primary key when it is not passed.
I would caution that if old and new records are being inserted mix and match, your scenario will probably fail, as new records will be getting old ID's before the old records are inserted. I recommend getting the max ID of the old table right now, and in the stored procedure setting the new primary key to be the Greater value of (old max + 1, current table max)
Others have shown you how to write such trigger.
Another, and often recommended, approach is to store both IDs in new database. Each record gets new ID in new system (by IDENTITY column or some other means). Additionally, if the record is imported from another system, it has associated OriginSystem and OriginID. This way you can keep old IDs for reference. This approach has additional benefit of being able to support new system to import data from, e.g. when merging or exchanging data with another system.
I have a question regarding locking in TSQL. Suppose I have a the following table:
A(int id, varchar name)
where id is the primary key, but is NOT an identity column.
I want to use the following pseudocode to insert a value into this table:
lock (A)
uniqueID = GenerateUniqueID()
insert into A values (uniqueID, somename)
unlock(A)
How can this be accomplished in terms of T-SQL? The computation of the next id should be done with the table A locked in order to avoid other sessions to do the same operation at the same time and get the same id.
If you have custom logic that you want to apply in generating the ids, wrap it up into a user defined function, and then use the user defined function as the default for the column. This should reduce concurrency issue similarly to the provided id generators by deferring the generation to the point of insert and piggy backing on the insert locking behavior.
create table ids (id int, somval varchar(20))
Go
Create function GenerateUniqueID()
returns int as
Begin
declare #ret int
select #ret = max(isnull(id,1)) * 2 from ids
if #ret is null set #ret = 2
return #ret
End
go
alter table ids add Constraint DF_IDS Default(dbo.GenerateUniqueID()) for Id
There are really only three ways to go about this.
Change the ID column to be an IDENTITY column where it auto increments by some value on each insert.
Change the ID column to be a GUID with a default constraint of NEWID() or NEWSEQUENTIALID(). Then you can insert your own value or let the table generate one for you on each insert.
On each insert, start a transaction. Then get the next available ID using something like select max(id)+1 . Do this in a single sql statement if possible in order to limit the possibility of a collision.
On the whole, most people prefer option 1. It's fast, easy to implement, and most people understand it.
I tend to go with option 2 with the apps I work on simply because we tend to scale out (and up) our databases. This means we routinely have apps with a multi-master situation. Be aware that using GUIDs as primary keys can mean your indexes are routinely trashed.
I'd stay away from option 3 unless you just don't have a choice. In which case I'd look at how the datamodel is structured anyway because there's bound to be something wrong.
You use the NEWID() function and you do not need any locking mechanism
You tell a column to be IDENTITY and you do not need any locking mechanism
If you generate these IDs manually and there is a chance parallel calls could generate the same IDs then something like this:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
#NextID = GenerateUniqueID()
WHILE EXISTS (SELECT ID FROM A WHERE ID = #NextID)
BEGIN
#NextID = GenerateUniqueID()
END
INSERT INTO A (ID, Text) VALUES (#NextID , 'content')
COMMIT TRANSACTION
#Markus, you should look at using either IDENTITY or NEWID() as noted in the other answers. if you absolutely can't, here's an option for you...
DECLARE #NewID INT
BEGIN TRAN
SELECT #NewID = MAX(ID) + 1
FROM TableA (tablockx)
INSERT TableA
(ID, OtherFields)
VALUES (#NewID, OtherFields)
COMMIT TRAN
If you're using SQL2005+, you can use the OUTPUT clause to do what you're asking, without any kind of lock (The table Test1 simulates the table you're inserted into, and since OUTPUT requires a temp table and not a variable to hold the results, #Result will do that):
create table test1( test INT)
create table #result (LastValue INT)
insert into test1
output INSERTED.test into #result(test)
select GenerateUniqueID()
select LastValue from #result
Just to update an old post. It is now possible with SQL Server 2012 to use a feature called Sequence. Sequences are created in much the same way a function and it is possible to specify the range, direction(asc, desc) and rollover point. After which it's possible to invoke the NEXT VALUE FOR method to generate the next value in the range.
See the following documentation from Microsoft.
http://technet.microsoft.com/en-us/library/ff878091.aspx