I have a column with two columns. one is TIMESTAMP and the other DIGITAL_BIT.
The value digital bit can be either 0 or 1 and changes a few times during the day. Every minute of the day is stored in this table. I would need to read somehow how many times a day this value changed from 0 to 1.
Is it possible to make a query that returns the count of this changes? What I have in mind is something like this:
select * from mytable where digital_bit = 1 and digital_bit (of previous row) = 0 order by timestamp
Can this be done with a query or do i have to process all data in my program?
Thanks
SAMPLE
timestamp | digital_bit
100000 | 0
100001 | 0
100002 | 1
100003 | 1
100004 | 0
100005 | 1
100006 | 0
100007 | 0
100008 | 1
the above should return 3 because for 3 times the value digital passed from 0 to 1. i need to count how often the value digital CHANGES from 0 to 1.
Here you go. This will get you a count of how many times digital_bit switched from 0 to 1 (in your example, this will return 3).
SELECT COUNT(*)
FROM mytable curr
WHERE curr.digital_bit = 1
AND (
SELECT digital_bit
FROM mytable prev
WHERE prev.timestamp < curr.timestamp
ORDER BY timestamp DESC
LIMIT 1
) = 0
SQLFiddle link
(Original answer relied on the timestamps being sequential: e.g. no jumps from 100001 to 100003. Answer has now been updated not to have that restriction.)
IF you have a result once per minte, you can simple join the table with itself, and
use timestamp+1 as well as leftbit != rightbit as join condition.
http://sqlfiddle.com/#!8/791c0/6
ALL Changes:
SELECT
COUNT(*)
FROM
test a
INNER JOIN
test b
ON
a.digital_bit != b.digital_bit
AND b.timestamp = a.timestamp+1;
Changes from 0 to 1
SELECT
COUNT(*)
FROM
test a
INNER JOIN
test b
ON
a.digital_bit = 0 AND
a.digital_bit != b.digital_bit
AND b.timestamp = a.timestamp+1;
Changes from 1 to 0
SELECT
COUNT(*)
FROM
test a
INNER JOIN
test b
ON
a.digital_bit = 1 AND
a.digital_bit != b.digital_bit
AND b.timestamp = a.timestamp+1;
Adapted from: How do I query distinct values within multiple sub record sets
select count(*)
from (select t1.*,
(select digital_bit
from table t2
where t2.timestamp < t1.timestamp
order by timestamp desc LIMIT 1
) as prevvalue
from table t1
) t1
where prevvalue <> digital_bit and digital_bit = 1;
This isn't likely to be efficient with a lot of data, but you can get all the rows and calculate a sequence number for them, then do the same again but with the sequence number offset by 1. Then join the 2 lots together where those calculated sequence numbers match but the first one has a digital bit of 0 and the other a digital bit of 1:-
SELECT COUNT(*)
FROM
(
SELECT mytable.timestamp, mytable.digital_bit, #aCount1:=#aCount1+1 AS SeqCount
FROM mytable
CROSS JOIN (SELECT #aCount1:=1) sub1
ORDER BY timestamp
) a
INNER JOIN
(
SELECT mytable.timestamp, mytable.digital_bit, #aCount2:=#aCount2+1 AS SeqCount
FROM mytable
CROSS JOIN (SELECT #aCount2:=0) sub1
ORDER BY timestamp
) b
ON a.SeqCount = b.SeqCount
AND a.digital_bit = 0
AND b.digital_bit = 1
EDIT - alternative solution and I would be interested to see how this performs. It avoids the need for adding a sequence number and also avoids a correlated sub query:-
SELECT COUNT(*)
FROM
(
SELECT curr.timestamp, MAX(curr2.timestamp) AS MaxTimeStamp
FROM mytable curr
INNER JOIN mytable curr2
ON curr.timestamp > curr2.timestamp
AND curr.digital_bit = 1
GROUP BY curr.timestamp
) Sub1
INNER JOIN mytable curr
ON Sub1.MaxTimeStamp = curr.timestamp
AND curr.digital_bit = 0
As I understood you have one query every minute. So you have no problem with performance.
You can add flag:
timestamp | digital_bit | changed
100000 | 0 | 0
100001 | 0 | 0
100002 | 1 | 1
100003 | 1 | 0
100004 | 0 | 1
100005 | 1 | 1
100006 | 0 | 1
100007 | 0 | 0
100008 | 1 | 1
And make check before insert:
SELECT digital_bit
FROM table
ORDER BY timestamp DESC
LIMIT 1
and if digital_bit is different insert new row with flag.
And then you just can take COUNT of flags:
SELECT COUNT(*)
FROM table
WHERE DATE BETWEEN (start, end)
AND changed = 1
Hope will see in answers better solution.
Related
I have 2 tables which are Teacher and Activities.
CREATE TABLE teacher (
TeacherId INT, BranchId VARCHAR(5));
INSERT INTO teacher VALUES
("1121","A"),
("1132","A"),
("1141","A"),
("2120","B"),
("2122","B");
CREATE TABLE activities (
ID INT, TeacherID INT, Hours INT);
INSERT INTO activities VALUES
(1,1121,2),
(2,1121,1),
(3,1132,1),
(4,1141,NULL),
(5,2120,NULL),
(6,2122,NULL);
NULL indicates no activities and will be convert to 0 on output table. I want to produce a query to count total of hours and count how many activities base on teacher hours such as the following table:
+-----------+------------+------------+
| Hours | A | B |
+-----------+------------+------------+
| 0 | 1 | 2 |
| 1 | 1 | 0 |
| 2 | 0 | 0 |
| 3 | 1 | 0 |
+-----------+------------+------------+
Edited: Sorry I don't know how to elaborate accurately, but here is the fiddle i received from other member https://www.db-fiddle.com/f/mmtuZquKyUqdhPvTFN9qaF/1
Edit: Last, modification need, to sum the hours and count the hours base on branch id and teacher id as the output.
Expected output here (red text): https://drive.google.com/file/d/1wyZ_aX5hz_7I1Ncf5sXLpstYk6FT8PMg/view?usp=sharing
We can handle this via the use of a calendar table of hours joined to an aggregation subquery:
SELECT
t1.Hours,
SUM(CASE WHEN t2.BranchId = 'A' THEN t2.cnt ELSE 0 END) AS A,
SUM(CASE WHEN t2.BranchId = 'B' THEN t2.cnt ELSE 0 END) AS B
FROM (SELECT 0 AS Hours UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) t1
LEFT JOIN
(
SELECT t.BranchId, COALESCE(a.Hours, 0) AS Hours, COUNT(*) AS cnt
FROM Teacher t
LEFT JOIN Activities a ON a.TeacherId = t.TeacherId
GROUP BY t.BranchId, COALESCE(a.Hours, 0)
) t2
ON t1.Hours = t2.Hours
GROUP BY
t1.Hours
ORDER BY
t1.Hours
Demo
This is basically a JOIN and aggregation . . . but you need to start with all the hours you want:
SELECT h.Hours,
COALESCE(SUM(t.BranchId = 'A'), 0) AS A,
COALESCE(SUM(t.BranchId = 'B'), 0) AS B
FROM (SELECT 0 AS Hours UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
) h LEFT JOIN
activities a
ON h.hours = COALESCE(a.hours, 0) LEFT JOIN
teacher t
ON t.TeacherId = a.TeacherId
GROUP BY h.Hours
ORDER BY h.Hours;
Here is a db<>fiddle.
I'm trying to write a query that will pull students that have passed tests 1 thru 3 AND failed test 4.
Students can retake tests so there may be failed records, followed by passed records for some tests, such is the case with student_id = 2 below.
Table setup like this -
test_id | student_id | status | completed_on
--------+------------+---------+------------
1 | 1 | passed | 2018-03-24
2 | 1 | passed | 2018-03-25
3 | 1 | passed | 2018-03-26
4 | 1 | failed | 2018-03-27
1 | 2 | failed | 2018-03-24
1 | 2 | passed | 2018-03-25
2 | 2 | passed | 2018-03-26
3 | 2 | passed | 2018-03-27
4 | 2 | failed | 2018-03-27
In this case the query should pull both student_id 1 and 2
I tried this but it obviously didn't work -
select *
from table
where (test_id = 1 and status = 'passed')
and (test_id = 2 and status = 'passed')
and (test_id = 3 and status = 'passed')
and (test_id = 4 and status = 'failed')
Demo
SELECT count(Z.Test_ID), Z.student_ID
FROM (SELECT distinct student_ID, test_ID, Status
FROM table) Z
WHERE (Z.Status = 'Passed' and Z.test_ID in (1,2,3,4))
OR (Z.status = 'Failed' and Z.test_ID = 4)
GROUP BY Z.Student_ID
HAVING count(Z.Test_ID) = 4;
This works by first ensuring we only have distinct records for each student, status, and test_ID. (derived table Z)
We then evaluate how many passes in tests 1,2,3,4 exist and existence of fails we have for test 4. if the count is anything other than 4 then we know either they didn't pass the tests 1-3 and fail 4 or they've passed test 4 as well.
I'm not claiming this is fast or most efficient, but it will do the job. Make sure you have the right indices on your table,
SELECT s1.student_id
FROM mytable s1
JOIN mytable s2 on s1.student_id=s2.student_id and s2.test_id=2 and s2.status='passed'
JOIN mytable s3 on s1.student_id=s3.student_id and s3.test_id=3 and s3.status='passed'
WHERE s1.test_id=1
AND s1.status='passed'
AND NOT EXISTS (
SELECT 1
FROM mytable s4
WHERE s4.student_id=s1.student_id
AND s4.test_id=4
AND s4.status='passed'
)
Another approach:
select distinct t1.student_id
from mytable t1
inner join
-- students passed all the 3 tests
(select student_id from mytable where test_id in (1, 2, 3)
and status = 'passed' group by student_id having count(distinct test_id) = 3 ) t2
on t1.test_id = 4 and t1.status = 'failed' and t1.student_id = t2.student_id
where not exists
(select 1 from mytable where student_id = t1.student_id and
status = 'passed' and test_id = 4)
PS. If a student passed a test (e.g. test 1) but later took it again and failed, the student would be considered as passed. Not sure if that is acceptable.
One approach you can use to meet your requirement is to basically make a pivot table. Any way you slice it you will probable need to use a subquery, here using the SUM function and CASE statements you can determine how many times each student passed a test.
Then, in your outer WHERE clause you can select only the rows where you got 1 or more 'passed' results for tests 1 through 3, and 0 'passed' results for test 4.
SQL Fiddle
SELECT student_id
FROM (SELECT student_id,
SUM(
CASE WHEN(test_id = 1 AND result = 'passed')
THEN 1
ELSE 0
END) AS "Test1",
SUM(
CASE WHEN(test_id = 2 AND result = 'passed')
THEN 1
ELSE 0
END) AS "Test2",
SUM(
CASE WHEN(test_id = 3 AND result = 'passed')
THEN 1
ELSE 0
END) AS "Test3",
SUM(
CASE WHEN(test_id = 4 AND result = 'passed')
THEN 1
ELSE 0
END) AS "Test4"
FROM TestResults
GROUP BY student_id) tr
WHERE Test1 > 0 AND Test2 > 0 AND Test3 > 0 AND Test4 = 0
Using this technique you can also determine how many times a student passed or failed a test. So for example, you can change the CASE statements to 'failed' and return the test columns in the outer query to see the number of times a student failed the test. For example:
SELECT student_id, Test1Fails
FROM (SELECT student_id,
SUM(
CASE WHEN(test_id = 1 AND result = 'failed')
THEN 1
ELSE 0
END) AS "Test1Fails"
FROM TestResults
GROUP BY student_id) tr
I have a query that returns the counts from a database. Sample output of the query:
23
14
94
42
23
12
The query:
SELECT COUNT(*)
FROM `submissions`
INNER JOIN `events`
ON `submissions`.event_id = `events`.id
WHERE events.user_id IN (
SELECT id
FROM `users`
WHERE users.created_at IS NOT NULL
GROUP BY `events`.id
Is there a way to easily take the output and split it into pre-defined ranges of values (0-100, 101-200, etc), indicating the number of rows that fall into a particular range?
Use a case expression in select clause.
SELECT `events`.id ,
case when COUNT(`events`.id) between 0 and 100 then '0 - 100'
when COUNT(`events`.id) between 100 and 200 then '100 - 200'
end as Range
FROM `submissions`
INNER JOIN `events`
ON `submissions`.event_id = `events`.id
WHERE events.user_id IN (
SELECT id
FROM `users`
WHERE users.created_at IS NOT NULL
GROUP BY `events`.id
Use conditional count by leveraging SUM() aggregate.
If you need your ranges in columns
SELECT SUM(CASE WHEN n BETWEEN( 0 AND 100) THEN 1 ELSE 0 END) '0-100',
SUM(CASE WHEN n BETWEEN(101 AND 200) THEN 1 ELSE 0 END) '101-200'
-- , add other ranges here
FROM (
SELECT COUNT(*) n
FROM submissions s JOIN events e
ON s.event_id = e.id JOIN users u
ON e.user_id = u.id
WHERE u.created_at IS NOT NULL
GROUP BY e.id
) q
Sample output
+-------+---------+
| 0-100 | 101-200 |
+-------+---------+
| 2 | 3 |
+-------+---------+
1 row in set (0.01 sec)
If you'd rather have it as a set you can do
SELECT CONCAT(r.min, '-', r.max) `range`,
SUM(n BETWEEN r.min AND r.max) count
FROM (
SELECT COUNT(*) n
FROM submissions s JOIN events e
ON s.event_id = e.id JOIN users u
ON e.user_id = u.id
WHERE u.created_at IS NOT NULL
GROUP BY e.id
) q CROSS JOIN (
SELECT 0 min, 100 max
UNION ALL
SELECT 101, 200
-- add other ranges here
) r
GROUP BY r.min, r.max
Sample output
+---------+-------+
| range | count |
+---------+-------+
| 0-100 | 2 |
| 101-200 | 3 |
+---------+-------+
2 rows in set (0.01 sec)
I have this table
uid | rid
-----------
1 | 4
1 | 13
2 | 4
3 | 13
I want my query to return the uid where it has rid = 13 AND does not have rid = 4 so the result would be 3
So far I have
SELECT distinct uid
FROM test
WHERE
ur.rid <> 4
AND ur.rid = 13
but of course this is returning 1 and 3
What am I missing from my query to get only 3?
Many thanks.
You can add NOT EXISTS:
SELECT DISTINCT uid
FROM test t1
WHERE t1.rid = 13
AND NOT EXISTS (SELECT 1
FROM test t2
WHERE t2.uid = t1.uid
AND t2.rid = 4);
LiveDemo
or using aggregation:
SELECT uid
FROM test
GROUP BY uid
HAVING COUNT(CASE WHEN rid = 13 THEN 1 END) > 0
AND COUNT(CASE WHEN rid = 4 THEN 1 END) = 0;
LiveDemo2
EDIT (by gordon):
The above answer is fine. I'm just adding this as a simplification to the second query (it didn't seem appropriate to put in a separate answer and it is too long for a comment):
SELECT uid
FROM test
GROUP BY uid
HAVING SUM(rid = 13) > 0 AND
SUM(rid = 4) = 0;
It takes advantage of a nice feature of MySQL.
SqlFiddleDemo
I'm trying to select the sum of the values in the isOK column for each Name separated, BUT only if isOK = 1 on Day = 2.
The query for the following example table tablename
Name | Day | isOK
char | int | int
-----------------
Flo | 1 | 1
Seb | 1 | 1
Tim | 1 | 0
Flo | 2 | 1
Seb | 2 | 0
Tim | 2 | 1
should give Flo: 2 and Tim: 1, but not Seb: 1, since his isOK on Day = 2 is 0.
I've tried using SUM(isOK) with IF constructs, but it's just not working. My alternative solution, to select all Name where isOK = 1 first and select the SUM(isOK) for each of the names is slow and seems in need of improvement.
I guess it's not that difficult, but I've been trying for hours now and I just can't combine my two queries into one.
One way to do this is to use a conditional expression together with a having clause like this:
select name, sum(isOk) ok_sum
from your_table
group by name
having sum(case when day = 2 and isOK = 1 then 1 else 0 end) > 0;
With your sample data the result would be:
name ok_sum
Flo 2
Tim 1
As MySQL evaluates boolean expressions as 1 or 0 it should be possible to reduce the condition to this:
having sum(day = 2 and isOK = 1) > 0;
Another way to do it would be to use a correlated subquery that makes sure there exists a row with Day = 2 and isOk = 1 for the Name:
select t1.name, sum(t1.isOk) ok_sum
from your_table t1
where exists (
select 1
from your_table t2
where t2.day = 2 and t2.isOK = 1 and t1.name = t2.name
)
group by t1.name
See this fiddle
TRY this :
SELECT
name, SUM(isok) AS isOk
FROM
table
GROUP BY `name`
HAVING SUM(`day` = 2 AND isok = 1) > 0;
SELECT x.name, SUM(y.isOK) total
FROM my_table x
JOIN my_table y
ON y.name = x.name
WHERE x.day = 2
AND x.isok=1
GROUP
BY x.name;