Oracle/SQL - Using the rank function - sql

What I'm trying to do is a list of persons from a table and in the event where a person exists more than once then return back their record that contains the highest ranked 'code'
Code ranking (high to low): T, E, F
So for the given dataset
Person Code
----------------
Tom F
Paul E
Mark F
Paul T
Mark E
Chris T
Chris E
I would get the following back from my query
Person Code
----------------
Tom F
Paul T
Mark E
Chris T
I'm assuming this is going to use the rank/analytic functions, but I'm just not familiar enough with them.
Thanks!

You can use the RANK function to rank the data
SQL> ed
Wrote file afiedt.buf
1 with data as (
2 select 'Tom' person, 'F' code from dual union all
3 select 'Paul', 'E' from dual union all
4 select 'Paul', 'T' from dual union all
5 select 'Mark', 'F' from dual union all
6 select 'Mark', 'E' from dual
7 )
8 select *
9 from (select person,
10 code,
11 rank() over (partition by person
12 order by (case when code='T' then 1
13 when code='E' then 2
14 when code='F' then 3
15 else null
16 end)) rnk
17* from data)
SQL> /
PERS C RNK
---- - ----------
Mark E 1
Mark F 2
Paul T 1
Paul E 2
Tom F 1
Elapsed: 00:00:00.00
Then, you just need to select the rows with a RNK of 1
SQL> ed
Wrote file afiedt.buf
1 with data as (
2 select 'Tom' person, 'F' code from dual union all
3 select 'Paul', 'E' from dual union all
4 select 'Paul', 'T' from dual union all
5 select 'Mark', 'F' from dual union all
6 select 'Mark', 'E' from dual
7 )
8 select *
9 from (select person,
10 code,
11 rank() over (partition by person
12 order by (case when code='T' then 1
13 when code='E' then 2
14 when code='F' then 3
15 else null
16 end)) rnk
17 from data)
18* where rnk = 1
SQL> /
PERS C RNK
---- - ----------
Mark E 1
Paul T 1
Tom F 1
Elapsed: 00:00:00.00

The shortest and most performant and Oracle specific solution:
SQL> create table mytable(person,code)
2 as
3 select 'Tom', 'F' from dual union all
4 select 'Paul', 'E' from dual union all
5 select 'Mark', 'F' from dual union all
6 select 'Paul', 'T' from dual union all
7 select 'Mark', 'E' from dual union all
8 select 'Chris', 'T' from dual union all
9 select 'Chris', 'E' from dual
10 /
Table created.
SQL> select person
2 , max(code) keep (dense_rank first order by decode(code,'T',1,'E',2,'F',3,4)) code
3 from mytable
4 group by person
5 /
PERSO C
----- -
Chris T
Mark E
Paul T
Tom F
4 rows selected.
Regards,
Rob.

i don't think RANK is what you need...
basically, your delete will look like this: (pseudo-query)
delete the rows from person
where that row is not in ( select the rows from person with the highest code )
edit:
this trick might help you too:
select person, code, decode( code, 'T', 1, 'E', 2, 'F', 3, 0 ) from mytable

Hum... Alternative suggestion with standard SQL.
Have a CODE_WEIGHT table such as:
CODE WEIGHT
T 3
E 2
F 1
Then group your query by Person (if this is the grouping criterion) and select the distinct code containing max(weight).
I'll post the query in a couple of minutes.
UPDATE
Ok, sorry for the delay.
Here is a solution using the previous stated table and #Randy trick:
SELECT
pp.person, decode(max(c.weight), 3, 'T', 2, 'E', 1, 'F', '') code
FROM
person pp INNER JOIN code_weight c on (pp.code = c.code)
GROUP BY
pp.person
ORDER BY
person DESC;
I'm pretty sure there is a way to dump Oracle proprietary function and get things done in pure SQL... Anyway, since you've asked for a Oracle solution, here it is.
UPDATE 2
And as promised, here's the best standard SQL version that I was able to come up with:
SELECT
p.person, c.code
FROM
(
SELECT
pp.person, MAX(cc.weight) weight
FROM
person pp INNER JOIN code_weight cc ON (pp.code = cc.code)
GROUP BY
pp.person
) p INNER JOIN code_WEIGHT c ON (p.weight = c.weight)
ORDER BY
p.person DESC;
Kinda ugly with the two joins... But it does the job without proprietary extensions. Any SQL guru knows how to optimize it?
Cheers,

Related

select only those users whose contacts length is not 5

I have table like this:
id
name
contact
1
A
65489
1
A
1
A
45564
2
B
3
C
12345
3
C
1234
4
D
32
4
D
324
I only want users who have no contact or the contact length is not five.
If the user has two or more contacts and the length of one of them is five and the rest is not, then such users should not be included in the table.
so,If the customer has at least one contact length of five, I do not want that.
so, i want table like this:
id
name
contact
2
B
4
D
32
4
D
324
Can you halp me?
You could actually do a range check here:
SELECT id, name, contact
FROM yourTable t1
WHERE NOT EXISTS (
SELECT 1
FROM yourTable t2
WHERE t2.id = t1.id AND TO_NUMBER(t2.contact) BETWEEN 10000 AND 99999
);
Note that if contact already be a numeric column, then just remove the calls to TO_NUMBER above and compare directly.
Yet another option:
SQL> with test (id, name, contact) as
2 (select 1, 'a', 65879 from dual union all
3 select 1, 'a', null from dual union all
4 select 1, 'a', 45564 from dual union all
5 select 2, 'b', null from dual union all
6 select 3, 'c', 12345 from dual union all
7 select 3, 'c', 1234 from dual union all
8 select 4, 'd', 32 from dual union all
9 select 4, 'd', 324 from dual
10 )
11 select *
12 from test a
13 where exists (select null
14 from test b
15 where b.id = a.id
16 group by b.id
17 having nvl(max(length(b.contact)), 0) < 5
18 );
ID N CONTACT
---------- - ----------
2 b
4 d 32
4 d 324
SQL>
COUNT analytic function can also be used to get the job done.
select id, name, contact
from (
select id, name, contact
, count( decode( length(contact), 5, 1, null ) ) over( partition by id, name ) cnt
from YourTable
)
where cnt = 0
demo

Oracle Finding a string match from multiple database tables

This is somewhat a complex problem to describe, but I'll try to explain it with an example. I thought I would have been able to use the Oracle Instr function to accomplish this, but it does not accept queries as parameters.
Here is a simplification of my data:
Table1
Person Qualities
Joe 5,6,7,8,9
Mary 7,8,10,15,20
Bob 7,8,9,10,11,12
Table2
Id Desc
5 Nice
6 Tall
7 Short
Table3
Id Desc
8 Angry
9 Sad
10 Fun
Table4
Id Desc
11 Boring
12 Happy
15 Cool
20 Mad
Here is somewhat of a query to give an idea of what I'm trying to accomplish:
select * from table1
where instr (Qualities, select Id from table2, 1,1) <> 0
and instr (Qualities, select Id from table3, 1,1) <> 0
and instr (Qualities, select Id from table3, 1,1) <> 0
I'm trying to figure out which people have at least 1 quality from each of the 3 groups of qualities (tables 2,3, and 4)
So Joe would not be returned in the results because he does not have the quality from each of the 3 groups, but Mary and Joe would since they have at least 1 quality from each group.
We are running Oracle 12, thanks!
Here's one option:
SQL> with
2 table1 (person, qualities) as
3 (select 'Joe', '5,6,7,8,9' from dual union all
4 select 'Mary', '7,8,10,15,20' from dual union all
5 select 'Bob', '7,8,9,10,11,12' from dual
6 ),
7 table2 (id, descr) as
8 (select 5, 'Nice' from dual union all
9 select 6, 'Tall' from dual union all
10 select 7, 'Short' from dual
11 ),
12 table3 (id, descr) as
13 (select 8, 'Angry' from dual union all
14 select 9, 'Sad' from dual union all
15 select 10, 'Fun' from dual
16 ),
17 table4 (id, descr) as
18 (select 11, 'Boring' from dual union all
19 select 12, 'Happy' from dual union all
20 select 15, 'Cool' from dual union all
21 select 20, 'Mad' from dual
22 ),
23 t1new (person, id) as
24 (select person, regexp_substr(qualities, '[^,]+', 1, column_value) id
25 from table1 cross join table(cast(multiset(select level from dual
26 connect by level <= regexp_count(qualities, ',') + 1
27 ) as sys.odcinumberlist))
28 )
29 select a.person,
30 count(b.id) bid,
31 count(c.id) cid,
32 count(d.id) did
33 from t1new a left join table2 b on a.id = b.id
34 left join table3 c on a.id = c.id
35 left join table4 d on a.id = d.id
36 group by a.person
37 having ( count(b.id) > 0
38 and count(c.id) > 0
39 and count(d.id) > 0
40 );
PERS BID CID DID
---- ---------- ---------- ----------
Bob 1 3 2
Mary 1 2 2
SQL>
What does it do?
lines #1 - 22 represent your sample data
T1NEW CTE (lines #23 - 28) splits comma-separated qualities into rows, per every person
final select (lines #29 - 40) are outer joining t1new with each of "description" tables (table2/3/4) and counting how many qualities are contained in there for each of person's qualities (represented by rows from t1new)
having clause is here to return only desired persons; each of those counts have to be a positive number
Maybe this will help:
{1} Create a view that categorises all qualities and allows you to SELECT quality IDs and categories . {2} JOIN the view to TABLE1 and use a join condition that "splits" the CSV value stored in TABLE1.
{1} View
create or replace view allqualities
as
select 1 as category, id as qid, descr from table2
union
select 2, id, descr from table3
union
select 3, id, descr from table4
;
select * from allqualities order by category, qid ;
CATEGORY QID DESCR
---------- ---------- ------
1 5 Nice
1 6 Tall
1 7 Short
2 8 Angry
2 9 Sad
2 10 Fun
3 11 Boring
3 12 Happy
3 15 Cool
3 20 Mad
{2} Query
-- JOIN CONDITION:
-- {1} add a comma at the start and at the end of T1.qualities
-- {2} remove all blanks (spaces) from T1.qualities
-- {3} use LIKE and the qid (of allqualities), wrapped in commas
--
-- inline view: use UNIQUE, otherwise we may get counts > 3
--
select person
from (
select unique person, category
from table1 T1
join allqualities A
on ',' || replace( T1.qualities, ' ', '' ) || ',' like '%,' || A.qid || ',%'
)
group by person
having count(*) = ( select count( distinct category ) from allqualities )
;
-- result
PERSON
Bob
Mary
Tested w/ Oracle 18c and 11g. DBfiddle here.

How to select a number of rows according to a column

So I have got two columns in an Oracle database:
Name / count
I would like to print the name x times, x being the count.
E.g. Paul / 5 would mean Paul being printed 5 times.
Sam / 6 would mean Sam being printed 6 times
Tried row_number over but not sure how it works?
You can use connect by as following:
SQL> WITH YOUR_TABLE AS
2 (SELECT 'paul' as NAME, 5 AS COUNT FROM DUAL UNION ALL
3 SELECT 'sam' as NAME, 6 AS COUNT FROM DUAL
4 ) -- YOUR ACTUAL QUERY STARTS FROM LINE#5
5 Select t.name, m.lvl
6 from your_table t
7 join
8 (Select level as lvl
9 from
10 (Select max(count) as maxcount
11 from your_table)
12 Connect by level <= maxcount) m
13 On (t.count >= m.lvl)
14 ORDER BY 1,2;
NAME LVL
---- ----------
paul 1
paul 2
paul 3
paul 4
paul 5
sam 1
sam 2
sam 3
sam 4
sam 5
sam 6
11 rows selected.
SQL>
Cheers!!
you need recursive query to achieve this.
with cte(nam, ctr) as (
select 'Paul' as nam, 5 as ctr from dual
union all
select 'Sam', 6 as ctr from dual
),
cte2(nam, ct, ctr) as (
select nam, 1 as ct, ctr from cte
union all
select nam, ct + 1, ctr from cte2
where ct<ctr
)select nam, ct from cte2
order by nam asc
output:
See sqlfiddle
this will work:
select name
from Table1,
(select level lvl
from dual
connect by level <= (select max(cnt) from Table1 )
)
where lvl <= cnt
order by name;
check fiddle:http://sqlfiddle.com/#!4/14a67/1
Thanks!!!
Yet another option (your query starts at line #4):
SQL> with your_table as
2 (select 'paul' as name, 5 as count from dual union all
3 select 'sam' as name, 6 as count from dual)
4 select name
5 from your_table cross join table (cast (multiset (select level from dual
6 connect by level <= count
7 ) as sys.odcinumberlist));
NAME
----
paul
paul
paul
paul
paul
sam
sam
sam
sam
sam
sam
11 rows selected.
SQL>
You can use a connect by level <= some_number logic containing cross join to link with your table tab :
with tab(Name,"count") as
( select 'Paul', 5 from dual union all select 'Sam', 6 from dual )
select name, level as seq
from dual d
cross join tab t
connect by level <= t."count"
and prior name = name
and prior sys_guid() is not null;
Demo

Replace duplicate values only in consecutive records with NULL [duplicate]

This question already has an answer here:
shows blanks for repeating values in a result set
(1 answer)
Closed 4 years ago.
Say I have two tables
Tran (Tran_Id, Tran_Name)
TFlag(Tran_Id, Flag)
My Query Result is Like:
TRAN_ID TRAN_NAME FLAG
-------------------------
101 Lend A
101 Lend B
101 Lend C
101 Lend D
102 Borrow E
101 Lend F
101 Lend G
I want the output to be like this:
TRAN_ID TRAN_NAME FLAG
-------------------------
101 Lend A
(null) (null) B
(null) (null) C
(null) (null) D
102 Borrow E
101 Lend F
(null) (null) G
You could use analytic LAG() OVER().
SQL Fiddle for a working demo.
For example,
SQL> SELECT
2 CASE
3 WHEN lag(tran_id) over(order by NULL) = tran_id
4 THEN NULL
5 ELSE tran_id
6 END tran_id,
7 CASE
8 WHEN lag(tran_name) over(order by NULL) = tran_name
9 THEN NULL
10 ELSE tran_name
11 END tran_name,
12 flag
13 FROM t;
TRAN_ID TRAN_N F
---------- ------ -
101 Lend A
B
C
D
102 Borrow E
101 Lend F
G
7 rows selected.
In both SQL Server and Oracle you can use LAG analytic function. Oracle:
WITH data (tran_id, tran_name, flag) AS (
SELECT 01, 'Lend', 'A' FROM DUAL UNION ALL
SELECT 101, 'Lend', 'B' FROM DUAL UNION ALL
SELECT 101, 'Lend', 'C' FROM DUAL UNION ALL
SELECT 101, 'Lend', 'D' FROM DUAL UNION ALL
SELECT 102, 'Borrow', 'E' FROM DUAL UNION ALL
SELECT 101, 'Lend', 'F' FROM DUAL UNION ALL
SELECT 101, 'Lend', 'G' FROM DUAL
)
SELECT
NVL2(keep, tran_id, null) tran_id,
NVL2(keep, tran_name, null) tran_name,
flag
FROM (
SELECT
tran_id, tran_name, flag,
CASE WHEN LAG(tran_id) OVER (ORDER BY flag) <> tran_id OR LAG(tran_name) OVER (ORDER BY flag) <> tran_name THEN 1 END keep
FROM
data)

Is it possible to compare tuples in oracle-compatible sql?

I'm not 100% if tuples is the term for what I'm talking about but I'm looking at something like this:
Table grades
user grade
------------
Jim B
Bill C
Tim A
Jim B+
I know I can do:
SELECT COUNT(*)
FROM grades
WHERE (
(user = 'Jim' AND grade = 'B')
OR (user = 'Tim' AND grade = 'C')
);
But is there a way to do something more like this?
SELECT COUNT(*)
FROM grades
WHERE (user, grade) IN (('Jim','B'), ('Tim','C'));
EDIT: As a side note, I'd only tested with:
(user, grade) = ('Tim','C')
And that fails, so I assumed IN would fail as well, but I was wrong (thankfully!).
The query you posted should be valid syntax
SQL> ed
Wrote file afiedt.buf
1 with grades as (
2 select 'Jim' usr, 'B' grade from dual
3 union all
4 select 'Bill', 'C' from dual
5 union all
6 select 'Tim', 'A' from dual
7 union all
8 select 'Jim', 'B+' from dual
9 )
10 select *
11 from grades
12 where (usr,grade) in (('Jim','B'),
13 ('Tim','C'),
14* ('Tim','A'))
SQL> /
USR GR
---- --
Jim B
Tim A
You could use a subquery to treat a list of tuples like a table:
SELECT COUNT(*)
FROM grades
JOIN (
SELECT 'Jim' as user, 'B' as grade from dual
UNION ALL
SELECT 'Tim', 'C' from dual
UNION ALL
SELECT 'Pim', 'D' from dual
) as SearchTarget
ON SearchTarget.user = grades.user
and SearchTarget.grade = grades.grade