Hello looking for help with ranking.
I'm working with Teradata using SQL and I'm trying to rank a list by a specific group and then by age.
For example: I want to rank by group then only rank those under the selected group that are under 21 years old.
However, when I use the query below it seems to not take into account the members in a group and assigns only if they meet the criteria in the case statement.
select
policy,
age,
case when age <'21' then '1'else '0' end as Under21,
case when age <'21' then dense_rank () over (order by group, age desc) else '0' end as Rank_Under_21
from Table
You can use the partition by clause:
dense_rank () over (partition by policy, case when age < 21 then 1 end
order by group, age desc)
NB: If age is a numerical field (it should be), then don't compare it with a string: leave out the quotes. If age is of a string type, then be aware that the comparison with another string will be alphabetical, and thus '9' > '21'.
Your code still ranks all ages, the CASE simply replaces the high age ranks with zero.
Another solution (besides to #trincot's answer) which moves the CASE into the RANK:
CASE
WHEN age < 21
THEN Rank ()
Over (PARTITION BY policy
ORDER BY CASE WHEN age < 21 THEN age END DESC)
ELSE 0
END
This also ranks all ages, but the high ones are sorted last und thus got a high rank, which is replaced by the outer CASE with zero.
The below code is for MS SQL Server. Thanks to #Victor Hugo Terceros, for sample code.
DECLARE #tbl TABLE
(
age INT,
grp VARCHAR(20)
)
INSERT INTO #tbl
SELECT 1,
'A'
UNION
SELECT 12,
'A'
UNION
SELECT 20,
'A'
UNION
SELECT 19,
'B'
UNION
SELECT 30,
'B'
UNION
SELECT 11,
'B'
UNION
SELECT 4,
'C'
UNION
SELECT 14,
'C'
UNION
SELECT 5,
'B'
UNION
SELECT 16,
'D'
SELECT grp AS Policy,
age,
under21 AS Under21,
CASE
WHEN under21 = 0 THEN Dense_rank()
OVER(
partition BY grp
ORDER BY under21age DESC)
ELSE 0
END AS Rank_Under_21
FROM (SELECT CASE
WHEN age < 21 THEN 0
ELSE 1
END AS Under21,
CASE
WHEN age < 21 THEN age
ELSE 0
END AS under21age,
age,
grp
FROM #tbl) AS t
What I suggest is a Partition by your Group column and then Rank by Age
---------------Test Table with data---------
declare #tbl table(age int, policy varchar(20))
insert into #tbl
select 1, 'A' union
select 12, 'A' union
select 20, 'A' union
select 19, 'B' union
select 30, 'B' union
select 11, 'B' union
select 4, 'C' union
select 14, 'C' union
select 5, 'B' union
select 16, 'D'
---------------Main Query--------------------
select policy,age,
'1' as Under21,
rank() over (partition by policy order by age desc) as Rank_Under_21
from #tbl
where age <21
union
select policy,age,
'0' as Under21,
0 as Rank_Under_21
from #tbl
where age >=21
order by policy asc,age desc
Related
I having two tables emp and type.
create table EMP(ID number(10), effective_date date);
EID Effective_date
--------------------
1 02/14/2023
2 02/15/2023
3 04/30/2023
4 03/24/2023
create table type(ID number(10),contract_type varchar2(2));
TID contract_type
------------------
1 P
1 S
1 P
2 S
2 S
3 P
3 S
4 S
I am looking EID which is having contract type is 'S' in type table. (or emp table with effective date is greater than sysdate and in the type table with only contract_type ='S')
Actual result :
2
4
My query is not giving the correct results.
select emp.EID
from emp,type
where EID = TID
contract_type ='S'
effective_date >= sysdate
group by TID
having count(TID) >= 1;
If you want to keep your idea with COUNT and GROUP BY, you should count other contract types than the 'S' ones and check this is 0:
SELECT e.eid
FROM emp e
JOIN type t ON e.eid = t.tid
WHERE
e.effective_date >= sysdate
GROUP BY e.eid
HAVING COUNT(CASE WHEN t.contract_type <> 'S' THEN 1 END) = 0;
This query will return 2 and 4 for your sample data.
Try out: db<>fiddle
Another option is as already said here using NOT EXISTS.
Take care of following difference to the NOT EXISTS approach: The query in Tim's answer will also fetch id's of table "emp" that don't appear at all in table "type". My query here will not fetch such id's.
It's up to you to decide whether this is possible at all and what to do in this case.
Changing JOIN to LEFT JOIN in above query will eliminate this difference.
I would use exists logic here:
SELECT EID
FROM EMP e
WHERE effective_date >= SYSDATE AND
NOT EXISTS (
SELECT 1
FROM "type" t
WHERE t.TID = e.EID AND
t.contract_type <> 'S'
);
You could use Count() Over() analytic function to check for type 'S' and number of different types per ID.
SELECT DISTINCT ID
FROM ( Select e.EID "ID",
Count(CASE t.CONTRACT_TYPE WHEN 'S' THEN 'S' END) Over(Partition By t.ID Order By t.ID) "NUM_OF_S",
Count(Distinct t.CONTRACT_TYPE) Over(Partition By t.ID) "NUM_OF_TYPES",
TRUNC(e.EFFECTIVE_DATE) - TRUNC(SYSDATE) "DAYS_AFTER_SYSDATE"
From emp_cte e
Inner Join type_cte t ON(t.ID = e.EID) )
WHERE NUM_OF_S > 0 And -- Type 'S' exists for ID AND
NUM_OF_TYPES = 1 And -- It is the only type AND
DAYS_AFTER_SYSDATE > 0 -- EFFECTIVE_DATE is after SYSDATE
With your sample data ...
WITH
emp_cte(EID, EFFECTIVE_DATE) AS
(
Select 1, To_Date('02/14/2023', 'mm/dd/yyyy') From Dual Union All
Select 2, To_Date('02/15/2023', 'mm/dd/yyyy') From Dual Union All
Select 3, To_Date('04/30/2023', 'mm/dd/yyyy') From Dual Union All
Select 4, To_Date('03/24/2023', 'mm/dd/yyyy') From Dual
),
type_cte(ID, CONTRACT_TYPE) AS
(
Select 1, 'P' From Dual Union All
Select 1, 'S' From Dual Union All
Select 1, 'P' From Dual Union All
Select 2, 'S' From Dual Union All
Select 2, 'S' From Dual Union All
Select 3, 'P' From Dual Union All
Select 3, 'S' From Dual Union All
Select 4, 'S' From Dual
)
... result would be ...
-- ID
-- ----------
-- 2
-- 4
I have ant an Oracle v11 database, and whilst I do not have the schema definition of the tables, I have illustrated what I am trying to achieve below.
This is what the table looks like
I am trying to transform the data by selecting only the latest rows, the table keeps an history of changes, I am not interested in the changes only the latest value for every present issue
This is what I have so far.
select issueno,
case (when fieldname = 'name' then string_value end) name,
case (when fieldname = 'point' then string_value end) point
from issues
where issueno = 1234
The issue with the query above is that it returns 4 rows, I would like to return only a single row.
You can get the latest date by using LAST ORDER BY clause within the MAX() KEEP (..) values for transition_date(or load_date column, depending on which you mean replace within the query) such as
WITH i AS
(
SELECT CASE WHEN fieldname = 'name' THEN
MAX(string_value) KEEP (DENSE_RANK LAST ORDER BY transition_date)
OVER (PARTITION BY issue_no, fieldname)
END AS name,
CASE WHEN fieldname = 'point' THEN
MAX(string_value) KEEP (DENSE_RANK LAST ORDER BY transition_date)
OVER (PARTITION BY issue_no, fieldname)
END AS point
FROM issues
)
SELECT MAX(name) AS name, MAX(point) AS point
FROM i
But, if ties(equal values) occur for the related date values, then consider using DENSE_RANK() function in order to compute the values returning equal to 1 along with ROW_NUMBER() to be able to use with the JOIN clause in the main query such as
WITH i AS
(
SELECT i.*,
DENSE_RANK() OVER ( PARTITION BY issue_no, fieldname
ORDER BY transition_date DESC) AS dr,
ROW_NUMBER() OVER ( PARTITION BY issue_no, fieldname
ORDER BY transition_date DESC) AS rn
FROM issues i
)
SELECT i1.string_value AS name, i2.string_value AS point
FROM ( SELECT string_value, rn FROM i WHERE dr = 1 AND fieldname = 'name' ) i1
FULL JOIN ( SELECT string_value, rn FROM i WHERE dr = 1 AND fieldname = 'point' ) i2
ON i2.rn = i1.rn
Demo
Assuming that you want to have the latest record by the column load_date
select issueno,
case (when fieldname = 'name' then string_value end) name,
case (when fieldname = 'point' then string_value end) point
from issues
where issueno = 1234 and
(fieldname , load_date) in (select fieldname ,max(load_date) from issues where issueno=1234 group by fieldname)
I would use a subquery + window function to achieve what you asked for (assuming you use are basing load_date to determine the latest record)
select issueno,
case (when fieldname = 'name' then string_value end) name,
case (when fieldname = 'point' then string_value end) point
from
(
SELECT name, point, ROW_NUMBER() OVER(PARTITION BY ISSUENO, FIELDNAME ORDER BY LOAD_DATE DESC) RN
FROM issues
)
where issueno = 1234
AND RN = 1
The syntax ROW_NUMBER() OVER ([query_partition_clause] order_by_clause) is actually a window function that assign a ranking to each rows governed by how you declare the rule in [query_partition_clause] order_by_clause
See whether something like this helps; read comments within code.
SQL> with issues (issueno, fieldname, string_value,
2 transition_date, transition_id, load_date)
3 as
4 -- sample data; you have it in a table, don't type that
5 (select 1234, 'name', null , date '2021-01-01', 1, date '2021-01-02' from dual union all
6 select 1234, 'name', 'Tom', date '2021-02-11', 2, date '2021-02-12' from dual union all
7 select 1234, 'point', '0' , date '2021-02-04', 3, date '2021-02-05' from dual union all
8 select 1234, 'point', '5' , date '2021-02-10', 5, date '2021-02-11' from dual
9 ),
10 -- query you need begins here
11 temp as
12 -- rank values partitioned by ISSUENO and FIELDNAME, sorted by TRANSITION_ID
13 (select issueno, fieldname, string_value,
14 row_number() over (partition by issueno, fieldname
15 order by transition_id desc) rn
16 from issues
17 )
18 select issueno,
19 max(case when fieldname = 'name' then string_value end) name,
20 max(case when fieldname = 'point' then string_value end) point
21 from temp
22 where rn = 1
23 group by issueno;
ISSUENO NAME POINT
---------- ---------- ----------
1234 Tom 5
SQL>
Is there a "better" way to refactor the query below that returns the number occurrences of a particular value (e.g. 'A') for each distinct id? The challenge seems to be keeping id = 2 in the result set even though the count is zero (id = 2 is never related to 'A'). It has a common table expression, NVL function, in-line view, distinct, and left join. Is all of that really needed to get this job done? (Oracle 19c)
create table T (id, val) as
select 1, 'A' from dual
union all select 1, 'B' from dual
union all select 1, 'A' from dual
union all select 2, 'B' from dual
union all select 2, 'B' from dual
union all select 3, 'A' from dual
;
with C as (select id, val, count(*) cnt from T where val = 'A' group by id, val)
select D.id, nvl(C.cnt, 0) cnt_with_zero from (select distinct id from T) D left join C on D.id = C.id
order by id
;
ID CNT_WITH_ZERO
---------- -------------
1 2
2 0
3 1
A simple way is conditional aggregation:
select id,
sum(case when val = 'A' then 1 else 0 end) as num_As
from t
group by id;
If you have another table with one row per id, you I would recommend:
select i.id,
(select count(*) from t where t.id = i.id and t.val = 'A') as num_As
from ids i;
Let's say if I search by a key, it returns 2 records with 2 different values for each record.
It will return value 'A' and value 'B' for the 1st and 2nd records respectively.
ID VALUE
1 A
1 B
If the returned records contains 'A' and 'B' then I want to change all their value to 'C'.
If the returned record only contains 'A' or 'B' then i don't want to change to 'C'
How do i use the decode or case function to do that?
I tried (Case when value in('A','B') then 'C' else value end)
but it also changes the records that only returns either 'A' or 'B' to 'C'
So basically if my result are like this :
ID VALUE
1 A
1 B
I want it to be like this
ID VALUE
1 C
1 C
If the result is
ID VALUE or ID VALUE
1 A 1 B
1 A 1 B
Then don't implement the above conversion rule.
Edit for clarity
select id, value from t1
where id =123
gives me below
ID VALUE
1 A
1 B
I want a condition that uses the value of the two records--change the value to 'C' only when clm1.value=A and clm2.value=B
something like below but it does not work.
select id,
case when value ='A' and value ='B' then 'C' else value end
from t1
where id=123
Sorry for the confusion.
Thanks
What about something like this:
create table csm (id int, value varchar(5))
insert into csm (id,value)
SELECT 1,'A' UNION
SELECT 1,'B' UNION
SELECT 2,'A' UNION
SELECT 3,'B' UNION
SELECT 4,'A' UNION
SELECT 4,'B' UNION
SELECT 4,'D'
SELECT t.id
, case when tsub.TotalTimes=2 AND tsub.NumTimes=2 THEN 'C' ELSE t.value END as Value
FROM csm t
JOIN (
SELECT id, COUNT(DISTINCT CASE WHEN value IN ('A','B') THEN value END) AS NumTimes
, COUNT(DISTINCT value) TotalTimes
FROM csm
GROUP BY id
) AS tsub ON t.id=tsub.id
I get the following output:
1 C
1 C
2 A
3 B
4 A
4 B
4 D
The subquery finds out the number of times A and B occur for that id, and then your case statement checks if that value is 2, and if so changes it to C.
Seems like a perfect match for an analytic function:
with v_data(id, value) as (
select 1, 'A' from dual union all
select 1, 'B' from dual union all
select 2, 'A' from dual union all
select 3, 'B' from dual union all
select 3, 'B' from dual
)
select
v1.*,
(case
when v1.cnt_distinct > 1 then 'C'
else v1.value end)
as new_value
from (
select
id,
value,
count(*) over (partition by id) as cnt_overal,
count(distinct value) over (partition by id) as cnt_distinct
from v_data)
v1
This computes the number of distinct values for each ID (using count(distinct...) and then replaces the values with C if the number of distinct values is larger than 1.
Table Schema
ID Status Patient
1 critical Gabriel
1 moderate Frank
1 critical Dorin
2 low Peter
3 critical Noman
3 moderate Johnson
Expected OutPut
ID Patient1 Patient2
1 Gabriel Dorin
3 Noman Null
Here I have to show only those patient whose situation is critcal.
I found the similar question Multiple column values in a single row, but its in SQL also the columns are hard coded.
Thanks!
First step is to select the critical patients and order them:
select id, patient, row_number() over (partition by id order by patient) as rnk
from your_table
where status='critical';
After this you can select first two critical patients in this manner:
select id,
max(case when rnk=1 then patient end) as Patient1,
max(case when rnk=2 then patient end) as Patient2
from (
select id,
patient,
row_number() over (partition by id order by patient) as rnk
from your_table
where status='critical'
)
group by id;
If you want a more flexible solution you can try a query like below, but you should choose the number of ranks in before the runtime:
with your_table as
(select 1 as id, 'critical' as status, 'Gabriel' as patient from dual
union all
select 1, 'moderate', 'Frank' from dual union all
select 1, 'critical', 'Dorin' from dual union all
select 1, 'critical', 'Vasile' from dual union all
select 2, 'low', 'Peter' from dual union all
select 3, 'critical', 'Noman' from dual union all
select 3, 'moderate', 'Johnson' from dual )
select * from (
select id, patient, row_number() over (partition by id order by patient) as rnk
from your_table
where status='critical'
)
pivot (max(patient) for rnk in (1, 2, 3))
order by 1 ;
(This is for three patients.)
Try to build query and execute the result to a cursor.
SET SERVEROUTPUT ON
DECLARE
v_fact NUMBER := 1;
v_max_cnt number:=1;
V_query CLOB:='';
BEGIN
select max(RNum) into v_max_cnt from(
select row_number() over (partition by ID order by ID) RNum from PATIENTSTATUS where status='critical'
)x;
FOR v_counter IN 1..v_max_cnt LOOP
V_query := V_query||v_fact||' as Patient'||v_fact||(case when v_fact=v_max_cnt then '' else ',' end);
v_fact:=v_fact+1;
END LOOP;
DBMS_OUTPUT.PUT_LINE ('select * from (
select id, patient, row_number() over (partition by id order by patient) as rnk
from PATIENTSTATUS
where status=''critical'')
pivot (max(patient) for rnk in ('||V_query||'))
order by 1;');
END;
From a procedure, data can be inserted to a cursor by
OPEN CUR_Your_Cursor FOR V_query;