MySQL возвращает неверные данные? - PullRequest
4 голосов
/ 19 января 2011

Недавно в сообществе Django появилась проблема с тестированием MySQL (с использованием MyISAM).

Вот билет на Джанго: http://code.djangoproject.com/ticket/14661

Один из разработчиков ядра Django придумал этот тест, и многие из нас смогли его повторить. У кого-нибудь есть догадки относительно того, с чем мы здесь сталкиваемся? Это просто ошибка в MySQL или я что-то упустил?

Вот тестовый код и запросы:

DROP TABLE IF EXISTS `testapp_tag`;
CREATE TABLE `testapp_tag` (
    `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
    `name` varchar(10) NOT NULL,
    `parent_id` integer
);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t1", NULL);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t2", 1);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t3", 1);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t4", 3);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t5", 3);
SELECT `testapp_tag`.`id`, `testapp_tag`.`name`, `testapp_tag`.`parent_id` FROM `testapp_tag` WHERE NOT ((`testapp_tag`.`id` IN (SELECT U0.`id` FROM `testapp_tag` U0 LEFT OUTER JOIN `testapp_tag` U1 ON (U0.`id` = U1.`parent_id`) WHERE U1.`id` IS NULL) AND `testapp_tag`.`id` IS NOT NULL)) ORDER BY `testapp_tag`.`name` ASC;
SELECT `testapp_tag`.`id`, `testapp_tag`.`name`, `testapp_tag`.`parent_id` FROM `testapp_tag` WHERE NOT ((`testapp_tag`.`id` IN (SELECT U0.`id` FROM `testapp_tag` U0 LEFT OUTER JOIN `testapp_tag` U1 ON (U0.`id` = U1.`parent_id`) WHERE U1.`id` IS NULL) AND `testapp_tag`.`id` IS NOT NULL)) ORDER BY `testapp_tag`.`name` ASC;

Вот вывод:

mysql> SELECT `testapp_tag`.`id`, `testapp_tag`.`name`, `testapp_tag`.`parent_id` FROM `testapp_tag` WHERE NOT ((`testapp_tag` .`id` IN (SELECT U0.`id` FROM `testapp_tag` U0 LEFT OUTER JOIN `testapp_tag` U1 ON (U0.`id` = U1.`parent_id`) WHERE U1.`id` IS  NULL) AND `testapp_tag`.`id` IS NOT NULL)) ORDER BY `testapp_tag`.`name` ASC;
+----+------+-----------+
| id | name | parent_id |
+----+------+-----------+
|  1 | t1   |      NULL |
|  3 | t3   |         1 |
|  5 | t5   |         3 |
+----+------+-----------+
3 rows in set (0.00 sec)

mysql> SELECT `testapp_tag`.`id`, `testapp_tag`.`name`, `testapp_tag`.`parent_id` FROM `testapp_tag` WHERE NOT ((`testapp_tag` .`id` IN (SELECT U0.`id` FROM `testapp_tag` U0 LEFT OUTER JOIN `testapp_tag` U1 ON (U0.`id` = U1.`parent_id`) WHERE U1.`id` IS NULL) AND `testapp_tag`.`id` IS NOT NULL)) ORDER BY `testapp_tag`.`name` ASC;
+----+------+-----------+
| id | name | parent_id |
+----+------+-----------+
|  1 | t1   |      NULL |
|  3 | t3   |         1 |
+----+------+-----------+
2 rows in set (0.01 sec)

Ответы [ 2 ]

4 голосов
/ 19 января 2011

Кажется очень интересным и выглядит как ошибка в оптимизаторе запросов MySql.

Если вы запустите это вместо простого, выберите:

EXPLAIN EXTENDED SELECT `testapp_tag`.`id`, ....;
SHOW WARNINGS;
EXPLAIN EXTENDED SELECT `testapp_tag`.`id`, ...;
SHOW WARNINGS;

Затем, сравнивая выходные данные изEXPLAIN EXTENDED warnings, Вы можете видеть, что в первый раз оптимизатор добавляет к выбору:

or (`test`.`testapp_tag`.`id` = 5)

Также обратите внимание, что удаление AND testapp_tag.id IS NOT NULL из WHERE, которое ничего не делает, так какполе помечено как NOT NULL, похоже, устраняет проблему.

4 голосов
/ 19 января 2011

Эта форма работает надежно:

SELECT T.`id`, T.`name`, T.`parent_id`
FROM `testapp_tag` T
WHERE NOT (T.`id` IN (
    SELECT U0.`id`
    FROM `testapp_tag` U0
    LEFT OUTER JOIN `testapp_tag` U1 ON (U0.`id` = U1.`parent_id`)
    WHERE U1.`id` IS NULL))
ORDER BY T.`name` ASC;

Дополнительный фильтр NOT + IN +, кажется, выбрасывает MySQL. Это определенно ошибка.

Тест в NOT () ищет 2 части. Если первая часть истинна, вторая не может быть истинной, независимо от того, может ли поле быть пустым или нет. Это избыточное предложение, которое, похоже, является причиной ошибки.

Исходя из ответа ScrumMeister, я подтверждаю, что ошибка связана с каким-то видом кэширования для последнего вставленного идентификатора в AUTO_INCREMENT.

DROP TABLE IF EXISTS `testapp_tag`;

CREATE TABLE `testapp_tag` (
    `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
    `name` varchar(10) NOT NULL,
    `parent_id` integer
);

start transaction;
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t1", NULL);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t2", 1);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t3", 1);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t4", 3);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t5", 3);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t6", 3);
INSERT INTO `testapp_tag` (`name`, `parent_id`) VALUES ("t7", 3);
commit;

delete from testapp_tag where id = 6;   #######

explain extended
SELECT T.`id`, T.`name`, T.`parent_id`
FROM `testapp_tag` T
WHERE NOT (T.`id` IN (
    SELECT U0.`id`
    FROM `testapp_tag` U0
    LEFT OUTER JOIN `testapp_tag` U1 ON (U0.`id` = U1.`parent_id`)
    WHERE U1.`id` IS NULL) AND T.`id` IS NOT NULL)
ORDER BY T.`name` ASC;
show warnings;

Производит этот план

select `test`.`t`.`id` AS `id`,`test`.`t`.`name` AS `name`,`test`.`t`.`parent_id` AS `parent_id`
from `test`.`testapp_tag` `T` where ((not(<in_optimizer>(`test`.`t`.`id`,
<exists>(select 1 AS `Not_used` from `test`.`testapp_tag` `U0` left join `test`.`testapp_tag` `U1` 
on((`test`.`u1`.`parent_id` = `test`.`u0`.`id`)) where (isnull(`test`.`u1`.`id`)
and (<cache>(`test`.`t`.`id`) = `test`.`u0`.`id`)))))) **or (`test`.`t`.`id` = 7)**)
order by `test`.`t`.`name`

Если вставка останавливается на t6, а удаление также на t6, ошибка маскируется, потому что добавленное предложение равно или (test.t.id = 6), которое мы уже удалили в строке, помеченной #### ###

...