One value for a group of columns in sql - sql

I have the columns like this, all columns coming from three different tables.
GROUPID COMPANYID CUSTID DEAL
121 A 1 DEAL1
121 A 1 DEAL2
121 A 1 DEAL3
121 B 2 DEAL1
121 B 2 DEAL2
121 B 2 DEAL3
i want the columns like
GROUPID COMPANYID CUSTID DEAL
121 A 1 DEAL1
DEAL2
DEAL3
B 2 DEAL1
DEAL2
DEAL3
Can you help me with this?
Thanks

Well, from my point of view, this is the client problem, not necessarily SQL one. Any decent reporting tool (such as Oracle Reports Builder, Oracle Apex Classic report, ...) is capable of breaking data on columns you choose.
Even the good, old SQL*Plus knows how to do that.
This is your current result:
SQL> select * from three_tables order by groupid, companyid, custid, deal;
GROUPID COMPANYID CUSTID DEAL
---------- ---------- ---------- -----
121 A 1 DEAL1
121 A 1 DEAL2
121 A 1 DEAL3
121 B 2 DEAL1
121 B 2 DEAL2
121 B 2 DEAL3
6 rows selected.
Break (as I said):
SQL> break on groupid on companyid on custid
SQL>
SQL> select * from three_tables order by groupid, companyid, custid, deal;
GROUPID COMPANYID CUSTID DEAL
---------- ---------- ---------- -----
121 A 1 DEAL1
DEAL2
DEAL3
B 2 DEAL1
DEAL2
DEAL3
6 rows selected.
SQL>
I suggest you do the same - set the breaking option in a tool you use.

i am just enhancing Tim answer by adding lag() window function
WITH yourTable AS (
SELECT 121 AS GROUPID, 'A' AS COMPANYID, 1 AS CUSTID, 'DEAL1' AS DEAL FROM dual UNION ALL
SELECT 121, 'A', 1, 'DEAL2' FROM dual UNION ALL
SELECT 121, 'A', 1, 'DEAL3' FROM dual UNION ALL
SELECT 121, 'B', 2, 'DEAL1' FROM dual UNION ALL
SELECT 121, 'B', 2, 'DEAL2' FROM dual UNION ALL
SELECT 121, 'B', 2, 'DEAL3' FROM dual UNION ALL
SELECT 123, 'c', 2, 'DEAL1' FROM dual UNION ALL
SELECT 123, 'c', 2, 'DEAL2' FROM dual
),
cte AS (
SELECT t.*, ROW_NUMBER() OVER (PARTITION BY GROUPID, COMPANYID, CUSTID ORDER BY DEAL) rn
FROM yourTable t
)
,cte1 as(
SELECT
GROUPID,
CASE WHEN rn = 1 THEN COMPANYID END AS COMPANYID,
CASE WHEN rn = 1 THEN CUSTID END AS CUSTID,
DEAL
FROM cte t
ORDER BY
GROUPID,
t.COMPANYID,
CUSTID,
DEAL) select case when lag(GROUPID)over(order by GROUPID)=GROUPID
then null else GROUPID end Gid,COMPANYID,CUSTID,DEAL from cte1
DEMO

This should not be done in SQL. You should be doing this in the reporting tool that you use. GroupID, CompanyID and CustID can be the dimensions and Deal can be the measure.

If you want to do that in SQL, then you probably need to use as many analytic functions as you have columns to outpout
select
case when GROUPID != lag(GROUPID, 1, 999999999)over(partition by GROUPID order by GROUPID) then GROUPID else null end GROUPID
, case when COMPANYID != lag(COMPANYID, 1, 'ZZZZZZZZZ')over(partition by GROUPID, COMPANYID order by COMPANYID) then COMPANYID else null end COMPANYID
, case when CUSTID != lag(CUSTID, 1, 999999999)over(partition by GROUPID, COMPANYID, CUSTID order by CUSTID) then CUSTID else null end CUSTID
, case when DEAL != lag(DEAL, 1, 'ZZZZZZZZZ')over(partition by GROUPID, COMPANYID, CUSTID, DEAL order by DEAL) then DEAL else null end DEAL
from your_table
;

Related

Find last and first row for every id

I have this table:
id
RANK
111
1
111
2
111
3
222
1
222
2
I want to add two colums that will show if this is the first/last row for each id
id
first
last
111
YES
NO
111
NO
NO
111
NO
YES
222
YES
NO
222
NO
YES
Let's first point out that sorting without column to sort this is no good idea.
Usually, an id is unique and will be incremented, so it will already be sufficient to order by id.
If this is not the case, there should be at least be another column with a meaningful value (for example also an incrementing number or a datetime) which can be used to sort the result.
So you should fix your table design if possible and add such a column or make your already existing id column unique.
If this is not possible and you really have to order just by the row number, you could do following:
SELECT id,
CASE WHEN rn = 1 THEN 'YES' ELSE 'NO' END AS first,
CASE WHEN rn = COUNT(*) OVER (PARTITION BY id)
THEN 'YES' ELSE 'NO' END AS last
FROM
(
SELECT
id,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY id) rn
FROM yourtable
);
If you have a column to sort (let's name it "rank"), this will be much safer:
SELECT id,
CASE WHEN rn1 = 1 THEN 'YES' ELSE 'NO' END AS first,
CASE WHEN rn2 = 1 THEN 'YES' ELSE 'NO' END AS last
FROM
(
SELECT
id,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY rank) rn1,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY rank DESC) rn2
FROM yourtable
);
Here's one option:
Sample data:
SQL> with
2 test (id, rank) as
3 (select 111, 1 from dual union all
4 select 111, 2 from dual union all
5 select 111, 3 from dual union all
6 select 222, 1 from dual union all
7 select 222, 2 from dual
8 ),
Query begins here:
9 temp as
10 (select id,
11 rank,
12 first_value(rank) over (partition by id) rnk_min,
13 last_value(rank) over (partition by id ) rnk_max
14 from test
15 )
16 select id,
17 case when rank = rnk_min then 'Yes' else 'No' end first,
18 case when rank = rnk_max then 'Yes' else 'No' end last
19 from temp
20 order by id, rank;
ID FIRST LAST
---------- ------- -------
111 Yes No
111 No No
111 No Yes
222 Yes No
222 No Yes
SQL>
If you don't have rows with the same rank per id, you may use lag/lead functions to mark first and last rows with a flag using default argument of these functions, which is used when the function leaves a window boundary.
with sample_tab (id, rank) as (
select 111, 1 from dual union all
select 111, 2 from dual union all
select 111, 3 from dual union all
select 222, 1 from dual union all
select 222, 2 from dual
)
select
id
, lag('No', 1, 'Yes') over(partition by id order by rank asc) as last
, lead('No', 1, 'Yes') over(partition by id order by rank asc) as last
from sample_tab
ID
LAST
LAST
111
Yes
No
111
No
No
111
No
Yes
222
Yes
No
222
No
Yes
If the data may have the same rank for multiple rows per id, you may use the same technique (a case when function goes beyound window boundary) with coalesce.
with sample_tab (id, rank) as (
select 111, 1 from dual union all
select 111, 2 from dual union all
select 111, 2 from dual union all
select 222, 1 from dual union all
select 222, 2 from dual
)
select
id
, coalesce(max('No') over(
partition by id order by rank asc
/*RANGE for logical offset,
setting the same flag for a group of first/last rows*/
range between 1 preceding and 1 preceding
), 'Yes') as first
, coalesce(max('No') over(
partition by id order by rank asc
range between 1 following and 1 following
), 'Yes') as last
from sample_tab
ID
FIRST
LAST
111
Yes
No
111
No
Yes
111
No
Yes
222
Yes
No
222
No
Yes
fiddle

Filter based on condition in WHERE clause

I have a table where I have to pick one of two if it is present. For example if a ID has ACCEPTED and SETTLED , I have to only pick SETTLED else the remaining. Only ACCEPTED/SETTLED always comes as duplicates
Input:
Output:
Query Tried:
SELECT * FROM TABLE
WHERE CASE WHEN "Status" IN ('ACCEPTED','SETTLED') THEN 'SETTLED'
WHEN "Status" IN ('ACCEPTED') THEN 'ACCEPTED'
ELSE "Status" END In ('SETTLED','ACCEPTED')
If your groups are defined by ID and Amount, you could do something like:
SELECT
t.ID,
MAX(t.Status),
t.Amount
FROM t
GROUP BY t.ID, t.Amount
ORDER BY t.ID
db<>fiddle
This is one option (sample data in lines #1 - 7; query begins at line #8). It ranks statuses so that SETTLED comes first, and then the rest of them.
SQL> with test (id, status, amount) as
2 (select 1, 'ACCEPTED', 13 from dual union all
3 select 1, 'SETTLED' , 13 from dual union all
4 select 2, 'SETTLED' , 155 from dual union all
5 select 3, 'ACCEPTED', 123 from dual union all
6 select 4, 'REJECTED', 140 from dual
7 )
8 select id, status, amount
9 from (select id, status, amount,
10 row_number() over (partition by id
11 order by case when status = 'SETTLED' then 1 else 2 end) rn
12 from test
13 )
14 where rn = 1;
ID STATUS AMOUNT
---------- -------- ----------
1 SETTLED 13
2 SETTLED 155
3 ACCEPTED 123
4 REJECTED 140
SQL>

ORACLE get rows with condition value equals something but not equals to anything else

I have rows that look like .
OrderNo OrderStatus SomeOtherColumn
A 1
A 1
A 3
B 1 X
B 1 Y
C 2
C 3
D 2
I want to return all orders that have only one possible value of orderstatus. For e.g Here order B has only order status 1 SO result should be
B 1 X
B 1 Y
Notes:
Rows can be duplicated with same order status. For e.g. B here.
I am interested in the order having a very peculiar status for e.g. 1 here and not having any other status. So if B had a status of 3 at any point of time it is disqualified.
You can use not exists:
select t.*
from t
where not exists (select 1
from t t2
where t.orderno = t2.orderno and t.OrderStatus = t2.OrderStatus
);
If you just want the orders where this is true, you can use group by and having:
select orderno
from t
group by orderno
having min(OrderStatus) = max(OrderStatus);
If you only want a status of 1 then add max(OrderStatus) = 1 to the having clause.
Here is one way to do it. It does not handle the case where the status can be NULL; if that is possible, you will need to explain how you want it handled.
SQL> create table test_data ( orderno, status, othercol ) as (
2 select 'A', 1, null from dual union all
3 select 'A', 1, null from dual union all
4 select 'A', 3, null from dual union all
5 select 'B', 1, 'X' from dual union all
6 select 'B', 1, 'Y' from dual union all
7 select 'C', 2, null from dual union all
8 select 'C', 3, null from dual union all
9 select 'D', 2, null from dual
10 );
Table created.
SQL> variable input_status number
SQL> exec :input_status := 1
PL/SQL procedure successfully completed.
SQL> column orderno format a8
SQL> column othercol format a8
SQL> select orderno, status, othercol
2 from (
3 select t.*, count(distinct status) over (partition by orderno) as cnt
4 from test_data t
5 )
6 where status = :input_status
7 and cnt = 1
8 ;
ORDERNO STATUS OTHERCOL
-------- ---------- --------
B 1 X
B 1 Y
One way to handle NULL status (if that may happen), if in that case the orderno should be rejected (not included in the output), is to define the cnt differently:
count(case when status != :input_status or status is null then 1 end)
over (partition by orderno) as cnt
and in the outer query change the WHERE clause to a single condition,
where cnt = 0
Count distinct OrderStatus partitioned by OrderNo and show only rows where number equals one:
select OrderNo, OrderStatus, SomeOtherColumn
from ( select t.*, count(distinct orderstatus) over (partition by orderno) cnt
from t )
where cnt = 1
SQLFiddle demo
Just wanted to add something to Gordon's answer, using a stats function:
select orderno
from t
group by orderno
having variance(orderstatus) = 0;

Efficient way to pull counts for all permutations of a field

I have an oracle DB w/ a table that contains records associated to a person (based on an ID). The records are categorized as category = 1, 2, or 3.
I would like to pull as follows:
- # of people with only a category 1 record (no category=2 or 3)
- # of people with only a category 2 record (no category=1 or 3)
- # of people with only a category 3 record (no category=1 or 2)
- # of people with both category 1 & 2 records (no category=3)
- # of people with both category 1 & 3 records (no category=2)
- # of people with all category records 1,2, & 3
- # of people with both a category 2 & 3 records (no category=1)
I could only think of the following solution (modified for each case):
select count(*) from table1
where id in (select id from table1 where category=1)
and id not in (select id from table1 where category=2)
and id not in (select id from table1 where category=3)
But, I believe this is a highly inefficient way of doing this, was wondering if anyone had quicker/better way of getting this info.
Thanks!
One way to do this is to bring the categories together, using listagg() and then reaggregate:
select categories, count(*)
from (select listagg(t1.category, ',') within group (order by t1.category) as categories, personid
from table1 t1
group by personid
) x
group by categories;
EDIT:
If you need distinct values:
select categories, count(*)
from (select listagg(t1.category, ',') within group (order by t1.category) as categories, personid
from (select distinct t1.category, t1.personid from table1 t1) t1
group by personid
) x
group by categories;
Here is a query that, for each ID, shows the count of distinct categories and the MIN and MAX category. This query can be used as a sub-query in further processing (you didn't explain exactly HOW you want the results to be presented). When the COUNT is 1, then the single category is that in the MIN_CAT column; when the COUNT is 3, then all three categories are present for that ID; and when the COUNT is 2, then the two categories that are present are in the MIN and the MAX columns. Whatever else you need to do from here should be very simple; for example you can now GROUP BY CT, MIN_CAT, MAX_CT and count ID's.
I do a count(distinct category) to allow the possibility of non-unique (id, category) - as illustrated in the sample data I include in a WITH clause (which is NOT part of the SQL query!)
with
test_data ( id, category ) as (
select 101, 3 from dual union all
select 101, 1 from dual union all
select 101, 3 from dual union all
select 104, 2 from dual union all
select 105, 2 from dual union all
select 105, 2 from dual union all
select 105, 1 from dual union all
select 106, 1 from dual union all
select 106, 2 from dual union all
select 106, 3 from dual union all
select 106, 3 from dual
)
select id,
count(distinct category) as ct,
min(category) as min_cat,
max(category) as max_cat
from test_data
group by id
;
ID CT MIN_CAT MAX_CAT
--- -- ------- -------
101 2 1 3
105 2 1 2
104 1 2 2
106 3 1 3
Oracle Setup:
CREATE TABLE test_data ( id, category ) as
select 101, 3 from dual union all
select 101, 1 from dual union all
select 101, 3 from dual union all
select 104, 2 from dual union all
select 105, 2 from dual union all
select 105, 2 from dual union all
select 105, 1 from dual union all
select 106, 1 from dual union all
select 106, 2 from dual union all
select 106, 3 from dual union all
select 106, 3 from dual union all
select 107, 1 from dual union all
select 107, 3 from dual;
Query:
SELECT c1,
c2,
c3,
LTRIM(
DECODE( c1, 1, ',1' ) || DECODE( c2, 1, ',2' ) || DECODE( c3, 1, ',3' ),
','
) AS categories,
COUNT(1) AS num_people,
LISTAGG( id, ',' ) WITHIN GROUP ( ORDER BY id ) AS people
FROM ( SELECT DISTINCT * FROM test_data )
PIVOT ( COUNT(1) FOR category IN ( 1 AS c1, 2 AS c2, 3 AS c3 ) )
GROUP BY c1, c2, c3;
Output:
C1 C2 C3 CATEGORIES NUM_PEOPLE PEOPLE
-- -- -- ---------- ---------- ----------
0 1 0 2 1 104
1 0 1 1,3 2 101,107
1 1 0 1,2 1 105
1 1 1 1,2,3 1 106

How to exclude one particular attribute of column from a case statement?

Assume that there is a column which has 2 attribute a and b. I only want it to consider only a to the overall case condition. How do I apply that in case statement?
Something like:
Case when student> 100 then 'big class'
else ''
End [big class ]
*exclude class 10th.*
I don't get your question, If you want to get the "big class" of Divison A ,It is not necessary for using case. or you have additional requirement.
;WITH SampleTable(GroupNo,NoOfStudent,Division)AS(
SELECT 'grp 1',105,'a' UNION ALL
SELECT 'grp 2',107,'a' UNION ALL
SELECT 'grp 3',108,'b' UNION ALL
SELECT 'grp 4',110,'b' UNION ALL
SELECT 'grp 5',85,'b' UNION ALL
SELECT 'grp 6',84,'a' UNION ALL
SELECT 'grp 7',83,'a' UNION ALL
SELECT 'grp 8',110,'b' UNION ALL
SELECT 'grp 9',105,'a' UNION ALL
SELECT 'grp 10',140,'b'
)
--1
SELECT * FROM SampleTable WHERE Division='A' AND NoOfStudent>100
GroupNo NoOfStudent Division
------- ----------- --------
grp 1 105 a
grp 2 107 a
grp 9 105 a
--2
SELECT * ,CASE WHEN Division='A' AND NoOfStudent>100 THEN 'big class' ELSE '' END AS [BigClass]
FROM SampleTable
GroupNo NoOfStudent Division BigClass
------- ----------- -------- ---------
grp 1 105 a big class
grp 2 107 a big class
grp 3 108 b
grp 4 110 b
grp 5 85 b
grp 6 84 a
grp 7 83 a
grp 8 110 b
grp 9 105 a big class
grp 10 140 b