Finding repeated occurrences with ranking functions - sql

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

Related

sql grouping grades

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

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.

Is there a way to find active users in SQL?

I'm trying to find the total count of active users in a database. "Active" users here as defined as those who have registered an event on the selected day or later than the selected day. So if a user registered an event on days 1, 2 and 5, they are counted as "active" throughout days 1, 2, 3, 4 and 5.
My original dataset looks like this (note that this is a sample - the real dataset will run to up to 365 days, and has around 1000 users).
Day ID
0 1
0 2
0 3
0 4
0 5
1 1
1 2
2 1
3 1
4 1
4 2
As you can see, all 5 IDs are active on Day 0, and 2 IDs (1 and 2) are active until Day 4, so I'd like the finished table to look like this:
Day Count
0 5
1 2
2 2
3 2
4 2
I've tried using the following query:
select Day as days, sum(case when Day <= days then 1 else 0 end)
from df
But it gives incorrect output (only counts users who were active on each specific days).
I'm at a loss as to what I could try next. Does anyone have any ideas? Many thanks in advance!
I think I would just use generate_series():
select gs.d, count(*)
from (select id, min(day) as min_day, max(day) as max_day
from t
group by id
) t cross join lateral
generate_series(t.min_day, .max_day, 1) gs(d)
group by gs.d
order by gs.d;
If you want to count everyone as active from day 1 -- but not all have a value on day 1 -- then use 1 instead of min_day.
Here is a db<>fiddle.
A bit verbose, but this should do:
with dt as (
select 0 d, 1 id
union all
select 0 d, 2 id
union all
select 0 d, 3 id
union all
select 0 d, 4 id
union all
select 0 d, 5 id
union all
select 1 d, 1 id
union all
select 1 d, 2 id
union all
select 2 d, 1 id
union all
select 3 d, 1 id
union all
select 4 d, 1 id
union all
select 4 d, 2 id
)
, active_periods as (
select id
, min(d) min_d
, max(d) max_d
from dt
group by id
)
, days as (
select distinct d
from dt
)
select d.d
, count(ap.id)
from days d
join active_periods ap on d.d between ap.min_d and ap.max_d
group by 1
order by 1 asc
You need count by day.
select
id,
count(*)
from df
GROUP BY
id

Week based count

I have a requirement to retrieve the data in the below fashion
Weeks delay_count
0 6
1 0
2 3
3 4
4 0
5 1
6 0
7 0
8 0
9 0
10 2
11 0
12 0
13 0
14 0
15 3
Here weeks is the hard coded column from 0 to 15 and delay_count is the derived column. I have a column delay_weeks. Based on the values in this column I need to populate the values in the delay_count column (derived column)
delay_weeks column values are below.
blank
blank
blank
2
10
5
blank
3
2
10
2
3
3
3
0
0
15
22
29
Conditions:
When delay_weeks is blank or 0 then count in the delay_count column should be 1
When delay_weeks is 3 then in the delay_count column the count should be 1 under week 3
When delay_weeks is 10 then in the delay_count column the count should be 1 under week 10
When delay_weeks is greater than or equal to 15 then in the delay_count column the count should be 1 under week 15.
I wrote code like below
SELECT "Weeks", a."delay_count"
FROM (SELECT LEVEL AS "Weeks"
FROM DUAL
CONNECT BY LEVEL <= 15) m,
(SELECT VALUE, COUNT (VALUE) AS "delay_numbers"
FROM (SELECT CASE
WHEN attr11.VALUE >= 15
THEN '15'
ELSE attr11.VALUE
END
VALUE
FROM docs,
(SELECT object_id, VALUE, attribute_type_id
FROM ATTRIBUTES
WHERE attribute_type_id =
(SELECT attribute_type_id
FROM attribute_types
WHERE name_display_code =
'ATTRIBUTE_TYPE.DELAY IN WEEKS')) attr11
WHERE docs.obj_id = attr11.object_id(+)
GROUP BY VALUE) a
WHERE m."Weeks" = a.VALUE(+)
select
weeks,
nvl(cnt, 0) as delay_count
from
(select level-1 as weeks from dual connect by level < 17)
left join (
select
nvl(least(attr11.value, 15), 0) as weeks,
count(0) as cnt
from
DOCS
left join (
ATTRIBUTES attr11
join ATTRIBUTE_TYPES atr_tp using(attribute_type_id)
)
on atr_tp.name_display_code = 'ATTRIBUTE_TYPE.DELAY IN WEEKS'
and docs.obj_id = attr11.object_id
group by nvl(least(attr11.value, 15), 0)
) using(weeks)
order by 1
Reverse-engineering the relevant parts of the table definitions, I think this gives you what you want:
select t.weeks, count(delay) as delay_count
from (select level - 1 as weeks from dual connect by level <= 16) t
left join (
select case when a.value is null then 0
when to_number(a.value) > 15 then 15
else to_number(a.value) end as delay
from docs d
left join (
select a.object_id, a.value
from attributes a
join attribute_types at on at.attribute_type_id = a.attribute_type_id
where at.name_display_code = 'ATTRIBUTE_TYPE.DELAY IN WEEKS'
) a on a.object_id = d.obj_id
) delays on delays.delay = t.weeks
group by t.weeks
order by t.weeks;
With what I think is matching data I get:
WEEKS DELAY_COUNT
---------- -----------
0 6
1 0
2 3
3 4
4 0
5 1
6 0
7 0
8 0
9 0
10 2
11 0
12 0
13 0
14 0
15 3
But obviously since you haven't given the real table structures I'm guessing a bit on the relationships. Obligatory SQL Fiddle.

Inclusion of both inclusive and exclusive range in the same table

i have the following table generated by SQL TABLE A
timeinterval count(exclusive range)
0-6 2
0-12 5
0-18 10
i want a table like this TABLE B
timeinterval count(exclusive range) count(inclusive range)
1-6 2 2
1-12 5 3
1-18 10 5
i have already generated table A and need table B. can i do something in SQL where i can add a query in the code for table A and do something like this (0-12)-(0-6) for 2nd row in table B.
code used for generating table A is
with ranges as
(
select 6 as val, 1 as count_all
union all
select 12, 1
union all
select 18, 1
union all
select 24, 1
union all
select 30, 1
union all
select 36, 1
union all
select 42, 1
union all
select 48, 1
union all
select 1, 0
)
select case when ranges.count_all = 0
then 'more'
else convert (varchar(10), ranges.val)
end [MetLifeExperienceMonths],
sum (case when (ranges.count_all = 0 and GoldListHistogram.MetLifeExperienceMonths>=1)
or
(GoldListHistogram.MetLifeExperienceMonths<= ranges.val and GoldListHistogram.MetLifeExperienceMonths>=1)
then 1 end) [count],
count(EmployeeID) as 'Total'
into yy
from GoldListHistogram
cross join ranges
where MetLifeExperienceMonths > 0
group by ranges.val, ranges.count_all
i need to modify the query such that i can subtract first two rows value for "count(exclusive range)" for every row staring from the 2nd row..like for 0-12(time interval) row i need to output a value that is difference of the first two rows..like row(i)=count(i)-count(i-1).
first column gives the time interval in 5 years (in months) second column calculates no. of employees in the exclusive range like (0-6,0-12,0-18)..6 ,12,18 being no. of months third column calculates no. of employees in the exclusive range like (0-6,6-12,12-18)
Could you not just add a start value to ranges? Something like:
with ranges as
(
select 6 as val, 0 as start, 1 as count_all
union all
select 12, 7, 1
union all
select 18, 13, 1
union all
select 24, 19, 1
union all
select 30, 25, 1
union all
select 36, 31, 1
union all
select 42, 37, 1
union all
select 48, 43, 1
union all
select 1, 49, 0
)
select case when ranges.count_all = 0
then 'more'
else convert (varchar(10), ranges.val)
end [MetLifeExperienceMonths],
sum (case when (ranges.count_all = 0 and GoldListHistogram.MetLifeExperienceMonths>=1)
or
(GoldListHistogram.MetLifeExperienceMonths=1)
then 1 else 0 end) [count inclusive],
sum (case when (ranges.count_all = 0 and GoldListHistogram.MetLifeExperienceMonths>=1)
or
(GoldListHistogram.MetLifeExperienceMonths=ranges.start)
then 1 else 0 end) [count exclusive],
count(EmployeeID) as 'Total'
into yy
from GoldListHistogram
cross join ranges
where MetLifeExperienceMonths > 0
group by ranges.val, ranges.count_all;