Oracle SQL compare records within a table - sql

I have a table like below:
S.No | Item_ID | Item_Revision | Code |
-----+---------+---------------+-------
1. | item1 | 0 | xyz |
2. | item2 | 0 | xyz |
3. | item3 | 0 | xyz |
4. | item1 | 1 | |
5. | item2 | 1 | abc |
6. | item3 | 1 | xyz |
I need to compare the records in the table to find the differences in code in different revisions of the items.
I want the result set as below:
| Item_ID | Code_Revision_0 | Code_Revision_1 |
| item1 | xyz | |
| item2 | xyz | abc |
I am not able to formulate an oracle query for this purpose.
Thanks in advance!

One basic idea is to use join:
select t0.item_id, t0.code as code_0, t1.code as code_1
from t t0 join
t t1
on t0.item_id = t1.item_id and
t0.item_revision = 0 and
t1.item_revision = 1
where t0.code <> t1.code;
However, if the code value is NULL (or an empty string), you need to be more careful:
where t0.code <> t1.code or (t0.code is null and t1.code is not null) or
(t0.code is not null and t1.code is null)

You can use a self join to do this.
select t1.item_id, t1.code code_rev_0, t2.code code_rev_1
from tablename t1
join tablename t2 on t1.item_id=t2.item_id
and t1.item_revision = 0 and t2.item_revision = 1
where nvl(t1.code,'a') <> nvl(t2.code,'a')

Here is a solution that uses the PIVOT operator instead of a self-join. If I am reading the execution plans correctly, this is slightly more efficient (cost of 13 vs. 17 for the join solution) for the input data you provided. You may want to test the two solutions on your actual data to see which works better.
with
input_data ( item_id, item_revision, code ) as (
select 'item1', 0, 'xyz' from dual union all
select 'item2', 0, 'xyz' from dual union all
select 'item3', 0, 'xyz' from dual union all
select 'item1', 1, '' from dual union all
select 'item2', 1, 'abc' from dual union all
select 'item3', 1, 'xyz' from dual
)
select *
from input_data
pivot (max(code) for item_revision in (0 as code_revision_0, 1 as code_revision_1))
where code_revision_0 != code_revision_1
or code_revision_0 is null and code_revision_1 is not null
or code_revision_0 is not null and code_revision_1 is null
;
OUTPUT:
ITEM_ CODE_REVISION_0 CODE_REVISION_1
----- ---------------- ----------------
item1 xyz
item2 xyz abc
2 rows selected.

Related

Possible to use a column name in a UDF in SQL?

I have a query in which a series of steps is repeated constantly over different columns, for example:
SELECT DISTINCT
MAX (
CASE
WHEN table_2."GRP1_MINIMUM_DATE" <= cohort."ANCHOR_DATE" THEN 1
ELSE 0
END)
OVER (PARTITION BY cohort."USER_ID")
AS "GRP1_MINIMUM_DATE",
MAX (
CASE
WHEN table_2."GRP2_MINIMUM_DATE" <= cohort."ANCHOR_DATE" THEN 1
ELSE 0
END)
OVER (PARTITION BY cohort."USER_ID")
AS "GRP2_MINIMUM_DATE"
FROM INPUT_COHORT cohort
LEFT JOIN INVOLVE_EVER table_2 ON cohort."USER_ID" = table_2."USER_ID"
I was considering writing a function to accomplish this as doing so would save on space in my query. I have been reading a bit about UDF in SQL but don't yet understand if it is possible to pass a column name in as a parameter (i.e. simply switch out "GRP1_MINIMUM_DATE" for "GRP2_MINIMUM_DATE" etc.). What I would like is a query which looks like this
SELECT DISTINCT
FUNCTION(table_2."GRP1_MINIMUM_DATE") AS "GRP1_MINIMUM_DATE",
FUNCTION(table_2."GRP2_MINIMUM_DATE") AS "GRP2_MINIMUM_DATE",
FUNCTION(table_2."GRP3_MINIMUM_DATE") AS "GRP3_MINIMUM_DATE",
FUNCTION(table_2."GRP4_MINIMUM_DATE") AS "GRP4_MINIMUM_DATE"
FROM INPUT_COHORT cohort
LEFT JOIN INVOLVE_EVER table_2 ON cohort."USER_ID" = table_2."USER_ID"
Can anyone tell me if this is possible/point me to some resource that might help me out here?
Thanks!
There is no such direct as #Tejash already stated, but the thing looks like your database model is not ideal - it would be better to have a table that has USER_ID and GRP_ID as keys and then MINIMUM_DATE as seperate field.
Without changing the table structure, you can use UNPIVOT query to mimic this design:
WITH INVOLVE_EVER(USER_ID, GRP1_MINIMUM_DATE, GRP2_MINIMUM_DATE, GRP3_MINIMUM_DATE, GRP4_MINIMUM_DATE)
AS (SELECT 1, SYSDATE, SYSDATE, SYSDATE, SYSDATE FROM dual UNION ALL
SELECT 2, SYSDATE-1, SYSDATE-2, SYSDATE-3, SYSDATE-4 FROM dual)
SELECT *
FROM INVOLVE_EVER
unpivot ( minimum_date FOR grp_id IN ( GRP1_MINIMUM_DATE AS 1, GRP2_MINIMUM_DATE AS 2, GRP3_MINIMUM_DATE AS 3, GRP4_MINIMUM_DATE AS 4))
Result:
| USER_ID | GRP_ID | MINIMUM_DATE |
|---------|--------|--------------|
| 1 | 1 | 09/09/19 |
| 1 | 2 | 09/09/19 |
| 1 | 3 | 09/09/19 |
| 1 | 4 | 09/09/19 |
| 2 | 1 | 09/08/19 |
| 2 | 2 | 09/07/19 |
| 2 | 3 | 09/06/19 |
| 2 | 4 | 09/05/19 |
With this you can write your query without further code duplication and if you need use PIVOT-syntax to get one line per USER_ID.
The final query could then look like this:
WITH INVOLVE_EVER(USER_ID, GRP1_MINIMUM_DATE, GRP2_MINIMUM_DATE, GRP3_MINIMUM_DATE, GRP4_MINIMUM_DATE)
AS (SELECT 1, SYSDATE, SYSDATE, SYSDATE, SYSDATE FROM dual UNION ALL
SELECT 2, SYSDATE-1, SYSDATE-2, SYSDATE-3, SYSDATE-4 FROM dual)
, INPUT_COHORT(USER_ID, ANCHOR_DATE)
AS (SELECT 1, SYSDATE-1 FROM dual UNION ALL
SELECT 2, SYSDATE-2 FROM dual UNION ALL
SELECT 3, SYSDATE-3 FROM dual)
-- Above is sampledata query starts from here:
, unpiv AS (SELECT *
FROM INVOLVE_EVER
unpivot ( minimum_date FOR grp_id IN ( GRP1_MINIMUM_DATE AS 1, GRP2_MINIMUM_DATE AS 2, GRP3_MINIMUM_DATE AS 3, GRP4_MINIMUM_DATE AS 4)))
SELECT qcsj_c000000001000000 user_id, GRP1_MINIMUM_DATE, GRP2_MINIMUM_DATE, GRP3_MINIMUM_DATE, GRP4_MINIMUM_DATE
FROM INPUT_COHORT cohort
LEFT JOIN unpiv table_2
ON cohort.USER_ID = table_2.USER_ID
pivot (MAX(CASE WHEN minimum_date <= cohort."ANCHOR_DATE" THEN 1 ELSE 0 END) AS MINIMUM_DATE
FOR grp_id IN (1 AS GRP1,2 AS GRP2,3 AS GRP3,4 AS GRP4))
Result:
| USER_ID | GRP1_MINIMUM_DATE | GRP2_MINIMUM_DATE | GRP3_MINIMUM_DATE | GRP4_MINIMUM_DATE |
|---------|-------------------|-------------------|-------------------|-------------------|
| 3 | | | | |
| 1 | 0 | 0 | 0 | 0 |
| 2 | 0 | 1 | 1 | 1 |
This way you only have to write your calculation logic once (see line starting with pivot).

Conditionally fallback to different join condition if stricter condition not matched

I have 2 tables j and c.
Both tables have columns ports and sec, and JOIN ON j.ports = c.ports and c.sec = j.sec.
For j.port = 'ABC', if there is no c.sec = j.sec for the same ports, then JOIN ON LEFT(c.sec, 6) = LEFT(j.sec, 6)
For other j.ports, I only want to join ON j.ports = c.ports and c.sec = j.sec
How can I do that?
Example Data
Table c
+------+------------+------------+
| Port | sec | Other |
+------+------------+------------+
| ABC | abcdefghij | ONE |
| ABC | klmnop | TWO |
| LMN | qwertyuiop | THREE |
| XYZ | asdfghjkl | FOUR |
+------+------------+------------+
Table j
+------+------------+
| Port | sec |
+------+------------+
| ABC | abcdefxxxx |
| ABC | klmnop |
| LMN | qwertyuiop |
| XYZ | zxcvbnm |
+------+------------+
EDITED: Desired Results
+------+------------+------------+
| Port | sec | other |
+------+------------+------------+
| ABC | abcdefghij | ONE | --> mactching on sec's 1st 6 characters
| ABC | klmnop | TWO | --> mactching on sec
| LMN | qwertyuiop | THREE | --> mactching on sec
+------+------------+------------+
This does conditional joining:
select t1.*, t2.*
from j t1 inner join c t2
on t2.ports = t1.ports and
case
when exists (select 1 from c where sec = t1.sec) then t1.sec
else left(t1.sec, 6)
end =
case
when exists (select 1 from c where sec = t1.sec) then t2.sec
else left(t2.sec, 6)
end
I question its efficiency but I think it does what you need.
See the demo.
You can do two outer joins and then do isnull type of operation. In oracle nvl is isnull of sqlserver
with c as
(
select 'ABC' port, 'abcdefghij' sec from dual
union all select 'ABC', 'klmnop' from dual
union all select 'LMN', 'qwertyuiop' from dual
union all select 'XYZ', 'asdfghjkl' from dual
),
j as
(
select 'ABC' port, 'abcdefxxxx' sec from dual
union all select 'ABC', 'klmnop' from dual
union all select 'LMN', 'qwertyuiop' from dual
union all select 'XYZ', 'zxcvbnm' from dual
)
select c.port, c.sec, nvl(j_full.sec, j_part.sec) j_sec
from c
left outer join j j_full on j_full.port = c.port and j_full.sec = c.sec
left outer join j j_part on j_part.port = c.port and substr(j_part.sec,1,6) = substr(c.sec,1,6)
order by 1,2
One way would be to just inner join on the less strict predicate then use a ranking function to discard unwanted rows in the event that c.port = 'ABC' and the stricter condition got a match for a particular c.port, c.sec combination.
with cte as
(
select c.port as cPort,
c.sec as cSec,
c.other as other,
j.sec as jSec,
RANK() OVER (PARTITION BY c.port, c.sec ORDER BY CASE WHEN c.port = 'ABC' AND j.sec = c.sec THEN 0 ELSE 1 END) AS rnk
from c inner join j on left(j.sec,6) = left(c.sec,6)
)
SELECT cPort, cSec, other, jSec
FROM cte
WHERE rnk = 1

Oracle Sql: Obtain a Sum of a Group, if Subgroup condition met

I have a dataset upon which I am trying to obain a summed value for each group, if a subgroup within each group meets a certain condition. I am not sure if this is possible, or if I am approaching this problem incorrectly.
My data is structured as following:
+----+-------------+---------+-------+
| ID | Transaction | Product | Value |
+----+-------------+---------+-------+
| 1 | A | 0 | 10 |
| 1 | A | 1 | 15 |
| 1 | A | 2 | 20 |
| 1 | B | 1 | 5 |
| 1 | B | 2 | 10 |
+----+-------------+---------+-------+
In this example I want to obtain the sum of values by the ID column, if a transaction does not contain any products labeled 0. In the above described scenario, all values related to Transaction A would be excluded because Product 0 was purchased. With the outcome being:
+----+-------------+
| ID | Sum of Value|
+----+-------------+
| 1 | 15 |
+----+-------------+
This process would repeat for multiple IDs with each ID only containing the sum of values if the transaction does not contain product 0.
Hmmm . . . one method is to use not exists for the filtering:
select id, sum(value)
from t
where not exists (select 1
from t t2
where t2.id = t.id and t2.transaction = t.transaction and
t2.product = 0
)
group by id;
Do not need to use correlated subquery with not exists.
Just use group by.
with s (id, transaction, product, value) as (
select 1, 'A', 0, 10 from dual union all
select 1, 'A', 1, 15 from dual union all
select 1, 'A', 2, 20 from dual union all
select 1, 'B', 1, 5 from dual union all
select 1, 'B', 2, 10 from dual)
select id, sum(sum_value) as sum_value
from
(select id, transaction,
sum(value) as sum_value
from s
group by id, transaction
having count(decode(product, 0, 1)) = 0
)
group by id;
ID SUM_VALUE
---------- ----------
1 15

Count missing values

I have a following table called Test:
Id | SomeId | Value
-----------------------------------------------------
019D9E52-41D1-45DF-81B6-C7CC484115A7 | 1 | 1
262640CA-65C2-4E30-8654-E187ACA1EEF4 | 1 | 1
53710AFC-4E19-4B1C-B68B-CDB713EC3D62 | 1 | 2
8FF7E77C-D04C-4961-82D9-87C2E5A1A096 | 1 | 2
-----------------------------------------------------
119D9E52-41D1-45DF-81B6-C7CC484115A7 | 2 | 1
762640CA-65C2-4E30-8654-E187ACA1EEF4 | 2 | 1
93710AFC-4E19-4B1C-B68B-CDB713EC3D62 | 2 | 2
4FF7E77C-D04C-4961-82D9-87C2E5A1A096 | 2 | 2
And there is a view called TestView:
SomeId | Value | Description
----------------------------
1 | 1 | 'One'
1 | 2 | 'Two'
1 | 3 | 'Three'
----------------------------
2 | 1 | 'One'
2 | 2 | 'Two'
These are just pseudo code examples.
I want to count all the values from the Test table (for a specific [SomeId]), and if value from the TestView (with a specific [SomeId]) is not in the Test table I just want to display 0 as count.
If I wanted to count values WHERE [Test].[SomeId] = 1, here's the expected result:
Value | Count
-----------------
One | 2
Two | 2
Three | 0
This is my query so far:
SELECT
tv.[Description] AS [Value],
COUNT(t.[Id]) - COUNT(tv.[Value]) AS [Count]
FROM [TestView] AS tv
LEFT JOIN [Test] AS t ON
t.[SomeId] = tv.[SomeId]
AND t.[Value] = tv.[Value]
WHERE
t.[SomeId] = 1
GROUP BY
tv.[Description]
But this gives me bad result... Anyways, here's the SQL Fiddle
EDIT:
This is just an addition to a Test table. What is Test table has one more foreign key Id, let's call it OtherId. Now when I use the query from the answer I won't get the result I wanted. Here's the modified query:
SELECT
t1.Description AS Value,
COUNT(t2.Value) AS Count
FROM TestView t1
LEFT JOIN test t2
ON t1.Value = t2.Value AND t1.SomeId = t2.SomeId
WHERE t1.SomeId = 1
AND t2.[OtherId] = *something* -- this is the addition
GROUP BY t1.Value, t1.Description
ORDER BY t1.Value;
Try this:
SELECT
t1.Description AS Value,
COUNT(t2.Value) AS Count
FROM TestView t1
LEFT JOIN test t2
ON t1.Value = t2.Value AND t1.SomeId = t2.SomeId
WHERE t1.SomeId = 1
GROUP BY t1.Value, t1.Description
ORDER BY t1.Value;
Demo
Below is your Solution
SELECT
tv.[Description] AS [Value],
COUNT(t.[Id]) AS [Count]
FROM [TestView] AS tv
LEFT OUTER JOIN [Test] AS t ON tv.SomeId = t.SomeId
AND t.Value = tv.value
AND t.[SomeId] = 1
GROUP BY
tv.[Description]

SQL:How to dynamically return error code for records which doesn't exist in table

I am trying to replicate a workplace scenario. The sqlfiddle for Oracle db is not working so I couldn't recreate the table.
Say I have a table like below
Table1
+----+------+
| ID | Col1 |
+----+------+
| 1 | A |
| 2 | B |
| 3 | C |
+----+------+
Now we run a query with where condition. The in clause for where is passed by user and run time and can change.
Suppose user inputs 1,2,4,5
So the SQL will be like
select t.* from Table1 t where t.id in (1,2,4,5);
The result of this query will be
+----+------+
| ID | Col1 |
+----+------+
| 1 | A |
| 2 | B |
+----+------+
Now output I am expecting should be something like below
+----+---------+------+
| ID | ErrCode | Col1 |
+----+---------+------+
| 1 | 0 | A |
| 2 | 0 | B |
| 4 | 404 | |
| 5 | 404 | |
+----+---------+------+
As 3 was not entered by user, we will not return it. But for 4 and 5, there is no record in our table, so I want to create another dummy column which will contain error code. The data columns should be null.
It is not mandatory that the user input should go to in clause. We can use it anywhere in the query.
I am thinking of some way of splitting the input id and use them as rows. Then use them to do left join with Table1 to find the records which exists and doesn't exist in Table1 and use case on that to decide among 0 or 404 as error code.
Appreciate any other way we can do it by query.
Here it goes
SQL> WITH table_filter AS
2 (SELECT regexp_substr(txt, '[^,]+', 1, LEVEL) id
3 FROM (SELECT '1,2,4,5' AS txt FROM dual) -- User input here
4 CONNECT BY regexp_substr(txt, '[^,]+', 1, LEVEL) IS NOT NULL),
5 table1 AS -- Sample data
6 (SELECT 1 id,
7 'A' col1
8 FROM dual
9 UNION ALL
10 SELECT 2,
11 'B'
12 FROM dual
13 UNION ALL
14 SELECT 3,
15 'C'
16 FROM dual)
17 SELECT f.id,
18 CASE
19 WHEN t.id IS NULL THEN
20 404
21 ELSE
22 0
23 END AS err_code,
24 t.col1
25 FROM table_filter f
26 LEFT OUTER JOIN table1 t
27 ON t.id = f.id;
ID ERR_CODE COL1
---------------------------- ---------- ----
1 0 A
2 0 B
5 404
4 404
SQL>
Oracle Setup:
CREATE TABLE Table1 ( id, col1 ) AS
SELECT 1, 'A' FROM DUAL UNION ALL
SELECT 2, 'B' FROM DUAL;
Query:
SELECT i.COLUMN_VALUE AS id,
NVL2( t.col1, 0, 404 ) AS ErrCode,
t.col1
FROM TABLE( SYS.ODCINUMBERLIST( 1, 2, 4, 5 ) ) i
LEFT OUTER JOIN
Table1 t
ON ( i.COLUMN_VALUE = t.id );
Output:
ID ERRCODE COL1
-- ------- ----
1 0 A
2 0 B
4 404
5 404
The collection of ids can be built dynamically using PL/SQL or an external language and then passed as a bind variable. See my answer here for an example.