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)