SQLinfo.ru - Все о MySQL

Форум пользователей MySQL

Задавайте вопросы, мы ответим

Вы не зашли.

#26 20.01.2014 07:44:56

vasya
Архат
MySQL Authorized Developer
Откуда: Орел
Зарегистрирован: 07.03.2007
Сообщений: 5828

Re: Неожиданное поведение подзапроса

Акутогава-сан, прошу простить мне косноязычность!

У нас есть исходный набор данных:

MariaDB [test]> select * from t_3839;
+------+-------+------+
| id   | login | time |
+------+-------+------+
|    1 |     1 |   10 |
|    2 |     1 |   15 |
|    3 |     2 |  100 |
|    4 |     2 |   80 |
|    5 |     3 |   11 |
|    6 |     4 |    8 |
+------+-------+------+
6 rows in set (0.11 sec)


Мы хотим для каждого значения login выбрать строку с максимальным значение time, но решить хотим затейливо, а именно, отсортируем данную таблицу в порядке (`login` asc, `time` desc), запишем результат во временную таблицу и, последовательно читая её, из каждой группы с одинаковым значением login возьмем с помощью пользовательских переменных только первую строку. Это и будет нужный нам ответ, который очевидно будет иметь вид:

+------+-------+------+
| id   | login | time |
+------+-------+------+
|    2 |     1 |   15 |
|    3 |     2 |  100 |
|    5 |     3 |   11 |
|    6 |     4 |    8 |
+------+-------+------+
 


Пишем красивый запрос:
MariaDB [test]> select t.* from
( select t_3839.* from t_3839, (select @i:=0,@n:=1,@l:=0) x order by login, time desc) t
where if (@l=login, @i:=@i+1,(@i:=0) or (@l:=login)) and @i<@n;

и получаем неожиданную хрень:

+------+-------+------+
| id   | login | time |
+------+-------+------+
|    2 |     1 |   15 |
+------+-------+------+
1 row in set (0.00 sec)

Неожиданную, потому что в предыдущих версиях, например, MariaDB 5.3 этот запрос работал.

Если мы посмотрим как выглядит запрос восстановленный из плана исполнения, то увидим:

select `t`.`id` AS `id`,`t`.`login` AS `login`,`t`.`time` AS `time` fro
m (select `test`.`t_3839`.`id` AS `id`,`test`.`t_3839`.`login` AS `login`,`test`
.`t_3839`.`time` AS `time` from `test`.`t_3839` order by `test`.`t_3839`.`login`
,`test`.`t_3839`.`time` desc) `t` where (if(((@`l`) = `t`.`login`),(@i:=((@`i`)
+ 1)),(<cache>((@i:=0)) or (@l:=`t`.`login`))) and ((@`i`) < (@`n`)))


Да, получилось очень наглядно :) Попробую объяснить что же здесь происходит.
Первоначальное определение переменных и помещение отсортированной таблицы во временную произошло успешно, т.е. наши переменные имеют значения @i:=0,@n:=1,@l:=0
и временная таблица t вида

+------+-------+------+
| id   | login | time |
+------+-------+------+
|    2 |     1 |   15 |
|    1 |     1 |   10 |
|    3 |     2 |  100 |
|    4 |     2 |   80 |
|    5 |     3 |   11 |
|    6 |     4 |    8 |
+------+-------+------+

Для первой строки сравниваем значение поля login с переменной @l. 1 не равно 0, следовательно вычисляем выражение ((@i:=0)) or (@l:=`t`.`login`)). @i присваивается значение 0, результат этого присвоения false; @l присваивается значение 1, результат этого присвоения true. Общий результат данного выражения тоже получается true. Второе условие @i<@n тоже выполняется, так как 0<1. Первая строка идет в окончательный ответ.
Для второй строки сравниваем значение поля login с переменной @l. 1 равно 1, следовательно вычисляем выражение @i:=@i+1. @i становится равной 2, результат присвоения true. Второе условие @i<@n не выполняется.
Для третьей строки сравниваем значение поля login с переменной @l. 2 не равно 1, и вот тут возникает проблема, так как сервер не вычисляет выражение ((@i:=0)) or (@l:=`t`.`login`)), а берет ранее вычисленный вариант для первой строки, так как данный результат запоминается <cache>((@i:=0)) or (@l:=`t`.`login`)). Т.е. выражение всегда true, а сами переменные не переопределяются. Легко понять, что в дальнейшем не одна строка выбрана не будет.

Но дело в том, что результат выражения ((@i:=0)) or (@l:=`t`.`login`)) не определен и зависит от значения поля `login`. Поле определено как int, т.е. может принимать значение 0, а в этом случае и всё выражение станет false. Однако оптимизатор кэширует его при первом вычислении. Откуда он знает, что там не будет нуля?

Ага, типа настолько умный,  понимает, что раз таблица отсортирована по `login` asc и начинается с 1, то 0 не будет?

Тогда ,
MariaDB [test]> update t_3839 set login=-1 where id=1;
MariaDB [test]> select t.* from ( select t_3839.* from t_3839, (select @i:=0,@n:
=1,@l:=0) x order by login, time desc) t where if (@l=login, @i:=@i+1,(@i:=0) or
 (@l:=login)) and @i<@n;
+------+-------+------+
| id   | login | time |
+------+-------+------+
|    1 |    -1 |   10 |
|    2 |     1 |   15 |
|    3 |     2 |  100 |
+------+-------+------+


MariaDB [test]> update t_3839 set login=1 where login=4;
Query OK, 1 rows affected (0.42 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MariaDB [test]> select t.* from ( select t_3839.* from t_3839, (select @i:=0,@n:
=1,@l:=0) x order by login, time desc) t where if (@l=login, @i:=@i+1,(@i:=0) or
 (@l:=login)) and @i<@n;
+------+-------+------+
| id   | login | time |
+------+-------+------+
|    1 |    -1 |   10 |
|    2 |     1 |   15 |
+------+-------+------+
2 rows in set (0.00 sec)


Как видим - нет, ни фига он не умный, и при отрицательном значении поля login также кэширует результат первого вычисления выражения ((@i:=0)) or (@l:=`t`.`login`)).

А это есть бага!

Неактивен

 

#27 20.01.2014 15:43:49

rgbeast
Администратор
MySQL Authorized Developer and DBA
Откуда: Москва
Зарегистрирован: 21.01.2007
Сообщений: 3878

Re: Неожиданное поведение подзапроса

Вася, ну за час можно разобраться. Но надо все-таки сделать пример нагляднее. Первое - убрать инициализацию из запроса, пусть это будет два запроса, как положено по мануалу. И еще - бага в слове <cache> в extended explain, это понятнее, чем результат запроса. И нужно привести результат для разных версий, включая EXPLAIN EXTENDED - есть шанс, что это будет понято (так как разный результат = бага в одной из версий).

Неактивен

 

#28 20.01.2014 15:50:09

rgbeast
Администратор
MySQL Authorized Developer and DBA
Откуда: Москва
Зарегистрирован: 21.01.2007
Сообщений: 3878

Re: Неожиданное поведение подзапроса

И еще - переменная @n ведь не нужна? Лучше сократить число переменных.

Неактивен

 

#29 20.01.2014 15:57:16

deadka
Администратор
Зарегистрирован: 14.11.2007
Сообщений: 2420

Re: Неожиданное поведение подзапроса

От ее использования можно отказаться, да.
Заменив
and @i<@n
на
and @i<1

И еще сузить область поиска можно было бы заменив login на числовое поле вместо текстового.

Ибо напомню, что в инструкции
or (@l:=login) or 1
or 1
неспроста было добавлено.


Зеленый свет для слабаков, долги отдают только трусы, тру гики работают только в консоли...

Неактивен

 

#30 22.01.2014 14:02:19

vasya
Архат
MySQL Authorized Developer
Откуда: Орел
Зарегистрирован: 07.03.2007
Сообщений: 5828

Re: Неожиданное поведение подзапроса

deadka написал:

И еще сузить область поиска можно было бы заменив login на числовое поле вместо текстового.

Ибо напомню, что в инструкции
or (@l:=login) or 1
or 1
неспроста было добавлено.

Всё-таки поклеп smile
У меня в последних примерах как раз по этой причине и был логин числом, потому как про условие (.. or 1) действительно можно заранее сказать, что это всегда истина.

Неактивен

 

#31 22.01.2014 14:21:02

vasya
Архат
MySQL Authorized Developer
Откуда: Орел
Зарегистрирован: 07.03.2007
Сообщений: 5828

Re: Неожиданное поведение подзапроса

Предлагаю такой вариант.

create table test (a int, b int, index(a,b)) engine=myisam;

insert into test values(-2,1),(-2,2),(-2,3),(-1,1),(1,1),(1,2);


MySQL 5.0.51a-community-log

MySQL 5.0 написал:

mysql> set @i:=0, @p:=-999;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test where if(@p=a,@i:=@i+1,(@i:=0) or (@p:=a)) and @i<1;
+------+------+
| a    | b    |
+------+------+
|   -2 |    1 |
|   -1 |    1 |
|    1 |    1 |
+------+------+
3 rows in set (0.00 sec)

mysql> explain extended select * from test where if(@p=a,@i:=@i+1,(@i:=0) or (@p                   :=a)) and @i<1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
         type: index
possible_keys: NULL
          key: a
      key_len: 10
          ref: NULL
         rows: 6
        Extra: Using where; Using index
1 row in set, 1 warning (0.01 sec)

mysql> show warnings\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: select `test`.`test`.`a` AS `a`,`test`.`test`.`b` AS `b` from `test`.`t                   est` where (if(((@p) = `test`.`test`.`a`),(@i:=((@i) + 1)),((@i:=0) or (@p:=`tes                   t`.`test`.`a`))) and ((@i) < 1))
1 row in set (0.00 sec)

5.5.34-MariaDB-log

MariaDB 5.5 написал:

MariaDB [test]> set @i:=0, @p:=-999;
Query OK, 0 rows affected (0.00 sec)

MariaDB [test]> select * from test where if(@p=a,@i:=@i+1,(@i:=0) or (@p:=a)) an
d @i<1;
+------+------+
| a    | b    |
+------+------+
|   -2 |    1 |
+------+------+
1 row in set (0.00 sec)

MariaDB [test]> explain extended select * from test where if(@p=a,@i:=@i+1,(@i:=
0) or (@p:=a)) and @i<1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
         type: index
possible_keys: NULL
          key: a
      key_len: 10
          ref: NULL
         rows: 6
     filtered: 100.00
        Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)

MariaDB [test]> show warnings\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: select `test`.`test`.`a` AS `a`,`test`.`test`.`b` AS `b` from `test`.`t
est` where (if(((@`p`) = `test`.`test`.`a`),(@i:=((@`i`) + 1)),(<cache>((@i:=0))
or (@p:=`test`.`test`.`a`))) and ((@`i`) < 1))
1 row in set (0.00 sec)

Из примера видно, что запрос в разных версиях дает разный результат и в случае MariaDB 5.5 сервер закэшировал выражение <cache>((@i:=0))
or (@p:=`test`.`test`.`a`)) при первом вичислении. Это ошибка так как результат данного выражения зависит от значения поля `a`.
Результат присвоения нуля пользовательской переменной будет false, а любого целого числа true

MariaDB [test]> select if(@q:=0,1,0);
+---------------+
| if(@q:=0,1,0) |
+---------------+
|             0 |
+---------------+
1 row in set (0.01 sec)

MariaDB [test]> select if(@q:=-5,1,0);
+----------------+
| if(@q:=-5,1,0) |
+----------------+
|              1 |
+----------------+
1 row in set (0.00 sec)


Следовательно при `a`=0 выражение ((@i:=0)) or (@p:=`test`.`test`.`a`)) принимает значение false, в остальных случаях true.

P.S. Я понимаю, что сравнивать MySQL 5.0 и MariaDB 5.5 не очень правильно, но под рукой другого нет. И подозреваю, что в MySQL 5.5 картина будет точно такая же как и в MariaDB 5.5
Проверьте, пожалуйста, это момент.

Неактивен

 

#32 24.01.2014 17:46:52

vasya
Архат
MySQL Authorized Developer
Откуда: Орел
Зарегистрирован: 07.03.2007
Сообщений: 5828

Re: Неожиданное поведение подзапроса

http://sqlfiddle.com/#!2/d8e10/1

Как и ожидалось в MySQL5.5 бага присутствует. show warnings сервис не показывает, но вывод можно сделать на основании результата выполнения самого запроса.
Кстати, удобный сервис для проверки запроса в разных версиях, правда из MySQL там только 5.1, 5.5, 5.6

Возвращаясь к началу темы, в 5.6 from-подзапрос не ликвидируется как не существенный. Эту оптимизацию в MySQL ещё не ввели.

Неактивен

 

#33 24.01.2014 17:56:42

rgbeast
Администратор
MySQL Authorized Developer and DBA
Откуда: Москва
Зарегистрирован: 21.01.2007
Сообщений: 3878

Re: Неожиданное поведение подзапроса

Наверное надо Монти сообщить о баге https://mariadb.com/kb/en/reporting-bugs/  и на bugs.mysql.com

Неактивен

 

Board footer

Работает на PunBB
© Copyright 2002–2008 Rickard Andersson