How to copy an entity tree in database - sql

I have a pretty standard relational data situation in which there is a root entity (and corresponding table) with children entities. These children have children entities and so on and so forth for about 6 levels. Each level has a many children to one parent relationship. I would like to write a procedure that effectively copies the root entity and all of its children entities (recursively copying the childrens' children), creating new entities for each along the way while storing each in its respective table.
I know this could be done with nested cursors, but I don't want to do it that way. I know that there is a more elegant solution out there I just need help creating it. I have a feeling that the solution lies in a combination of OUTPUT clauses and MERGE statements.
If you could, please tailor your answer to the novice SQL developer level. I will need an explanation or a link to an explanation for any structure you use that is outside of the basic SELECT INSERT UPDATE and DELETE.
Thank you for your time.

You need to use common table expression. Check this:
http://blog.sqlauthority.com/2012/04/24/sql-server-introduction-to-hierarchical-query-using-a-recursive-cte-a-primer/

I'll assume that you want to copy a subset of the data that is in a hierarchy of tables. By hierarchy I mean tables that are interrelated through foreign keys in the obvious sense of the word. For example, Customers would be a root table, Orders a child of it and OrderDetails another child (at the 3rd level).
First we copy the root table of the hierarchy:
MERGE RootTable as target
USING (
SELECT *
FROM RootTable
WHERE SomeCondition
) AS src
ON 1=2 -- this is so that all rows that do not match will be added
WHEN NOT MATCHED THEN INSERT (AllColumns) VALUES (AllColumns)
OUTPUT src.ID as OldID, INSERTED.ID as NewID INTO #RootTableMapping
Now we have a 1 to 1 mapping of the copy source and copy target IDs of the root table in #RootTableMapping. Also, all root rows were copied.
We now need to copy all child tables. Here's the statement for one:
MERGE ChildTable as target
USING (
SELECT *, #RootTableMapping.NewID AS NewParentID
FROM ChildTable
JOIN #RootTableMapping ON ChildTable.RootID = #RootTableMapping.OldID
WHERE SomeCondition
) AS src
WHEN NOT MATCHED THEN INSERT (AllColumns, RootID) VALUES (AllColumns, NewParentID)
Here, we obtain for each child row the ID of the cloned root table row so that we can link up the hierarchy. For that we use #RootTableMapping. We copy all columns unmodified, except for the ID of the parent which we substitute with the NewID from the mapping.
You'd need one such MERGE statement for each child table. The concept also extends to hierarchies with more than 2 levels by adding additional joins. All levels except for the bottom level must record the mapping of copy-source IDs to copy-target IDs to allow the next layer below to obtain the new IDs.
Feel free to ask further questions in case I did not make everything clear enough. I know this is a rough sketch.

Related

Using CTEs to show transitive relationships in data

I'm working on an archaeological database which includes a couple of tables describing the spatial relationship between stratigraphic units. It's quite simple -- a unit is either above or below another unit. For this I have a table that records unit_1, unit_2 and the type of spatial relationship between them (above or below). I also want to generate a view which also records the transitive counterpart. In other words, if unit A is above unit B, I also want a temporary row stating that Unit B is below unit A.
This is how my CTE looks currently. The error I get is "ERROR: relation "matrix_cte" does not exist", so this is probably not the way to do it. But the idea here is that when the relation is 'above' (which is the same as 1), the INSERT command should add a new line to the table created where the two units are reversed, and the relationship is below (or 2). Any help greatly appreciated, thanks in advance.
WITH matrix_cte (unit, related_unit, relationship)
AS (SELECT lookup_unit,
lookup_unit_2,
lookup_unit_relationship
FROM register_unit_matrix)
INSERT INTO matrix_cte(unit, related_unit, relationship)
SELECT lookup_unit_2, lookup_unit, 2
FROM (register_unit_matrix
INNER JOIN matrix_cte ON ((register_unit_matrix.lookup_unit = matrix_cte.unit)))
WHERE relationship = 1;
You can't INSERT into CTE. CTE is a logical table, it is an alias to a result set. You can SELECT from CTE.
Not really sure what you are trying to achieve there.
ERROR: relation "matrix_cte" does not exist
This error messages means that you can INSERT only into the relations (tables). CTE is not a table, it is not a permanent object in the database and your database doesn't have a table called matrix_cte.
To generate all relationships, both direct and inverse you can UNION two result sets together. If your original table has only relationships in one direction, then you can use UNION ALL and the query will be faster. I mean, if original table never has two rows for the same pair of units:
unit1, unit2, 1
unit2, unit1, 2
then you can use UNION ALL below. If original table may have such duplicates, you should use UNION to remove extra duplicates.
-- all direct relationships as they are
SELECT
lookup_unit,
lookup_unit_2,
lookup_unit_relationship
FROM register_unit_matrix
UNION
-- inverse all relationships
SELECT
lookup_unit_2,
lookup_unit,
CASE WHEN lookup_unit_relationship = 1 THEN 2 ELSE 1 END AS lookup_unit_relationship
FROM register_unit_matrix
You can put this query above into a view, or use as is.

Displaying multiple columns on one row (SQL)

I have a report I am trying to make that displays parent information and all children in one household on ONE row.
There is no "parent" table that stores the information on parents and there is no ID that links parents to child and no ID that links sibling to sibling. The only way to tell if they are siblings is if they have the same address (logic being that if they have the same address, they live together, and are part of the same household). All the information is pulled from a "student" table or a custom field in the student table that stores the parent information, address they live at, etc.
Instead of displaying parent info twice I want to display
the information like this:
Parent_name, address, phone,child1_name, child1_schoolname, child1_age, child2_name, child2_schoolname, child2_age, etc(for every child in that household)
The problem is that not every household will have the same amount of children and I can only link siblings by their address.
How can I display all information for each household on ONE row? Is this possible and how? I've tried pivot table but with no avail.
This is a classic 'you shouldn't be doing reports in the database' question. A database is for data retrieval, not data formatting. But let's assume you know this and need to do it anyway for some reason.
The algorithm I'd use for this would be
Create some windowed queries across the data; group by address (the joinable value) and sort by age desc.
Create a query that utilize this window and returns the first item in each group.
Create additional queries that return the second, the third, the fourth, in each group. etc.
Outer join these together.
This is going to be far easier if you define some maximum number of siblings (five?) as opposed to dynamically building these siblings.
If the parents are in the same table, how do you know which items are parents and which are children?
In case you have two tables one for Parent(first table) and one for Children(second table) as below:
You can do something like that in your data model:
select Parent.NAME as parent_name,
Parent.ADDRESS as parent_address,
Parent.PHONE AS phone,
(
select listagg(Child.NAME,',')
within group(order by Child.NAME)
from CHILD Child
where Child.ADDRESS=Parent.ADDRESS
)as children_names,
(
select
listagg(Child.AGE,',')
within group(order by Child.NAME)
from CHILD Child
where Child.ADDRESS=Parent.ADDRESS
)as children_ages
from PARENT Parent .
And you will have the output query result:
Listagg is your solution which operates as you want bringing muliple rows in one.
However,listagg is compatible for database 11g and newest versions,
so in case you have older version,this is not going to work.
Hope this help.

Inserting Data into Tables and Integrity

This is my first post, so please excuse me for any obvious or simple questions as I am very new to programming and all my projects are a first to me.
I am currently working on my first database project. A relational database using Oracle sql. I'm new on my course, so I am not sure on all the concepts yet, but working at it.
I have used some modelling software to help me construct a 13 table database. I have setup all my columns and assigned primary and foreign keys to all 13 tables. What I am looking to do now is insert 10 rows of test data into each table. I have done the parent tables but am confused about the child tables. When I assign ID numbers to all the parent tables primary keys, will the child tables foreign keys be populated at the same time?
I have not used sequences yet as I'm not 100% how to make them work, but instead inputted my own values like 100, 101, 102 etc. I know those values need to be in the foreign key, but wouldn't manually inserting them into many tables get confusing?
Is there an easier approach to this or am I over complicating the process?
I will need to use some queries later but I just want to be happy that the data is sound.
Thanks for your help
Rob
No, the child table data won't be populated automatically-- if there is a child table, that implies that there is a 0 or 1 to m relationship between the two. One row in the parent table may have 0 rows in the child table or it may have dozens so nothing could possibly be populated automatically.
If you are manually assigning primary key values, you'd need to hard code those same values as the foreign key values when you insert data into the child tables. In the real world, you wouldn't manually insert data into many tables at once, you'd have an application that did so and that knew what keys to use based on parameters passed in or by getting the currval of the sequence used to populate the primary key after inserting into the parent table.
Its necessary that data for foreign key should be present in parent table, but not the other way around.
If you want to create test data, i suggest you use something like below query.
insert into child_table(fk_column,column1,column2....)
select pk_column,'#dummy_value1#','#dummy_value2#',..
from parent_table
if you have 10 rows in parent, this will add 10 rows in child.
If you want more rows, e.g. 100 for each parent value you need to duplicate the parent data. for that use below query.
insert into child_table(fk_column,column1,column2....)
select pk_column,'#dummy_value1#','#dummy_value2#',..
from parent_table
join (select level from dual connect by level<10)
this will add 100 child values for 10 parent values..

Select all tables in schema based on their parent-child hierarchy

My requirement is to delete data from couple of table selected dynamically based on search condition.
So my cursor should fetch tables in their parent-child hierarchy so that it will not give exception 'CHILD RECORD FOUND' while deleting records.
lets take exable
Table A is child of Table B
Table B is child of Table C
Table D is child of table G
So it should delete in this sequence.
A then
B or D then
D or G
If you use cascading foreign keys, you don't have to worry about table order. Just delete from the top of the hierarchy (table G in in your example) and then all dependent rows in the descendent tables will be deleted atomically.
Read more about Cascading Referential Integrity Constraints.
Re your comment:
Also it should be generic enough to handle delete even if delete cascade is not there and we do not have knowledge of dependencies.
See these questions for tips on discovering constraints in Oracle:
List of foreign keys and the tables they reference
Oracle - Recursive query (for table dependencies) is not recursing (not as much as I'd like)
Thanks for the question regarding "All Parent - Child tables in the database", version 8.x

Ordered tree in a database

I have a folder structure which I need to store in a database. Each folder has a name, a primary key and a foreign key to their parent folder. So a folder can have sub folders.
What I am having trouble with is when a user wants to move up or down or add a new sub folder (adding a sub folder is added at the end of the tree) and I want to record the order of the sub folders.
How do I record the user ordering in a table?
So if I have sub folders B, C and D under A (in that order) and I move D up, then the order is B, D and then C. How do you reflect this in a database?
If you are looking to create a sub-hirarchy you can apply a order by clause with a integer to sort. for example
ID, ParentID, SortOrder, Name
Where SortOrder is a integer between 0 and foldercount-1 then just apply a
ORDER BY SortOrder ASC
I would also suggest you add a Lineage and Depth field to your table, that way you can easily traverse up and down the tree a bit quicker without as expensive queries. Have a look at this article.
There is an alternative way to keep hierarchical structures in one table, called nested sets. This model allow faster queries for children or parent nodes by the price of alterning tree.
Consider table
id left right node
0 0 9 root node
1 1 4 left node
2 5 8 right node
3 2 3 left sub node
.... etc
In order to get all parents of node N we need to find all records where (left-i; right-i) will include set (letf-N; right-N)
In order to get all children of node N you need to find all records where (left-i; right-i) are included in (left-N; right-N)
So nested set model allows to make simple hierarchical queries without recursion.
Here is wiki on Nested Set Model
Have you considered the HierarchyID data type?
http://msdn.microsoft.com/en-us/library/bb677290
See http://msdn.microsoft.com/en-us/magazine/cc794278.aspx
Perhaps what you're looking for is hierarchical queries. This enables you to automatically do a depth-first query on your hierarchical table, and define a sort order on any level of the hierarchy (so that each folder is ordered, but after each item you will get its sub-items before getting the next item on that folder).