Mysql не использует индексы, когда присутствует оператор "не в"
Структура таблицы:
CREATE TABLE `test` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`from` int(10) unsigned NOT NULL,
`to` int(10) unsigned NOT NULL,
`message` text NOT NULL,
`sent` int(10) unsigned NOT NULL DEFAULT '0',
`read` tinyint(1) unsigned NOT NULL DEFAULT '0',
`direction` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `one` (`to`,`direction`,`from`,`id`),
KEY `two` (`from`,`direction`,`to`,`id`),
KEY `three` (`read`,`direction`,`to`),
KEY `four` (`read`,`direction`,`from`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
У меня странная проблема. Посмотрите следующий запрос:
select test.id, test.from, test.to, test.message, test.sent, test.read, test.direction from test
where (
(test.to = 244975 and test.direction <> 2 and test.direction <> 3 and
(
(test.from = 204177 and test.id > 5341203) OR
(test.from = 214518 and test.id > 5336549) OR
(test.from = 231429 and test.id > 5338284) OR
(test.from = 242739 and test.id > 5339541) OR
(test.from = 243834 and test.id > 5340438) OR
(test.from = 244354 and test.id > 5337489) OR
(test.from = 244644 and test.id > 5338572) OR
(test.from = 244690 and test.id > 5338467)
)
)
or
(test.from = 244975 and test.direction <> 1 and test.direction <> 3 and
(
(test.to = 204177 and test.id > 5341203) OR
(test.to = 214518 and test.id > 5336549) OR
(test.to = 231429 and test.id > 5338284) OR
(test.to = 242739 and test.id > 5339541) OR
(test.to = 243834 and test.id > 5340438) OR
(test.to = 244354 and test.id > 5337489) OR
(test.to = 244644 and test.id > 5338572) OR
(test.to = 244690 and test.id > 5338467)
)
)
or
(test.read <> 1 and test.direction <> 3 and test.direction <> 2 and test.to = 244975 and test.from not in (204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690)
)
or
(test.read <> 1 and test.direction = 2 and test.from = 244975 and test.to not in (204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690)
)
)
order by test.id;
Если я объясню этот запрос, он пройдет через все строки:
1 SIMPLE test index PRIMARY,one,two,three,four PRIMARY 4 1440596 Using where
Если я удалю оба оператора "не в", то он отлично работает:
select test.id, test.from, test.to, test.message, test.sent, test.read, test.direction from test
where (
(test.to = 244975 and test.direction <> 2 and test.direction <> 3 and
(
(test.from = 204177 and test.id > 5341203) OR
(test.from = 214518 and test.id > 5336549) OR
(test.from = 231429 and test.id > 5338284) OR
(test.from = 242739 and test.id > 5339541) OR
(test.from = 243834 and test.id > 5340438) OR
(test.from = 244354 and test.id > 5337489) OR
(test.from = 244644 and test.id > 5338572) OR
(test.from = 244690 and test.id > 5338467)
)
)
or
(test.from = 244975 and test.direction <> 1 and test.direction <> 3 and
(
(test.to = 204177 and test.id > 5341203) OR
(test.to = 214518 and test.id > 5336549) OR
(test.to = 231429 and test.id > 5338284) OR
(test.to = 242739 and test.id > 5339541) OR
(test.to = 243834 and test.id > 5340438) OR
(test.to = 244354 and test.id > 5337489) OR
(test.to = 244644 and test.id > 5338572) OR
(test.to = 244690 and test.id > 5338467)
)
)
or
(test.read <> 1 and test.direction <> 3 and test.direction <> 2 and test.to = 244975
)
or
(test.read <> 1 and test.direction = 2 and test.from = 244975
)
)
order by test.id;
Теперь запрос объяснения возвращает:
1 SIMPLE test index_merge PRIMARY,one,two,three,four one,two 5,5 30 Using sort_union(one,two); Using where; Using filesort
Я не уверен, почему это не работает. Что мне не хватает в индексах?
Ответы
Ответ 1
Я не уверен, почему это не работает. Что мне не хватает в индексах?
Я уверен, что планировщик запросов работает отлично, вы не пропускаете ничего в индексах, которые могли бы помочь в этом случае. Планировщик запросов решил, что быстрее использовать другой индекс, потому что два запроса очень разные.
Мы можем заставить оптимизатор использовать объединение индексов для нас, что сделает его значительно быстрее. Вы можете сохранить not in
и не изменять никаких операторов or
. Я выполнил некоторые базовые критерии метода, который я использовал против метода union. Предостережения применяются, потому что ваша конфигурация БД может значительно отличаться от моей. Выполняя запрос 1000 раз и делая это 3 раза, я взял лучшее время для каждого запроса...
Оптимизированный запрос, показанный ниже
real 0m15.410s
user 0m6.681s
sys 0m2.641s
Повторно написано как множество союзов
real 0m17.747s
user 0m6.798s
sys 0m2.812s
Подумайте, как оптимизатор и работайте с меньшими данными
Следующий SQL на несколько порядков быстрее в тестах на базе данных из 4 миллионов строк. Ключевым изменением является следующая строка
(select * from test where test.from_ in (244975, 204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690) or test.to_ in (244975, 204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690)) as test
Эта одна строка значительно уменьшает набор данных, над которым mysql должен работать, потому что мы используем in
вместо not in
. Это новый запрос, я пытался не слишком сильно изменять исходный запрос.
select SQL_NO_CACHE test.id, test.from_, test.to_, test.message, test.sent, test.read_, test.direction
from (select * from test where test.from_ in (244975, 204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690) or test.to_ in (244975, 204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690)) as test
where (
(test.to_ = 244975 and test.direction <> 2 and test.direction <> 3 and test.from_ in (204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690) and
(
(test.from_ = 204177 and test.id > 5341203) OR
(test.from_ = 214518 and test.id > 5336549) OR
(test.from_ = 231429 and test.id > 5338284) OR
(test.from_ = 242739 and test.id > 5339541) OR
(test.from_ = 243834 and test.id > 5340438) OR
(test.from_ = 244354 and test.id > 5337489) OR
(test.from_ = 244644 and test.id > 5338572) OR
(test.from_ = 244690 and test.id > 5338467)
)
)
or
(test.from_ = 244975 and test.direction <> 1 and test.direction <> 3 and test.to_ in (204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690) and
(
(test.to_ = 204177 and test.id > 5341203) OR
(test.to_ = 214518 and test.id > 5336549) OR
(test.to_ = 231429 and test.id > 5338284) OR
(test.to_ = 242739 and test.id > 5339541) OR
(test.to_ = 243834 and test.id > 5340438) OR
(test.to_ = 244354 and test.id > 5337489) OR
(test.to_ = 244644 and test.id > 5338572) OR
(test.to_ = 244690 and test.id > 5338467)
))
or
(test.read_ <> 1 and test.direction <> 2 and test.direction <> 3 and test.to_ = 244975 and test.from_ not in (204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690))
or
(test.read_ <> 1 and test.direction = 2 and test.from_ = 244975 and test.to_ not in (204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690))
)
order by test.id;
План объяснения для этого выглядит совсем по-другому...
mysql> \. sql_fixed.sql
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: <derived2>
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 226
filtered: 100.00
Extra: Using where; Using filesort
*************************** 2. row ***************************
id: 2
select_type: DERIVED
table: test
type: index_merge
possible_keys: one,two
key: two,one
key_len: 4,4
ref: NULL
rows: 226
filtered: 100.00
Extra: Using sort_union(two,one); Using where
2 rows in set, 1 warning (0.01 sec)
Оптимизатор, являющийся умным сразу, может видеть, что ему не нужна большая часть данных, потому что мы сказали ему использовать инструкцию in
с несколькими ключами. Большинство оптимизаторов запросов присоединяют высокую стоимость доступа к диску, поэтому все, что уменьшает это, как правило, предпочитает оптимизатор.
НЕ IN vs IN
not in
и in
очень разные. Разница между ними в этом случае - шаблон доступа, мне нужны данные временно или как часть набора результатов. Когда вы используете not in
с несколькими ключами, а индекс содержит миллионы ключей, может потребоваться прочитать много записей, если данные являются частью набора результатов. Даже при использовании индексов not in
можно считывать миллионы записей с диска... in
с помощью нескольких клавиш, и именно эти клавиши вам нужны, чтобы найти и использовать небольшое подмножество. Два шаблона доступа очень разные. Следующий пример может помочь сделать это понятным...
1. I don't want these 10 items from a 1,000,000 records I need the other 999,990, this reads the whole index.
2. I only want these 10 from a 1,000,000 records. This might only require one disk seek.
Число 2 быстрее, потому что из шаблона доступа, то есть я нашел 10, мне нужно, Nunmber 1. может потребоваться прочитать миллион записей.
Оптимизатор запросов MySQL видит это, т.е. последние два оператора OR запрашивают большие подмножества данных из таблицы или индекса, т.е. случая 1. выше. Увидев это и тот факт, что ему нужно было использовать первичный ключ, оптимизатор решил, что быстрее использовать первичный ключ.
Когда вы удаляете изменения not in
, т.е. теперь планировщик запросов может использовать индексы, потому что в двух других предложениях or
они действуют get me the few from the many
, и он выполняет index_merge на двух ключах, которые совместно используют to
и a from
, а также id
.
Чтобы увидеть, что я имею в виду, не удаляйте часть не в части запроса, измените ее на in
, чтобы узнать, что происходит, на моей машине план запроса изменился, чтобы использовать индекс диапазона.
Ответ 2
Если ваша версия mySQL меньше 5.0.7, проблема mysql может быть причиной
Посмотрите этот билет в отслеживании ошибок MySQL https://bugs.mysql.com/bug.php?id=10561
Ответ 3
Смешивание AND
и OR
часто вызывает странный план запросов в MySQL, по моему опыту. У меня недостаточно данных для проверки таблицы, но я бы попробовал переписать ваш запрос с помощью UNION ALL
. В конце концов, OR
в WHERE
в основном UNION
.
Идея состоит в том, чтобы разбить ее на более мелкие условия, поэтому MySQL может использовать разные индексы, оптимизированные для каждой части, а не для всех помех.
SELECT * FROM (
SELECT
test.id, test.from, test.to, test.message, test.sent, test.read, test.direction
FROM
test
WHERE
test.to = 244975
AND test.direction <> 2
AND test.direction <> 3
AND (
(test.from = 204177 AND test.id > 5341203) OR
(test.from = 214518 AND test.id > 5336549) OR
(test.from = 231429 AND test.id > 5338284) OR
(test.from = 242739 AND test.id > 5339541) OR
(test.from = 243834 AND test.id > 5340438) OR
(test.from = 244354 AND test.id > 5337489) OR
(test.from = 244644 AND test.id > 5338572) OR
(test.from = 244690 AND test.id > 5338467)
)
UNION ALL
SELECT
test.id, test.from, test.to, test.message, test.sent, test.read, test.direction
FROM
test
WHERE
test.from = 244975
AND test.direction <> 1
AND test.direction <> 3
AND (
(test.to = 204177 and test.id > 5341203) OR
(test.to = 214518 and test.id > 5336549) OR
(test.to = 231429 and test.id > 5338284) OR
(test.to = 242739 and test.id > 5339541) OR
(test.to = 243834 and test.id > 5340438) OR
(test.to = 244354 and test.id > 5337489) OR
(test.to = 244644 and test.id > 5338572) OR
(test.to = 244690 and test.id > 5338467)
)
UNION ALL
SELECT
test.id, test.from, test.to, test.message, test.sent, test.read, test.direction
FROM
test
WHERE
test.read <> 1
AND test.direction <> 3
AND test.direction <> 2
AND test.to = 244975
AND test.from NOT IN (204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690)
UNION ALL
SELECT
test.id, test.from, test.to, test.message, test.sent, test.read, test.direction
FROM
test
WHERE
test.read <> 1
AND test.direction = 2
AND test.from = 244975
AND test.to NOT IN (204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690)
) test ORDER BY test.id
Ответ 4
Было бы неплохо иметь дамп пробных данных для тестирования, но я все-таки создал некоторые свои собственные. Затем я разбил каждый из четырех внешних условий ИЛИ на подзапросы, СОЕДИНЯЕТ их и переместил порядок в конечный набор результатов.
У меня были проблемы с индексами при использовании сложных предложений WHERE, и для меня это похоже на то, что у вас есть приложение для чата/обмена сообщениями и вы пытаетесь получать сообщения от конкретного пользователя в одном запросе. Лично я бы разделил их на отдельные запросы, чтобы упростить код/запросы.
Вот мой запрос:
SELECT test.id, test.from, test.to, test.message, test.sent, test.read, test.direction
FROM (
SELECT *
FROM test
WHERE test.to = 244975
AND test.direction not in (2,3)
AND (
(test.from = 204177 AND test.id > 5341203)
OR (test.from = 214518 AND test.id > 5336549)
OR (test.from = 231429 AND test.id > 5338284)
OR (test.from = 242739 AND test.id > 5339541)
OR (test.from = 243834 AND test.id > 5340438)
OR (test.from = 244354 AND test.id > 5337489)
OR (test.from = 244644 AND test.id > 5338572)
OR (test.from = 244690 AND test.id > 5338467)
)
UNION
SELECT *
FROM test
WHERE test.from = 244975
AND test.direction not in (1,3)
AND (
(test.to = 204177 AND test.id > 5341203)
OR (test.to = 214518 AND test.id > 5336549)
OR (test.to = 231429 AND test.id > 5338284)
OR (test.to = 242739 AND test.id > 5339541)
OR (test.to = 243834 AND test.id > 5340438)
OR (test.to = 244354 AND test.id > 5337489)
OR (test.to = 244644 AND test.id > 5338572)
OR (test.to = 244690 AND test.id > 5338467)
)
UNION
SELECT *
FROM test
WHERE test.read != 1
AND test.direction not in (2,3)
AND test.to = 244975
AND test.from not in (204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690)
UNION
SELECT *
FROM test
WHERE test.read != 1
AND test.direction = 2
AND test.from = 244975
AND test.to not in (204177, 214518, 231429, 242739, 243834, 244354, 244644, 244690)
) test
ORDER BY test.id;
Ответ 5
Это, вероятно, связано с дополнительным уровнем вложенности/сложности, условие in
на дополнительном столбце добавляет к вашему предложению where.
сортировать объединение объединения индекса ваш второй запрос использует преобразование предложения where в набор диапазон условий в сочетании с OR
.
Каждое значение, которое вы сравниваете с помощью in
, считается другим предикатом диапазона, поэтому добавление двух условий in
с 8 значениями каждого в ваш первый запрос добавляет еще 64 предиката.
По мере того, как число предикатов увеличивается, в какой-то момент оптимизатор решает, что быстрее просто сканировать всю таблицу.
Ответ 6
Начните с этого
select a.id, a.from, a.to, a.message, a.sent,
a.read, a.direction
from ( ( SELECT * FROM test WHERE test.to = 244975 ) UNION DISTINCT
( SELECT * FROM test WHERE test.from = 244975 ) ) a
where ... -- but change `test` to `a`
Предполагая, что в подзапросе есть намного меньше строк, чем test
, это может работать быстрее.
Теперь, чтобы использовать "ленивую оценку", чтобы ускорить ее:
select a.id, a.from, a.to, a.message, a.sent,
a.read, a.direction
from ( ( SELECT id FROM test WHERE test.to = 244975 ) UNION DISTINCT
( SELECT id FROM test WHERE test.from = 244975 ) ) b -- Note `b`
JOIN test AS a USING(id) -- added
where ... -- but change `test` to `a`
Это может помочь, потому что он не тащит все столбцы.
Эта последняя версия нуждается только
PRIMARY KEY(id)
INDEX(from, id)
INDEX(to, id)