How to get second largest column value and column name - sql

How can I get second largest column value and its name?
My current query gives it mostly correct but in cases where largest and second largest values are same I am getting wrong values.
select item_code, A, B, C,
greatest(A, B, C) as largest1,
greatest(case when largest1 = A then 0 else A end,
case when largest1 = B then 0 else B end,
case when largest1 = C then 0 else C end) as largest2,
(case largest1 when A then 'A'
when B then 'B'
when C then 'C' end) as largest1_column_name,
(case largest2 when A then 'A'
when B then 'B'
when C then 'C' else 'None' end) as largest2_column_name
from table1
Below is the sample table:
+-----------+----+----+----+
| item_code | A | B | C |
+-----------+----+----+----+
| p1 | 20 | 30 | 40 |
| p2 | 50 | 30 | 10 |
| p3 | 30 | 50 | 10 |
| p4 | 30 | 30 | 30 |
| p5 | 50 | 50 | 10 |
| p6 | 0 | 0 | 0 |
+-----------+----+----+----+
Below is expected output:
+-----------+----+----+----+----------+----------+----------------------+----------------------+
| item_code | A | B | C | largest1 | largest2 | largest1_column_name | largest2_column_name |
+-----------+----+----+----+----------+----------+----------------------+----------------------+
| p1 | 20 | 30 | 40 | 40 | 30 | C | B |
| p2 | 50 | 30 | 10 | 50 | 30 | A | B |
| p3 | 30 | 50 | 10 | 50 | 30 | B | A |
| p4 | 30 | 30 | 30 | 30 | 30 | A | B |
| p5 | 50 | 50 | 10 | 50 | 50 | A | B |
| p6 | 0 | 0 | 0 | 0 | 0 | A | B |
+-----------+----+----+----+----------+----------+----------------------+----------------------+
This is the output I am getting from my query (I have marked wrong as comment):
+-----------+----+----+----+----------+-------------+----------------------+----------------------+
| item_code | A | B | C | largest1 | largest2 | largest1_column_name | largest2_column_name |
+-----------+----+----+----+----------+-------------+----------------------+----------------------+
| p1 | 20 | 30 | 40 | 40 | 30 | C | B |
| p2 | 50 | 30 | 10 | 50 | 30 | A | B |
| p3 | 30 | 50 | 10 | 50 | 30 | B | A |
| p4 | 30 | 30 | 30 | 30 | 0/*wrong*/ | A | NULL/*wrong*/ |
| p5 | 50 | 50 | 10 | 50 | 10/*wrong*/ | A | C/*wrong*/ |
| p6 | 0 | 0 | 0 | 0 | 0/*wrong*/ | A | A/*wrong*/ |
+-----------+----+----+----+----------+-------------+----------------------+----------------------+

I tried a slight variation of this (listagg instead of string_agg) in Snowflake and it seemed to be getting the expected result
with cte (item_code, abc, id) as
(select item_code, a, 'a' from table1 union all
select item_code, b, 'b' from table1 union all
select item_code, c, 'c' from table1)
select item_code,
max(case when id='a' then abc end) a,
max(case when id='b' then abc end) b,
max(case when id='c' then abc end) c,
split_part(string_agg(abc::varchar,',' order by abc desc),',',1) largest1,
split_part(string_agg(abc::varchar,',' order by abc desc),',',2) largest2,
split_part(string_agg(id,',' order by abc desc),',',1) largest1_col,
split_part(string_agg(id,',' order by abc desc),',',2) largest2_col
from cte
group by item_code;

This might be simpler achieved by unpivoting the rows, ranking the values, and then using conditional aggregation. In Postgres, you could phrase this as:
select t.*, x.*
from table1 t1
cross join lateral (
select
min(val) filter(where rn = 1) largest1,
min(val) filter(where rn = 2) largest2,
min(col) filter(where rn = 1) largest1_column_name,
min(col) filter(where rn = 2) largest2_column_name
from (
select x.*, dense_rank() over(order by val desc) rn
from (values ('a', a), ('b', b), ('c', c)) as x(col, val)
) x
) x

Related

Subtract values in each row of a column based on a WHERE and GROUP BY statement in SQL

I would like to subtract each row "Value" with the "Value" where Sub1=0 grouping by ID_1 and ID_2 using a SQL query.
This is the table structure:
------------------------------------
ID_1 |ID_2 | sub1 | Value
------------------------------------
1 | a | 0 | 20
1 | a | 50 | 30
1 | a | 100 | 40
1 | b | 0 | 25
1 | b | 50 | 30
1 | b | 100 | 50
2 | a | 0 | 5
2 | a | 50 | 10
2 | a | 100 | 30
2 | b | 0 | 25
2 | b | 50 | 50
2 | b | 100 | 70
I would like to group by ID_1 and ID_2 and subtract each row's value with the value where the Sub1=0
Output table should be :
------------------------------------
ID_1 |ID_2 | sub1 | Value | Diff
------------------------------------
1 | a | 0 | 20 | 0
1 | a | 50 | 30 | 10
1 | a | 100 | 40 | 20
1 | b | 0 | 25 | 0
1 | b | 50 | 30 | 5
1 | b | 100 | 50 | 25
2 | a | 0 | 5 | 0
2 | a | 50 | 10 | 5
2 | a | 100 | 30 | 25
2 | b | 0 | 25 | 0
2 | b | 50 | 50 | 25
2 | b | 100 | 70 | 45
Use a window function:
select t.*,
(value -
sum(case when sub1 = 0 then value else 0 end) over (partition by id_1, id_2)
) as diff
from t;
This should work:
select t1.*, t1.value - t2.value as diff
from t t1
left join t t2 on t2.id_1 = t1.id_1 and t2.id_2 = t1.id_2 and t2.sub1 = 0
See it here:
http://sqlfiddle.com/#!9/cab4d5/1

Oracle SQL - Generate aggregate rows for certain rows using select

I have a table like below.
|FILE| ID |PARENTID|SHOWCHILD|CAT1|CAT2|CAT3|TOTAL|
|F1 | A1 | P1 | N | 3 | 2 | 6 | 11 |
|F2 | A2 | P2 | N | 4 | 7 | 3 | 14 |
|F3 | A3 | P1 | N | 3 | 1 | 1 | 5 |
|F4 | LG1| | Y | 6 | 3 | 7 | 16 |
|F5 | LG2| | Y | 4 | 7 | 3 | 14 |
Now, Is it possible if I want to find the total (ie) aggregate of cat1, cat2, cat3 & total only for rows which has showChild as 'Y' and add that to the resultset.
|Tot| Res | Res | N | 10 | 10 | 10 | 30 |
Expected final output:
|FILE| ID |PARENTID|SHOWCHILD|CAT1|CAT2|CAT3|TOTAL|
|F1 | A1 | P1 | N | 3 | 2 | 6 | 11 |
|F2 | A2 | P2 | N | 4 | 7 | 3 | 14 |
|F3 | A3 | P1 | N | 3 | 1 | 1 | 5 |
|F4 | LG1| | Y | 6 | 3 | 7 | 16 |
|F5 | LG2| | Y | 4 | 7 | 3 | 14 |
|Tot | Res| Res | N | 10 | 10 | 10 | 30 |
Here I have added the Tot row(last row) after considering only the rows which has showchild as 'Y' and added that to the resultset.
I am trying for a solution without using UNION
Any help on achieving the above results is highly appreciated.
Thank you.
One approach would be to use a union:
WITH cte AS (
SELECT "FILE", ID, PARENTID, SHOWCHILD, CAT1, CAT2, CAT3, TOTAL, 1 AS position
FROM yourTable
UNION ALL
SELECT 'Tot', 'Res', 'Res', 'N', SUM(CAT1), SUM(CAT2), SUM(CAT3), SUM(TOTAL), 2
FROM yourTable
WHERE SHOWCHILD = 'Y'
)
SELECT "FILE", ID, PARENTID, SHOWCHILD, CAT1, CAT2, CAT3, TOTAL
FROM cte
ORDER BY
position,
"FILE";
Demo
You can try using UNION
select FILE,ID ,PARENTID,SHOWCHILD,CAT1,CAT2,CAT3,TOTAL from table1
union
select 'Tot','Res','Res','N',sum(cat1), sum(cat2),sum(cat3), sum(total)
from table1 where SHOWCHILD='Y'
I see you already accepted an answer, but you did ask for a solution that did not involve UNION. One such solution would be to use GROUPING SETS.
GROUPING SETS allow you to specify different grouping levels in your query and generate aggregates at each of those levels. You can use it to generate an output row for each record plus a single "total" row, as per your requirements. The function GROUPING can be used in expressions to identify whether each output row is in one group or the other.
Example, with test data:
with input_data ("FILE", "ID", PARENTID, SHOWCHILD, CAT1, CAT2, CAT3, TOTAL ) AS (
SELECT 'F1','A1','P1','N',3,2,6,11 FROM DUAL UNION ALL
SELECT 'F2','A2','P2','N',4,7,3,14 FROM DUAL UNION ALL
SELECT 'F3','A3','P1','N',3,1,1,5 FROM DUAL UNION ALL
SELECT 'F4','LG1','','Y',6,3,7,16 FROM DUAL UNION ALL
SELECT 'F5','LG2','','Y',4,7,3,14 FROM DUAL )
SELECT decode(grouping("FILE"),1,'Tot',"FILE") "FILE",
decode(grouping("ID"),1,'Res',"ID") "ID",
decode(grouping(parentid),1, 'Res',parentid) parentid,
decode(grouping(showchild),1, 'N',showchild) showchild,
decode(grouping("FILE"),1,sum(decode(showchild,'Y',cat1,0)),sum(cat1)) cat1,
decode(grouping("FILE"),1,sum(decode(showchild,'Y',cat2,0)),sum(cat2)) cat2,
decode(grouping("FILE"),1,sum(decode(showchild,'Y',cat3,0)),sum(cat3)) cat3,
decode(grouping("FILE"),1,sum(decode(showchild,'Y',total,0)),sum(total)) total
from input_data
group by grouping sets (("FILE", "ID", parentid, showchild), ())
+------+-----+-----+----------+-----------+------+------+------+-------+
| FILE | F2 | ID | PARENTID | SHOWCHILD | CAT1 | CAT2 | CAT3 | TOTAL |
+------+-----+-----+----------+-----------+------+------+------+-------+
| F1 | F1 | A1 | P1 | N | 3 | 2 | 6 | 11 |
| F2 | F2 | A2 | P2 | N | 4 | 7 | 3 | 14 |
| F3 | F3 | A3 | P1 | N | 3 | 1 | 1 | 5 |
| F4 | F4 | LG1 | - | Y | 6 | 3 | 7 | 16 |
| F5 | F5 | LG2 | - | Y | 4 | 7 | 3 | 14 |
| Tot | Tot | Res | Res | N | 10 | 10 | 10 | 30 |
+------+-----+-----+----------+-----------+------+------+------+-------+

SQL Server pivot with "ties"

Here is my source data:
+-------+-------+-------+------+
| Categ | Nm | Value | Rnk |
+-------+-------+-------+------+
| A | Tom | 37 | 1 |
| A | Joe | 36 | 2 |
| A | Eddie | 35 | 3 |
| B | Seth | 28 | 1 |
| B | Ed | 25 | 2 |
| B | Billy | 22 | 3 |
| C | Julie | 42 | 1 |
| C | Jenny | 41 | 2 |
| C | April | 40 | 3 |
| C | Mary | 40 | 3 |
| C | Laura | 40 | 3 |
+-------+-------+-------+------+
And here is the output I would like to produce:
+------+--------+--------+-------+
| Rnk | A | B | C |
+------+--------+--------+-------+
| 1 | Tom | Seth | Julie |
| 2 | Joe | Ed | Jenny |
| 3 | Eddie | Billy | April |
| 3 | (null) | (null) | Mary |
| 3 | (null) | (null) | Laura |
+------+--------+--------+-------+
I have used the following approach (which I understand through other posts may be superior to actually using PIVOT)...and this gets me to where I see Julie/Jenny/April, but not Mary/Laura (obviously, since it is pulling the MIN in the event of a 'tie').
SELECT Rnk
, min(CASE WHEN Categ = 'A' THEN Nm END) as A
, min(CASE WHEN Categ = 'B' THEN Nm END) as B
, min(CASE WHEN Categ = 'C' THEN Nm END) as C
FROM Tbl
GROUP BY Rnk
How to get to my desired output?
Well, if you want multiple rows for each rank, you can't aggregate by rank, or at least by rank alone. So, calculate the rank-within-the-rank or as the following query calls it, the sub_rnk:
SELECT Rnk,
min(CASE WHEN Categ = 'A' THEN Nm END) as A,
min(CASE WHEN Categ = 'B' THEN Nm END) as B,
min(CASE WHEN Categ = 'C' THEN Nm END) as C
FROM (select t.*, row_number() over (partition by categ, rnk order by newid()) as sub_rnk
from Tbl t
) t
GROUP BY rnk, sub_rnk
ORDER BY rnk;

Unpivot tax data

I have data in this form:
department_id | VAT_id | Tax_amount | Net_amount | Gross_amount | Date | Invoice_no
1 | 3 | 10 | 90 | 100 | 20130101 | A5
1 | 8 | 5 | 35 | 40 | 20130101 | A5
3 | 3 | 5 | 45 | 50 | 20130101 | A8
And I want to transform it into:
Department_id | Vat_id | Amount | Date | Invoice_No
1 | 3 | 10 | 20130101 | A5
1 | 0 | 90 | 20130101 | A5
1 | -1 | 100 | 20130101 | A5
1 | 8 | 5 | 20130101 | A5
1 | 0 | 35 | 20130101 | A5
1 | -1 | 40 | 20130101 | A5
3 | 3 | 5 | 20130101 | A8
3 | 0 | 45 | 20130101 | A8
3 | -1 | 50 | 20130101 | A8
Vat_id value 0 is for net amount
Vat_id value -1 is for the gross amount.
How can I verticalize this data so that I can keep going forward?
You can use the UNPIVOT function to perform this:
select department_id,
case
when col = 'net_amount' then 0
when col = 'Gross_amount' then -1
else vat_id end vat_od,
amount,
invoice_no
from yourtable
unpivot
(
amount
for col in ([Tax_amount], [Net_amount], [Gross_amount])
) unpiv
See SQL Fiddle with Demo.
If you do not have access to the unpivot function, then you can use a UNION ALL query.
select department_id,
case
when col = 'net_amount' then 0
when col = 'Gross_amount' then -1
else vat_id end vat_od,
amount,
invoice_no
from
(
select department_id, vat_id,
'tax_amount' col, tax_amount amount, invoice_no
from yourtable
union all
select department_id, vat_id,
'Net_amount' col, Net_amount amount, invoice_no
from yourtable
union all
select department_id, vat_id,
'Gross_amount' col, Gross_amount amount, invoice_no
from yourtable
) src
See SQL Fiddle with Demo
Both queries will return:
| DEPARTMENT_ID | VAT_OD | AMOUNT | INVOICE_NO |
------------------------------------------------
| 1 | 3 | 10 | A5 |
| 1 | 0 | 90 | A5 |
| 1 | -1 | 100 | A5 |
| 1 | 8 | 5 | A5 |
| 1 | 0 | 35 | A5 |
| 1 | -1 | 40 | A5 |
| 3 | 3 | 5 | A8 |
| 3 | 0 | 45 | A8 |
| 3 | -1 | 50 | A8 |

MySQL: Pivot + Counting

I need help with a SQL that will convert this table:
===================
| Id | FK | Status|
===================
| 1 | A | 100 |
| 2 | A | 101 |
| 3 | B | 100 |
| 4 | B | 101 |
| 5 | C | 100 |
| 6 | C | 101 |
| 7 | A | 102 |
| 8 | A | 102 |
| 9 | B | 102 |
| 10 | B | 102 |
===================
to this:
==========================================
| FK | Count 100 | Count 101 | Count 102 |
==========================================
| A | 1 | 1 | 2 |
| B | 1 | 1 | 2 |
| C | 1 | 1 | 0 |
==========================================
I can so simple counts, etc., but am struggling trying to pivot the table with the information derived. Any help is appreciated.
Use:
SELECT t.fk,
SUM(CASE WHEN t.status = 100 THEN 1 ELSE 0 END) AS count_100,
SUM(CASE WHEN t.status = 101 THEN 1 ELSE 0 END) AS count_101,
SUM(CASE WHEN t.status = 102 THEN 1 ELSE 0 END) AS count_102
FROM TABLE t
GROUP BY t.fk
use:
select * from
(select fk,fk as fk1,statusFK from #t
) as t
pivot
(COUNT(fk1) for statusFK IN ([100],[101],[102])
) AS pt
Just adding a shortcut to #OMG's answer.
You can eliminate CASE statement:
SELECT t.fk,
SUM(t.status = 100) AS count_100,
SUM(t.status = 101) AS count_101,
SUM(t.status = 102) AS count_102
FROM TABLE t
GROUP BY t.fk