Sorting by max value [duplicate] - sql

This question already has answers here:
How to select records with maximum values in two columns?
(2 answers)
Closed 9 years ago.
I have a table that looks like this in an Oracle DB:
TransactionID Customer_id Sequence Activity
---------- ------------- ---------- -----------
1 85 1 Forms
2 51 2 Factory
3 51 1 Forms
4 51 3 Listing
5 321 1 Forms
6 321 2 Forms
7 28 1 Text
8 74 1 Escalate
And I want to be able to sort out all rows where sequence is the highest for each customer_id.
I there a MAX() function I could use on sequence but based on customer_id somehow?
I would like the result of the query to look like this:
TransactionID Customer_id Sequence Activity
---------- ------------- ---------- -----------
1 85 1 Forms
4 51 3 Listing
6 321 2 Forms
7 28 1 Text
8 74 1 Escalate

select t1.*
from your_table t1
inner join
(
select customer_id, max(Sequence) mseq
from your_table
group by customer_id
) t2 on t1.customer_id = t2.customer_id and t1.sequence = t2.mseq

SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE tbl ( TransactionID, Customer_id, Sequence, Activity ) AS
SELECT 1, 85, 1, 'Forms' FROM DUAL
UNION ALL SELECT 2, 51, 2, 'Factory' FROM DUAL
UNION ALL SELECT 3, 51, 1, 'Forms' FROM DUAL
UNION ALL SELECT 4, 51, 3, 'Listing' FROM DUAL
UNION ALL SELECT 5, 321, 1, 'Forms' FROM DUAL
UNION ALL SELECT 6, 321, 2, 'Forms' FROM DUAL
UNION ALL SELECT 7, 28, 1, 'Text' FROM DUAL
UNION ALL SELECT 8, 74, 1, 'Escalate' FROM DUAL;
Query 1:
SELECT
MAX( TransactionID ) KEEP ( DENSE_RANK LAST ORDER BY Sequence ) AS TransactionID,
Customer_ID,
MAX( Sequence ) KEEP ( DENSE_RANK LAST ORDER BY Sequence ) AS Sequence,
MAX( Activity ) KEEP ( DENSE_RANK LAST ORDER BY Sequence ) AS Activity
FROM tbl
GROUP BY Customer_ID
ORDER BY TransactionID
Results:
| TRANSACTIONID | CUSTOMER_ID | SEQUENCE | ACTIVITY |
|---------------|-------------|----------|----------|
| 1 | 85 | 1 | Forms |
| 4 | 51 | 3 | Listing |
| 6 | 321 | 2 | Forms |
| 7 | 28 | 1 | Text |
| 8 | 74 | 1 | Escalate |

Please Try it
with cte as
(
select Customer_id,MAX(Sequence) as p from Tablename group by Customer_id
)
select b.* from cte a join Tablename b on a.p = b.Sequence where a.p = b.Sequence and a.Customer_id=b.Customer_id order by b.TransactionID

Related

Counting Rows under a Specific Header Row

I am trying to count the number of rows under specific "header rows" - for example, I have a table that looks like this:
Row # | Description | Repair_Code | Data Type
1 | FRONT LAMP | (null) | Header
2 | left head lamp | 1235 | Database
3 | right head lamp | 1236 | Database
4 | ROOF | (null) | Header
5 | headliner | 1567 | Database
6 | WHEELS | (null) | Header
7 | right wheel | 1145 | Database
Rows 1, 4 and 6 are header rows (categories) and the others are descriptors under each of those categories. The Data Type column denotes if the row is a header or not.
I want to be able to count the number of rows under the header rows to return something that looks like:
Header | Occurrences
FRONT LAMP | 2
ROOF | 1
WHEELS | 1
Thank you for the help!
Data model looks wrong. If that's some kind of a hierarchy, table should have yet another column which represents a "parent row#".
The way it is now, it's kind of questionable whether you can - or can not - do what you wanted. The only thing you can rely on is row#, which is sequential in your example. If that's not the case, then you have a problem.
So: if you use a lead analytic function for all header rows, then you could do something like this (sample data in rows #1 - 7; query that might help begins at line #8):
SQL> with test (rn, description, code) as
2 (select 1, 'front lamp' , null from dual union all
3 select 2, 'left head lamp' , 1235 from dual union all
4 select 3, 'right head lamp', 1236 from dual union all
5 select 4, 'roof' , null from dual union all
6 select 5, 'headliner' , 1567 from dual
7 ),
8 hdr as
9 -- header rows
10 (select rn,
11 description,
12 lead(rn) over (order by rn) next_rn
13 from test
14 where code is null
15 )
16 select h.description,
17 count(*)
18 from hdr h join test t on t.rn > h.rn
19 and (t.rn < h.next_rn or h.next_rn is null)
20 group by h.description;
DESCRIPTION COUNT(*)
--------------- ----------
front lamp 2
roof 1
SQL>
If data model was different (note parent_rn column), then you wouldn't depend on sequential row# values, but
SQL> with test (rn, description, code, parent_rn) as
2 (select 0, 'items' , null, null from dual union all
3 select 1, 'front lamp' , null, 0 from dual union all
4 select 2, 'left head lamp' , 1235, 1 from dual union all
5 select 3, 'right head lamp', 1236, 1 from dual union all
6 select 4, 'roof' , null, 0 from dual union all
7 select 5, 'headliner' , 1567, 4 from dual
8 ),
9 calc as
10 (select parent_rn,
11 sum(case when code is null then 0 else 1 end) cnt
12 from test
13 connect by prior rn = parent_rn
14 start with parent_rn is null
15 group by parent_rn
16 )
17 select t.description,
18 c.cnt
19 from test t join calc c on c.parent_rn = t.rn
20 where nvl(c.parent_rn, 0) <> 0;
DESCRIPTION CNT
--------------- ----------
front lamp 2
roof 1
SQL>
I would approach this using window functions. Assign a group to each header by doing a cumulative count of the NULL values of repair_code. Then aggregate:
select max(case when repair_code is null then description end) as description,
count(repair_code) as cnt
from (select t.*,
sum(case when repair_code is null then 1 else 0 end) over (order by row#) as grp
from t
) t
group by grp
order by min(row#);
Here is a db<>fiddle.

SQL: summing a column starting from row immediately after two consecutive 'trigger' values in another column

How to sum all values after two consecutive YES's in the CONDITION_SATISFIED column?
ID | CONDITION_SATISFIED | VALUE
--------------------------------
1 | NO | 100
2 | NO | 300
3 | NO | 500
4 | YES | 100
5 | YES | 300
6 | NO | 500 <-
7 | NO | 100 <-
8 | YES | 300 <-
9 | NO | 500 <-
--------------------------------
SUM | 1400
Note: further occurrences of YES/NO are ignored once the summation is started.
I've gotten to the point where I am able to generate two extra columns for the CONDITION_SATISFIED column like this:
ID | CONDITION_SATISFIED | VALUE RANK | REPEAT_COUNT
-------------------------------- -------------------
1 | NO | 100 1 | 3
2 | NO | 300 1 | 3
3 | NO | 500 1 | 3
4 | YES | 100 2 | 2
5 | YES | 300 2 | 2
6 | NO | 500 3 | 2 <- start from here
7 | NO | 100 3 | 2
8 | YES | 300 4 | 1
9 | NO | 500 5 | 1
-------------------------------- -------------------
But I'm not able to figure out how to get the first instance of REPEAT_COUNT >= 2 AND CONDITION_SATISFIED = 'YES', and then start the summation immediately after the 2nd YES (as indicated).
Hmmm . . . You can get the first where the two yesses are using lag():
select t.*
from (select t.*,
lag(condition_satisfied) over (order by id) as prev_cs,
lag(condition_satisfied, 2) over (order by id) as prev2_cs
from t
) t
where prev2_cs = 'YES' and prev_cs = 'YES';
Then you can just use this in a query:
select t.*
from t join
(select min(t.id) as id
from (select t.*,
lag(condition_satisfied) over (order by id) as prev_cs,
lag(condition_satisfied, 2) over (order by id) as prev2_cs
from t
) t
where prev2_cs = 'YES' and prev_cs = 'YES'
) yy
on t.id >= yy.id;
Oracle 12c: pattern matching
with t1 (id, condition_satisfied, value) as (
select 1, 'NO' , 100 from dual union all
select 2, 'NO' , 300 from dual union all
select 3, 'NO' , 500 from dual union all
select 4, 'YES', 100 from dual union all
select 5, 'YES', 300 from dual union all
select 6, 'NO' , 500 from dual union all
select 7, 'NO' , 100 from dual union all
select 8, 'YES', 300 from dual union all
select 9, 'NO' , 500 from dual)
select sum(v_value) as sum_value
from t1
match_recognize(
order by id
measures s.value as v_value
all rows per match
pattern (yes{2} s+)
define
yes as condition_satisfied = 'YES'
);
SUM_VALUE
----------
1400
If you have version lower than 12 no need to self-join and generate/prevent duplicates:
with s (id, condition_satisfied, value) as (
select 1, 'NO' , 100 from dual union all
select 2, 'NO' , 300 from dual union all
select 3, 'NO' , 500 from dual union all
select 4, 'YES', 100 from dual union all
select 5, 'YES', 300 from dual union all
select 6, 'NO' , 500 from dual union all
select 7, 'YES' , 100 from dual union all
select 8, 'YES', 300 from dual union all
select 9, 'NO' , 500 from dual)
select sum(value) sum_value
from
(select s.*, min(first_id) over () min_id
from
(select s.*,
case when condition_satisfied = 'YES' and condition_satisfied = lag(condition_satisfied) over (order by id) then id end first_id
from s
) s
)
where id > min_id;
SUM_VALUE
----------
1400

SQL report - Matching percent value with a number

I have a small issue with my report and I need to know if its even possible to do it?
Im using Oracle12c and the tool OBIEE, im trying to create a custom column with numbers values (1 and 2) that are matching my results from my "Percent" column in a way I described below.
Here is my results in table:
I will give u an example of how it should work:
Emilian is an owner of few customers, the customers have their annual revenue listed and the column next to it its the Percent value of the total customer revenue for Emilian. Now, in my custom column I need to show "1" for customers that contribute more than (or exact) 80% of his total and "2" for the rest. So in Emilian Case, his first two customers will be "1" since 78% + 14% is already above 80% and the rest will be "2". For other Owners that only have one customer, all of them logically would be matched with "1" since their contribution is 100%
Hope I made this clear, will be veery grateful for the help with coding it :)
Alex
There's probably a much more efficient way to do this. I built up what you need to get at with a series of sub-selects. This still doesn't handle the equal percents, but you said that isn't an expected problem. I'd still watch out for it.
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE t1 ( ownerId int, customerId int, revenue int ) ;
INSERT INTO t1 ( ownerid, customerid, revenue )
SELECT 1, 1, 99 FROM dual UNION ALL
SELECT 1, 2, 200 FROM dual UNION ALL
SELECT 1, 3, 300 FROM dual UNION ALL
SELECT 1, 4, 400 FROM dual UNION ALL
SELECT 2, 5, 100 FROM dual UNION ALL
SELECT 2, 6, 100 FROM dual UNION ALL
SELECT 2, 7, 200 FROM dual UNION ALL
SELECT 2, 8, 600 FROM dual UNION ALL
SELECT 3, 9, 100 FROM dual UNION ALL
SELECT 3, 10, 900 FROM dual UNION ALL
SELECT 4, 11, 1000 FROM dual UNION ALL
SELECT 5, 12, 1000 FROM dual UNION ALL
SELECT 6, 13, 200 FROM dual UNION ALL
SELECT 6, 14, 200 FROM dual UNION ALL
SELECT 6, 15, 200 FROM dual UNION ALL
SELECT 6, 16, 200 FROM dual UNION ALL
SELECT 6, 17, 200 FROM dual UNION ALL
SELECT 42, 736784, 1480000 FROM dual UNION ALL
SELECT 42, 736580, 280160 FROM dual UNION ALL
SELECT 42, 1040137, 112486 FROM dual UNION ALL
SELECT 42, 738685, 22903 FROM dual UNION ALL
SELECT 42, 736781, 56 FROM dual
;
Query 1:
SELECT s3.ownerID, s3.customerID, s3.revenue, s3.OwnerRevenue
, CAST(s3.customerRevPct AS decimal(5,2)) AS customerRevPct
, CASE WHEN s3.PctRT < 80 OR s3.custCount = 1 THEN 1 ELSE 2 END AS customCol
/* Do the running pcts add up to 80+? 1 customer = 100% == 1. What if all are pcts are equal? */
FROM (
SELECT s2.*
, 100-SUM(nvl(s2.customerRevPct,0)) OVER (PARTITION BY s2.ownerID ORDER BY s2.customerRevPct, s2.customerID RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS pctRT
, COUNT(*) OVER (PARTITION BY s2.ownerID ORDER BY (s2.ownerID) ) AS custCount /* Is there only 1 customer? */
FROM (
SELECT s1.*
, ( ( ( s1.revenue * 1.0 ) / s1.ownerRevenue ) * 100 ) AS customerRevPct
FROM (
SELECT t1.ownerID, t1.customerID, t1.revenue
, SUM(t1.revenue) OVER ( PARTITION BY t1.ownerID ) AS ownerRevenue
FROM t1
) s1
) s2
) s3
WHERE ownerID = 42 /* REMOVE THIS LINE - TESTING ONLY */
ORDER BY s3.ownerID, s3.customerRevPct DESC
Results:
| OWNERID | CUSTOMERID | REVENUE | OWNERREVENUE | CUSTOMERREVPCT | CUSTOMCOL |
|---------|------------|---------|--------------|----------------|-----------|
| 42 | 736784 | 1480000 | 1895605 | 78.08 | 1 |
| 42 | 736580 | 280160 | 1895605 | 14.78 | 1 |
| 42 | 1040137 | 112486 | 1895605 | 5.93 | 2 |
| 42 | 738685 | 22903 | 1895605 | 1.21 | 2 |
| 42 | 736781 | 56 | 1895605 | 0 | 2 |
EDIT: I changed the Fiddle to illustrate your data example.
create table custrev(owner varchar2(100), cust_id number, revenue number);
insert into custrev values('Emilian',1,1480000);
insert into custrev values('Emilian',2,280160);
insert into custrev values('Emilian',3,112486);
insert into custrev values('Emilian',4,22903);
insert into custrev values('Emilian',5,56);
insert into custrev values('Andy',6,1378);
insert into custrev values('Sandy',7,560000);
commit;
Below is the SQL for your requirement.
select owner, cust_id, revenue, pct,
case when pct = 100 then 1
when flg is null or flg < 80 then 1
else 2 end flag_col
from (select owner, cust_id, revenue, pct,--cumulative_sum,
lag(cumulative_sum) over(partition by owner
order by revenue desc) flg
from (select owner, cust_id, revenue, pct,
sum(pct) over(partition by owner
order by revenue desc
rows between unbounded preceding
and current row) cumulative_sum
from (select owner, cust_id, revenue,
round(ratio_to_report(revenue) over(partition by owner)*100,2) pct
from custrev)
)
)
order by owner, revenue desc;
Output:
OWNER CUST_ID REVENUE PCT FLAG_COL
Andy 6 1378 100 1
Emilian 1 1480000 78.08 1
Emilian 2 280160 14.78 1
Emilian 3 112486 5.93 2
Emilian 4 22903 1.21 2
Emilian 5 56 0 2
Sandy 7 560000 100 1
Alex,
OBIEE is based on models. Not on SQL.
So sorry to say this but the SQL code will help you exactly zero...

create window group based on value of preceding row

I have a table like so:
#standardSQL
WITH k AS (
SELECT 1 id, 1 subgrp, 'stuff1' content UNION ALL
SELECT 2, 2, 'stuff2' UNION ALL
SELECT 3, 3, 'stuff3' UNION ALL
SELECT 4, 4, 'stuff4' UNION ALL
SELECT 5, 1, 'ostuff1' UNION ALL
SELECT 6, 2, 'ostuff2' UNION ALL
SELECT 7, 3, 'ostuff3' UNION ALL
SELECT 8, 4, 'ostuff4'
)
and like to group based on the subgrp value to re-create the missing grp: if subgrp value is smaller than previous row, belongs to same group.
Intermediate result would be:
| id | grp | subgrp | content |
| 1 | 1 | 1 | stuff1 |
| 2 | 1 | 2 | stuff2 |
| 3 | 1 | 3 | stuff3 |
| 4 | 1 | 4 | stuff4 |
| 5 | 2 | 1 | ostuff1 |
| 6 | 2 | 2 | ostuff2 |
| 7 | 2 | 3 | ostuff3 |
| 8 | 2 | 4 | ostuff4 |
on which I can then apply
SELECT id, grp, ARRAY_AGG(STRUCT(subgrp, content)) rcd
FROM k ORDER BY id, grp
to have I nice nested structure.
Notes:
with 'id' ordered, subgrp is always in sequence so no 3 before 2
groups are not always 4 subgrp's - this is just to illustrate so cannot hardcode
Problem: how can I (re)create the grp column here ? I played with several Window functions to no avail.
EDIT
Although Gordon's answer work, it took 3min over 104M records to run and I had to remove an ORDER BY on the final resultset because of Resources exceeded during execution: The query could not be executed in the allotted memory. ORDER BY operator used too much memory.
Anyone having an alternative solution for large dataset ?
A simple way to assign the group is to do a cumulative count of the subgrp = 1 values:
select k.*,
sum(case when subgrp = 1 then 1 else 0 end) over (order by id) as grp
from k;
You can also do it your way, using lag() and a cumulative sum. That requires a subquery:
select k.*,
sum(case when prev_subgrp = subgrp then 0 else 1 end) over (order by id) as grp
from (select k.*,
lag(subgrp) over (order by id) as prev_subgrp
from k
) k
Below can potentially perform better - but has limitation - I assume there is no gaps in numbering within subgroups and respective ids
#standardSQL
WITH k AS (
SELECT 1 id, 1 subgrp, 'stuff1' content UNION ALL
SELECT 2, 2, 'stuff2' UNION ALL
SELECT 3, 3, 'stuff3' UNION ALL
SELECT 4, 4, 'stuff4' UNION ALL
SELECT 5, 1, 'ostuff1' UNION ALL
SELECT 6, 2, 'ostuff2' UNION ALL
SELECT 7, 3, 'ostuff3' UNION ALL
SELECT 8, 4, 'ostuff4'
)
SELECT
ROW_NUMBER() OVER(ORDER BY id) grp,
rcd
FROM (
SELECT
MIN(id) id,
ARRAY_AGG(STRUCT(subgrp, content)) rcd
FROM k
GROUP BY id - subgrp
)
result is
Row grp rcd.subgrp rcd.content
1 1 1 stuff1
2 stuff2
3 stuff3
4 stuff4
2 2 1 ostuff1
2 ostuff2
3 ostuff3
4 ostuff4

Putting two rows together on the same row in Oracle

Let's take as example this table, which associate a product to the number
of stars he received from customer's feedbacks (it happens to be ordered by number of stars):
| ProductID | Stars |
|-----------|---------|
| 23 | 10 |
| 12 | 10 |
| 17 | 9 |
| 5 | 8 |
| 20 | 8 |
| 18 | 7 |
How would I select (showing them on the same row) the IDs of the products
in pairs?
Like this:
| Product1 | Product2 |
|-----------|-------------|
| 23 | 12 |
| 17 | 5 |
| 20 | 18 |
Or like this:
| Products |
|------------------|
| 23 12 |
| 17 5 |
| 20 18 |
Use listagg to produce option2
select stars, listagg(ProductID, ' ') within group (order by ProductID) as Products
from Table1
group by Stars
It is not clear why in your output you picked 5 before 20; remember that rows in a table are NOT ORDERED. In my solution I order by stars and then by productid; if your rows are ordered by something else as well, you may use that instead.
You may change 2 to 7 in the divisions by 2 if you want to group 7 values at a time.
This is offered only to show that this CAN be done in Oracle SQL (in the database). It SHOULDN'T, but that's for you to decide.
with
inputs ( productid, stars) as (
select 23, 10 from dual union all
select 12, 10 from dual union all
select 17, 9 from dual union all
select 5, 8 from dual union all
select 20, 8 from dual union all
select 18, 7 from dual
)
-- end of test data, solution begins below
select listagg(productid, ' ') within group (order by rn) as result
from ( select productid, stars,
row_number() over (order by stars desc, productid desc) as rn
from inputs
)
group by ceil(rn/2)
order by ceil(rn/2)
;
RESULT
------
23 12
17 20
5 18
WITH t
AS (SELECT 23 product_id, 10 stars FROM DUAL
UNION ALL
SELECT 12, 10 FROM DUAL
UNION ALL
SELECT 17, 9 FROM DUAL
UNION ALL
SELECT 5, 8 FROM DUAL
UNION ALL
SELECT 20, 8 FROM DUAL
UNION ALL
SELECT 18, 7 FROM DUAL),
t2
AS ( SELECT product_id,
stars,
ROW_NUMBER () OVER (ORDER BY stars DESC)
+ MOD (ROW_NUMBER () OVER (ORDER BY stars DESC), 2)
grp
FROM t
ORDER BY stars DESC)
SELECT LISTAGG (product_id, ' ') WITHIN GROUP (ORDER BY stars DESC, ROWNUM)
AS product_id
FROM t2
GROUP BY grp
result
23 12
17 5
20 18
On oracle you can use a function call listagg which works sort of like join in C# (and other languages.) For it to work you need to have something to group buy -- it seems you want to group by every other when ordered by a column. First make a row number for the column and then group by that div 2
so
SELECT LISTAGG(ProductID, ' ') WITHIN GROUP (ORDER BY ProductID) AS ProductList
FROM (
SELECT ProductID, FLOOR((ROW_NUMBER() OVER (ORDER BY Stars)+1)/2) as GroupByMe FROM Table
) X
GROUP BY GroupByMe
If you want to do every 3, just /3 instead.
More interesting (because more people want to see it) is getting a list for every amount of stars, that looks like this:
SELECT Stars as StarCount, LISTAGG(ProductID, ' ') WITHIN GROUP (ORDER BY ProductID) As ProductList
FROM Table
GROUP BY Stars
of course this looks like a prior answer because this is what people would expect you would want.