sql grouping grades - sql

I have a table for subjects as follows:
id Subject Grade Ext
100 Math 6 +
100 Science 4 -
100 Hist 3
100 Geo 2 +
100 CompSi 1
I am expecting output per student in a class(id = 100) as follows:
Grade Ext StudentGrade
6 + 1
6 0
6 - 0
5 + 0
5 0
5 - 0
4 + 0
4 0
4 - 1
3 + 0
3 1
3 - 0
2 + 1
2 0
2 - 0
1 + 0
1 1
1 - 0
I would want this done on oracle/sql rather than UI. Any inputs please.

You should generate rows first, before join them with your table like below. I use the with clause here to generate the 18 rows in your sample.
with rws (grade, ext) as (
select ceil(level/3), decode(mod(level, 3), 0, '+', 1, '-', null)
from dual
connect by level <= 3 * 6
)
select r.grade, r.ext, nvl2(t.Ext, 1, 0) studentGrade
from rws r
left join your_table t
on t.Grade = r.Grade and decode(t.Ext, r.Ext, 1, 0) = 1
order by 1 desc, decode(r.ext, null, 2, '-', 3, '+', 1)

You could do something like this. In the WITH clause I generate two small "helper" tables (really, inline views) for grades from 1 to 6 and for "extensions" of +, null and -. In the "extensions" view I also create an "ordering" column to use in ordering the final output (if you are wondering why I included that).
Also in the WITH clause I included sample data - you will have to remove that and instead use your actual table name in the main query.
The idea is to cross-join "grades" and "extensions", and left-outer-join the result to your input data. Count the grades from the input data, grouped by grade and extension, and after filtering the desired id. The decode thing in the join condition is needed because for extension we want to treat null as equal to null - something that decode does nicely.
with
sample_inputs (id, subject, grade, ext) as (
select 100, 'Math' , 6, '+' from dual union all
select 100, 'Science', 4, '-' from dual union all
select 100, 'Hist' , 3, null from dual union all
select 100, 'Geo' , 2, '+' from dual union all
select 100, 'CompSi' , 1, null from dual
)
, g (grade) as (select level from dual connect by level <= 6)
, e (ord, ext) as (
select 1, '+' from dual union all
select 2, null from dual union all
select 3, '-' from dual
)
select g.grade, e.ext, count(t.grade) as studentgrade
from g cross join e left outer join sample_inputs t
on t.grade = g.grade and decode(t.ext, e.ext, 0) = 0
and t.id = 100 -- change this as needed!
group by g.grade, e.ext, e.ord
order by g.grade desc, e.ord
;
OUTPUT:
GRADE EXT STUDENTGRADE
----- --- ------------
6 + 1
6 0
6 - 0
5 + 0
5 0
5 - 0
4 + 0
4 0
4 - 1
3 + 0
3 1
3 - 0
2 + 1
2 0
2 - 0
1 + 0
1 1
1 - 0

It looks like you want sparse data to be filled in as part of joining students and subjects.
Since Oracle 10g the correct way to do this has been with a "partition outer join".
The documentation has examples.
https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/SELECT.html#GUID-CFA006CA-6FF1-4972-821E-6996142A51C6

Related

SQL - how to combine sequential rows based on next row

There is a table I need to join back to itself to created a purchased parts report. But, I keep getting duplicated rows.
How to create a sql that will combine rows based on value of the next row?
Order No and Order Line fields uniquely identify a row in a sales order.
CP_COMP_SEQ is simply a list of all the components needed to manufacture Sales Order and Order Line.
MFG_PURCH_FLG is a flag 'M' means it's a Make part. 'P' means it's a part we need to Purchase.
If an M row follows another M row, then the first component has no purchased parts. If one or more P row(s) follow an M row, then all those sequential rows need to be purchased. Meaning we need to add them to a Purchase Parts report for a buyer to fill out a Purchase Order.
So, in the image below, Row 1 needs two purchased parts to complete. Row 4 doesn't need any purchased parts (because it's followed by another M row). Row 5 needs three purchased parts. And row 9 doesn't need any purchased parts.
Source table
order_no
order_line_no
cp_comp_seq
inv_item
mfg_purch_flg
qty
1
2
1
146FV
M
2
1
2
2
2085
P
4
1
2
3
2095
P
4
1
2
4
ZBAR007
M
1
1
2
5
1467V
M
1
1
2
6
2085
P
2
1
2
7
2095
P
2
1
2
8
3060
P
1
1
2
9
ZBAR007
M
1
2
1
1
xxx
x
x
2
1
2
xxx
x
x
I would like to have the results be one row per item that needs to be purchased based on the previous row that is a 'M' make item. Below is what I would like as the result:Result table
order_no
order_line_no
cp_comp_seq
inv_item
purchase_item
qty
1
2
1
146FV
2085
4
1
2
1
146FV
2095
4
1
2
5
147FV
2085
2
1
2
5
147FV
2095
2
1
2
5
147FV
3060
1
Any help would be greatly appreciated! Thanks in advance.
I tried joining based on rownumber - 1, but that doesn't stop when the Purchase flag changes. I tried looking at Run Groups but was unable to make that work as well.
Any help would be greatly appreciated! Thanks in advance.
You could use analytic function to get the sequence number of M flag of every P flag:
Select t.ORDER_NO, t.ORDER_LINE_NO, t.CP_COMP_SEQ, t.INV_ITEM, t.MFG_PURCH_FLG, t.QTY,
MAX(CASE WHEN t.MFG_PURCH_FLG = 'M' THEN t.CP_COMP_SEQ END) OVER(Order By t.CP_COMP_SEQ Rows Between Unbounded Preceding And Current Row) "M_SEQ"
From tbl t
ORDER_NO ORDER_LINE_NO CP_COMP_SEQ INV_ITEM MFG_PURCH_FLG QTY M_SEQ
---------- ------------- ----------- -------- ------------- --- ----------
1 2 1 146FV M 2 1
1 2 2 2085 P 4 1
1 2 3 2095 P 4 1
1 2 4 ZBAR007 M 1 4
1 2 5 1467V M 1 5
1 2 6 2085 P 2 5
1 2 7 2095 P 2 5
1 2 8 3060 P 1 5
1 2 9 ZBAR007 M 1 9
2 1 10 xxx x x 9
2 1 11 xxx x x 9
If you put it as a subquery in the FROM clause like here:
SELECT t.ORDER_NO, t.ORDER_LINE_NO,
(Select CP_COMP_SEQ From tbl Where CP_COMP_SEQ = t.M_SEQ) "CP_COMP_SEQ",
(Select INV_ITEM From tbl Where CP_COMP_SEQ = t.M_SEQ) "INV_ITEM",
t.INV_ITEM "PURCHASE_ITEM", t.QTY
FROM ( Select t.ORDER_NO, t.ORDER_LINE_NO, t.CP_COMP_SEQ, t.INV_ITEM, t.MFG_PURCH_FLG, t.QTY,
MAX(CASE WHEN t.MFG_PURCH_FLG = 'M' THEN t.CP_COMP_SEQ END) OVER(Order By t.CP_COMP_SEQ Rows Between Unbounded Preceding And Current Row) "M_SEQ"
From tbl t
) t
WHERE t.MFG_PURCH_FLG = 'P'
ORDER BY t.CP_COMP_SEQ
... then, with your sample data ...
WITH
tbl (ORDER_NO, ORDER_LINE_NO, CP_COMP_SEQ, INV_ITEM, MFG_PURCH_FLG, QTY) AS
(
Select 1, 2, 1, '146FV', 'M', '2' From Dual Union All
Select 1, 2, 2, '2085', 'P', '4' From Dual Union All
Select 1, 2, 3, '2095', 'P', '4' From Dual Union All
Select 1, 2, 4, 'ZBAR007', 'M', '1' From Dual Union All
Select 1, 2, 5, '1467V', 'M', '1' From Dual Union All
Select 1, 2, 6, '2085', 'P', '2' From Dual Union All
Select 1, 2, 7, '2095', 'P', '2' From Dual Union All
Select 1, 2, 8, '3060', 'P', '1' From Dual Union All
Select 1, 2, 9, 'ZBAR007', 'M', '1' From Dual Union All
Select 2, 1, 10, 'xxx', 'x', 'x' From Dual Union All
Select 2, 1, 11, 'xxx', 'x', 'x' From Dual
)
... the result would be:
ORDER_NO ORDER_LINE_NO CP_COMP_SEQ INV_ITEM PURCHASE_ITEM QTY
---------- ------------- ----------- -------- ------------- ---
1 2 1 146FV 2085 4
1 2 1 146FV 2095 4
1 2 5 1467V 2085 2
1 2 5 1467V 2095 2
1 2 5 1467V 3060 1
Take a look at the cross apply syntax. This syntax allows you to drive the inner query off each row in the other query.
select *
from AnyTableYouWant T1
cross apply
(
select top 1 ID from AnyTableYouWant T2
where T1.ID = T2.ID
order by T2.ID desc
) T3
T1 or T2 can be the same or different tables. As long as you come up with join criteria. A cross apply is different from a typical left, right or inner join. There's plenty of literature on these queries. This answer is not meant to be a diatribe on how it works.
After reading this question more closely, it would benefit from just walking through a result set sequentially and not trying to do it all in SQL. If you want to do it all in SQL, lookup up CURSORs. For maintenance reasons using python or C# or any other procedural language. Query all the records from the parts table make sure it's sorted the way you have it listed, then walk through the results one record at a time and apply the business logic you described.

count zeros between 1s in same column

I've data like this.
ID IND
1 0
2 0
3 1
4 0
5 1
6 0
7 0
I want to count the zeros before the value 1. So that, the output will be like below.
ID IND OUT
1 0 0
2 0 0
3 1 2
4 0 0
5 1 1
6 0 0
7 0 2
Is it possible without pl/sql? I tried to find the differences between row numbers but couldn't achieve it.
The match_recognize clause, introduced in Oracle 12.1, can do quick work of such "row pattern recognition" problems. The solution is just a bit complex due to the special treatment of a "last row" with ID = 0, but it is straightforward otherwise.
As usual, the with clause is not part of the solution; I include it to test the query. Remove it and use your actual table and column names.
with
inputs (id, ind) as (
select 1, 0 from dual union all
select 2, 0 from dual union all
select 3, 1 from dual union all
select 4, 0 from dual union all
select 5, 1 from dual union all
select 6, 0 from dual union all
select 7, 0 from dual
)
select id, ind, out
from inputs
match_recognize(
order by id
measures case classifier() when 'Z' then 0
when 'O' then count(*) - 1
else count(*) end as out
all rows per match
pattern ( Z* ( O | X ) )
define Z as ind = 0, O as ind != 0
);
ID IND OUT
---------- ---------- ----------
1 0 0
2 0 0
3 1 2
4 0 0
5 1 1
6 0 0
7 0 2
You can treat this as a gaps-and-islands problem. You can define the "islands" by the number of "1"s one or after each row. Then use a window function:
select t.*,
(case when ind = 1 or row_number() over (order by id desc) = 1
then sum(1 - ind) over (partition by grp)
else 0
end) as num_zeros
from (select t.*,
sum(ind) over (order by id desc) as grp
from t
) t;
If id is sequential with no gaps, you can do this without a subquery:
select t.*,
(case when ind = 1 or row_number() over (order by id desc) = 1
then id - coalesce(lag(case when ind = 1 then id end ignore nulls) over (order by id), min(id) over () - 1)
else 0
end)
from t;
I would suggest removing the case conditions and just using the then clause for the expression, so the value is on all rows.

Oracle - theoretical sql query for create intervals

Is it possible to solve this situation by sql query in ORACLE?
I have a table like this:
TYPE UNIT
A 230
B 225
C 60
D 45
E 5
F 2
I need to separate units to the three(variable) 'same'(equally sized) intervals and foreach figure out the count? It means something like this:
0 - 77 -> 4
78 - 154 -> 0
155 - 230 -> 2
You can use the maximum value and a connect-by query to generate the upper and lower values for each range:
select ceil((level - 1) * int) as int_from,
floor(level * int) - 1 as int_to
from (select round(max(unit) / 3) as int from t42)
connect by level <= 3;
INT_FROM INT_TO
---------- ----------
0 76
77 153
154 230
And then do a left outer join to your original table to do the count for each range, so you get the zero value for the middle range:
with intervals as (
select ceil((level - 1) * int) as int_from,
floor(level * int) - 1 as int_to
from (select round(max(unit) / 3) as int from t42)
connect by level <= 3
)
select i.int_from || '-' || i.int_to as range,
count(t.unit)
from intervals i
left join t42 t
on t.unit between i.int_from and i.int_to
group by i.int_from, i.int_to
order by i.int_from;
RANGE COUNT(T.UNIT)
---------- -------------
0-76 4
77-153 0
154-230 2
Yes, this can be done in Oracle. The hard part is the definition of the bounds. You can use the maximum value and some arithmetic on a sequence with values of 1, 2, and 3.
After that, the rest is just a cross join and aggregation:
with bounds as (
select (case when n = 1 then 0
when n = 2 then trunc(maxu / 3)
else trunc(2 * maxu / 3)
end) as lowerbound,
(case when n = 1 then trunc(maxu / 3)
when n = 2 then trunc(2*maxu / 3)
else maxu
end) as upperbound
from (select 1 as n from dual union all select 2 from dual union all select 3 from dual
) n cross join
(select max(unit) as maxu from atable t)
)
select b.lowerbound || '-' || b.upperbound,
sum(case when units between b.lowerbound and b.upperbound then 1 else 0 end)
from atable t cross join
bounds b
group by b.lowerbound || '-' || b.upperbound;

Finding repeated occurrences with ranking functions

Please help me generate the following query i've been struggling with for some time now. Lets' say I have a simple table with month number and information whether there were any failed events in this particular month
Below a script to generate sample data:
WITH DATA(Month, Success) AS
(
SELECT 1, 0 UNION ALL
SELECT 2, 0 UNION ALL
SELECT 3, 0 UNION ALL
SELECT 4, 1 UNION ALL
SELECT 5, 1 UNION ALL
SELECT 6, 0 UNION ALL
SELECT 7, 0 UNION ALL
SELECT 8, 1 UNION ALL
SELECT 9, 0 UNION ALL
SELECT 10, 1 UNION ALL
SELECT 11, 0 UNION ALL
SELECT 12, 1 UNION ALL
SELECT 13, 0 UNION ALL
SELECT 14, 1 UNION ALL
SELECT 15, 0 UNION ALL
SELECT 16, 1 UNION ALL
SELECT 17, 0 UNION ALL
SELECT 18, 0
)
Given the definition of a "repeated failure ":
When event failure occurs during at least 4 months in any 6 months period then the last month with such failure is a "repeated failure" my query should return the following output
Month Success RepeatedFailure
1 0
2 0
3 0
4 1
5 1
6 0 R1
7 0 R2
8 1
9 0
10 1
11 0 R3
12 1
13 0
14 1
15 0
16 1
17 0
18 0 R1
where:
R1 -1st repeated failure in month no 6 (4 failures in last 6 months).
R2 -2nd repeated failure in month no 7 (4 failures in last 6 months).
R3 -3rd repeated failure in month no 11 (4 failures in last 6 months).
R1 -again 1st repeated failure in month no 18 because Repeated Failures should be again numbered from the beginning when new Repeated Failure occurs for the first time in last 6 reporting periods
Repeated Failures are numerated consecutively because based on its number i must apply appropriate multiplier:
1st repated failure - X2
2nd repeated failure - X4
3rd and more repeated failure -X5.
I'm sure this can be improved, but it works. We essentially do two passes - the first to establish repeated failures, the second to establish what kind of repeated failure each is. Note that Intermediate2 can definitely be done away with, I've only separated it out for clarity. All the code is one statement, my explanation is interleaved:
;WITH DATA(Month, Success) AS
-- assuming your data as defined (with my edit)
,Intermediate AS
(
SELECT
Month,
Success,
-- next column for illustration only
(SELECT SUM(Success)
FROM DATA hist
WHERE curr.Month - hist.Month BETWEEN 0 AND 5)
AS SuccessesInLastSixMonths,
-- next column for illustration only
6 - (SELECT SUM(Success)
FROM DATA hist
WHERE curr.Month - hist.Month BETWEEN 0 AND 5)
AS FailuresInLastSixMonths,
CASE WHEN
(6 - (SELECT SUM(Success)
FROM DATA hist
WHERE curr.Month - hist.Month BETWEEN 0 AND 5))
>= 4
THEN 1
ELSE 0
END AS IsRepeatedFailure
FROM DATA curr
-- No real data until month 6
WHERE curr.Month > 5
)
At this point we have established, for each month, whether it's a repeated failure, by counting the failures in the six months up to and including it.
,Intermediate2 AS
(
SELECT
Month,
Success,
IsRepeatedFailure,
(SELECT SUM(IsRepeatedFailure)
FROM Intermediate hist
WHERE curr.Month - hist.Month BETWEEN 0 AND 5)
AS RepeatedFailuresInLastSixMonths
FROM Intermediate curr
)
Now we have counted the number of repeated failures in the six months leading up to now
SELECT
Month,
Success,
CASE IsRepeatedFailure
WHEN 1 THEN 'R' + CONVERT(varchar, RepeatedFailuresInLastSixMonths)
ELSE '' END
AS RepeatedFailureText
FROM Intermediate2
so we can say, if this month is a repeated failure, what cardinality of repeated failure it is.
Result:
Month Success RepeatedFailureText
----------- ----------- -------------------------------
6 0 R1
7 0 R2
8 1
9 0
10 1
11 0 R3
12 1
13 0
14 1
15 0
16 1
17 0
18 0 R1
(13 row(s) affected)
Performance considerations will depend on on how much data you actually have.
;WITH DATA(Month, Success) AS
(
SELECT 1, 0 UNION ALL
SELECT 2, 0 UNION ALL
SELECT 3, 0 UNION ALL
SELECT 4, 1 UNION ALL
SELECT 5, 1 UNION ALL
SELECT 6, 0 UNION ALL
SELECT 7, 0 UNION ALL
SELECT 8, 1 UNION ALL
SELECT 9, 0 UNION ALL
SELECT 10, 1 UNION ALL
SELECT 11, 0 UNION ALL
SELECT 12, 1 UNION ALL
SELECT 13, 0 UNION ALL
SELECT 14, 1 UNION ALL
SELECT 15, 0 UNION ALL
SELECT 16, 1 UNION ALL
SELECT 17, 0 UNION ALL
SELECT 18, 0
)
SELECT DATA.Month,DATA.Success,Isnull(convert(Varchar(10),b.result),'') +
Isnull(CONVERT(varchar(10),b.num),'') RepeatedFailure
FROM (
SELECT *, ROW_NUMBER() over (order by Month) num FROM
( Select * ,(case when (select sum(Success)
from DATA where MONTH>(o.MONTH-6) and MONTH<=(o.MONTH) ) <= 2
and o.MONTH>=6 then 'R' else '' end) result
from DATA o
) a where result='R'
) b
right join DATA on DATA.Month = b.Month
order by DATA.Month

How to transpose recordset columns into rows

I have a query whose code looks like this:
SELECT DocumentID, ComplexSubquery1 ... ComplexSubquery5
FROM Document
WHERE ...
ComplexSubquery are all numerical fields that are calculated using, duh, complex subqueries.
I would like to use this query as a subquery to a query that generates a summary like the following one:
Field DocumentCount Total
1 dc1 s1
2 dc2 s2
3 dc3 s3
4 dc4 s4
5 dc5 s5
Where:
dc<n> = SUM(CASE WHEN ComplexSubquery<n> > 0 THEN 1 END)
s <n> = SUM(CASE WHEN Field = n THEN ComplexSubquery<n> END)
How could I do that in SQL Server?
NOTE: I know I could avoid the problem by discarding the original query and using unions:
SELECT '1' AS TypeID,
SUM(CASE WHEN ComplexSubquery1 > 0 THEN 1 END) AS DocumentCount
SUM(ComplexSubquery1) AS Total
FROM (SELECT DocumentID, BLARGH ... AS ComplexSubquery1) T
UNION ALL
SELECT '2' AS TypeID,
SUM(CASE WHEN ComplexSubquery2 > 0 THEN 1 END) AS DocumentCount
SUM(ComplexSubquery2) AS Total
FROM (SELECT DocumentID, BLARGH ... AS ComplexSubquery2) T
UNION ALL
...
But I want to avoid this route, because redundant code makes my eyes bleed. (Besides, there is a real possibility that the number of complex subqueries grow in the future.)
WITH Document(DocumentID, Field) As
(
SELECT 1, 1 union all
SELECT 2, 1 union all
SELECT 3, 2 union all
SELECT 4, 3 union all
SELECT 5, 4 union all
SELECT 6, 5 union all
SELECT 7, 5
), CTE AS
(
SELECT DocumentID,
Field,
(select 10) As ComplexSubquery1,
(select 20) as ComplexSubquery2,
(select 30) As ComplexSubquery3,
(select 40) as ComplexSubquery4,
(select 50) as ComplexSubquery5
FROM Document
)
SELECT Field,
SUM(CASE WHEN RIGHT(Query,1) = Field AND QueryValue > 1 THEN 1 END ) AS DocumentCount,
SUM(CASE WHEN RIGHT(Query,1) = Field THEN QueryValue END ) AS Total
FROM CTE
UNPIVOT (QueryValue FOR Query IN
(ComplexSubquery1, ComplexSubquery2, ComplexSubquery3,
ComplexSubquery4, ComplexSubquery5)
)AS unpvt
GROUP BY Field
Returns
Field DocumentCount Total
----------- ------------- -----------
1 2 20
2 1 20
3 1 30
4 1 40
5 2 100
I'm not 100% positive from your example, but perhaps the PIVOT operator will help you out here? I think if you selected your original query into a temporary table, you could pivot on the document ID and get the sums for the other queries.
I don't have much experience with it though, so I'm not sure how complex you can get with your subqueries - you might have to break it down.