How to gather hierarchical data items and build a tree considering their dependencies - sql

There is a table containing hierarchical data, e.g.:
| table "attribute_instances" |
+----+----------+------------+---------------+----------+
| id | tree_ref | parent_ref | attribute_ref | data_ref |
+----+----------+------------+---------------+----------+
| 1 | 1 | -1 | 1 | 1 |
| 2 | 1 | 1 | 2 | 2 |
| 3 | 2 | -1 | 1 | 3 |
| 4 | 2 | 3 | 2 | 2 |
It contains many separate trees (see tree_ref), each of them instantiating some attributes (see attribute_ref) and have a data reference data_reference, where data might be referenced in other trees, too.
Now, those trees should be merged into a single tree, in which (by now) up to 5 attributes may be chosen as level for that tree, e.g.:
attribute => level
------------------
2 => 1
1 => 2
What I need is one or more queries, that collects the data from table attribute_instances and gives a result as follows:
| table "merged_attribute_instances" |
+----+------------+---------------+----------+
| id | parent_ref | attribute_ref | data_ref |
| 5 | -1 | 2 | 2 |
| 6 | 5 | 1 | 1 |
| 7 | 5 | 1 | 3 |
This is the desired merged tree:
id:5 - data_ref:2
id:6 - data_ref:1
id:7 - data_ref:3
Note, that attribute_ref = 2 occurs only once in the resulting tree, as all instances of it have same data_ref value (that is 2).
I've tried some joins like
select *
from attribute_instances a
join attribute_instances b on a.tree_ref = b.tree_ref
But that seems to me being bad for having user-defined tree depth. I'm sure there is a better solution.
UPDATE: I should add, that table merged_attribute_instances is a temporary table. And the collecting query is iterated with for..do. In the loop the collected attribute_instances are then added to the temporary table.

OK, then use this:
SET TERM ^ ;
create or alter procedure GETTREENODES
returns (
ID integer,
TREE_REF integer,
PARENT_REF integer,
ATTRIBUTE_REF integer,
DATA_REF integer)
as
declare variable DATAREFEXISTS varchar(4096);
begin
DATAREFEXISTS = ',';
for
Select id, tree_ref, parent_ref, attribute_ref, data_ref from attribute_instances
into :id, :tree_ref, :parent_ref, :attribute_ref, :data_ref
do begin
IF (position(',' || data_ref || ',', DATAREFEXISTS) =0) THEN
begin
suspend;
DATAREFEXISTS = DATAREFEXISTS || data_ref || ',' ;
end
end
end^
SET TERM ; ^
/* Following GRANT statetements are generated automatically */
GRANT SELECT ON ATTRIBUTE_INSTANCES TO PROCEDURE GETTREENODES;
/* Existing privileges on this procedure */
GRANT EXECUTE ON PROCEDURE GETTREENODES TO SYSDBA;
Call it like this:
Select * from gettreenodes
order by tree_ref, parent_ref

Related

Is there a way in Postgres / SQL to substitute characters in strings from columns where the characters in the string are column names?

I have a table called sentences, and a table called logs.
The sentences table looks like this:
|------------|---------------------------------------|
| id | sentence |
|------------|---------------------------------------|
| 1 | [var1] says hello! |
|------------|---------------------------------------|
| 2 | [var1] says [var2]! |
|------------|---------------------------------------|
| 3 | [var1] says [var2] and [var3]! |
|------------|---------------------------------------|
| 4 | [var4] says [var2] to [var1]! |
|------------|---------------------------------------|
The logs table looks like this:
|------------|------------------|--------------|--------------|--------------|--------------|
| id | sentenceId | var1 | var2 | var3 | var4 |
|------------|------------------|--------------|--------------|--------------|--------------|
| 1 | 1 | Sam | | | |
|------------|------------------|--------------|--------------|--------------|--------------|
| 2 | 2 | Joe | what's up | | |
|------------|------------------|--------------|--------------|--------------|--------------|
| 3 | 3 | Tim | hey | how are you | |
|------------|------------------|--------------|--------------|--------------|--------------|
| 4 | 4 | Joe | hi | | Tiffany |
|------------|------------------|--------------|--------------|--------------|--------------|
The result I am trying to get is:
|------------|-----------------------------------------|
| logs.id | sentences.sentence |
|------------|-----------------------------------------|
| 1 | [Sam] says hello! |
|------------|-----------------------------------------|
| 2 | [Joe] says [what's up]! |
|------------|-----------------------------------------|
| 3 | [Tim] says [hey] and [how are you]! |
|------------|-----------------------------------------|
| 4 | [Tiffany] says [hi] to [Joe] |
|------------|-----------------------------------------|
I'm not sure how to write the SQL query to make the database do the text substitutions for me.
I could just select everything from both tables using an inner join, and then loop through in code and do the substitutions myself. I.e.:
SELECT logs.id, sentences.sentence, logs.var1, logs.var2, logs.var3, logs.var4 FROM logs INNER JOIN sentences ON logs.sentenceId = sentences.id
And then in code:
logs.forEach(log => log.sentence.replace(/\[(.*?)\]/g, ($matchedString, $columnName) => log[$columnName] ))
But if possible, I'd like the database to do that for me so that I don't have to select more data than I need.
I would write a function to do that:
create function replace_vars(p_sentence text, p_vars jsonb)
returns text
as
$$
declare
l_rec record;
l_result text;
begin
l_result := p_sentence;
for l_rec in select * from jsonb_each_text(jsonb_strip_nulls(p_vars)) as x(var,value)
loop
l_result := replace(l_result, l_rec.var, l_rec.value);
end loop;
return l_result;
end;
$$
language plpgsql;
Then you can use it like this:
select s.id, s.sentence, replace_vars(s.sentence, to_jsonb(l)) new_sentence
from sentences s
left join logs l on l.sentenceid = s.id;
Online example
Elegance is always nice, but sometimes brute force gets it done.
with logsnn (sentenceid, var1, var2, var3,var4) as
( select sentenceid
, coalesce(var1,'')
, coalesce(var2,'')
, coalesce(var3,'')
, coalesce(var4,'')
from logs
)
select s.id
,(replace(replace(replace(replace(s.sentence
, '[var1]',l.var1)
, '[var2]',l.var2)
, '[var3]',l.var3)
, '[var4]',l.var4)
) AS sentence
from sentences s
left join logsnn l
on l.sentenceid = s.id;
If you really need the brackets on the result change the replacement settings to
'[var1]','[' || l.var1 || ']')
The answer by #JNevill is close to the same, bit I believe that one will return Null if any of var1,var2,var3, or var4 are Null. The CTE here changes Null with the empty string. Postgres does not consider the empty string the same as null.

Mutating error on an AFTER insert trigger

CREATE OR REPLACE TRIGGER TRG_INVOICE
AFTER INSERT
ON INVOICE
FOR EACH ROW
DECLARE
V_SERVICE_COST FLOAT;
V_SPARE_PART_COST FLOAT;
V_TOTAL_COST FLOAT;
V_INVOICE_DATE DATE;
V_DUEDATE DATE;
V_REQ_ID INVOICE.SERVICE_REQ_ID%TYPE;
V_INV_ID INVOICE.INVOICE_ID%TYPE;
BEGIN
V_REQ_ID := :NEW.SERVICE_REQ_ID;
V_INV_ID := :NEW.INVOICE_ID;
SELECT SUM(S.SERVICE_COST) INTO V_SERVICE_COST
FROM INVOICE I, SERVICE_REQUEST SR, SERVICE S, SERVICE_REQUEST_TYPE SRT
WHERE I.SERVICE_REQ_ID = SR.SERVICE_REQ_ID
AND SR.SERVICE_REQ_ID = SRT.SERVICE_REQ_ID
AND SRT.SERVICE_ID = S.SERVICE_ID
AND I.SERVICE_REQ_ID = V_REQ_ID;
SELECT SUM(SP.PRICE) INTO V_SPARE_PART_COST
FROM INVOICE I, SERVICE_REQUEST SR, SERVICE S, SERVICE_REQUEST_TYPE SRT,
SPARE_PART_SERVICE SRP,
SPARE_PART SP
WHERE I.SERVICE_REQ_ID = SR.SERVICE_REQ_ID
AND SR.SERVICE_REQ_ID = SRT.SERVICE_REQ_ID
AND SRT.SERVICE_ID = S.SERVICE_ID
AND S.SERVICE_ID = SRP.SERVICE_ID
AND SRP.SPARE_PART_ID = SP.SPARE_PART_ID
AND I.SERVICE_REQ_ID = V_REQ_ID;
V_TOTAL_COST := V_SERVICE_COST + V_SPARE_PART_COST;
SELECT SYSDATE INTO V_INVOICE_DATE FROM DUAL;
SELECT ADD_MONTHS(SYSDATE, 1) INTO V_DUEDATE FROM DUAL;
UPDATE INVOICE
SET COST_SERVICE_REQ = V_SERVICE_COST, COST_SPARE_PART =
V_SPARE_PART_COST,
TOTAL_BALANCE = V_TOTAL_COST, PAYMENT_DUEDATE = V_DUEDATE, INVOICE_DATE =
V_INVOICE_DATE
WHERE INVOICE_ID = V_INV_ID;
END;
I'm trying to calculate some columns after the user inserts a row.
Using the service_request_id I want to calculate the service/parts/total cost. Also, I would like to generate the creation and due dates. But, I keep getting
INVOICE is mutating, trigger/function may not see it
Not sure how the table is mutating after the insert statement.
Not sure how the table is mutating after the insert statement.
Imagine a simple table:
create table x(
x int,
my_sum int
);
and an AFTER INSERT FOR EACH ROW trigger, similar to yours, which calculates a sum of all values in the table and updates my_sum column.
Now imagine this insert statement:
insert into x( x )
select 1 as x from dual
connect by level <= 1000;
This single statement basically inserts 1000 records, each one with 1 value, see this demo: http://sqlfiddle.com/#!4/0f211/7
Since in SQL each individual statement must be ATOMIC (more on this here: Statement-Level Read Consistency, Oracle is free to perform this query in any way as long as the final result is correct (consistent). It can save records in the order of execution, maybe in reverse order, it can divide the batch into 10 threads and do it in parallel.
Since the trigger is fired individually after inserting each row, and it cannot know in advance the "final" result, then considering the above all the below results are possible depending on "internal" method choosed by Oracle to execute this query. As you see, these result do not meet the definition of consistency. And Oracle prevents this issuing mutating table error.
In other words - your assumption are bad and your design is flawed, you need to change it.
| X | MY_SUM |
|---|--------|
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 1 | 4 |
...
...
or maybe :
| X | MY_SUM |
|---|--------|
| 1 | 1000 |
| 1 | 1000 |
| 1 | 1000 |
| 1 | 1000 |
| 1 | 1000 |
| 1 | 1000 |
| 1 | 1000 |
...
or maybe:
| X | MY_SUM |
|---|--------|
| 1 | 4 |
| 1 | 8 |
| 1 | 12 |
| 1 | 16 |
| 1 | 20 |
| 1 | 24 |
| 1 | 28 |
...
...

SQL - Convert non-null adjacency list to path

I am working with some tables that represent a file system, and I need to select the full path of each folder as a flattened string.
The first table lists the details of each folder:
CREATE TABLE Folders(
FolderID int IDENTITY(1,1) NOT NULL,
[Name] nvarchar(255) NOT NULL)
The second table lists transitive closures of folder relationships:
CREATE TABLE FolderClosures(
FolderClosuresID int IDENTITY(1,1) NOT NULL,
AncestorFolderID int NOT NULL, --Foreign key to Folders.FolderID
DescendantFolderID int NOT NULL --Foreign key to Folders.FolderID
IsDirect bit NOT NULL)
For sample data, let's assume the following folders exist:
Documents/
Documents/Finance/
Documents/HumanResources/
Documents/HumanResources/Training/
These would be persisted in those tables as follows:
| FolderID | Name |
+----------+----------------+
| 1 | Documents |
| 2 | Finance |
| 3 | HumanResources |
| 4 | Training |
| FolderClosureID | AncestorFolderID | DescendantFolderID | IsDirect |
+-----------------+------------------+--------------------+----------+
| 1 | 1 | 1 | 0 |
| 2 | 2 | 2 | 0 |
| 3 | 1 | 2 | 1 |
| 4 | 3 | 3 | 0 |
| 5 | 1 | 3 | 1 |
| 6 | 4 | 4 | 0 |
| 7 | 1 | 4 | 0 |
| 8 | 3 | 4 | 1 |
Some details to note:
Every folder has an "identity row" in FolderClosures, where AncestorFolderID = DescendantFolderID AND IsDirect = 0.
Every folder that is not a top-level folder has exactly one row in FolderClosures where IsDirect = 1
FolderClosures can contain many rows per folder, where AncestorFolderID <> DescendantFolderID AND IsDirect = 0. Each of these represents a "grandparent" or more distant relationship.
Since no columns are nullable, no rows explicitly state that a given folder is a top-level folder. This can only be discerned by checking that there are no rows in FolderClosures where IsDirect = 1 AND DescendantFolderID = SomeID where SomeID is the ID of the folder in question.
I want to be able to run a query that returns this data:
| FolderID | Path |
+----------+------------------------------------+
| 1 | Documents/ |
| 2 | Documents/Finance/ |
| 3 | Documents/HumanResources/ |
| 4 | Documents/HumanResources/Training/ |
Folders may be nested at unlimited depth, but realistically probably only up to 10 levels. Queries may require returning paths for a few thousand folders.
I've found a lot of advice on creating this type of query when data is persisted as an adjacency list, but I haven't been able to find an answer for a transitive closure setup like this. The adjacency list solutions I've found rely on rows being persisted with nullable parent folder IDs, but that doesn't work here.
How can I get the desired output?
If it helps, I am using SQL Server 2016.
One way to get desired output is to do a recursive query. For this, I think the best is to only use the rows that have IsDirect = 1 and use the anchor as all folders that don't have direct parent in FolderClosures, which should be all your root folders.
WITH FoldersCTE AS (
SELECT F.FolderID, CAST(F.Name as NVARCHAR(max)) Path
FROM Folders F
WHERE NOT EXISTS (
SELECT 1 FROM FolderClosures FC WHERE FC.IsDirect = 1 AND FC.DescendantFolderID = F.FolderID
)
UNION ALL
SELECT F.FolderID, CONCAT(PF.Path, '\', F.Name)
FROM FoldersCTE PF
INNER JOIN FolderClosures FC
ON FC.AncestorFolderID = PF.FolderId
AND FC.IsDirect = 1
INNER JOIN Folders F
ON F.FolderID = FC.DescendantFolderID
)
SELECT *
FROM FoldersCTE
OPTION (MAXRECURSION 1000) --> how many nested levels you think you will have
This produces:
FolderID Path
1 Documents
2 Documents\Finance
3 Documents\HumanResources
4 Documents\HumanResources\Training
Hope it helps.

Copy and Cascade insert using PL/SQL

Given data structure:
I have the following table My_List, where Sup_ID is Primary Key
My_List
+--------+----------+-----------+
| Sup_ID | Sup_Name | Sup_Code |
+--------+----------+-----------+
| 1 | AA | 23 |
| 2 | BB | 87 |
| 3 | CC | 90 |
+--------+----------+-----------+
And the following table _MyList_details, where Buy_ID is Primary Key and Sup_ID is Foreign Key points at My_List.Sup_ID
My_List_details
+--------+--------+------------+------------+------------+
| Buy_ID | Sup_ID | Sup_Detail | Max_Amount | Min_Amount |
+--------+--------+------------+------------+------------+
| 23 | 1 | AAA | 1 | 10 |
| 33 | 2 | BBB | 11 | 20 |
| 43 | 3 | CCC | 21 | 30 |
+--------+--------+------------+------------+------------+
Finally, I have the table My_Sequence as follow:
My_Sequence
+-----+------+
| Seq | Name |
+-----+------+
| 4 | x |
| 5 | y |
| 6 | z |
+-----+------+
---------------------------------------------------
Objectives
Write PL/SQL script to:
Using a cursor, I need to copy My_List records and re-insert it with the new Sup_ID copied from My_Sequence.Seq.
I need to copy My_List_details records and re-insert them with the new Sup_ID foreign key.
------------------------------------------------------------------------------
Expected Outcome
My_List
+--------+----------+----------+
| Sup_ID | Sub_Name | Sub_Code |
+--------+----------+----------+
| 1 | AA | 23 |
| 2 | BB | 87 |
| 3 | CC | 90 |
| 4 | AA | 23 |
| 5 | BB | 87 |
| 6 | CC | 90 |
+--------+----------+----------+
My_List_details
+--------+--------+------------+------------+------------+
| Buy_ID | Sup_ID | Sub_Detail | Max_Amount | Min_Amount |
+--------+--------+------------+------------+------------+
| 23 | 1 | AAA | 1 | 10 |
| 33 | 2 | BBB | 11 | 20 |
| 43 | 3 | CCC | 21 | 30 |
| 53 | 4 | AAA | 1 | 10 |
| 63 | 5 | BBB | 11 | 20 |
| 73 | 6 | CCC | 21 | 30 |
+--------+--------+------------+------------+------------+
What I have started with is the following:
DECLARE
NEW_Sup_ID Sup_ID%type := Seq;
c_Sup_Name Sup_Name%type;
c_Sup_Code Sup_Code%type;
c_Buy_ID Buy_ID%type;
c_Sup_Detail Sup_Detail%type;
c_Max_Amount Max_Amount%type
c_My_Min_Amount Min_Amount%type
CURSOR c_My_List
IS
SELECT * FROM My_List;
CURSOR c_My_List_details
IS
SELECT * FROM My_List_details
BEGIN
FETCH c_My_List INTO NEW_Sup_ID, c_Sup_Name, c_Sup_Code;
INSERT INTO My_List;
FETCH c_My_List_details INTO c_Buy_ID, NEW_Sup_ID, c_Sup_Detail, c_Max_Amount, c_Min_Amount
INSERT INTO My_List_details
END;
/
Aside from the syntax errors, I do not see my script copy row by row and insert them to both tables accordingly. Further, the number of My_Sequence records is bigger than the number of My_List records. So what I need is, if My_List records are 50, I need the script to copy the first 50 Seq from My_Sequence.
---------------------------------------------------------------------------------
Question
How to achieve this result? I have searched and found Tom Kyte for cascade update but I am not sure if I do need to use this package, I am a bit beginner in PL/SQL and it is a bit complicated for me to utilize such a comprehensive package. Further, it's for cascade update and my case is about re-insert. I'd appreciate any help
The following Sql Statements will perform the task on the schema defined at this SqlFiddle. Note that I have changed a couple of field and table names - because they clash with Oracle terms. SqlFiddle seems to have some problems with my code, but it has been tested on another (amphibious) client which shall remain nameless.
The crucial point (As I said in my comments) is deriving a rule to map old sequence number to new. The view SEQUENCE_MAP performs this task in the queries below.
You may be disappointed by my reply because it depends upon there being the exact same number of sequence records as LIST/LIST_DETAILS, and hence it can only be run once. Your final PL/SQL can perform the necessary checks, I hope.
Hopefully it is a matter of refining the sequence_map logic to get you where you want to be.
Avoid using cursors; ideally when manipulating relational data you need to think in terms of sets of data rather than rows. This is because if you use set-thinking Oracle can do its magic in optimising, parallelising and so-on. Oracle is brilliant at scaling up - If a table is split over multiple disks, for example, it may process your request with data from the multiple disks simultaneously. If you force it into a row-by-row, procedural logic you may find that the applications you write do not scale up well.
CREATE OR REPLACE VIEW SEQUENCE_MAP AS (
SELECT OLD_SEQ, NEW_SEQ FROM
(
( SELECT ROWNUM AS RN, SUP_ID AS OLD_SEQ FROM
(SELECT SUP_ID FROM LIST ORDER BY SUP_ID) ) O
JOIN
( SELECT ROWNUM AS RN, SUP_ID AS NEW_SEQ FROM
(SELECT SEQ AS SUP_ID FROM SEQUENCE_TABLE ORDER BY SEQ) ) N
ON N.RN = O.RN
)
);
INSERT INTO LIST
(
SELECT
NEW_SEQ, SUB_NAME, SUB_CODE
FROM
SEQUENCE_MAP
JOIN LIST L ON
L.SUP_ID = SEQUENCE_MAP.OLD_SEQ
);
INSERT INTO LIST_DETAILS
(
SELECT
BUY_ID, NEW_SEQ, SUB_DETAIL, MAX_FIELD, MIN_FIELD
FROM
SEQUENCE_MAP
JOIN LIST_DETAILS L ON
L.SUP_ID = SEQUENCE_MAP.OLD_SEQ
);
I would do 2 inner loops, and search the next sequence to use.
I imagine the new buy_id is assigned via trigger using a sequence, or something equivalent, else you'll have to generate it in your code.
I have no Oracle database available to test it, so don't pay attention to syntax.
DECLARE
NEW_Sup_ID Sup_ID%type := Seq;
c_Sup_ID Sup_ID%type := Seq;
c_Sup_Name Sup_Name%type;
c_Sup_Code Sup_Code%type;
c_Buy_ID Buy_ID%type;
c_Sup_Detail Sup_Detail%type;
c_Max_Amount Max_Amount%type;
c_My_Min_Amount Min_Amount%type;
CURSOR c_My_List
IS
SELECT * FROM My_List;
CURSOR c_My_List_details
IS
SELECT * FROM My_List_details where sup_id=c_Sup_ID;
BEGIN
for c_My_List IN c_Sup_ID, c_Sup_Name, c_Sup_Code loop
select min(seq) from My_sequence into NEW_Sup_ID;
INSERT INTO My_List (sup_id,...) values (NEW_Sup_ID,...);
for c_My_List_details IN c_Buy_ID, NEW_Sup_ID, c_Sup_Detail, c_Max_Amount, c_Min_Amount loop
INSERT INTO My_List_details (sup_id, ...) values (NEW_Sup_ID,...);
end loop;
deelte from from My_sequence where seq= NEW_Sup_ID;
end loop;
commit;
END;
/

Is there a single query that can update a "sequence number" across multiple groups?

Given a table like below, is there a single-query way to update the table from this:
| id | type_id | created_at | sequence |
|----|---------|------------|----------|
| 1 | 1 | 2010-04-26 | NULL |
| 2 | 1 | 2010-04-27 | NULL |
| 3 | 2 | 2010-04-28 | NULL |
| 4 | 3 | 2010-04-28 | NULL |
To this (note that created_at is used for ordering, and sequence is "grouped" by type_id):
| id | type_id | created_at | sequence |
|----|---------|------------|----------|
| 1 | 1 | 2010-04-26 | 1 |
| 2 | 1 | 2010-04-27 | 2 |
| 3 | 2 | 2010-04-28 | 1 |
| 4 | 3 | 2010-04-28 | 1 |
I've seen some code before that used an # variable like the following, that I thought might work:
SET #seq = 0;
UPDATE `log` SET `sequence` = #seq := #seq + 1
ORDER BY `created_at`;
But that obviously doesn't reset the sequence to 1 for each type_id.
If there's no single-query way to do this, what's the most efficient way?
Data in this table may be deleted, so I'm planning to run a stored procedure after the user is done editing to re-sequence the table.
You can use another variable storing the previous type_id (#type_id). The query is ordered by type_id, so whenever there is a change in type_id, sequence has to be reset to 1 again.
Set #seq = 0;
Set #type_id = -1;
Update `log`
Set `sequence` = If(#type_id=(#type_id:=`type_id`), (#seq:=#seq+1), (#seq:=1))
Order By `type_id`, `created_at`;
I don't know MySQL very well, but you could use a sub query though it may be very slow.
UPDATE 'log' set 'sequence' = (
select count(*) from 'log' as log2
where log2.type_id = log.type_id and
log2.created_at < log.created_at) + 1
You'll get duplicate sequences, though, if two type_ids have the same created_at date.