TSQL Reinsert deleted row w/ a PK - tsql

[just sharing...] Long time ago DB had no reference constraints and someone hard-deleted a staff person from PK-autoincrementing table that would not allow PK turned off. Orphaned a bunch of data, and I had to reinsert the row with the former PK value. DB does not allow table structure changes but allows renaming.

Without knowing exactly what you mean by auto-incrementing or why it doesn't allow 'PK Turn off', we can't really pose a different solution.
If by auto-incrementing you mean IDENTITY, then you can use SET IDENTITY_INSERT OFF to allow explicit insertion of identity values.

Here's what I did:
/****
create protoTable w/ same structure as your mainTable that has the data you are trying to fix in this example the fieldname of the primary key is FldPK
assumption here is that your primary key is getting auto incremented
get a list of the fields in the mainTable that are NOT NULL.
in this example those fields are <not null fields>
get a list of all fields in the mainTable
in this example <all fields>, rather than spell out the fields. DO NOT INCLUDE the primary key field name
***/
declare #x int, #y int, #iLast int
select #iLast = (select MAX( FldPK ) from mainTable)
set #x = 1
while #x <= #iLast
begin
select #y = (select COUNT(*) from mainTable where FldPK = #x)
if #y = 1
begin
insert into protoTable(<all fields>)
select <all fields> from mainTable where FldPK = #x
end
else
begin
insert into protoTable (<not null fields> )values('N','xyz'+convert(varchar,#x)) /*or whatever values are valid to fulfill not null*/
/* this is where you keep one or more of the missing rows to update later with the lost data */
if #x <> 126
begin
delete protoTable where FldPK = #x
end
end
set #x=#x+1
end
Then renamed mainTable for archive and protoTable to mainTable. If anyone has a slicker way of doing this I'd love to see it.

Related

Create unique constraint initially disabled

This is my table :
CREATE TABLE [dbo].[TestTable]
(
[Name1] varchar(50) COLLATE French_CI_AS NOT NULL,
[Name2] varchar(255) COLLATE French_CI_AS NULL,
CONSTRAINT [TestTable_uniqueName1] UNIQUE ([Name1]),
CONSTRAINT [TestTable_uniqueName1Name2] UNIQUE ([Name1], [Name2])
)
ALTER TABLE [dbo].[TestTable]
ADD CONSTRAINT [TestTable_uniqueName1]
UNIQUE NONCLUSTERED ([Name1])
ALTER TABLE [dbo].[TestTable]
ADD CONSTRAINT [TestTable_uniqueName1Name2]
UNIQUE NONCLUSTERED ([Name1], [Name2])
GO
ALTER INDEX [TestTable_uniqueName1]
ON [dbo].[TestTable]
DISABLE
GO
My idea is to enable/disable one or other unique contraint depending on the customer application. With this way, I can catch the thrown exception in my c# code, and display a specific error message to the GUI.
Now, my problem is to alter the collation of columns Name1 & Name2, I need to make them case sensitive (French_CS_AS). To alter these fields, I have to drop the two constraints and recreate it. According to the explained schema, I cannot create an enabled constraint and then disable it, because by some customers, I have duplicate keys for one or other constraint.
For my update script, my idea number 1 was
Save the name of enabled constraints in a temp table
Drop the constraints
Alter columns
Create DISABLED unique constraints
Enable specific constraints according to the saved values in points 1.
My problem is in point 4., I don't find how to create a disabled unique constraint with an ALTER TABLE statement. Is it possible to create it directly in the sys.indexes table ?
My idea number 2 was
Rename TestTable to TestTableCopy
Recreate TestTable with the new fields collation, and otherwise the same schema (indexes, FK, triggers, ...)
Disable specifical unique contraints in TestTable
Migrate data from TestTableCopy to TestTable
Drop TestTableCopy
In this way, my fear is to loose some links with other tables/dependencies, beceause it is a central table in my database.
Is there any other way to achieve my goal?
If necessary, I can use unique indexes instead of unique constraints.
It looks like it is impossible to create a unique index on a column that already has duplicate values.
So, rather than having a disabled unique index either:
not have an index at all (which is the same as having a disabled index from the query processor point of view),
or create a non-unique index.
For those instanses where your client has unique data create unique index. For those instanses where your client has non-unique data create non-unique index.
CREATE PROCEDURE [dbo].[spUsers_AddUsers]
#Name1 varchar(50) ,
#Name2 varchar(50) ,
#Unique bit
AS
declare #err int
begin tran
if #Unique = 1 begin
if not exists (SELECT * FROM Users WHERE Name1 = #Name1 and Name2 = #Name2)
begin
INSERT INTO Users (Name1,Name2)
VALUES (#Name1,#Name2)
set #err = ##ERROR
end else
begin
UPDATE Users
set Name1 = #Name1,
Name2 = #Name2
where Name1 = #Name1 and Name2 = #Name2
set #err = ##ERROR
end
end else begin
if not exists ( SELECT * FROM Users WHERE Name1 = #Name1 )
begin
INSERT INTO Users (Name1,Name2)
VALUES (#Name1,#Name2)
set #err = ##ERROR
end else
begin
UPDATE Users
set Name1 = #Name1,
Name2 = #Name2
where Name1 = #Name1
set #err = ##ERROR
end
if #err = 0 commit tran
else rollback tran
So first you check if you need an unique Name1 and Name2 or just Name1. Then if you do you an insert/update based on what constrain you have.

How do you get the identity value after using MERGE when there is a match?

Say I have a table with an identity field. I want to insert a record in it if it doesn't already exist. In the below example, I check if the value stored in #Field1 already exists in the table. If not, I insert a new record:
Definition of the table:
MyTable (MyTableId int Identity not null, Field1 int not null, Field2 int not null)
This is how I check if the value already exists and insert it if necessary
merge MyTable as t
using (#Field1, #Field2) as s (Field1,Field2)
on (t.Field1=s.Field1)
when not matched then
insert (Field1,Field2) values (s.Field1,s.Field2);
Getting the identity value when the record didn't already exist in the table can be done by adding:
output Inserted.MyTableId
but what if the record was already in the table (ie if there was a match)?
The only way I found is to query the table after executing the Merge statement:
select MyTableId from MyTable where Field1=#Field1
Is there a way to get the identity value directly from the Merge?
In the case when the record already exists, you can store the matched id into a variable like this:
DECLARE #MatchedId INTEGER;
MERGE MyTable as t
....
....
WHEN MATCHED THEN
UPDATE SET #MatchedId = t.MyTableId;
UPDATE:
Here's a full example. This demonstrates one way:
DECLARE #UpdateVariable bit
DECLARE #ChangeResult TABLE (ChangeType VARCHAR(10), Id INTEGER)
DECLARE #Data TABLE (Id integer IDENTITY(1,1), Val VARCHAR(10))
INSERT #Data ([Val]) VALUES ('A');
MERGE #data AS TARGET
USING (SELECT 'A' AS Val UNION ALL SELECT 'B' AS Val) AS SOURCE ON TARGET.Val = SOURCE.Val
WHEN NOT MATCHED THEN
INSERT ([Val])
VALUES (SOURCE.Val)
WHEN MATCHED THEN
UPDATE SET #UpdateVariable = 1
OUTPUT $action, inserted.Id INTO #ChangeResult;
SELECT * FROM #data
SELECT * FROM #ChangeResult
Points to note are:
$action will give you what type of action was performed for a row (INSERT, UPDATE, DELETE)
#ChangeResult table will hold the info as to what types of changes were made
for the WHEN MATCHED case, I am basically setting a dummy variable. This doesn't serve any purpose here other than to ensure the UPDATE path gets hit to generate the UPDATE row in the output. i.e. that #UpdateVariable is not used for anything else. If you actually wanted to update the existing row, then you'd put a proper UPDATE in here, but in the case where you don't want to actually UPDATE the existing row, then this "dummy" update seems to be required.
Here is an alternative and slightly simpler approach (in my opinion):
DECLARE #Id [int];
MERGE INTO [MyTable] AS [t]
USING (VALUES
(#FieldA, #FieldB)
)
AS [x] (FieldA, FieldB)
ON [t].[FieldA] = [x].[FieldA]
AND [t].[FieldB] = [x].[FieldB]
WHEN NOT MATCHED BY TARGET THEN
INSERT (FieldA, FieldB)
VALUES (FieldA, FieldB)
WHEN MATCHED THEN
UPDATE SET #Id = [t].[Id]
IF #Id IS NULL
BEGIN
SET #Id = CAST(SCOPE_IDENTITY() as [int]);
END
SELECT #Id;
If the merge statement resulted in a match then #Id will be set to the identity of the matching row. In the event of no match, the new row will have been inserted with its new identity ready to be selected from SCOPE_IDENTITY().
Here other alternative:
DECLARE #FakeVar BIT
MERGE MyTable AS T
USING (VALUES(#Field1, #Field2)) AS S (Field1, Field2)
ON (T.Field1 = S.Field1)
WHEN NOT MATCHED THEN
INSERT (Field1, Field2)
VALUES (S.Field1, T.Field2)
WHEN MATCHED THEN
UPDATE SET #FakeVar = NULL -- do nothing, only force "an update" to ensure output on updates
OUTPUT INSERTED.MyTableId ;
If you check the OUPUT doc
INSERTED
Is a column prefix that specifies the value added by the insert or update operation.
you only need to do "something" on the update set clause
When a Merge has existing insert and an update code, the update most likely requires a primary key id. That value is generally passed to the stored procedure as a separate variable. Since that variable is null on a insert and has a value on an update, it is no issue to get the last insert with Ident_Current targeting the insert table.
Example to Merge into a Project table
Merge info.Project as Target
...
on #projectId = TARGET.ProjectId
When not matched then
... -- Insert
When matched then
... -- Update
;
if (#projectId is null)
set #projectId = IDENT_CURRENT('info.Project')

T-SQL Parent Child hierarchy

I have a table and initially it has one entry as follows
ID ParentID Title
1 NULL All
This table I am using to create tree structures. The table needs to be filled with the following data stored in CSV file. Each line represents one tree path
All;World
All;World;NA
All;World;NA;Canada //Here each item represents the tree node and separated by ;
--
--
I am looking to write a query which will take input like All;World;NA and create entry in table if does not exist and return the ID of the created entry or an existing entry. So in my example with input All;World;NA the table should look like this after query is run
ID ParentID Title
1 NULL All
2 1 World
3 2 NA
and it has created 2 entries and ID=3 returned since path All;World;NA represents ID=3
If I give another input like All;World;NA;Canada, then it will create one more entry and ID=4 returned.
If I re-run query with input All;World;NA;Canada then it will find that entry exists and return ID=4
Can anyone help?
Here is something you can start with:
CREATE TABLE TREE
(
ID INT NOT NULL IDENTITY(1,1),
ParentID INT,
Name varchar(32)
);
GO
CREATE PROC InsertIntoTree #node varchar(512)
AS
BEGIN
DECLARE #xml xml,#str varchar(100),#delimiter varchar(10)
SET #delimiter =';'
SET #xml = cast(('<X>'+replace(#node,#delimiter ,'</X><X>')+'</X>') as xml)
DECLARE #nodes TABLE (NodeName varchar(32));
INSERT INTO #nodes(NodeName)
SELECT C.value('.', 'varchar(10)') as NodeName FROM #xml.nodes('X') as X(C)
DECLARE #nodename varchar(32)
DECLARE #nodeid int
DECLARE #parentNodeId int
SET #parentNodeId = null
SELECT TOP 1 #nodename=Nodename from #nodes
WHILE (##ROWCOUNT <> 0 and #nodename IS NOT NULL)
BEGIN
SET #nodeid = null
SELECT #nodeid=Id FROM TREE WHERE Name=#nodename
IF(#nodeid is null )
BEGIN
INSERT INTO TREE(ParentID,Name) VALUES(#parentNodeId,#nodename)
SET #nodeid = ##IDENTITY
END
SET #parentNodeId = #nodeid
delete from #nodes where NodeName=#nodename
SELECT TOP 1 #nodename=Nodename from #nodes
END
END
GO
To test it:
InsertIntoTree 'A;B;C'
select * from tree
I leave it to the ready to optimize.

SELECT or INSERT a row in one command

I'm using PostgreSQL 9.0 and I have a table with just an artificial key (auto-incrementing sequence) and another unique key. (Yes, there is a reason for this table. :)) I want to look up an ID by the other key or, if it doesn't exist, insert it:
SELECT id
FROM mytable
WHERE other_key = 'SOMETHING'
Then, if no match:
INSERT INTO mytable (other_key)
VALUES ('SOMETHING')
RETURNING id
The question: is it possible to save a round-trip to the DB by doing both of these in one statement? I can insert the row if it doesn't exist like this:
INSERT INTO mytable (other_key)
SELECT 'SOMETHING'
WHERE NOT EXISTS (SELECT * FROM mytable WHERE other_key = 'SOMETHING')
RETURNING id
... but that doesn't give the ID of an existing row. Any ideas? There is a unique constraint on other_key, if that helps.
Have you tried to union it?
Edit - this requires Postgres 9.1:
create table mytable (id serial primary key, other_key varchar not null unique);
WITH new_row AS (
INSERT INTO mytable (other_key)
SELECT 'SOMETHING'
WHERE NOT EXISTS (SELECT * FROM mytable WHERE other_key = 'SOMETHING')
RETURNING *
)
SELECT * FROM new_row
UNION
SELECT * FROM mytable WHERE other_key = 'SOMETHING';
results in:
id | other_key
----+-----------
1 | SOMETHING
(1 row)
No, there is no special SQL syntax that allows you to do select or insert. You can do what Ilia mentions and create a sproc, which means it will not do a round trip fromt he client to server, but it will still result in two queries (three actually, if you count the sproc itself).
using 9.5 i successfully tried this
based on Denis de Bernardy's answer
only 1 parameter
no union
no stored procedure
atomic, thus no concurrency problems (i think...)
The Query:
WITH neworexisting AS (
INSERT INTO mytable(other_key) VALUES('hello 2')
ON CONFLICT(other_key) DO UPDATE SET existed=true -- need some update to return sth
RETURNING *
)
SELECT * FROM neworexisting
first call:
id|other_key|created |existed|
--|---------|-------------------|-------|
6|hello 1 |2019-09-11 11:39:29|false |
second call:
id|other_key|created |existed|
--|---------|-------------------|-------|
6|hello 1 |2019-09-11 11:39:29|true |
First create your table ;-)
CREATE TABLE mytable (
id serial NOT NULL,
other_key text NOT NULL,
created timestamptz NOT NULL DEFAULT now(),
existed bool NOT NULL DEFAULT false,
CONSTRAINT mytable_pk PRIMARY KEY (id),
CONSTRAINT mytable_uniq UNIQUE (other_key) --needed for on conflict
);
you can use a stored procedure
IF (SELECT id FROM mytable WHERE other_key = 'SOMETHING' LIMIT 1) < 0 THEN
INSERT INTO mytable (other_key) VALUES ('SOMETHING')
END IF
I have an alternative to Denis answer, that I think is less database-intensive, although a bit more complex:
create table mytable (id serial primary key, other_key varchar not null unique);
WITH table_sel AS (
SELECT id
FROM mytable
WHERE other_key = 'test'
UNION
SELECT NULL AS id
ORDER BY id NULLS LAST
LIMIT 1
), table_ins AS (
INSERT INTO mytable (id, other_key)
SELECT
COALESCE(id, NEXTVAL('mytable_id_seq'::REGCLASS)),
'test'
FROM table_sel
ON CONFLICT (id) DO NOTHING
RETURNING id
)
SELECT * FROM table_ins
UNION ALL
SELECT * FROM table_sel
WHERE id IS NOT NULL;
In table_sel CTE I'm looking for the right row. If I don't find it, I assure that table_sel returns at least one row, with a union with a SELECT NULL.
In table_ins CTE I try to insert the same row I was looking for earlier. COALESCE(id, NEXTVAL('mytable_id_seq'::REGCLASS)) is saying: id could be defined, if so, use it; whereas if id is null, increment the sequence on id and use this new value to insert a row. The ON CONFLICT clause assure
that if id is already in mytable I don't insert anything.
At the end I put everything together with a UNION between table_ins and table_sel, so that I'm sure to take my sweet id value and execute both CTE.
This query needs to search for the value other_key only once, and is a "search this value" not a "check if this value not exists in the table", that is very heavy; in Denis alternative you use other_key in both types of searches. In my query you "check if a value not exists" only on id that is a integer primary key, that, for construction, is fast.
Minor tweak a decade late to Denis's excellent answer:
-- Create the table with a unique constraint
CREATE TABLE mytable (
id serial PRIMARY KEY
, other_key varchar NOT NULL UNIQUE
);
WITH new_row AS (
-- Only insert when we don't find anything, avoiding a table lock if
-- possible.
INSERT INTO mytable ( other_key )
SELECT 'SOMETHING'
WHERE NOT EXISTS (
SELECT *
FROM mytable
WHERE other_key = 'SOMETHING'
)
RETURNING *
)
(
-- This comes first in the UNION ALL since it'll almost certainly be
-- in the query cache. Marginally slower for the insert case, but also
-- marginally faster for the much more common read-only case.
SELECT *
FROM mytable
WHERE other_key = 'SOMETHING'
-- Don't check for duplicates to be removed
UNION ALL
-- If we reach this point in iteration, we needed to do the INSERT and
-- lock after all.
SELECT *
FROM new_row
) LIMIT 1 -- Just return whatever comes first in the results and allow
-- the query engine to cut processing short for the INSERT
-- calculation.
;
The UNION ALL tells the planner it doesn't have to collect results for de-duplication. The LIMIT 1 at the end allows the planner to short-circuit further processing/iteration once it knows there's an answer available.
NOTE: There is a race condition present here and in the original answer. If the entry does not already exist, the INSERT will fail with a unique constraint violation. The error can be suppressed with ON CONFLICT DO NOTHING, but the query will return an empty set instead of the new row. This is a difficult problem because getting that info from another transaction would violate the I in ACID.

In SQL Server 2000, how to delete the specified rows in a table that does not have a primary key?

Let's say we have a table with some data in it.
IF OBJECT_ID('dbo.table1') IS NOT NULL
BEGIN
DROP TABLE dbo.table1;
END
CREATE TABLE table1 ( DATA INT );
---------------------------------------------------------------------
-- Generating testing data
---------------------------------------------------------------------
INSERT INTO dbo.table1(data)
SELECT 100
UNION ALL
SELECT 200
UNION ALL
SELECT NULL
UNION ALL
SELECT 400
UNION ALL
SELECT 400
UNION ALL
SELECT 500
UNION ALL
SELECT NULL;
How to delete the 2nd, 5th, 6th records in the table? The order is defined by the following query.
SELECT data
FROM dbo.table1
ORDER BY data DESC;
Note, this is in SQL Server 2000 environment.
Thanks.
In short, you need something in the table to indicate sequence. The "2nd row" is a non-sequitur when there is nothing that enforces sequence. However, a possible solution might be (toy example => toy solution):
If object_id('tempdb..#NumberedData') Is Not Null
Drop Table #NumberedData
Create Table #NumberedData
(
Id int not null identity(1,1) primary key clustered
, data int null
)
Insert #NumberedData( data )
SELECT 100
UNION ALL SELECT 200
UNION ALL SELECT NULL
UNION ALL SELECT 400
UNION ALL SELECT 400
UNION ALL SELECT 500
UNION ALL SELECT NULL
Begin Tran
Delete table1
Insert table1( data )
Select data
From #NumberedData
Where Id Not In(2,5,6)
If ##Error <> 0
Commit Tran
Else
Rollback Tran
Obviously, this type of solution is not guaranteed to work exactly as you want but the concept is the best you will get. In essence, you stuff your rows into a table with an identity column and use that to identify the rows to remove. Removing the rows entails emptying the original table and re-populating with only the rows you want. Without a unique key of some kind, there just is no clean way of handling this problem.
As you are probably aware you can do this in later versions using row_number very straightforwardly.
delete t from
(select ROW_NUMBER() over (order by data) r from table1) t
where r in (2,5,6)
Even without that it is possible to use the undocumented %%LOCKRES%% function to differentiate between 2 identical rows
SELECT data,%%LOCKRES%%
FROM dbo.table1`
I don't think that's available in SQL Server 2000 though.
In SQL Sets don't have order but cursors do so you could use something like the below. NB: I was expecting to be able to use DELETE ... WHERE CURRENT OF but that relies on a PK so the code to delete a row is not as simple as I was hoping for.
In the event that the data to be deleted is a duplicate then there is no guarantee that it will delete the same row as CURRENT OF would have. However in this eventuality the ordering of the tied rows is arbitrary anyway so whichever row is deleted could equally well have been given that row number in the cursor ordering.
DECLARE #RowsToDelete TABLE
(
rowidx INT PRIMARY KEY
)
INSERT INTO #RowsToDelete SELECT 2 UNION SELECT 5 UNION SELECT 6
DECLARE #PrevRowIdx int
DECLARE #CurrentRowIdx int
DECLARE #Offset int
SET #CurrentRowIdx = 1
DECLARE #data int
DECLARE ordered_cursor SCROLL CURSOR FOR
SELECT data
FROM dbo.table1
ORDER BY data
OPEN ordered_cursor
FETCH NEXT FROM ordered_cursor INTO #data
WHILE EXISTS(SELECT * FROM #RowsToDelete)
BEGIN
SET #PrevRowIdx = #CurrentRowIdx
SET #CurrentRowIdx = (SELECT TOP 1 rowidx FROM #RowsToDelete ORDER BY rowidx)
SET #Offset = #CurrentRowIdx - #PrevRowIdx
DELETE FROM #RowsToDelete WHERE rowidx = #CurrentRowIdx
FETCH RELATIVE #Offset FROM ordered_cursor INTO #data
/*Can't use DELETE ... WHERE CURRENT OF as here that requires a PK*/
SET ROWCOUNT 1
DELETE FROM dbo.table1 WHERE (data=#data OR data IS NULL OR #data IS NULL)
SET ROWCOUNT 0
END
CLOSE ordered_cursor
DEALLOCATE ordered_cursor
To perform any action on a set of rows (such as deleting them), you need to know what identifies those rows.
So, you have to come up with criteria that identifies the rows you want to delete.
Providing a toy example, like the one above, is not particularly useful.
You plan ahead and if you anticipate this is possible you add a surrogate key column or some such.
In general you make sure you don't create tables without PK's.
It's like asking "Say I don't look both directions before crossing the road and I step in front of a bus."