Split table and insert with identity link - sql-server

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.

Related

I would like to sum all of the data in a specific table column, if the appropriate ID of that table exists in another one

I would like to SUM a value called amount from table 1, the considered, to be summed, values should only be the ones presenting in table 2. Meaning that, for the amount of row 1 in table 1 to be considered in the sum. the ID of that row 1 should be present in table 2.
Thanks,
This might be the answer but you really should have put some example tables in your example. I fancied helping as have 10 mins, this example you can run.
You can see that table 1 is referenced twice from Table2 and 3 just the once ,so the result ignores multiple occurrences, hence the WHERE EXISTS syntax.
This sums up all the numbers in Table1 that are referenced in Table2.
BEGIN TRANSACTION
BEGIN TRY
CREATE TABLE #Table1 (
Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[Number] DECIMAL NOT NULL
)
INSERT INTO #Table1 ([Number])
VALUES('1'),
('1'),
('1'),
('1')
SELECT * FROM #Table1
CREATE TABLE #Table2 (
Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
Table1Id INT NOT NULL,
CONSTRAINT FK_Table2_Table1 FOREIGN KEY (Table1Id) REFERENCES #Table1 (Id)
)
INSERT INTO #Table2 ([Table1Id])
VALUES('1'),
('1'),
('3')
SELECT * FROM #Table2
SELECT SUM(T1.Number) AS SummedNumbersThatAreReferencedByTable2
FROM #Table1 AS T1
WHERE EXISTS(
SELECT TOP 1 *
FROM #Table2 AS T2
WHERE T2.Table1Id = T1.Id
)
ROLLBACK TRANSACTION
END TRY
BEGIN CATCH
PRINT 'Rolling back changes, there was an error!!'
ROLLBACK TRANSACTION
DECLARE #Msg NVARCHAR(MAX)
SELECT #Msg=ERROR_MESSAGE()
RAISERROR('Error Occured: %s', 20, 101,#msg) WITH LOG
END CATCH
If this is the answer then please mark it as so, cheers

Random Primary Key missing in SQL Server selection

I am facing a problem when I query master table (having ~700 Million records and high transactional table) to look for newly inserted records. My aim is to get all the newly created IDs from the #IDs temp table (Min and Max records) and dump it in another child table. But random IDs are missing in the child table.
Setup:
We have a primary and secondary server (SQL Server 2016) and they are in sync mode.
Tables:
CREATE TABLE tblMaster
(
ID BIGINT IDENTITY(1,1) NOT NULL,
EmployeeID INT NOT NULL
)
CREATE TABLE tblChild
(
ChildID IDENTITY(1,1),
ID BIGINT NOT NULL,
TransactionDate Datetime NOT NULL
)
tblChild.ID references tblMaster.ID.
Stored procedure:
DECLARE #MaxID BIGINT
SELECT #MaxID = MAX(ID) FROM tblChild WITH(NOLOCK)
SET #MaxID = ISNULL(#MaxID, 0)
DROP TABLE IF EXISTS #IDS
SELECT ID
INTO #IDS
FROM tblMaster WITH(NOLOCK)
WHERE ID > #MaxID
--25k RECORDS BATCH INSERT INTO tblChild - MAINLY TAKE CARE NEWLY inserted records
STARTIDS:
IF EXISTS (SELECT TOP 1 * FROM #IDS)
BEGIN
DROP TABLE IF EXISTS #TOPIDS
SELECT TOP 25000 ID INTO #TOPIDS
FROM #IDS
ORDER BY ID ASC
INSERT INTO tblChild (ID, CreatedBy, CreatedDate)
SELECT ID, SYSTEM_USER, GETDATE()
FROM #TOPIDS
DELETE AA
FROM #IDS AA
INNER JOIN #TOPIDS BB ON AA.ID = BB.ID
GOTO STARTIDS
END
Please help where it's going wrong.

Throw error on invalid insertion in SQL Server

I have a SQL Server 2012 database with two tables:
CREATE TABLE Products
(
Id INT IDENTITY(1, 1) NOT NULL,
Code NVARCHAR(50) NOT NULL,
Name NVARCHAR(50) NOT NULL,
CONSTRAINT PK_Product
PRIMARY KEY CLUSTERED (Id ASC)
);
CREATE TABLE BlockedProductCodes
(
Code NVARCHAR(50) NOT NULL,
ReasonCode INT NOT NULL
CONSTRAINT PK_BlockedProductCodes
PRIMARY KEY CLUSTERED (Code ASC)
);
I want to be able to prevent products being inserted into the Products table if their product code exists in the BlockedProductCodes table.
The only way I could think of doing this was with a BEFORE INSERT trigger:
CREATE TRIGGER trg_Products_BEFORE_INSERT
ON Products
INSTEAD OF INSERT AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT Code
FROM BlockedProductCodes BPC
INNER JOIN inserted I ON BPC.Code = I.Code)
BEGIN
RAISERROR('The product has been blocked!', 16, 1);
END
ELSE
BEGIN
INSERT Product (Id, Code, Name)
SELECT Id, Code, Name
FROM INSERTED
END
SET NOCOUNT OFF;
END
But this caused an error with the identity column:
Cannot insert explicit value for identity column in table 'Products' when IDENTITY_INSERT is set to OFF
Can anyone suggest a way to fix this or a better approach?
Please note, this check is also made at the application level, but I want enforce that at the data table level.
Thanks.
Update: using check constraint
I have tried the following that seems to work..
CREATE FUNCTION dbo.IsCodeBlocked
(
#code nvarchar(50)
)
RETURNS BIT
WITH SCHEMABINDING
AS
BEGIN
DECLARE #ret bit
IF (#Code IN (SELECT Code FROM dbo.BlockedProductCodes))
SET #ret = 1
ELSE
SET #ret = 0
RETURN #ret
END
GO
ALTER TABLE Products
ADD CONSTRAINT CheckValidCode
CHECK (dbo.IsCodeBlocked(Code) = 0);
GO
insert Products (Code, Name) values ('xyz', 'Test #1')
go
insert Products (Code, Name) values ('abc', 'Test #2')
-- Fails with "The INSERT statement conflicted with the
-- CHECK constraint 'CheckValidCode'."
go
I am not sure if it is particularly 'safe' or performant. I will also test out the indexed view approach suggested by Damien.
One way you can implement this is by abusing an indexed view:
CREATE TABLE dbo.Products (
Id INT IDENTITY (1, 1) NOT NULL,
Code NVARCHAR(50) NOT NULL,
Name NVARCHAR(50) NOT NULL,
CONSTRAINT PK_Product PRIMARY KEY CLUSTERED (Id ASC)
);
GO
CREATE TABLE dbo.BlockedProductCodes (
Code NVARCHAR(50) NOT NULL,
ReasonCode INT NOT NULL
CONSTRAINT PK_BlockedProductCodes PRIMARY KEY CLUSTERED (Code ASC)
);
GO
CREATE TABLE dbo.Two (
N int not null,
constraint CK_Two_N CHECK (N > 0 and N < 3),
constraint PK_Two PRIMARY KEY (N)
)
GO
INSERT INTO dbo.Two(N) values (1),(2)
GO
create view dbo.DRI_NoBlockedCodes
with schemabinding
as
select
1 as Row
from
dbo.Products p
inner join
dbo.BlockedProductCodes bpc
on
p.Code = bpc.Code
inner join
dbo.Two t
on
1=1
GO
CREATE UNIQUE CLUSTERED INDEX IX_DRI_NoBlockedCodes on dbo.DRI_NoBlockedCodes (Row)
And now we attempt to insert:
INSERT INTO dbo.BlockedProductCodes (Code,ReasonCode) values ('abc',10)
GO
INSERT INTO dbo.Products (Code,Name) values ('abc','def')
And we get:
Msg 2601, Level 14, State 1, Line 42
Cannot insert duplicate key row in object 'dbo.DRI_NoBlockedCodes' with unique index 'IX_DRI_NoBlockedCodes'. The duplicate key value is (1).
The statement has been terminated.
So if that error message is acceptable to you, this could be one way to go. Note, that if you have a numbers table, you can use that instead of my dummy Two table.
The trick here is to construct the view in such a way so that, if there's ever a match between the Products and BlockedProductCodes tables, we produce a multi-row result set. But we've also ensured that all rows have a single constant column value and there's a unique index on the result - so the error is generated.
Note that I've used my convention of prefixing the table name with DRI_ when it exists solely to enforce an integrity constraint - I don't intend that anyone will ever query this view (indeed, as shown above, this view must always in fact be empty)

Is there a way to retrieve inserted identity as well as some values from the query in an INSERT SELECT?

I have a situation in which I need to insert some values from a query into a table that has an identity PK. For some of the records, I need also to insert values in another table which has a 1-to-1 (partial) relationship:
CREATE TABLE A (
Id int identity primary key clustered,
Somevalue varchar(100),
SomeOtherValue int)
CREATE TABLE B (Id int primary key clustered,
SomeFlag bit)
DECLARE #inserted TABLE(NewId int, OldId)
INSERT INTO A (Somevalue)
OUTPUT Inserted.Id into #inserted(NewId)
SELECT SomeValue
FROM A
WHERE <certain condition>
INSERT INTO B (Id, SomeFlag)
SELECT
i.NewId, B.SomeFlag
FROM #inserted i
JOIN A ON <some condition>
JOIN B ON A.Id = B.Id
The problem is that the query from A in the first INSERT/SELECT returns records that can only be differentiated by the Id, which I cannot insert. Unfortunately I cannot change the structure of the A table, to insert the "previous" Id which would solve my problem.
Any idea that could lead to a solution?
With INSERT ... OUTPUT ... SELECT ... you can't output columns that are not in the target table. You can try MERGE instead:
MERGE INTO A as tgt
USING (SELECT Id, SomeValue FROM A WHERE <your conditions>) AS src
ON 0 = 1
WHEN NOT MATCHED THEN
INSERT (SomeValue)
VALUES (src.SomeValue)
OUTPUT (inserted.Id, src.Id) -- this is your new Id / old Id mapping
INTO #inserted
;
SCOPE_IDENTITY() returns the last identity value generated by the current session and current scope. You could stick that into a #table and use that to insert into B
SELECT SCOPE_IDENTITY() as newid into #c
Though, your INSERT INTO B join conditions implies to me that the value in B is already known ?

Best way to get multiple newly created key values from table inserts using SQL Server?

The function Scope_Identity() will provide the last generated primary key value from a table insert. Is there any generally accepted way to get multiple keys from an insertion of a set (an insert resulting from a select query)?
In SQL Server 2005 onwards, you can use the OUTPUT clause to get a returned set of values. From the linked article:
The following example creates the
EmployeeSales table and then inserts
several rows into it using an INSERT
statement with a SELECT statement to
retrieve data from source tables. The
EmployeeSales table contains an
identity column (EmployeeID) and a
computed column (ProjectedSales).
Because these values are generated by
the SQL Server Database Engine during
the insert operation, neither of these
columns can be defined in #MyTableVar.
USE AdventureWorks ;
GO
IF OBJECT_ID ('dbo.EmployeeSales', 'U') IS NOT NULL
DROP TABLE dbo.EmployeeSales;
GO
CREATE TABLE dbo.EmployeeSales
( EmployeeID int IDENTITY (1,5)NOT NULL,
LastName nvarchar(20) NOT NULL,
FirstName nvarchar(20) NOT NULL,
CurrentSales money NOT NULL,
ProjectedSales AS CurrentSales * 1.10
);
GO
DECLARE #MyTableVar table(
LastName nvarchar(20) NOT NULL,
FirstName nvarchar(20) NOT NULL,
CurrentSales money NOT NULL
);
INSERT INTO dbo.EmployeeSales (LastName, FirstName, CurrentSales)
OUTPUT INSERTED.LastName,
INSERTED.FirstName,
INSERTED.CurrentSales
INTO #MyTableVar
SELECT c.LastName, c.FirstName, sp.SalesYTD
FROM HumanResources.Employee AS e
INNER JOIN Sales.SalesPerson AS sp
ON e.EmployeeID = sp.SalesPersonID
INNER JOIN Person.Contact AS c
ON e.ContactID = c.ContactID
WHERE e.EmployeeID LIKE '2%'
ORDER BY c.LastName, c.FirstName;
SELECT LastName, FirstName, CurrentSales
FROM #MyTableVar;
GO
SELECT EmployeeID, LastName, FirstName, CurrentSales, ProjectedSales
FROM dbo.EmployeeSales;
GO
Use the row count and last identity value....
DECLARE #LastID int
DECLARE #Rows int
--your insert from a select here
SELECT #LastID=##IDENTITY, #Rows=##ROWCOUNT
--set of rows you want...
SELECT * FROM YourTable Where TableID>#LastID-#Rows AND TableID<=#LastID

Resources