Sum Values Till the Next Appearance of a Condition - mysql

Thank you for coming in.
I have a table like this:
And here is what I want to do: Segregated by id, I want to sum up the Val based on the condition.
For example, for id=1, I want the sum of Val till condition A firstly appear, then another sum of Val between the first A and the second A, and sum of Val between the second and the third A... The sum of Val when condition = B follows the same logic, but should not be influenced by A. Finally, each sum of Val only sums the same id.
How should I do this? I tried group by and partition by, but unable to obtain an ideal result. The ideal output would be like the Sum column in the picture.
Thank you very much.

Assuming that there is a column that specifies the ordering, then you can do what you want. SQL tables represent unordered sets. There is no ordering unless a column specifies the ordering.
You seem to want to define groups for As and Bs. You can do this using window functions. This is a little strange, because you want different groupings -- a case expression can handle that. Here is the idea:
select t.*,
(case when condition = 'A'
then sum(val) over (partition by id, grp_a order by <ordering col>)
when condition = 'B'
then sum(val) over (partition by id order by <ordering col>)
end) as calculation
from (select t.*,
sum(case when condition = 'A' then 1 else 0 end) over (partition by id order by <ordering col> desc) as grp_a
from t
) t;

You are looking for something like this:
WITH somatory as (SELECT id, cond, v,
SUM(v) OVER (PARTITION BY id ORDER BY id ROWS UNBOUNDED PRECEDING) as sumV
from foo),
conditional_somatory as (
select id, cond, v, sumV,
case when cond = 'A' then sumV end as somatoryA,
case when cond = 'B' then sumV end as somatoryB
from somatory
),
last_somatories as (
select id, cond, v, sumV,
SomatoryA,
max(coalesce(somatoryA,0)) over (partition by id order by id ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) as lastSomatoryA,
SomatoryB,
max(coalesce(somatoryB,0)) over (partition by id order by id ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) as lastSomatoryB
from conditional_somatory)
select id, cond, v,
case when cond = 'A' then sumV - lastSomatoryA
when cond = 'B' then sumV - lastSomatoryB
end as somatory
from last_somatories
See working fiddle.
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=2b2d23b1421aa8fb45aa82ed8bad8b32

Related

Ordered Analytical Functions not allowed in GROUP BY Clause

I have a query joining 2 tables with a SELECT statement for fields in both tables, and an Ordered Analytical Function to calculate the total volume for specific customers.
I'm getting the error "Ordered Analytical Functions not allowed in GROUP BY Clause" when I try to group the fields. I need the GROUP BY because there are other fields that need to be grouped but I also need the SUM() OVER (PARTITION BY()) for other calculations. How can I create the subquery so as to get rid of the error?
The query is something like this:
a.cust_no,
b.cust_name,
a.location,
c.product,
SUM(a.volume),
SUM(a.weight),
SUM(volume) OVER (PARTITION BY cust_no ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS fixed_volume,
SUM(CASE WHEN a_flag = 'Y' THEN volume ELSE 0 END) OVER (PARTITION BY cust_no ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS bias_volume
FROM test_table a
JOIN test_table2 b ON a.cust_no = b.cust_no
JOIN test_table3 c ON a.cust_no = c.cust_no
GROUP BY 1,2,3,4
There's no OLAP function in your GROUP BY clause.
I would expect a 3504 Selected non-aggregate values must be part of the associated group, this should fix it:
select
a.cust_no,
b.cust_name,
a.location,
c.product,
SUM(a.volume),
SUM(a.weight),
--volume is a detail row -> doesn't exist after aggregation
--SUM(a.volume) OVER (PARTITION BY cust_no) AS fixed_volume,
--aggregated volume can be used
SUM(SUM(a.volume)) OVER (PARTITION BY cust_no) AS fixed_volume,
--same logic
SUM(SUM(CASE WHEN a_flag = 'Y' THEN a.volume ELSE 0 END)) OVER (PARTITION BY cust_no ) AS bias_volume
FROM test_table a
JOIN test_table2 b ON a.cust_no = b.cust_no
JOIN test_table3 c ON a.cust_no = c.cust_no
GROUP BY 1,2,3,4

Sql on handling blanks

I am trying to make a query, in that I take the average of the value for that group, and mark it in Average Column for that group. Now if for a group in input, has blanks, it should not calculate the average, and the output should be just left blank.
How should I do this so that even those blanks get handled?
I tried this:
select avg(value) over (partition by "Group") from table
AVG calculates the average of a set of numbers. So there can be no blanks (white space) in that column, but NULL.
AVG ignores all NULL values, which is not what you want it to do, because for a set of 2, 4, NULL, you want the result NULL and not (2 + 4) / 2 = 2.
But you can check whether there appears a NULL in the set or not. E.g.:
select
grp,
value,
case when count(value) over(partition by grp) = count(*) over (partition by grp) then
avg(value) over (partition by grp)
else
null
end as average_value
from mytable
order by grp;
Demo: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=e5783cd10c5d26e1798c2e1b1e022189

SQL query to get percentages within a grouping

I've looked over similar questions and I just can't seem to get this right.
I have a table with three columns: ID, Date, and Method. None are unique.
I want to be able to see for any given date, how many rows match a certain pattern on Method.
So, for example, if the table has 100 rows, and 8 of them have the date "01-01-2020" and of those 8, two of them have a method of "A", I would want a return row that says "01-01-2020", "8", "2", and "25%".
My SQL is pretty rudimentary. I have been able to make a query to get me the count of each method by date:
select Date, count(*) from mytable WHERE Method="A" group by Date;
But I haven't been able to figure out how to put together the results that I am needing. Can someone help me out?
You could perform a count over a case expression for that method, and then divide the two counts:
SELECT date,
COUNT(*),
COUNT(CASE method WHEN 'A' THEN 1 END),
COUNT(CASE method WHEN 'A' THEN 1 END) / COUNT(*) * 100
FROM mytable
GROUP BY date
I'm assuming you're interested in all methods rather than just 'A', so you could do the following:
with ptotals as
(
SELECT
thedate,
count(*) as NumRows
FROM
mytable
group by
thedate
)
select
mytable.thedate,
mytable.themethod,
count(*) as method_count,
100 * count(*) / max(ptotals.NumRows) as Pct
from
mytable
inner join
ptotals
on
mytable.thedate = ptotals.thedate
group by
mytable.thedate,
mytable.themethod
You can use AVG() for the ratio/percentage:
SELECT date, COUNT(*),
SUM(CASE WHEN method = 'A' THEN 1 ELSE 0 END),
AVG(CASE WHEN method = 'A' THEN 100.0 ELSE 0 END)
FROM t
GROUP BY date;

can case statement be applied on a column generated in a select statement?

I have generated a column in select statement now I want to apply a case statement over it. I know case statement can only be applied on the column which is present in database but I want to know is there any alternative?
My code is:
SELECT B.reg_no,B.dist_no,B.RDT_NAME,A.YTD_PQ,
RANK() OVER (PARTITION BY B.reg_no ORDER BY A.YTD_PQ DESC) AS Rank_1,
CASE Rank_1 WHEN '1' THEN 'YES' ELSE 'NO' END AS NVARCHAR(10)
FROM OTHER_AWARDS AS B
JOIN MT_D AS A
ON A.RDT = B.RDT
now I want to know how can i apply Case on the column Rank_1 which is generated in the select statement.
As I recall you can't refer to a computed column on the same level of a statement as it isn't known (or processed) at the time you refer to it, but reusing the rank() function in the case statement should work, so try this:
SELECT B.reg_no,B.dist_no,B.RDT_NAME,A.YTD_PQ,
RANK() OVER (PARTITION BY B.reg_no ORDER BY A.YTD_PQ DESC) AS Rank_1,
CASE RANK() OVER (PARTITION BY B.reg_no ORDER BY A.YTD_PQ DESC) WHEN '1' THEN 'YES' ELSE 'NO' END
FROM OTHER_AWARDS AS B
JOIN MT_D AS A
ON A.RDT = B.RDT
Yes. Wrap the query as an inner query and all columns can then be treated like regular columns, eg:
select case when computed_column ... end
from (
select rank() ... as computed_column
from ...
} q
which in your case would look like:
SELECT *, CASE Rank_1 WHEN '1' THEN 'YES' ELSE 'NO' END AS NVARCHAR(10)
FROM (
SELECT B.reg_no,B.dist_no,B.RDT_NAME,A.YTD_PQ,
RANK() OVER (PARTITION BY B.reg_no ORDER BY A.YTD_PQ DESC) AS Rank_1
FROM OTHER_AWARDS AS B
JOIN MT_D AS A ON A.RDT = B.RDT
) q

how do I write an IF ELSE into this query to make it work?

table looks something like this: (yes those are & signs. ignore the dashes)
ID-VALUE-NUM
-1-YES----2-
-1-NO-----3-
-2-YES----1-
-2-NO-----1-
-3-&&&----1-
-3-&------2-
-3-&&-----2-
what I need to do:
for each ID, I need to get the value with the highest NUM, in the case of a tie and VALUE has &s then it would pick the shortest. if the value is YES/NO then it will pick YES.
result desired
ID-VALUE-NUM
-1-NO-----3-
-2-YES----1-
-3-&------2-
I think I have to put a IF statement in there somewhere but I'm not sure how.
Here is one way. The join finds the maximum num. Then the select uses logic to choose the right value based on your rules:
select t.id,
(case when count(*) = 1 then min(value)
when max(value like '%&%') > 0 then min(value)
when max(value = 'Yes') > 0 and max(value = 'No') > 0 then 'Yes'
else max(value)
end) as value,
t.num
from t join
(select id, max(num) as maxnum
from t
group by id
) tm
on t.id = tm.id and t.num = tm.maxnum
group by t.id, t.num