Oracle - Loop through hierarchised records - sql

So, since I struggled to find an accurate title, I think a detailled shema will be much more understandable.
I have this table PROGRAM that I will reduce to 3 fields for simplicity:
ID |NAME |ID_ORIGINAL_PROGRAM
1 |name_1 |
2 |name_2 |1
3 |name_3 |1
4 |name_4 |2
5 |name_5 |3
6 |name_6 |
7 |name_7 |6
I'm trying to find a query that will allow me, with any ID as parameter to gather all the related programs to this id.
And I need to be able to send a parameter than does not necessarily has to be the "father" id of the hierarchy.
For example, if parameter ID is 1, then results will be:
ID
2
3
4
5
If parameter ID is 4, then the results will be:
ID
1
2
3
5
It seems like I'm missing some kind "loop" logic that I can't clearly identify.
I looked up at "CONNECT BY PRIOR" but was not able to grasp the concept enough to understand how to deploy it.
Edit:
So it seems I found a way through:
SELECT ID
FROM PROGRAM
START WITH ID = 67256
CONNECT BY NOCYCLE ID_ORIGINAL_PROGRAM = PRIOR ID
OR ID = PRIOR ID_ORIGINAL_PROGRAM
order by ID
I'm a bit concerned by the performances though (it takes 1 second to perform)

I suppose you need
with program( id, id_original_program ) as
(
select 1, null from dual union all
select 2, 1 from dual union all
select 3, 1 from dual union all
select 4, 2 from dual union all
select 5, 3 from dual union all
select 6, null from dual union all
select 7, 6 from dual
)
select id, sys_connect_by_path(id, '->') as path
from program
where id_original_program is not null
connect by prior id = id_original_program
start with id = 1 -- 4
order by id;
ID PATH
2 ->1->2
3 ->1->3
4 ->1->2->4
5 ->1->3->5
if value 4 is substituted, then you get
ID PATH
4 ->4
only.
Whether you substitute 1 or 4, you'll get the same result for your query.

Related

How to modify Column Data in BigQuery Table

I've been trying to search online however, I am only able to see how to add, remove, change a column in a table. Basically, I need to go through an entire column of email addresses in BigQuery and add a 2nd email address in each of the rows.
ID|Name |email
1 |Name1|email1#address.com
2 |Name2|email2#address.com
3 |Name3|email3#address.com
4 |Name4|email4#address.com
5 |Name5|email5#address.com
6 |Name6|email6#address.com
What I am looking for is some script that will go through a column and add a 2nd emailadd with a "," in the middle so that it'll look like this:
ID|Name |email
1 |Name1|email1#address.com,secondemail#address.com
2 |Name2|email2#address.com,secondemail#address.com
3 |Name3|email3#address.com,secondemail#address.com
4 |Name4|email4#address.com,secondemail#address.com
5 |Name5|email5#address.com,secondemail#address.com
6 |Name6|email6#address.com,secondemail#address.com
While all of the beginning data remains intact. Please let me know if this is possible. Also the "secondemail#address.com" is just one email address it doesn't change per user. I just need this format for a business reason.
Below is for BigQuery Standard SQL
#standardSQL
SELECT * REPLACE(email || ',secondemail#address.com' AS email)
FROM `project.dataset.table`
You can test, play with above using sample data from your question, as in below example
#standardSQL
WITH `project.dataset.table` AS (
SELECT 1 id, 'Name1' name, 'email1#address.com' email UNION ALL
SELECT 2, 'Name2', 'email2#address.com' UNION ALL
SELECT 3, 'Name3', 'email3#address.com' UNION ALL
SELECT 4, 'Name4', 'email4#address.com' UNION ALL
SELECT 5, 'Name5', 'email5#address.com' UNION ALL
SELECT 6, 'Name6', 'email6#address.com'
)
SELECT * REPLACE(email || ',secondemail#address.com' AS email)
FROM `project.dataset.table`
with output
Row id name email
1 1 Name1 email1#address.com,secondemail#address.com
2 2 Name2 email2#address.com,secondemail#address.com
3 3 Name3 email3#address.com,secondemail#address.com
4 4 Name4 email4#address.com,secondemail#address.com
5 5 Name5 email5#address.com,secondemail#address.com
6 6 Name6 email6#address.com,secondemail#address.com

oracle sql parent child with for multiple columns

I have a users table having two columns for Approval Hierarchy , Table structure is like below
User_ID Submit_to Approve_to
1 2 3
2 4 5
3 6 2
4 2 3
5 1 0
Data is just For example 0 Mention no Approver :
Submit to and Approve to both will be Approvers
I need a Query which can give the Approves details in sequence that who will be next approver for entry user have created .
Using CONNECT BY, you can connect the parent/child relationship within a table. The query below will show the approval path for each user.
Query
WITH
d (user_id, submit_to, approve_to)
AS
(SELECT 1, 2, 3 FROM DUAL
UNION ALL
SELECT 2, 4, 5 FROM DUAL
UNION ALL
SELECT 3, 6, 2 FROM DUAL
UNION ALL
SELECT 4, 2, 3 FROM DUAL
UNION ALL
SELECT 5, 1, 0 FROM DUAL)
SELECT d.user_id, LTRIM (SYS_CONNECT_BY_PATH (user_id, '<'), '<') AS approval_path
FROM d
START WITH approve_to = 0
CONNECT BY NOCYCLE PRIOR user_id = approve_to
ORDER BY user_id;
Result
In the APPROVAL_PATH column, the left most number is the USER_ID giving final approval and the right most number is the USER_ID initially submitting whatever needs to be approved.
USER_ID | APPROVAL_PATH
-----------------------
1 | 5<2<3<1
2 | 5<2
3 | 5<2<3
4 | 5<2<3<4
5 | 5

PostgreSQL Order by stepping numbers

I need to order records from a table by a column. The old system the customer was using manually selected level 1 items, then all the children of level 1 items for level 2, then so on and so forth through level 5. That is horrible IMHO, as it requires hundreds of queries and calls to the DB.
So in the new DB structure I'm trying to make it all one query to the DB if possible and have it order it correctly the first time. The customer wants it displayed to them this way so I have no choice but to figure out a way to order this way.
This is an example of the items and their level codes (1 being the single digit codes, 2 the 2 digit codes, 3 for 4 digit codes, 4 for 6 digit codes and level 5 for 8 digit codes):
It's supposed to order basically everything that starts with a 5 goes under Code 5. Everything that starts with a 51 goes under code 51. If you look at the column n_mad_id it links to the "Mother" ID of the code that is the mother of that code, so code 51's mother is code 5. Code 5101's mother is code 51. Code 5201's mother is code 52. And so on and so forth.
Then the n_nivel column is the level that the code belongs to. Each code has a level and a mother. The top level codes (i.e. 1, 2, 3, 4, 5) are all level 1 since they are only one digit.
I was hoping that there might be an easy ORDER BY way to do this. I've been playing with it for two days and can't seem to get it to obey.
The absolutely simplest way would be to cast the n_cod field to text and then order on that:
SELECT *
FROM mytable
WHERE left(n_cod::text, 1) = '5' -- optional
ORDER BY n_cod::text;
Not pretty, but functional.
You could consider changing your table definition to make n_cod of type char(8) because you do not use it as a number anyway (in the sense of performing calculations). That would make the query a lot faster.
Interesting task. As I understand that you want to get result in order like
n_id n_cod n_nivel n_mad_id
10 5 1 0
11 51 2 10
12 5101 3 11
14 510101 4 12
...
13 52 2 10
...
?
If yes then it may do the trick:
with recursive
tt(n_id, n_mad_id, n_cod, x) as (
select t.n_id, t.n_mad_id, t.n_cod, array[t.n_id]
from yourtable t where t.n_mad_id = 0
union all
select t.n_id, t.n_mad_id, t.n_cod, x || t.n_id
from tt join yourtable t on t.n_mad_id = tt.n_id)
select * from tt order by x;
Here is my original test query:
create table t(id, parent) as values
(1, null),
(3, 1),
(7, 3),
(5, 3),
(6, 5),
(2, null),
(8, 2),
(4, 2);
with recursive
tt(id, parent, x) as (
select t.id, t.parent, array[t.id] from t where t.parent is null
union all
select t.id, t.parent, x || t.id from tt join t on t.parent = tt.id)
select * from tt order by x;
and its result:
id | parent | x
----+--------+-----------
1 | (null) | {1}
3 | 1 | {1,3}
5 | 3 | {1,3,5}
6 | 5 | {1,3,5,6}
7 | 3 | {1,3,7}
2 | (null) | {2}
4 | 2 | {2,4}
8 | 2 | {2,8}
(8 rows)
Read about recursive queries.

Select to build groups by (analytic) sum

Please help me to build a sql select to assign (software development) tasks to a software release. Actually this is a fictive example to solve my real business specific problem.
I have a relation Tasks:
ID Effort_In_Days
3 3
1 2
6 2
2 1
4 1
5 1
I want to distribute the Tasks to releases which are at most 2 days long (tasks longer than 2 shall still be put into one release). In my real problem I have much more "days" available to distribute "tasks" to. Expected output:
Release Task_ID
1 3
2 1
3 6
4 2
4 4
5 5
I think I need to use analytic functions, something with sum(effort_in_days) over and so on, to get the result. But I'm I haven't used analytic functions much and didn't find an example that's close enough to my specific problem. I need to build groups (releases) if a sum (>= 2) is reached.
I would do something like:
with data as (
select 3 ID, 3 Effort_In_Days from dual union all
select 1 ID, 2 Effort_In_Days from dual union all
select 6 ID, 2 Effort_In_Days from dual union all
select 2 ID, 1 Effort_In_Days from dual union all
select 4 ID, 1 Effort_In_Days from dual union all
select 5 ID, 1 Effort_In_Days from dual
)
select id, effort_in_days, tmp, ceil(tmp/2) release
from (
select id, effort_in_days, sum(least(effort_in_days, 2)) over (order by effort_in_days desc rows unbounded preceding) tmp
from data
);
Which results in:
ID EFFORT_IN_DAYS TMP RELEASE
---------- -------------- ---------- ----------
3 3 2 1
1 2 4 2
6 2 6 3
2 1 7 4
4 1 8 4
5 1 9 5
Basically, I am using least() to convert everything over 2 down to 2. Then I am putting all rows in descending order by that value and starting to assign releases. Since they are in descending order with a max value of 2, I know I need to assign a new release every time when I get to a multiple of 2.
Note that if you had fractional values, you could end up with releases that do not have a full 2 days assigned (as opposed to having over 2 days assigned), which may or may not meet your needs.
Also note that I am only showing all columns in my output to make it easier to see what the code is actually doing.
This is an example of a bin-packing problem (see here). There is not an optimal solution in SQL, that I am aware of, except in some boundary cases. For instance, if all the tasks have the same length or if all the tasks are >= 2, then there is an easy-to-find optimal solution.
A greedy algorithm works pretty well. This is to put a given record in the first bin where it fits, probably going through the list in descending size order.
If your problem is really as you state it, then the greedy algorithm will work to produce an optimal solution. That is, if the maximum value is 2 and the efforts are integers. There might even be a way to calculate the solution in SQL in this case.
Otherwise, you will need pl/sql code to achieve an approximate solution.
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE data AS
select 3 ID, 3 Effort_In_Days from dual union all
select 1 ID, 2 Effort_In_Days from dual union all
select 6 ID, 2 Effort_In_Days from dual union all
select 2 ID, 1 Effort_In_Days from dual union all
select 4 ID, 1 Effort_In_Days from dual union all
select 5 ID, 1 Effort_In_Days from dual union all
select 9 ID, 2 Effort_In_Days from dual union all
select 7 ID, 1 Effort_In_Days from dual union all
select 8 ID, 1 Effort_In_Days from dual;
Query 1:
Give the rows an index so that they can be kept in order easily;
Assign groups to the rows where the Effort_In_Days is 1 so that all adjacent rows with Effort_In_Days of 1 are in the same group and rows separated by higher values for Effort_In_Days are in different groups;
Assign a cost of 1 to each row where the Effort_In_Days is higher than 1 or where Effort_In_Days is 1 and the row has an odd row number within the group; then
Finally, the release is the sum of all the costs for the row and all preceding rows.
Like this:
WITH indexes AS (
SELECT ID,
Effort_In_Days,
ROWNUM AS idx
FROM Data
),
groups AS (
SELECT ID,
Effort_In_Days,
idx,
CASE Effort_In_Days
WHEN 1
THEN idx - ROW_NUMBER() OVER ( PARTITION BY Effort_In_Days ORDER BY idx )
END AS grp
FROM indexes
ORDER BY idx
),
costs AS (
SELECT ID,
Effort_In_Days,
idx,
CASE Effort_In_Days
WHEN 1
THEN MOD( ROW_NUMBER() OVER ( PARTITION BY grp ORDER BY idx ), 2 )
ELSE 1
END AS cost
FROM groups
ORDER BY idx
)
SELECT ID,
Effort_In_Days,
SUM( cost ) OVER ( ORDER BY idx ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS Release
FROM costs
ORDER BY idx
Results:
| ID | EFFORT_IN_DAYS | RELEASE |
|----|----------------|---------|
| 3 | 3 | 1 |
| 1 | 2 | 2 |
| 6 | 2 | 3 |
| 2 | 1 | 4 |
| 4 | 1 | 4 |
| 5 | 1 | 5 |
| 9 | 2 | 6 |
| 7 | 1 | 7 |
| 8 | 1 | 7 |

Recursive SQL CTE's and Custom Sort Ordering

Image you are creating a DB schema for a threaded discussion board. Is there an efficient way to select a properly sorted list for a given thread? The code I have written works but does not sort the way I would like it too.
Let's say you have this data:
ID | ParentID
-----------------
1 | null
2 | 1
3 | 2
4 | 1
5 | 3
So the structure is supposed to look like this:
1
|- 2
| |- 3
| | |- 5
|- 4
Ideally, in the code, we want the result set to appear in the following order: 1, 2, 3, 5, 4
PROBLEM: With the CTE I wrote it is actually being returned as: 1, 2, 4, 3, 5
I know this would be easy to group/order by using LINQ but I am reluctant to do this in memory. It seems like the best solution at this point though...
Here is the CTE I am currently using:
with Replies as (
select c.CommentID, c.ParentCommentID 1 as Level
from Comment c
where ParentCommentID is null and CommentID = #ParentCommentID
union all
select c.CommentID, c.ParentCommentID, r.Level + 1 as Level
from Comment c
inner join Replies r on c.ParentCommentID = r.CommentID
)
select * from Replies
Any help would be appreciated; Thanks!
I'm new to SQL and had not heard about hierarchyid datatype before. After reading about it from this comment I decided I may want to incorporate this into my design. I will experiment with this tonight and post more information if I have success.
Update
Returned result from my sample data, using dance2die's suggestion:
ID | ParentID | Level | DenseRank
-------------------------------------
15 NULL 1 1
20 15 2 1
21 20 3 1
17 22 3 1
22 15 2 2
31 15 2 3
32 15 2 4
33 15 2 5
34 15 2 6
35 15 2 7
36 15 2 8
I am sure that you will love this.
I recently find out about Dense_Rank() function, which is for "ranking within the partition of a result set" according to MSDN
Check out the code below and how "CommentID" is sorted.
As far as I understand, you are trying to partition your result set by ParentCommentID.
Pay attention to "denserank" column.
with Replies (CommentID, ParentCommentID, Level) as
(
select c.CommentID, c.ParentCommentID, 1 as Level
from Comment c
where ParentCommentID is null and CommentID = 1
union all
select c.CommentID, c.ParentCommentID, r.Level + 1 as Level
from Comment c
inner join Replies r on c.ParentCommentID = r.CommentID
)
select *,
denserank = dense_rank() over (partition by ParentCommentID order by CommentID)
from Replies
order by denserank
Result below
You have to use hierarchyid (sql2008 only) or a bunch of string (or byte) concatenation.
Hmmmm - I am not sure if your structure is the best suited for this problem. Off the top of my head I cannot think of anyway to sort the data as you want it within the above query.
The best I can think of is if you have a parent table that ties your comments together (eg. a topic table). If you do you should be able to simply join your replies onto that (you will need to include the correct column obviously), and then you can sort by the topicID, Level to get the sort order you are after (or whatever other info on the topic table represents a good value for sorting).
Consider storing the entire hierarchy (with triggers to update it if it changes ) in a field.
This field in your example would have:
1
1.2
1.2.3
1.2.5
1.4
then you just have to sort on that field, try this and see:
create table #temp (test varchar (10))
insert into #temp (test)
select '1'
union select '1.2'
union select '1.2.3'
union select '1.2.5'
union select '1.4'
select * from #temp order by test asc