views:

829

answers:

7

I have a primary key that I don't want to auto increment (for various reasons) and so I'm looking for a way to simply increment that field when I INSERT. By simply, I mean without stored procedures and without triggers, so just a series of SQL commands (preferably one command).

Here is what I have tried thus far:

BEGIN TRAN

INSERT INTO Table1(id, data_field)
VALUES ( (SELECT (MAX(id) + 1) FROM Table1), '[blob of data]');

COMMIT TRAN;

* Data abstracted to use generic names and identifiers

However, when executed, the command errors, saying that

"Subqueries are not allowed in this context. only scalar expressions are allowed"

So, how can I do this/what am I doing wrong?


EDIT: Since it was pointed out as a consideration, the table to be inserted into is guaranteed to have at least 1 row already.

+1  A: 

Try this instead:

INSERT INTO Table1 (id, data_field)
SELECT id, '[blob of data]' FROM (SELECT MAX(id) + 1 as id FROM Table1) tbl

I wouldn't recommend doing it that way for any number of reasons though (performance, transaction safety, etc)

Eric Petroelje
What would you recommend they do instead?
Gern Blandston
Since the id field is indexed, is the performance hit significant? Also, if wrapped in a transaction, it should be atomic and safe, correct?
cdeszaq
I'm just not sure if the table will be locked when you do the SELECT MAX(id).. part. If not, there's the potential that two threads could get the same id. Someone who knows SQL server a bit better would be able to tell you if that is actually safe or not - my intuition is that it isn't, but I'm really not sure.
Eric Petroelje
The SELECT MAX(ID) statement, in and of itself, is atomic (it's a single transaction).
Garrett
I provided code that will show that this will collide if you run it in a loop from 2 different connections
SQLMenace
A: 

If you're doing it in a trigger, you could make sure it's an "INSTEAD OF" trigger and do it in a couple of statements:

DECLARE @next INT
SET @next = (SELECT (MAX(id) + 1) FROM Table1)

INSERT INTO Table1
VALUES (@next, inserted.datablob)

The only thing you'd have to be careful about is concurrency - if two rows are inserted at the same time, they could attempt to use the same value for @next, causing a conflict.

Does this accomplish what you want?

rwmnau
No, triggers can't be used unless there is no other way.
cdeszaq
A: 

This should work:

INSERT INTO Table1 (id, data_field)
SELECT (SELECT (MAX(id) + 1) FROM Table1), '[blob of data]';

Or this (substitute LIMIT for other platforms):

INSERT INTO Table1 (id, data_field)
SELECT TOP 1
    MAX(id) + 1, '[blob of data]'
FROM
   Table1
ORDER BY
   [id] DESC;
gbn
Any comment on why -1 from someone?
gbn
A: 
declare @nextId int
set @nextId = (select MAX(id)+1 from Table1)

insert into Table1(id, data_field) values (@nextId, '[blob of data]')

commit;

But perhaps a better approach would be using a scalar function getNextId('table1')

Jon Masters
getNextId(<Table>) would be a..custom stored procedure, or an included one? If included, what platforms is it on, since I don't think it is standard.
cdeszaq
So your function would use dynamic SQL? Or a separate key table?
gbn
+1  A: 

It could be because there are no records so the sub query is returning NULL...try

INSERT INTO tblTest(RecordID, Text) VALUES ((SELECT ISNULL(MAX(RecordID), 0) + 1 FROM tblTest), 'asdf')

Jon
In this case, it is guaranteed that there is at least one existing row, but good thing to remember. +1
cdeszaq
you can't have a query inside VALUES
SQLMenace
@Jon: poster tried this already
gbn
Works for me in sql 2008
Jon
I'm using 2005 :)
cdeszaq
+4  A: 

You understand that you will have collisions right?

you need to do something like this and this might cause deadlocks so be very sure what you are trying to accomplish here

DECLARE @id int
BEGIN TRAN

    SELECT @id = MAX(id) + 1 FROM Table1 WITH (UPDLOCK, HOLDLOCK)
    INSERT INTO Table1(id, data_field)
    VALUES (@id ,'[blob of data]')
COMMIT TRAN

To explain the collision thing, I have provided some code

first create this table and insert one row

CREATE TABLE Table1(id int primary key not null, data_field char(100))
GO
Insert Table1 values(1,'[blob of data]')
Go

Now open up two query windows and run this at the same time

declare @i int
set @i =1
while @i < 10000
begin
BEGIN TRAN

INSERT INTO Table1(id, data_field)
SELECT MAX(id) + 1, '[blob of data]' FROM Table1

COMMIT TRAN;
set @i =@i + 1
end

You will see a bunch of these

Server: Msg 2627, Level 14, State 1, Line 7 Violation of PRIMARY KEY constraint 'PK__Table1__3213E83F2962141D'. Cannot insert duplicate key in object 'dbo.Table1'. The statement has been terminated.

SQLMenace
Why would there be collisions? Such things are handled by the transaction, yes? Additionally, where would the deadlocks come from?
cdeszaq
not if you run under the default transactional level (read commited), what prohibits two threads to read the same max value?
SQLMenace
A: 

It seems very odd to do this sort of thing w/o an IDENTITY (auto-increment) column, making me question the architecture itself. I mean, seriously, this is the perfect situation for an IDENTITY column. It might help us answer your question if you'd explain the reasoning behind this decision. =)

Having said that, some options are:

  • using an INSTEAD OF trigger for this purpose. So, you'd do your INSERT (the INSERT statement would not need to pass in an ID). The trigger code would handle inserting the appropriate ID. You'd need to use the WITH (UPDLOCK, HOLDLOCK) syntax used by another answerer to hold the lock for the duration of the trigger (which is implicitly wrapped in a transaction) & to elevate the lock type from "shared" to "update" lock (IIRC).
  • you can use the idea above, but have a table whose purpose is to store the last, max value inserted into the table. So, once the table is set up, you would no longer have to do a SELECT MAX(ID) every time. You'd simply increment the value in the table. This is safe provided that you use appropriate locking (as discussed). Again, that avoids repeated table scans every time you INSERT.
  • use GUIDs instead of IDs. It's much easier to merge tables across databases, since the GUIDs will always be unique (whereas records across databases will have conflicting integer IDs). To avoid page splitting, sequential GUIDs can be used. This is only beneficial if you might need to do database merging.
  • Use a stored proc in lieu of the trigger approach (since triggers are to be avoided, for some reason). You'd still have the locking issue (and the performance problems that can arise). But sprocs are preferred over dynamic SQL (in the context of applications), and are often much more performant.

Sorry about rambling. Hope that helps.

Garrett