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.
Related
I have a select query that returns a dataset with "n" records in one column. I would like to use this column as the parameter in a stored procedure. Below a reduced example of my case.
The query:
SELECT code FROM rawproducts
The dataset:
CODE
1
2
3
The stored procedure:
ALTER PROCEDURE [dbo].[MyInsertSP]
(#code INT)
AS
BEGIN
INSERT INTO PRODUCTS description, price, stock
SELECT description, price, stock
FROM INVENTORY I
WHERE I.icode = #code
END
I already have the actual query and stored procedure done; I just am not sure how to put them both together.
I would appreciate any assistance here! Thank you!
PS: of course the stored procedure is not as simple as above. I just choose to use a very silly example to keep things small here. :)
Here's two methods for you, one using a loop without a cursor:
DECLARE #code_list TABLE (code INT);
INSERT INTO #code_list SELECT code, ROW_NUMBER() OVER (ORDER BY code) AS row_id FROM rawproducts;
DECLARE #count INT;
SELECT #count = COUNT(*) FROM #code_list;
WHILE #count > 0
BEGIN
DECLARE #code INT;
SELECT #code = code FROM #code_list WHERE row_id = #count;
EXEC MyInsertSP #code;
DELETE FROM #code_list WHERE row_id = #count;
SELECT #count = COUNT(*) FROM #code_list;
END;
This works by putting the codes into a table variable, and assigning a number from 1..n to each row. Then we loop through them, one at a time, deleting them as they are processed, until there is nothing left in the table variable.
But here's what I would consider a better method:
CREATE TYPE dbo.code_list AS TABLE (code INT);
GO
CREATE PROCEDURE MyInsertSP (
#code_list dbo.code_list)
AS
BEGIN
INSERT INTO PRODUCTS (
[description],
price,
stock)
SELECT
i.[description],
i.price,
i.stock
FROM
INVENTORY i
INNER JOIN #code_list cl ON cl.code = i.code;
END;
GO
DECLARE #code_list dbo.code_list;
INSERT INTO #code_list SELECT code FROM rawproducts;
EXEC MyInsertSP #code_list = #code_list;
To get this to work I create a user-defined table type, then use this to pass a list of codes into the stored procedure. It means slightly rewriting your stored procedure, but the actual code to do the work is much smaller.
(how to) Run a stored procedure using select columns as input
parameters?
What you are looking for is APPLY; APPLY is how you use columns as input parameters. The only thing unclear is how/where the input column is populated. Let's start with sample data:
IF OBJECT_ID('dbo.Products', 'U') IS NOT NULL DROP TABLE dbo.Products;
IF OBJECT_ID('dbo.Inventory','U') IS NOT NULL DROP TABLE dbo.Inventory;
IF OBJECT_ID('dbo.Code','U') IS NOT NULL DROP TABLE dbo.Code;
CREATE TABLE dbo.Products
(
[description] VARCHAR(1000) NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE dbo.Inventory
(
icode INT NOT NULL,
[description] VARCHAR(1000) NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE dbo.Code(icode INT NOT NULL);
INSERT dbo.Inventory
VALUES (10,'',20.10,3),(11,'',40.10,3),(11,'',25.23,3),(11,'',55.23,3),(12,'',50.23,3),
(15,'',33.10,3),(15,'',19.16,5),(18,'',75.00,3),(21,'',88.00,3),(21,'',100.99,3);
CREATE CLUSTERED INDEX uq_inventory ON dbo.Inventory(icode);
The function:
CREATE FUNCTION dbo.fnInventory(#code INT)
RETURNS TABLE AS RETURN
SELECT i.[description], i.price, i.stock
FROM dbo.Inventory I
WHERE I.icode = #code;
USE:
DECLARE #code TABLE (icode INT);
INSERT #code VALUES (10),(11);
SELECT f.[description], f.price, f.stock
FROM #code AS c
CROSS APPLY dbo.fnInventory(c.icode) AS f;
Results:
description price stock
-------------- -------- -----------
20.10 3
40.10 3
Updated Proc (note my comments):
ALTER PROC dbo.MyInsertSP -- (1) Lose the input param
AS
-- (2) Code that populates the "code" table
INSERT dbo.Code VALUES (10),(11);
-- (3) Use CROSS APPLY to pass the values from dbo.code to your function
INSERT dbo.Products ([description], price, stock)
SELECT f.[description], f.price, f.stock
FROM dbo.code AS c
CROSS APPLY dbo.fnInventory(c.icode) AS f;
This ^^^ is how it's done.
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.
[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.
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')
Running the following query (SQL Server 2000) the execution plan shows that it used an index seek and Profiler shows it's doing 71 reads with a duration of 0.
select top 1 id from table where name = '0010000546163' order by id desc
Contrast that with the following with uses an index scan with 8500 reads and a duration of about a second.
declare #p varchar(20)
select #p = '0010000546163'
select top 1 id from table where name = #p order by id desc
Why is the execution plan different? Is there a way to change the second method to seek?
thanks
EDIT
Table looks like
CREATE TABLE [table] (
[Id] [int] IDENTITY (1, 1) NOT NULL ,
[Name] [varchar] (13) COLLATE Latin1_General_CI_AS NOT NULL)
Id is primary clustered key
There is a non-unique index on Name and a unique composite index on id/name
There are other columns - left them out for brevity
Now you've added the schema, please try this. SQL Server treats length differences as different data types and will convert the varchar(13) column to match the varchar(20) variable
declare #p varchar(13)
If not, what about collation coercien? Is the DB or server different to the column?
declare #p varchar(13) COLLATE Latin1_General_CI_AS NOT NULL
If not, add this before and post results
SET SHOWPLAN_TEXT ON
GO
If the name column is NVARCHAR then u need your parameter to be also of the same type. It should then pick it up by index seek.
declare #p nvarchar(20)
select #p = N'0010000546163'
select top 1 id from table where name = #p order by id desc