SQL - Looping - copy category - sql-server

Assume Category table has categoryId, name and parentCategoryId columns, where categoryId is an identity field.
now what if I have to replicate a category and assign it to a new parent? Because it's not a simple insert statement. I have to insert all the subcategories and their subcategories and so on. How would I keep track of their identity fields? I would need that to assign parentCategoryId to their subcategories when inserting.
Is this question clear?

Not sure if you're asking 2 questions or just 1, i.e. replicating an entire category as a new category (i.e. copying a category) vs. re-assigning an existing category to a new parent - each would be different problems/solutions, but let's start with copying an entire category.
First, if you're using an identity-based column, the ONLY way you can do it without using the "set identity_insert on" option would be to cursor through the entire tree, starting from the root nodes and working down (i.e. insert the top-level category(ies), get the newly created identity values, insert the second-level categories, etc.). If you are in a scenario where you can make use of "set identity_insert on", or if you can replace the use of identities with explicit numbers, then you can leverage the code below.
In this code, you'll notice the use of CTE's, recursive CTE's, and ranking functions, so this assumes Sql 2005 or above. Also, the lvl, path, and cnt columns are simply included for demo purposes you can use to view if you like, not required in any final solution:
declare #root_category_id bigint,
#start_new_id_value bigint;
-- What category id do we want to move?
select #root_category_id = 3;
-- Get the current max id and pad a bit...
select #start_new_id_value = max(categoryId)
from Category;
select #start_new_id_value = coalesce(#start_new_id_value,0) + 100;
-- Show our values
select #root_category_id, #start_new_id_value;
begin tran;
set identity_insert Category on;
-- This query will give you the entire category tree
with subs (catId, parentCatId, catName, lvl, path, new_id, new_parent_id, cnt) as (
-- Anchor member returns a row for the input manager
select catId, parentCatId, catName, 0 as lvl,
cast(cast(catId as varchar(10)) as varchar(max)) as path,
#start_new_id_value + row_number() over(order by catId) - 1 as new_id,
cast(parentCatId as bigint) as new_parent_id,
count(*) over(partition by 0) as cnt
from Category
where catId = #root_category_id
union all
-- recursive member returns next level of children
select c.catId, c.parentCatId, c.catName, p.lvl + 1,
cast(p.path + '.' + cast(catId as varchar(10)) as varchar(max)),
p.cnt + row_number() over(order by c.catId) + p.new_id - 1 as new_id,
p.new_id as new_parent_id,
count(*) over(partition by p.lvl) as cnt
from subs as p -- Parent
join Category as c -- Child
on c.parentCatId = p.catId
)
-- Perform the insert
insert Category
(categoryId, Name, parentCategoryId)
select s.catId, s.catName, s.parentCatId
from subs s
--order by path;
set identity_insert Category off;
commit tran;

Interesting question.
If you're in SQL 2005+, I suppose you could build a CTE with the full tree of the category to replicate, which will put it in a temporary location to work with, away from the main table.
Then you can use a cursor to work your way down the tree and update the ID number of the parent ...
Now that I type it, it doesn't seem the most efficient solution. Perhaps you could instead do a fancy SELECT statement, which updates the ID of the parent ID as it's SELECTing?

Are you aware of nested sets? It's an alternative way of representing data in problems like this. I don't know if it would help here, but if you're not aware of it you might want to consider it. http://intelligent-enterprise.informationweek.com/001020/celko.jhtml

Related

Select latest row on duplicate values while transfering table?

I have a logging table that is live which saves my value to a table frequently.
My plan is to take those values and put them on a temporary table with
SELECT * INTO #temp from Block
From there I guess my block table is empty and the logger can keep on logging new values.
The next step is that I want to save them in a existing table. I wanted to use
INSERT INTO TABLENAME(COLUMN1,COLUMN2...) SELECT (COLUMN1,COLUMN2...) FROM #temp
The problem is that the #temp table has duplicates primary keys. And I only want to store the last ID.
I have tried DISTINCT but it didn't work. Could not get ROW_Count to work. Are there any ideas on how I should do it? I wish to make it with as few reads as possible.
Also, in the future I plan to send them to another database, how do I do that on SQL Server? I guess it's something like FROM Table [in databes]?
I couldn't get the blocks to copy. But here goes:
create TABLE Product_log (
Grade char(64),
block_ID char(64) PRIMARY KEY NOT NULL,
Density char(64),
BatchNumber char(64) NOT NULL,
BlockDateID Datetime
);
That is my table i want to store the data in. There I do not wish to have duplicates on the id. The problem is, while logging I get duplicates since I log on change. Lets say that the batchid is 1, if it becomes 2 while logging. I will get a blockid twice, both with batch number 1 and 2. How do I pick the latter?
Hope I explained enough for guidance. While logging they look like this:
id SiemensTiaV15_s71200_BatchTester_NewBatchIDValue_VALUE SiemensTiaV15_s71200_BatchTester_TestWriteValue_VALUE SiemensTiaV15_s71200_BatchTester_TestWriteValue_TIMESTAMP SiemensTiaV15_s71200_MainTank_Density_VALUE SiemensTiaV15_s71200_MainTank_Grade_VALUE
1 00545 S0047782 2020-06-09 11:18:44.583 0 xxxxx
2 00545 S0047783 2020-06-09 11:18:45.800 0 xxxxx
Please use below query,
select * from
(select id, SiemensTiaV15_s71200_BatchTester_NewBatchIDValue_VALUE,SiemensTiaV15_s71200_BatchTester_TestWriteValue_VALUE, SiemensTiaV15_s71200_BatchTester_TestWriteValue_TIMESTAMP, SiemensTiaV15_s71200_MainTank_Density_VALUE,SiemensTiaV15_s71200_MainTank_Grade_VALUE,
row_number() over (partition by SiemensTiaV15_s71200_BatchTester_NewBatchIDValue_VALUE order by SiemensTiaV15_s71200_BatchTester_TestWriteValue_TIMESTAMP desc) as rnk
from table_name) qry
where rnk=1;
INTO #temp FROM Block; INSERT INTO Product_log(Grade, block_ID, Density, BatchNumber, BlockDateID)
selct NewBatchIDValue_VALUE, TestWriteValue_VALUE, TestWriteValue_TIMESTAMP,
Density_VALUE, Grade_VALUE from
(select NewBatchIDValue_VALUE, TestWriteValue_VALUE,
TestWriteValue_TIMESTAMP, Density_VALUE, Grade_VALUE, row_number() over
(partition by BatchTester_NewBatchIDValue_VALUE order by
BatchTester_TestWriteValue_VALUE) as rnk from #temp) qry
where rnk = 1;

SSRS Stepped reported based on number

Using SSRS with SQL Server 2008 R2 (Visual Studio environment).
I am trying to produce a stepped down report based on a level/value in a table on sql server. The level act as a indent position with sort_value been the recursive parent in the report.
Sample of table in SQL Server:
Sample of output required
OK, I've come up with a solution but please note the following before you proceed.
1. The process relies on the data being in the correct order, as per your sample data.
2. If this is your real data structure, I strongly recommend you review it.
OK, So the first things I did was recreate your table exactly as per example. I called the table Stepped as I couldn't think of anything else!
The following code can then be used as your dataset in SSRS but you can obviously just run the T-SQL directly to see the output.
-- Create a copy of the data with a row number. This means the input data MUST be in the correct order.
DECLARE #t TABLE(RowN int IDENTITY(1,1), Sort_Order int, [Level] int, Qty int, Currency varchar(20), Product varchar(20))
INSERT INTO #t (Sort_Order, [Level], Qty, Currency, Product)
SELECT * FROM Stepped
-- Update the table so each row where the sort_order is NULL will take the sort order from the row above
UPDATE a SET Sort_Order = b.Sort_Order
FROM #t a
JOIN #t b on a.RowN = b.rowN+1
WHERE a.Sort_Order is null and b.Sort_Order is not null
-- repeat this until we're done.
WHILE ##ROWCOUNT >0
BEGIN
UPDATE a SET Sort_Order = b.Sort_Order
FROM #t a
JOIN #t b on a.RowN = b.rowN+1
WHERE a.Sort_Order is null and b.Sort_Order is not null
END
-- Now we can select from our new table sorted by both sort oder and level.
-- We also separate out the products based on their level.
SELECT
CASE Level WHEN 1 THEN Product ELSE NULL END as ProdLvl_1
, CASE Level WHEN 2 THEN Product ELSE NULL END as ProdLvl_2
, CASE Level WHEN 3 THEN Product ELSE NULL END as ProdLvl_3
, QTY
, Currency
FROM #t s
ORDER BY Sort_Order, Level
The output looks like this...
You may also want to consider swapping out the final statement for this.
-- Alternatively use this style and use a single column in the report.
-- This is better if the number of levels can change.
SELECT
REPLICATE('--', Level-1) + Product as Product
, QTY
, Currency
FROM #t s
ORDER BY Sort_Order, Level
As this will give you a single column for 'product' indented like this.

TSQL Incrementing Count of Variable

I have a UI that allows a user to select one or more fields they want to add to a table. This data also has an orderID associated with it that determines the field order.
When the user adds new fields, I need to find the last orderID this user used and increment it by 1, submitting all of the new fields.
For example, if there is a single record that already exists in the database, it would have an orderID of 1. When I choose to add three more fields, it would check to see the last orderID I used (1) and then increment it for each of the new records it adds, 1-4.
-- Get the last ID orderID for this user and increment it by 1 as our starting point
DECLARE #lastID INT = (SELECT TOP 1 orderID FROM dbo.BS_ContentRequests_Tasks_User_Fields WHERE QID = #QID ORDER BY orderID DESC)
SET #lastID = #lastID+1;
-- Create a temp table to hold our fields that we are adding
DECLARE #temp AS TABLE (fieldID int, orderID int)
-- Insert our fields and incremented numbers
INSERT INTO #temp( fieldID, orderID )
SELECT ParamValues.x1.value('selected[1]', 'int'),
#lastID++
FROM #xml.nodes('/root/data/fields/field') AS ParamValues(x1);
Obviously the #lastID++ part is where my issue is but hopefully it helps to understand what I am trying to do.
What other method could be used to handle this?
ROW_NUMBER() ought to do it.
select x.Value,
ROW_NUMBER() over (order by x.Value) + #lastID
from (
select 10 ParamValues.x1.value('selected[1]', 'int') Value
from #xml.nodes('/root/data/fields/field') AS ParamValues(x1)
) x
You could use a column with IDENTITY(1,1)
If you want OrderID to be unique across the entire table then see below:
Click here to take a look at another post that addresses this issue.
There are multiple ways to approach this issue, but in this case, the easiest, while reasonable, means may be to use an identity column. However, that is not as extensible as using a sequence. If you feel that you may need more flexibility in the future, then use a sequence.
If you want OrderID to be unique across the fields inserted in one batch then see below:
You should take a closer look at Chris Steele's answer.

If the contents of a database table cell is a list, how do I check if the list contains a specific value in T-SQL?

I have a SQL Server database table with a column called resources. Each cell of this column contains a comma-delimited list of integers. So the table data might look like this:
Product_ID Resources Condition
1 12,4,253 New
2 4,98,102,99 New
3 245,88 Used
etc....
I want to return the rows where a resource ID number is contained in the resources column. This doesn't work, but something like this:
SELECT *
FROM product_table
WHERE resources CONTAINS 4
If this was working, it would return the rows for product_id 1 and 2 because both of the resources cells in those rows contain the value 4. It would not return product_id 3, even though the resources cell for that row has the number 4 in it, because it's not the full comma-delimited value.
What is the correct way to do this?
Use the Split function as outlined in this resource:
CREATE FUNCTION Split
(
#delimited nvarchar(max),
#delimiter nvarchar(100)
) RETURNS #t TABLE
(
-- Id column can be commented out, not required for sql splitting string
id int identity(1,1), -- I use this column for numbering splitted parts
val nvarchar(max)
)
AS
BEGIN
declare #xml xml
set #xml = N'<root><r>' + replace(#delimited,#delimiter,'</r><r>') + '</r></root>'
insert into #t(val)
select
r.value('.','varchar(5)') as item
from #xml.nodes('//root/r') as records(r)
RETURN
END
GO
-- Create the test table and insert the test data
create table #test
(
product_id int,
resources nvarchar(max),
condition nvarchar(10)
);
insert into #test (product_id, resources, condition)
select 1, '12,4,253', 'new'
union
select 2, '4,98,102,99', 'new'
union
select 3, '245,88', 'used';
-- Use the Split function and cross apply to grab the data you need
select product_id, val, condition
from #test
cross apply dbo.split(#test.resources,',') split
where val = '4' -- split returns a string so use single quotes
you could do like this...
select *
from product_table
where ',' + resources + ',' like '%,4,%'
but this probably won't use an index so it'll be slow if the table is large.
A better solution if possible is to normalize by having an extra table with product_id and resource_id with values like (1,12), (1,4), (1, 253), (2,4), etc. It would be much faster because it'd use indexes
First of all, if you have any control over the schema, the actually correct way to do this is to have a many-to-many table of resources, so that you don't need to have a comma-separated list.
Barring that, you'd need a set of LIKE cases that are joined with OR, to deal with the different cases where the item you want is the first item, the last item, one of the middle items, or the only item.
Or use the split function as described here
and call it like this:
select * from Products where exists (select * from dbo.Split(',', Resources) where s = '4')

T-SQL: Best way to copy hierarchy data?

My database looks like this:
Questionnaire
Id
Description
Category
id
description
QuestionnaireId (FK)
Question
id
CategoryId (FK)
field
When I copy a questionnaire, I'd like to copy all the underlying tables. So this means that the table Questionnaire gets a new Id. Then, all the belonging categories of the questionnaire must also be copied. So the newly inserted categories must get the new questionnaire Id. After the categories, the questions must be copied. But the categoryId must be updated to the newly inserted category.
How can I do this using t-sql?
This is pretty easy to accomplish, but you have to keep track of everything as you go. I would generally create a single SP for this, which takes as an input the questionnaire to copy.
DECLARE #newQuestionnaireId INT
INSERT INTO Questionnaire
(Id,Description)
SELECT Id, Description
FROM Questionnaire
WHERE ID = #sourceQuestionnaireID
SET #newquestionnaireId = SCOPE_IDENTITY()
At this point you have a new header record, and the newly generated Id for the copy. The next step is to load the categories into a temp table which has an extra field for the new Id
DECLARE #tempCategories TABLE (id INT, description VARCHAR(50),newId INT)
INSERT INTO #tempCategories(id,description)
SELECT id, description FROM Category
WHERE questionnaireId = #sourceQuestionnaireId
Now, you have a temp table with all the categories to insert, along with a field to backfill the new ID for this category. Use a cursor to go over the list inserting the new record, and use a similar SCOPE_IDENTITY call to backfill the new Id.
DECLARE cuCategory CURSOR FOR SELECT Id, Description FROM #tempCategories
DECLARE #catId INT, #catDescription, #newCatId INT
OPEN cuCategory
FETCH NEXT FROM cuCategory INTO #catId,#catDescription
WHILE ##FETCH_STATUS<>0
BEGIN
INSERT INTO Category(description,questionnaireId)
VALUES(#catDescription,#newQuestionnaireId)
SET #newCatId = SCOPE_IDENTITY()
UPDATE #tempCategories SET newCatId=#newCatId
WHERE id=#catId
FETCH NEXT FROM cuCategory INTO #catId,#catDescription
END
CLOSE cuCategory
DEALLOCATE cuCategory
At this point you now have a temp table which maps the catId from the original questionnaire to the catId for the new questionnaire. This can be used to fill the final table in much the same way - which i'll leave as an excercise for you, but feel free to post back here if you have difficulty.
Finally, I would suggest that this whole operation is carried out within a transaction to save you from half completed copies when something goes wrong.
A couple of disclaimers: The above was all typed quickly, dont expect it to work off the bat. Second, Ive assumed that all your PK's are identity fields, which they should be! If they're not just replace the SCOPE_IDENTITY() calls with the appropriate logic to generate the next ID.
Edit: documentation for Cursor operations can be foundhere
I had a problem like this and began to implement the solution suggested by #Jamiec but I quickly realised that I needed a better solution because my model is much larger than that in the example cited here. I have one master table with three intermediate tables, each of which have one or more tertiary tables. And the three intermediates each had something like 50 columns. This would mean a lot of work to type all that up, particularly in the fetch part with the temporary memvars. I tried to find a way to FETCH directly into the temp table but it seems you cannot do that.
What I did was add a column to the intermediate tables called OriginalId. Here is my code translated into the model used by the asker:
DECLARE #newQuestionnaireId INT
INSERT INTO Questionnaire (Id,Description)
SELECT Id, Description FROM Questionnaire
WHERE ID = #sourceQuestionnaireID
SET #newquestionnaireId = SCOPE_IDENTITY()
INSERT INTO Category(QuestionnaireId, description, originalId)
SELECT #newquestionnaireId, description, id FROM Category
WHERE questionnaireId = #sourceQuestionnaireId
INSERT INTO Question SELECT Category.Id, Question.Field
FROM Question join Category on Question.CategoryId = Category.OriginalId
WHERE Category.QuestionnaireId = #newquestionnaireId
In my model the id fields are all Identities so you do not supply them in the inserts.
Another thing I discovered before I gave up on the CURSOR approach was this clever little trick to avoid having to type the FETCH statement twice by using an infinite WHILE loop with a BREAK:
here is a way that does not have cursors, it relies on remembering the order of events, and then using that to resolve the children.
Declare #Parrent TABLE( ID int PRIMARY KEY IDENTITY, Value nvarchar(50))
Declare #Child TABLE( ID int PRIMARY KEY IDENTITY, ParrentID int, Value nvarchar(50))
insert into #Parrent (Value) Values ('foo'),('bar'),('bob')
insert into #Child (ParrentID, Value) Values (1,'foo-1'),(1,'foo-2'),(2,'bar-1'),(2,'bar-2'),(3,'bob')
declare #parrentToCopy table (ID int) -- you can me this a collection
insert into #parrentToCopy values (2)
select * from #Parrent p inner join #Child c on p.ID = c.ParrentID order by p.ID asc, c.ID asc
DECLARE #Ids TABLE( nID INT);
INSERT INTO #Parrent (Value)
OUTPUT INSERTED.ID
INTO #Ids
SELECT
Value
FROM #Parrent p
inner join #parrentToCopy pc on pc.ID=p.ID
ORDER BY p.ID ASC
INSERT INTO #Child (ParrentID, Value)
SELECT
nID
,Value
FROM #Child c
inner join (select ID, ROW_NUMBER() OVER (ORDER BY ID ASC) AS 'RowNumber' from #parrentToCopy) o ON o.ID = c.ParrentID
inner join (select nID, ROW_NUMBER() OVER (ORDER BY nID ASC) AS 'RowNumber' from #Ids) n ON o.RowNumber = n.RowNumber
select * from #Parrent p inner join #Child c on p.ID = c.ParrentID order by p.ID asc, c.ID asc
full post is here http://bashamer.wordpress.com/2011/10/04/copying-hierarchical-data-in-sql-server/

Resources