Adding max(value)+1 in new row, can this be a problem? - sql

In a SQL Server table I have the following 2 columns:
RowId: primary key, numaric, identity column and auto insert.
MailId: Non key, numaric, non identity and non auto insert.
Mail Id can be duplicate. In case of new MailId I will check max(MailId)+1 and insert it in new row and in case of duplication value will be coming as parameter.
Logic looks fine but here is an issue, I was just considering (yet chacnes of accurance are ver low) In the same time there can be two different new MailId requests. Can this casue logical error ? For example when code checked max(MailId)+1 was 101 and I stored it in a variable but may be before next insert statment executs a new record inserted in table. Now max(MailId)+1 in table will be 102 but value in variable will be 101 ?
Any suggestion please I want to control this error chances as well.
EDIT
(I am not using identity(1,1) because I also have to pass custom values in it)

Why would you use a custom-rolled Identity field when there is such a great one already in SQL Server?
Just use INT Identity (1,1) for your ID field and it will automatically increment each time a row is inserted. It also handles concurrency much better than pretty much anything you could implement manually.
EDIT:
Sample of a manual ID value:
SET IDENTITY_INSERT MyTable ON
INSERT INTO MyTable (IdField, Col1, Col2, Col3,...)
VALUES
(1234, 'Col1', 'Col2', 'Col3',...)
SET IDENTITY_INSERT MyTable OFF
You need to include an explicit field list for the INSERT.

Use OUTPUT on your insert to be sure that you have the right value. If you insert and then select MAX, it is possible that someone could "sneak" in and end up with duplication. That is, you insert MAX + 1, at the same time someone else inserts MAX + 1 then you select MAX and they select MAX, you both have the same value. Whereas if you INSERT and use OUTPUT, you'll be sure that you're unique. This is rarely a problem, but if you have a lot of activity, it can happen (speaking from experience).
EDIT
USE AdventureWorks2008R2;
GO
DECLARE #MyTableVar table(
EmpID int NOT NULL,
OldVacationHours int,
NewVacationHours int,
ModifiedDate datetime);
UPDATE TOP (10) HumanResources.Employee
SET VacationHours = VacationHours * 1.25,
ModifiedDate = GETDATE()
OUTPUT inserted.BusinessEntityID,
deleted.VacationHours,
inserted.VacationHours,
inserted.ModifiedDate
INTO #MyTableVar;
--Display the result set of the table variable.
SELECT EmpID, OldVacationHours, NewVacationHours, ModifiedDate
FROM #MyTableVar;
GO
--Display the result set of the table.
SELECT TOP (10) BusinessEntityID, VacationHours, ModifiedDate
FROM HumanResources.Employee;
GO

Related

Insert new record into autonumbered table, and then use the autonumber in another table

I'm writing a stored procedure to insert data from a form into two tables. One table has an autonumbered identity field. I need to insert the data into that table, find the newly created autonumber, and use that number to insert data into another table. So, to boil it down, I have a one-to-many link between the two tables and I need to make sure the identity field gets inserted.
Is this code the best way to do something like this, or am I missing something obvious?
CREATE PROCEDURE [dbo].[sp_Insert_CRT]
(
#TRACKING_ID int,
#CUST_NUM int,
#TRACKING_ITEM_ID int,
#STATEMENT_NUM nvarchar (200) = null,
#AMOUNT numeric (15, 2),
#BBL_ADJUSTED int = NULL,
#PAID_VS_BILLED int = NULL,
#ADJUSTMENT_TYPE int = NULL,
#ENTERED_BY nvarchar (10) = NULL,
#ENTERED_DATE date = NULL,
#AA_STATUS int = NULL
)
AS
BEGIN
-- Insert data into CRT_Main, where Tracking_ID is an autonumber field
INSERT into tbl_CRT_Main
(
-- TRACKING_ID
CUST_NUM
,TRACKING_ITEM_ID
,STATEMENT_NUM
,AMOUNT
)
VALUES
(
-- #TRACKING_ID
#CUST_NUM
,#TRACKING_ITEM_ID
,#STATEMENT_NUM
,#AMOUNT
)
-- Find the newly generated autonumber, and use it in another table
BEGIN TRANSACTION
DECLARE #TrackID int;
SELECT #TrackID = coalesce((select max(TRACKING_ID) from tbl_CRT_Main), 1)
COMMIT
INSERT into tbl_CRT_Admin_Adjustment
(
TRACKING_ID
,BBL_ADJUSTED
,PAID_VS_BILLED
,[ADJUSTMENT_TYPE]
,[ENTERED_BY]
,[ENTERED_DATE]
,AA_STATUS
)
VALUES
(
#TrackID
,#BBL_ADJUSTED
,#PAID_VS_BILLED
,#ADJUSTMENT_TYPE
,#ENTERED_BY
,#ENTERED_DATE
,#AA_STATUS
)
END
SELECT #TrackID = coalesce((select max(TRACKING_ID) from tbl_CRT_Main), 1)
No, don't do this. This will get you the maximum value of TRACKING_ID yes, but that doesn't mean that's the value that was created for your INSERT. If multiple INSERT statements were being run by different connections then very likely you would get the wrong value.
Instead, use SCOPE_IDENTITY to get the value:
SET #TrackID = SCOPE_IDENTITY();
Also, there is no need to wrap the above in an explicit transaction like you have with your SELECT MAX(). Instead, most likely, the entire batch in the procedure should be inside it's own explicit transaction, with a TRY...CATCH so that you can ROLLBACK the whole batch in the event of an error.

How to get the MAX Id value from a table in MS SQL Server

I'm trying to update the table data with some row that should have defined a MAX Id + 1 value for the Movement Id field which is the PK in my table.
The Movement Id field is the INT PK, the Added At field is a date and the IncOutc is the INT field that can store either 1 or 2(Income or Outcome).
query.sql
DECLARE #max_id INT;
SET #max_id = SELECT MAX([Movement Id]) FROM Movement
INSERT INTO Movement([Movement Id], [Added At], IncOutc)
VALUES (max_id, GETDATE(), 1)
I tried the query above but got an error: Incorrect syntax near the keyword 'SELECT'. (on line 1).
I already tried inserting the values like this:
VALUES (SELECT MAX([Movement Id]) FROM Movement, max_id, GETDATE(), 1)
But got an error saying: Incorrect syntax near '1'. (on line 4)
You don't. You use an IDENTITY column or SEQUENCE when you create the table. So:
CREATE TABLE Movements (
Movement_Id INT IDENTITY(1, 1) PRIMARY KEY,
Added_At DATETIME DEFAULT GETDATE(),
IncOutc Int
);
Then you insert to it as:
INSERT INTO Movements (IncOutc)
VALUES (1);
Movement_Id and Added_At are given appropriate default values on the insert.
Attempting to do this outside the database is very problematic. In particular, two inserts at the same time might generate the same id in the table -- presumably not what you want. Preventing that requires locking the table, which is very expensive. Especially considering that SQL Server has built-in functionality to do this.
To answer your actual question, you use:
SET #max_id = (SELECT MAX([Movement Id]) FROM Movement);
Notice the statement terminator. That is a good habit to develop. You might wonder what happens when your table has no rows. I suggest you try it and see for yourself.

Select row just inserted without using IDENTITY column in SQL Server 2012

I have a bigint PK column which is NOT an identity column, because I create the number in a function using different numbers. Anyway, I am trying to save this bigint number in a parameter #InvID, then use this parameter later in the procedure.
ScopeIdentity() is not working for me, it saved Null to #InvID, I think because the column is not an identity column. Is there anyway to select the record that was just inserted by the procedure without adding an extra ID column to the table?
It would save me a lot of effort and work if there is a direct way to select this record and not adding an id column.
insert into Lab_Invoice(iID, iDate, iTotal, iIsPaid, iSource, iCreator, iShiftID, iBalanceAfter, iFileNo, iType)
values (dbo.Get_RI_ID('True'), GETDATE(),
(select FilePrice from LabSettings), 'False', #source, #user, #shiftID, #b, #fid, 'Open File Invoice');
set #invID = CAST(scope_identity() AS bigint);
P.S. dbo.Get_RI_ID('True') a function returns a bigint.
Why don't you use?
set #invId=dbo.Get_RI_ID('True');
insert into Lab_Invoice(iID,iDate,iTotal,iIsPaid,iSource,iCreator,iShiftID,iBalanceAfter,iFileNo,iType)
values(#invId,GETDATE(),(select FilePrice from LabSettings),'False',#source,#user,#shiftID,#b,#fid,'Open File Invoice');
You already know that big id value. Get it before your insert statement then use it later.
one way to get inserted statement value..it is not clear which value you are trying to get,so created some example with dummy data
create table #test
(
id int
)
declare #id table
(
id int
)
insert into #test
output inserted.id into #id
select 1
select #invID=id from #id

Set Identity ON with a merge statement

I am inserting and deleting elements in a table, as a result, when I want to insert a new element, it takes a new id number, but this id is not taking the last id+1. For example: the last id is 5 and I inserted a 5 elements and deleted after that, the new id will take the value of 11, and I need 6. Here is my code
CREATE TABLE #FC
(
Code varchar(25),
Description varchar(50),
Category varchar(10),
CreatedDate datetime,
LastModifiedDate datetime
);
--Adding just one record
INSERT INTO #FC (Code, Description, Category, CreatedDate, LastModifiedDate)
VALUES ('DELETE_MEMBER', 'Delete Member', 'POLICY', #Now, #Now);
;
SET IDENTITY_INSERT [dbo].[Function_Code] ON;
MERGE
INTO [dbo].[Function_Code] AS T
USING #FC AS S
ON (T.Code = S.Code) AND (T.Description = S.Description) AND(T.Category = S.Category)
WHEN MATCHED THEN
UPDATE SET
[Code] = S.[Code]
, [Description] = S.Description
, [Category] = S.Category
, [CreatedDate] = S.CreatedDate
, [LastModifiedDate] = S.LastModifiedDate
WHEN NOT MATCHED THEN
INSERT (Code, Description, Category, CreatedDate, LastModifiedDate)
VALUES(S.Code, S.Description, S.Category, S.CreatedDate, S.LastModifiedDate)
;
SET IDENTITY_INSERT [dbo].[Function_Code] OFF;
An identity is a technical field that you should not handle yourself. If you want to manage the sequence yourself, then don't use an identity field.
Nevertheless, if you really want to do it, you'll have to reseed the table to the desired value :
DELETE YourTable
DECLARE #n INT;
SELECT #n = MAX(YourId) FROM YourTable
DBCC CHECKIDENT ('YourTable', RESEED, #n)
INSERT YourTable
What you are asking is dangerous. If you make a column an identity column, don't touch it, let sql server do its job. Otherwise you can start getting primary key errors. The identity column is ready to insert 11. You insert six through eleven in your code by running it multiple time and you can get a primary key error next time the identity tries to insert a row into the table.
As Thomas Haratyk said you can reseed your table. Or you can use:
select MAX(YourId) + 1 FROM YourTable
and insert that into your identity column if you are sure you will always insert an id that has already been used by the identity column.
However, if you are commonly overwriting the default identity behavior, it may be better to manage this column yourself because deleting from an identity column results in gaps by default.

INSERT SELECT in Firebird

I'm new to firebird and I have verious issues. I want to insert various lines into a table selected from another table.
Here's the code:
/*CREATE GENERATOR POS; */
SET GENERATOR POS TO 1;
SET TERM ^;
create trigger BAS_pkassign
for MATERIAL
active before insert position 66
EXECUTE BLOCK
AS
declare posid bigint;
select gen_id(POS, 1)
from RDB$DATABASE
into :posid;
BEGIN
END
SET TERM ; ^
INSERT INTO MATERIAL ( /*ID */ LOCATION, POSID, ARTID, ARTIDCONT, QUANTITY )
SELECT 1000, ':posid', 309, BAS_ART.ID, 1
FROM BAS_ART
WHERE BAS_ART.ARTCATEGORY LIKE '%MyWord%'
The ID should autoincrement from 66 on. The posid should autoincrement from 1 on.
Actually it is not inserting anything.
I'm using Firebird Maestro and have just opened the SQL Script Editor (which doesnt throw any error message on executing the script).
Can anybody help me?
Thanks!
Additional information:
The trigger should autoincrement the column "ID" - but I dont know how exactly I can change it so it works.. The ':posid' throws an error using it :posid but like this theres no error (I guess its interpretated as a string). But how do I use it right?
I dont get errors when I execute it. The table structure is easy. I have 2 tables:
1.
Material (
ID (INTEGER),
Location (INTEGER),
POSID (INTEGER),
ARTID (INTEGER),
ARTIDCONT (INTEGER),
QUANTITY (INTEGER),
OTHERCOLUMN (INTEGER))
and the 2. other table
BAS_ART (ID (INTEGER), ARTCATEGORY (VARCHAR255))
-> I want to insert all entries from the table BAS_ART which contain "MyWord" in the column ARTCATEGORY into the MATERIAL table.
I don't understand why you need the trigger at all.
This problem:
I want to insert all entries from the table BAS_ART which contain "MyWord" into the MATERIAL table
Can be solved with a single insert ... select statement.
insert into material (id, location, posid, artid, quantity)
select next value for seq_mat_id, 1000, next value for seq_pos, id, 1
from bas_art
where artcategory = 'My Word';
This assumes that there is a second sequence (aka "generator") that is named seq_mat_id that provides the new id for the column material.id
For most of my answer I will assume a very simple table:
CREATE TABLE MyTable (
ID BIGINT PRIMARY KEY,
SomeValue VARCHAR(255),
posid INTEGER
)
Auto-increment identifier
Firebird (up to version 2.5) does not have an identity column type (this will be added in Firebird 3), instead you need to use a sequence (aka generator) and a trigger to get this.
Sequence
First you need to create a sequence using CREATE SEQUENCE:
CREATE SEQUENCE seqMyTable
A sequence is atomic which means interleaving transactions/connections will not get duplicate values, it is also outside transaction control, which means that a ROLLBACK will not revert to the previous value. In most uses a sequences should always increase, so the value reset you do at the start of your question is wrong for almost all purposes; for example another connection could reset the sequence as well midway in your execution leaving you with unintended duplicates of POSID.
Trigger
To generate a value for an auto-increment identifier, you need to use a BEFORE INSERT TRIGGER that assigns a generated value to the - in this example - ID column.
CREATE TRIGGER trgMyTableAutoIncrement FOR MyTable
ACTIVE BEFORE INSERT POSITION 0
AS
BEGIN
NEW.ID = NEXT VALUE FOR seqMyTable;
END
In this example I always assign a generated value, other examples assign a generated value only when the ID is NULL.
Getting the value
To get the generated value you can use the RETURNING-clause of the INSERT-statement:
INSERT INTO MyTable (SomeValue) VALUES ('abc') RETURNING ID
INSERT INTO ... SELECT
Using INSERT INTO ... SELECT you can select rows from one table and insert them into others. The reason it doesn't work for you is because you are trying to assign the string value ':pos' to a column of type INTEGER, and that is not allowed.
Assuming I have another table MyOtherTable with a similar structure as MyTable I can transfer values using:
INSERT INTO MyTable (SomeValue)
SELECT SomeOtherValue
FROM MyOtherTable
Using INSERT INTO ... SELECT it is not possible to obtain the generated values unless only a single row was inserted.
Guesswork with regard to POSID
It is not clear to me what POSID is supposed to be, and what values it should have. It looks like you want to have an increasing value starting at 1 for a single INSERT INTO ... SELECT. In versions of Firebird up to 2.5 that is not possible in this way (in Firebird 3 you would be able to use ROW_NUMBER() for this).
If my guess is right, then you will need to use an EXECUTE BLOCK (or a stored procedure) to assign and increase the value for every row to be inserted.
The execute block would be something like:
EXECUTE BLOCK
AS
DECLARE posid INTEGER = 1;
DECLARE someothervalue VARCHAR(255);
BEGIN
FOR SELECT SomeOtherValue FROM MyOtherTable INTO :someothervalue DO
BEGIN
INSERT INTO MyTable (SomeValue, posid) VALUES (:someothervalue, :posid);
posid = posid + 1;
END
END
Without an ORDER BY with the SELECT the value of posid is essentially meaningless, because there is no guaranteed order.