How to calculate leave-out-median using PL/SQL analytic function - sql

I'm trying to write an analytic function in PL/SQL that, when applied to a column within a table, returns for each row in the table, the median of the column excluding the given row.
An example to clarify: Suppose I have a table TABLE consisting of one column X that takes on the following values:
1
2
3
4
5
I want to define an analytic function LOOM() such that:
SELECT LOOM(X)
FROM TABLE
delivers the following:
3.5
3.5
3
2.5
2.5
i.e., for each row, the median of X, excluding the given row. I've been struggling to build the desired LOOM() function.

I'm not sure if there is a "clever" way to do this. You can do the calculation with a correlated subquery.
Assuming the x values are unique -- as in your example --
with t as (
select 1 as x from dual union all
select 2 as x from dual union all
select 3 as x from dual union all
select 4 as x from dual union all
select 5 as x from dual
)
select t.*,
(select median(x)
from t t2
where t2.x <> t.x
) as loom
from t;
EDIT:
A more efficient method uses analytic functions but requires more direct calculation of the median. For instance:
with t as (
select 1 as x from dual union all
select 2 as x from dual union all
select 3 as x from dual union all
select 4 as x from dual union all
select 5 as x from dual
)
select t.*,
(case when mod(cnt, 2) = 0
then (case when x <= candidate_1 then candidate_2 else candidate_1 end)
else (case when x <= candidate_1 then (candidate_2 + candidate_3)/2
when x = candidate_2 then (candidate_1 + candidate_3)/2
else (candidate_1 + candidate_2) / 2
end)
end) as loom
from (select t.*,
max(case when seqnum = floor(cnt / 2) then x end) over () as candidate_1,
max(case when seqnum = floor(cnt / 2) + 1 then x end) over () as candidate_2,
max(case when seqnum = floor(cnt / 2) + 2 then x end) over () as candidate_3
from (select t.*,
row_number() over (order by x) as seqnum,
count(*) over () as cnt
from t
) t
) t

Related

how to get distinct values from multiple columns in 1 single row in oracle sql

I have a row of data like this
id first_cd sec_cd third_cd fourth_cd fifth_cd sixth_cd
1 A B null C C D
output should be :
id first_cd sec_cd third_cd fourth_cd fifth_cd sixth_cd
1 A B C D D D
I need to get distinct values from the columns and remove nulls where there are.
if, first_cd...sixth_cd are columns on the same row.
1 A B null C C D are the values
Anyway to do in this in oracle sql
This is a good place to use lateral joins:
select t.*, x.*
from t cross join lateral
(select max(case when seqnum = 1 then cd end) as cd1,
max(case when seqnum = 2 then cd end) as cd2,
max(case when seqnum = 3 then cd end) as cd3,
max(case when seqnum = 4 then cd end) as cd4,
max(case when seqnum = 5 then cd end) as cd5,
max(case when seqnum = 6 then cd end) as cd6
from (select t.*, row_number() over (order by n) as seqnum
from (select t.cd1 as cd, 1 as n from dual union all
select t.cd2, 2 from dual union all
select t.cd3, 3 from dual union all
select t.cd4, 4 from dual union all
select t.cd5, 5 from dual union all
select t.cd6, 6 from dual
) x
where cd is not null
) x
) x;
Note: This returns the excess values as NULL, which seems more in line with your problem.

count zeros between 1s in same column

I've data like this.
ID IND
1 0
2 0
3 1
4 0
5 1
6 0
7 0
I want to count the zeros before the value 1. So that, the output will be like below.
ID IND OUT
1 0 0
2 0 0
3 1 2
4 0 0
5 1 1
6 0 0
7 0 2
Is it possible without pl/sql? I tried to find the differences between row numbers but couldn't achieve it.
The match_recognize clause, introduced in Oracle 12.1, can do quick work of such "row pattern recognition" problems. The solution is just a bit complex due to the special treatment of a "last row" with ID = 0, but it is straightforward otherwise.
As usual, the with clause is not part of the solution; I include it to test the query. Remove it and use your actual table and column names.
with
inputs (id, ind) as (
select 1, 0 from dual union all
select 2, 0 from dual union all
select 3, 1 from dual union all
select 4, 0 from dual union all
select 5, 1 from dual union all
select 6, 0 from dual union all
select 7, 0 from dual
)
select id, ind, out
from inputs
match_recognize(
order by id
measures case classifier() when 'Z' then 0
when 'O' then count(*) - 1
else count(*) end as out
all rows per match
pattern ( Z* ( O | X ) )
define Z as ind = 0, O as ind != 0
);
ID IND OUT
---------- ---------- ----------
1 0 0
2 0 0
3 1 2
4 0 0
5 1 1
6 0 0
7 0 2
You can treat this as a gaps-and-islands problem. You can define the "islands" by the number of "1"s one or after each row. Then use a window function:
select t.*,
(case when ind = 1 or row_number() over (order by id desc) = 1
then sum(1 - ind) over (partition by grp)
else 0
end) as num_zeros
from (select t.*,
sum(ind) over (order by id desc) as grp
from t
) t;
If id is sequential with no gaps, you can do this without a subquery:
select t.*,
(case when ind = 1 or row_number() over (order by id desc) = 1
then id - coalesce(lag(case when ind = 1 then id end ignore nulls) over (order by id), min(id) over () - 1)
else 0
end)
from t;
I would suggest removing the case conditions and just using the then clause for the expression, so the value is on all rows.

Query Optimization - To repeat a pattern in Oracle SQL

Introduction: I can do this in MS-Excel, it takes me 1 minute, but I m trying to get this in Oracle SQL
Here is my Code:
SELECT A.*, (CASE WHEN A.r = 1 then 'X1' when A.r = 2 then 'X2' when A.r = 3 then 'X3' when A.r = 4
then 'X4' when A.r = 5 then 'X2' when A.r = 6 then 'X6' end) X FROM
(
Select Rownum r
From dual
Connect By Rownum <= 6 ) A
This is the Output:
Now, what if I have to do it for 25000 numbers, meaning when (rownum <= 25000) currently I have it only for 6, Is there a better method to do this with out case statement?
If you want to repeat this pattern of 6 rows for the remaining rows, then you can do:
select t.*,
(case when mod(rownum, 6) = 5 then 'X2'
else 'X' || (mod(rownum - 1, 6) + 1)
end)
from t;

Select values from different rows of same column INTO multiple variables in Oracle SQL

Here's the example:
ID | value
1 51
2 25
3 11
4 27
5 21
I need to get first three parameters and place them into variables e.g. out_x, out_y, out_z.
Is it possible to do it without multiple selects?
You can do something like this:
select max(case when id = 1 then value end),
max(case when id = 2 then value end),
max(case when id = 3 then value end)
into out_x, out_y, out_z
from t
where id in (1, 2, 3);
However, I think three queries of the form:
select value into out_x
from t
where id = 1;
is a cleaner approach.
You can use a PIVOT:
SELECT x, y, z
INTO out_x, out_y, out_z
FROM your_table
PIVOT ( MAX( value ) FOR id IN ( 1 AS x, 2 AS y, 3 AS z ) )
Or, if you do not know which IDs you need (but just want the first 3) then:
SELECT x, y, z
INTO out_x, out_y, out_z
FROM (
SELECT value, ROWNUM AS rn
FROM ( SELECT value FROM your_table ORDER BY id )
WHERE ROWNUM <= 3
)
PIVOT ( MAX( value ) FOR rn IN ( 1 AS x, 2 AS y, 3 AS z ) )

compare within groups to fetch common value

I have a table with the follwing data
Case 1:
table1
-------------
id type
-------------
1 X
1 Y
2 X
3 Z
3 X
-------------
Now as you see X is common to all the id ,so i need to return X in this case
Case2 :
table1
-------------
id type
-------------
1 X
1 Y
2 X
2 Y
3 X
3 Y
--------------
In this case both X and Y are common,then i need to return both Xand Y comma seperated (X,y)
Case 3
table1
-------------
id type
-------------
1 X
1 Y
2 X
2 Y
3 X
3 Y
4 NULL
------------------
If a null came to any of the record , i need to return NULL
Actually ,the data i have shouwn you , is been populated from 3 tables , so i have already written the query for that ,but now i need to compare the groups for the common data within groups ,that is confusing me ,how to compare the groups ?
Note :Here group is based on ID
Any help would be appriciated
you could count the occurances compared to the count of the IDs?
with data as (select rownum id, 'X' type from dual connect by level <= 3
union all
select rownum id, 'Y' type from dual connect by level <= 3
union all
select 3 id, 'Z' type from dual)
select wm_concat(distinct type)
from (select type, count(*) over (partition by type) cnt, count(distinct id) over () total_ids
from data)
where cnt = total_ids;
in 11g you have LISTAGG instead of WM_CONCAT of course. if for each id, the same type occurs many times, you can change count(*) over (partition by type) to count(distinct id) over (partition by type)
edit:
If you had
3, Z
3, NULL
(rather than 4, NULL) and also want to return a NULL rather than a delimited list in that case then you could add a check (with the 4, NULL above it would return a null even on the prior SQL version as the counts would'nt tie up):
with data as (select rownum id, 'X' type from dual connect by level <= 3
union all
select rownum id, 'Y' type from dual connect by level <= 3
union all
select 3 id, 'Z' type from dual)
select wm_concat(distinct type)
from (select type, count(*) over (partition by type) cnt, count(distinct id) over
() total_ids,
max(case when type is null then 'Y' else 'N' end) over () has_null_in_set
from data)
where cnt = total_ids
and has_null_in_set = 'N';