简体   繁体   中英

MySQL - Select row only if previous row field was 0?

I have a table as below:

 CREATE TABLE IF NOT EXISTS `status` 
 (`code` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY
 ,`IMEI` varchar(15) NOT NULL
 ,`ACC` tinyint(1) NOT NULL
 ,`datetime` datetime NOT NULL
 );

 INSERT INTO status VALUES
 (1,    123456789012345,    0,    '2014-07-09 10:00:00'),
 (2,    453253453334445,    0,    '2014-07-09 10:05:00'),
 (3,    912841851252151,    0,    '2014-07-09 10:08:00'),
 (4,    123456789012345,    1,    '2014-07-09 10:10:00'),
 (5,    123456789012345,    1,    '2014-07-09 10:15:00');

I need to get all rows for a given IMEI (eg 123456789012345) where ACC=1 AND the previous row for same IMEI has ACC=0. The rows may be one after the other or very apart.

Given the exampl above, I'd want to get the 4th row (code 4) but not 5th (code 5).

Any ideas? Thanks.

Assuming that you mean previous row by datetime

SELECT * 
  FROM status s
 WHERE s.imei='123456789012345'
   AND s.acc=1
   AND (
  SELECT acc
    FROM status
   WHERE imei=s.imei
     AND datetime<s.datetime
ORDER BY datetime DESC
   LIMIT 1
       ) = 0

The way I would approach this problem is much different from the approaches given in other answers.

The approach I would use would be to

1) order the rows, first by imei , and then by datetime within each imei . (I'm assuming that datetime is how you are going to determine if a row is "previous" to another row.

2) sequentially process the rows, first comparing imei from the current row to the imei from the previous row, and then checking if the ACC from the current row is 1 and the ACC from the previous row is 0 . Then I would know that the current row was a row to be returned.

3) for each processed row, in the resultset, include a column that indicates whether the row should be returned or not

4) return only the rows that have the indicator column set

A query something like this:

SELECT t.code
     , t.imei
     , t.acc
     , t.datetime
  FROM ( SELECT IF(s.imei=@prev_imei AND s.acc=1 AND @prev_acc=0,1,0) AS ret
              , s.code                                                AS code
              , @prev_imei := s.imei                                  AS imei
              , @prev_acc  := s.acc                                   AS acc
              , s.datetime                                            AS datetime
           FROM (SELECT @prev_imei := NULL, @prev_acc := NULL) i
          CROSS
           JOIN `status` s
          WHERE s.imei = '123456789012345'
          ORDER BY s.imei, s.datetime, s.code
       ) t
 WHERE t.ret = 1

(I can unpack that a bit, to explain how it works.)

But the big drawback of this approach is that it requires MySQL to materialize the inline view as a derived table (temporary MyISAM table). If there was no predicate (WHERE clause) on the status table, the inline view would essentially be a copy of the entire status table. And with MySQL 5.5 and earlier, that derived table won't be indexed. So, this could present a performance issue for large sets.

Including predicates (eg WHERE s.imei = '123456789' to limit rows from the status table in the inline view query may sufficiently limit the size of the temporary MyISAM table.

The other gotcha with this approach is that the behavior of user-defined variables in the statement is not guaranteed. But we do observe a consistent behavior, which we can make use of; it does work, but the MySQL documentation warns that the behavior is not guaranteed.


Here's a rough overview of how MySQL processes this query.

First, MySQL runs the query for the inline view aliased as i . We don't really care what this query returns, except that we need it to return exactly one row, because of the JOIN operation. What we care about is the initialization of the two MySQL user-defined variables, @prev_imei and @prev_acc . Later, we are going to use these user-defined variables to "preserve" the values from the previously processed row, so we can compare those values to the current row.

The rows from the status table are processed in sequence, according to the ORDER BY clause. (This may change in some future release, but we can observe that it works like this in MySQL 5.1 and 5.5.)

For each row, we compare the values of imei and acc from the current row to the values preserved from the previous row. If the boolean in the IF expression evaluates to TRUE, we return a 1, to indicate that this row should be returned. Otherwise, we return a 0, to indicate that we don't want to return this row. (For the first row processed, we previously initialized the user-defined variables to NULL, so the IF expression will evaluate to 0.)

The @prev_imei := s.imei and @prev_acc := s.acc assigns the values from the current row to the user-defined values, so they will be available for the next row processed.

Note that it's important that the tests of the user-defined variables (the first expression in the SELECT list) before we overwrite the previous values with the values from the current row.

We can run just the query from the inline view t , to observe the behavior.

The outer query returns rows from the inline view that have the derived ret column set to a 1, rows that we wanted to return.

select * from status s1
WHERE 
ACC = 1
AND code = (SELECT MIN(CODE) FROM status WHERE acc = 1 and IMEI = s1.IMEI)
AND EXISTS (SELECT * FROM status WHERE IMEI = s1.IMEI AND ACC = 0)
AND IMEI = 123456789012345
SELECT b.code,b.imei,b.acc,b.datetime
  FROM 
     ( SELECT x.*
            , COUNT(*) rank 
         FROM status x
         JOIN status y
           ON y.imei = x.imei
          AND y.datetime <= x.datetime
        GROUP
           BY x.code
     ) a
  JOIN
     ( SELECT x.*
            , COUNT(*) rank 
         FROM status x
         JOIN status y
           ON y.imei = x.imei
          AND y.datetime <= x.datetime
        GROUP
           BY x.code
     ) b
    ON b.imei = a.imei
   AND b.rank = a.rank + 1
 WHERE b.acc = 1
   AND a.acc = 0;

you can do a regular IN() and then group any duplicates (you could also use a limit but that would only work for one IMEI )

SETUP:

INSERT INTO `status` 
VALUES
 (1,    123456789012345,    0,    '2014-07-09 10:00:00'),
 (2,    453253453334445,    0,    '2014-07-09 10:05:00'),
 (3,    912841851252151,    0,    '2014-07-09 10:08:00'),
 (4,    123456789012345,    1,    '2014-07-09 10:10:00'),
 (5,    123456789012345,    1,    '2014-07-09 10:15:00'),
 (6,    123456789012345,    1,    '2014-07-09 10:15:00'),
 (7,    453253453334445,    1,    '2014-07-09 10:15:00');

QUERY:

SELECT * FROM status
WHERE ACC = 1 AND IMEI IN(
    SELECT DISTINCT IMEI FROM status
    WHERE ACC = 0)
GROUP BY imei;

RESULTS:

works with multiple IMEI that have a 0 then a 1... IMAGE


EDIT: if you would like to go by the date entered as well then you can just order it first by date and then group.

SELECT * FROM(
    SELECT * FROM status
    WHERE ACC = 1 AND IMEI IN(
        SELECT DISTINCT IMEI FROM status
        WHERE ACC = 0)
    ORDER BY datetime
) AS t
GROUP BY imei;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM