Calculate a column value backwards over a series of previous rows/RECURSIVE/CONNECTED BY - sql

need your help. I guess/hope there is a function for that. I found "CONNECT DBY" and "WITH RECURSIVE AS ..." but it doesn't seem to solve my problem.
GIVEN TABLES:
Table A
+------+------------+----------+
| id | prev_id | date |
+------------------------------+
| 1 | | 20200101 |
| 23 | 1 | 20200104 |
| 34 | 23 | 20200112 |
| 41 | 34 | 20200130 |
+------------------------------+
Table B
+------+-----------+
| ref_id | key |
+------------------+
| 41 | abc |
+------------------+
(points always to the lates entry in table "A". Update, no history)
Join Statement:
SELECT
id, prev_id, key, date
FROM A
LEFT OUTER JOIN B ON B.ref_id = A.id
GIVEN psql result set:
+------+------------+----------+-----------+
| id | prev_id | key | date |
+------------------------------+-----------+
| 1 | | | 20200101 |
| 23 | 1 | | 20200104 |
| 34 | 23 | | 20200112 |
| 41 | 34 | abc | 20200130 |
+------------------------------+-----------+
DESIRED output:
+------+------------+----------+-----------+
| id | prev_id | key | date |
+------------------------------+-----------+
| 1 | | abc | 20200101 |
| 23 | 1 | abc | 20200104 |
| 34 | 23 | abc | 20200112 |
| 41 | 34 | abc | 20200130 |
+------------------------------+-----------+
The rows of the result set are connected by columns 'id' and 'prev_id'.
I want to calculate the "key" column in a reasonable time.
Keep in mind, this is a very simplified example. Normally there are a lot of more rows and different keys and id's

I understand that you want to bring the hierarchy of each row in tableb. Here is one approach using a recursive query:
with recursive cte as (
select a.id, a.prev_id, a.date, b.key
from tablea a
inner join tableb b on b.ref_id = a.id
union all
select a.id, a.prev_id, a.date, c.key
from cte c
inner join tablea a on a.id = c.prev_id
)
select * from cte

Related

Replace nulls of a column with column value from another table

I have data flowing from two tables, table A and table B. I'm doing an inner join on a common column from both the tables and creating two more new columns based on different conditions. Below is a sample dataset:
Table A
| Id | StartDate |
|-----|------------|
| 119 | 01-01-2018 |
| 120 | 01-02-2019 |
| 121 | 03-05-2018 |
| 123 | 05-08-2021 |
TABLE B
| Id | CodeId | Code | RedemptionDate |
|-----|--------|------|----------------|
| 119 | 1 | abc | null |
| 119 | 2 | abc | null |
| 119 | 3 | def | null |
| 119 | 4 | def | 2/3/2019 |
| 120 | 5 | ghi | 04/7/2018 |
| 120 | 6 | ghi | 4/5/2018 |
| 121 | 7 | jkl | null |
| 121 | 8 | jkl | 4/4/2019 |
| 121 | 9 | mno | 3/18/2020 |
| 123 | 10 | pqr | null |
What I'm basically doing is joining the tables on column 'Id' when StartDate>2018 and create two new columns - 'unlock' by counting CodeId when RedemptionDate is null and 'Redeem' by counting CodeId when RedmeptionDate is not null. Below is the SQL query:
WITH cte1 AS (
SELECT a.id, COUNT(b.CodeId) AS 'Unlock'
FROM TableA AS a
JOIN TableB AS b ON a.Id=b.Id
WHERE YEAR(a.StartDate) >= 2018 AND b.RedemptionDate IS NULL
GROUP BY a.id
), cte2 AS (
SELECT a.id, COUNT(b.CodeId) AS 'Redeem'
FROM TableA AS a
JOIN TableB AS b ON a.Id=b.Id
WHERE YEAR(a.StartDate) >= 2018 AND b.RedemptionDate IS NOT NULL
GROUP BY a.id
)
SELECT cte1.Id, cte1.Unlocked, cte2.Redeemed
FROM cte1
FULL OUTER JOIN cte2 ON cte1.Id = cte2.Id
If I break down the output of this query, result from cte1 will look like below:
| Id | Unlock |
|-----|--------|
| 119 | 3 |
| 121 | 1 |
| 123 | 1 |
And from cte2 will look like below:
| Id | Redeem |
|-----|--------|
| 119 | 1 |
| 120 | 2 |
| 121 | 2 |
The last select query will produce the following result:
| Id | Unlock | Redeem |
|------|--------|--------|
| 119 | 3 | 1 |
| null | null | 2 |
| 121 | 1 | 2 |
| 123 | 1 | null |
How can I replace the null value from Id with values from 'b.Id'? If I try coalesce or a case statement, they create new columns. I don't want to create additional columns, rather replace the null values from the column values coming from another table.
My final output should like:
| Id | Unlock | Redeem |
|-----|--------|--------|
| 119 | 3 | 1 |
| 120 | null | 2 |
| 121 | 1 | 2 |
| 123 | 1 | null |
If I'm following correctly, you can use apply with aggregation:
select a.*, b.*
from a cross apply
(select count(RedemptionDate) as num_redeemed,
count(*) - count(RedemptionDate) as num_unlock
from b
where b.id = a.id
) b;
However, the answer to your question is to use coalesce(cte1.id, cte2.id) as id.

UNION ALL not performing as expected - Oracle SQL

I have two tables:
tableA
| Part | Val |
|:----:|:---:|
| AA | 3 |
| AB | 2 |
| AC | 11 |
| AD | 6 |
| AE | 3 |
tableB
| Part | Val |
|:----:|:---:|
| AC | 9 |
| AF | 5 |
| AG | 1 |
| AH | 10 |
| AI | 97 |
I would like to union these tables to achieve this result:
| Part | ValA | ValB |
|:----:|:----:|:----:|
| AA | 3 | 0 |
| AB | 2 | 0 |
| AC | 11 | 9 |
| AD | 6 | 0 |
| AE | 3 | 0 |
| AF | 0 | 5 |
| AG | 0 | 1 |
| AH | 0 | 10 |
| AI | 0 | 97 |
I have tried:
SELECT * FROM tableA
UNION ALL
SELECT * FROM tableB
But that results in only one column of vals, which I do not want.
How can I merge these tables and create two columns, one for each table, where if the part does not appear in the other table, its value can just be 0?
SQL FIDDLE for reference.
It appears that you want to join the tables, not union them
select nvl(a.Part, b.Part) as Part,
nvl( a.Val, 0 ) as ValA,
nvl( b.Val, 0 ) as ValB
from tableA a
full outer join tableB b
on( a.Part = b.Part )
order by 1
Note that using case-sensitive identifiers like you do in your fiddle is generally frowned upon. It tends to make writing queries more complicated than it needs to be and it tends to get annoying to have to include the double quotes around every column name.
Demo
You can try below -
select part,max(valA),max(valB) from
(
select part, val as valA, 0 as valB from tableA
union all
select part, 0 , val from tableB
)A group by part

SQL Insert multiple value to one key staff from two tables (try to improve)

Basically, I want to insert multiple values to a signal staff form two tables, Value_table and Staff_table, which look like this:
Staff_table
| staff_num | staff_name | staff_role |
+---------------------+------------------+------------------+
| 1 | Bill | 1 |
| 2 | James | 1 |
| 3 | Gina | 2 |
| 4 | Tim | 3 |
Value_table
| value | value_name | value_state |
+------------------+------------------+------------------+
| 123 | Food | 0 |
| 476 | Drink | 1 |
| 656 | Dinner | 1 |
| 77 | Phone | 1 |
And the result should look like this:
| staff_num | value |
+---------------------+------------------+
| 1 | 123 |
| 1 | 476 |
| 1 | 656 |
| 1 | 77 |
| 2 | 123 |
| 2 | 476 |
| 2 | 656 |
| 2 | 77 |
.
.
.
.
I find a SQL way to do it which is the following code.
INSERT INTO Result_table
SELECT DISTINCT a.staff_code, c.func_num
FROM Staff_table a
JOIN Value_table c ON c.value BETWEEN 0 AND 656;
It works but I don't think the last line
JOIN Value_table c ON c.value BETWEEN 0 AND 656;
is a good way to select all the value from the table.
Is there a better way to do it in SQL?
Alternatively you may use CROSS JOIN as
INSERT INTO Result_table
SELECT distinct a.staff_code,c.func_num
FROM Staff_table a
CROSS JOIN Value_table c
Since, there's no need to restrict the value column which is already between 0 and 656 depending on the sample data.
Edit : If you want to restrict add a WHERE condition below as
INSERT INTO Result_table
SELECT distinct a.staff_code,c.func_num
FROM Staff_table a
CROSS JOIN Value_table c
WHERE c.value BETWEEN 77 and 123

Oracle SQL Left join same table unknown amount of times

I have this table
| old | new |
|------|-------|
| a | b |
| b | c |
| d | e |
| ... | ... |
| aa | bb |
| bb | ff |
| ... | ... |
| 11 | 33 |
| 33 | 523 |
| 523 | 4444 |
| 4444 | 21444 |
The result I want to achieve is
| old | newest |
|------|--------|
| a | e |
| b | e |
| d | e |
| ... | |
| aa | ff |
| bb | ff |
| ... | |
| 11 | 21444 |
| 33 | 21444 |
| 523 | 21444 |
| 4444 | 21444 |
I can hard code the query to get the result that I want.
SELECT
older.old,
older.new,
newer.new firstcol,
newer1.new secondcol,
…
newerX-1.new secondlastcol,
newerX.new lastcol
from Table older
Left join Table newer
on older.old = newer.new
Left join Table newer1
on newer.new = newer1.old
…
Left join Table newerX-1
on newerX-2.new = newerX-1.old
Left join Table newerX
on newerX-1.new = newerX.old;
and then just take the first value from the right that is not null.
Illustrated here:
| old | new | firstcol | secondcol | thirdcol | fourthcol | | lastcol |
|------|-------|----------|-----------|----------|-----------|-----|---------|
| a | b | c | e | null | null | ... | null |
| b | c | e | null | null | null | ... | null |
| d | e | null | null | null | null | ... | null |
| ... | ... | ... | ... | ... | ... | ... | null |
| aa | bb | ff | null | null | null | ... | null |
| bb | ff | null | null | null | null | ... | null |
| ... | ... | ... | ... | ... | ... | ... | null |
| 11 | 33 | 523 | 4444 | 21444 | null | ... | null |
| 33 | 523 | 4444 | 21444 | null | null | ... | null |
| 523 | 4444 | 21444 | null | null | null | ... | null |
| 4444 | 21444 | null | null | null | null | ... | null |
The problem is that the length of "the replacement chain" is always changing (Can vary from 10 to 100).
There must be a better way to do this?
What you are looking for is a recursive query. Something like this:
with cte (old, new, lev) as
(
select old, new, 1 as lev from mytable
union all
select m.old, cte.new, cte.lev + 1
from mytable m
join cte on cte.old = m.new
)
select old, max(new) keep (dense_rank last order by lev) as new
from cte
group by old
order by old;
The recursive CTE creates all iterations (you can see this by replacing the query by select * from cte). And in the final query we get the last new per old with Oracle's KEEP LAST.
Rextester demo: http://rextester.com/CHTG34988
I'm trying to understand how you group your rows to determine different "newest" values. Are these the groupings you want based on the old field?
Group 1 - one letter (a, b, d)
Group 2 - two letters (aa, bb)
Group 3 - any number (11, 33, 523, 4444)
Is this correct? If so, you just need to group them by an expression and then use a window function MAX(). Something like this:
SELECT
"old",
MAX() OVER(PARTITION BY MyGrouping) AS newest
FROM (
SELECT
"old",
CASE
WHEN NOT IS_NUMERIC("old") THEN 'string' || CHAR_LENGTH("old") -- If string, group by string length
ELSE 'number' -- Otherwise, group as a number
END AS MyGrouping
FROM MyTable
) src
I don't know if Oracle has equivalents of the IS_NUMERIC and CHAR_LENGTH functions, so you need to check on that. If not, replace that expression with something similar, like this:
https://www.techonthenet.com/oracle/questions/isnumeric.php

Joining multiple tables to produce one data source

I have four tables with similar format but different values.
Table A
| ID | Date | Photo
| 14 | 10/10/24 | 1
| 15 | 10/11/24 | 2
| 16 | 10/12/24 | 1
| 17 | 10/13/24 | 1
Table B
| ID | Date | Photo
| 14 | 10/10/24 | 1
| 15 | 10/11/24 | 1
| 17 | 10/16/24 | 1
| 18 | 10/17/24 | 1
Table C
| ID | Date | Photo
| 14 | 10/10/24 | 1
| 15 | 10/11/24 | 4
| 19 | 10/18/24 | 4
| 20 | 10/19/24 | 1
I need to get one data source that looks like this below, that is a full outer join of the above tables, where the ID and Date fields as the only fields with non values.
Table C
| ID | Date | Photo | Image | Cat
| 14 | 10/10/2014 | 1 | 1 | 1
| 15 | 10/11/2014 | 2 | 1 | 4
| 16 | 10/12/2014 | 1 | NULL | NULL
| 17 | 10/16/2014 | NULL | 1 | NULL
| 18 | 10/14/2014 | NULL | NULL | NULL
| 18 | 10/17/2014 | NULL | 1 | NULL
| 19 | 10/15/2014 | NULL | NULL | 4
| 20 | 10/16/2014 | 1 | NULL | NULL
| 20 | 10/19/2014 | NULL | NULL | 1
You mention FULL JOIN so I'm assuming you use a database that supports them. You can use COALESCE() to return the populated ID and Date, and also to simplify your JOIN criteria:
SELECT COALESCE(a.ID,b.ID,c.ID) AS ID
,COALESCE(a.Date,b.Date,c.Date) AS Date
,a.Photo
,b.Image
,c.Cat
FROM TableA a
FULL JOIN TableB b
ON a.ID = b.ID AND a.Date = b.Date
FULL JOIN TableC c
ON COALESCE(a.ID,b.ID) = c.ID AND COALESCE(a.Date,b.Date) = c.Date
You can use a FULL OUTER JOIN and COALESCE or NVL to ensure that the ID and Date columns are not null.
SELECT COALESCE(a.ID, b.ID,c.ID) AS ID,
COALESCE(a."Date", b."Date", c."Date") AS "Date",
a.Photo,
b.Image,
c.Cat
FROM TableA a
FULL OUTER JOIN
TableB b
ON ( a.ID = b.ID )
FULL OUTER JOIN
TableC c
ON ( c.ID = COALESCE( a.ID, b.ID ) );
SQLFIDDLE