Mysql: внутреннее соединение по первичному ключу для 2 идентификаторов дает проверенный диапазон для каждой записи

Я получаю «Диапазон проверен для каждой записи (индексная карта: 0x1)» в EXPLAIN SELECT при выполнении ВНУТРЕННЕГО СОЕДИНЕНИЯ для ПЕРВИЧНОГО ключа с двумя значениями (с использованием конструкций IN или OR)

Вот запрос:

SELECT *
FROM message AS m
INNER JOIN user AS u
ON u.id = m.sender_id OR u.id = m.receiver_id

Когда я делаю объяснение, это дает мне:

+----+-------------+-------+------+---------------+------+---------+------+-------+-----------------------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows  | Extra                                         |
+----+-------------+-------+------+---------------+------+---------+------+-------+-----------------------------------------------+
|  1 | SIMPLE      | u     | ALL  | PRIMARY       | null | null    | null | 75000 | Range checked for each record (index map: 0x1)|
+----+-------------+-------+------+---------------+------+---------+------+-------+-----------------------------------------------+

Не может быть...

Если я попробую это, я получу тот же результат:

SELECT *
FROM message AS m
INNER JOIN user AS u
ON u.id IN(m.sender_id, m.receiver_id)

Но если я сделаю это, все будет работать нормально, и я проанализирую только 1 строку:

SELECT *
FROM message AS m
INNER JOIN user AS u
ON u.id = m.sender_id

Как это возможно? Я присоединяюсь к первичному ключу со значениями того же типа. (фактический запрос «немного» сложнее, но ничего особенного, 2 внутренних соединения и, наконец, одно левое соединение)

Должно быть 2 строки, точка.

Спасибо за любой вклад в это (провел некоторое исследование, но не нашел ничего ценного, кроме «пожалуйста, добавьте индекс», который, очевидно, здесь не применим)

РЕДАКТИРОВАТЬ: да, я попробовал оператор USE INDEX, но все равно не повезло

РЕДАКТИРОВАТЬ: Вот очень простая схема для воспроизведения этого странного поведения MySQL:

CREATE TABLE test_user (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(30),
    PRIMARY KEY (id)
);

CREATE TABLE test_message (
    id INT NOT NULL AUTO_INCREMENT, 
    sender_id INT NOT NULL,
    receiver_id INT NOT NULL,
    PRIMARY KEY (id),
    INDEX idx_sender (sender_id),
    INDEX idx_receiver (receiver_id)
);

EXPLAIN SELECT *
FROM test_message AS m
INNER JOIN test_user AS u
    ON u.id = m.sender_id OR u.id = m.receiver_id;

person Lideln Kyoku    schedule 03.11.2014    source источник
comment
Является ли внутреннее соединение правильным методом соединения? Вы также не должны использовать FULL?   -  person hansmei    schedule 03.11.2014
comment
Хмммм, я вижу, у вас есть только первичные индексы, настроившие идентификационные номера. Попробуйте добавить индексы к sender_id и Receiver_id, это может помочь в сопряжении.   -  person David162795    schedule 04.11.2014
comment
Хорошо, я сделал, это не помогает с конструкцией IN, но работает с конструкцией OR! Я думал, что индексы должны быть установлены только для тех столбцов, в которых выполняется поиск (user.id), а не для тех (уже выбранных), откуда берутся тестируемые значения (task.id_user). Не могли бы вы опубликовать фактический ответ (который мне будет разрешено принять) и объяснить мне (и другим), почему индекс также должен быть установлен в справочном столбце, а не только в искомом? Спасибо, Дэвид!   -  person Lideln Kyoku    schedule 04.11.2014
comment
Подождите, я говорил слишком быстро. Это все еще не работает. Он исправляет только один единственный вариант использования, где я это делаю: u.id = t.id_user OR u.id = t.id_user (что бессмысленно, но все еще вызывает проблему и делает запрос еще проще). Мне жаль, что это не работает. Любая другая идея? Я уверен, что я не единственный, кто пытается сравнить столбец с двумя столбцами, используя ИЛИ. Я удивлен, что не столкнулся с этим раньше.   -  person Lideln Kyoku    schedule 04.11.2014
comment
@hansmei, MySQL не поддерживает FULL OUTER JOIN. А в SQL нет такого понятия, как FULL INNER JOIN.   -  person Bill Karwin    schedule 03.11.2017
comment
Пожалуйста, не уточняйте в комментариях; отредактируйте свой вопрос. Также, пожалуйста, не продолжайте добавлять EDIT. Просто отредактируйте свой вопрос, чтобы он стал лучшей автономной современной версией. Прошлые версии доступны по отредактированной ссылке.   -  person philipxy    schedule 04.11.2017
comment
Привет. Что значит, работает нормально? Или не работает. Кроме того, вы на самом деле не говорите, что вы ожидаете. Хотя вам может показаться, что это очевидно. Пожалуйста, прочтите минимально воспроизводимый пример и действуйте. Здесь у вас нет примера ввода, вывода и желаемого вывода. И что означает, что я получаю только 1 проанализированную строку? И приведите полный пример (код и данные), демонстрирующий вашу проблему. И в вашей спецификации, какие другие объявляемые уникальные и наборы столбцов FKs и ненулевые столбцы? PS Что имеется в виду под справочными и искомыми столбцами?   -  person philipxy    schedule 04.11.2017
comment
@LidelnKyoku - Что-то не так с первым запросом и его EXPLAIN -- В запросе упоминаются две таблицы; EXPLAIN упоминает только один. Пожалуйста исправьте.   -  person Rick James    schedule 06.11.2017


Ответы (2)


Как правило, MySQL может использовать только один индекс для каждой ссылки на таблицу в запросе (существует ссылка index-merge, но это работает не так часто, как вы думаете).

В вашем условии соединения есть OR между двумя сравнениями с индексированными столбцами, и оптимизатор не может выбрать, какое из них лучше использовать, прежде чем данные в таблице будут проверены построчно.

Распространенным обходным решением является выполнение UNION между более простыми запросами вместо условия OR.

mysql> EXPLAIN 
    SELECT * FROM test_message AS m 
    INNER JOIN test_user AS u ON u.id = m.sender_id 
  UNION
    SELECT * FROM test_message AS m 
    INNER JOIN test_user AS u ON u.id = m.receiver_id;

+----+--------------+------------+--------+---------------+---------+---------+--------------------+------+-----------------+
| id | select_type  | table      | type   | possible_keys | key     | key_len | ref                | rows | Extra           |
+----+--------------+------------+--------+---------------+---------+---------+--------------------+------+-----------------+
|  1 | PRIMARY      | m          | ALL    | idx_sender    | NULL    | NULL    | NULL               |    1 | NULL            |
|  1 | PRIMARY      | u          | eq_ref | PRIMARY       | PRIMARY | 4       | test.m.sender_id   |    1 | NULL            |
|  2 | UNION        | m          | ALL    | idx_receiver  | NULL    | NULL    | NULL               |    1 | NULL            |
|  2 | UNION        | u          | eq_ref | PRIMARY       | PRIMARY | 4       | test.m.receiver_id |    1 | NULL            |
| NULL | UNION RESULT | <union1,2> | ALL    | NULL          | NULL    | NULL    | NULL               | NULL | Using temporary |
+----+--------------+------------+--------+---------------+---------+---------+--------------------+------+-----------------+

При этом используются правильные поиски по индексу в обоих подзапросах, но для последующего завершения UNION необходимо использовать временную таблицу. В конечном счете, это может быть промывка для производительности. Зависит от того, сколько строк данных необходимо проверить и сколько строк будет получено в результате.

person Bill Karwin    schedule 02.11.2017
comment
Я не могу проверить, но считаю, что использование UNION ALL поможет избежать временной таблицы (потенциально это может привести к дублированию, но я полагаю, что это довольно редкий случай, чтобы отправить сообщение самому себе) - person skyboyer; 08.11.2017
comment
@skyboyer: MySQL 5.7.3 и более поздние версии могут удалять временную таблицу при использовании UNION ALL. См. специальные примечания на странице dev.mysql. com/doc/relnotes/mysql/5.7/en/news-5-7-3.html. Более ранние версии MySQL не имеют этой оптимизации. - person Bill Karwin; 08.11.2017

Проблема хорошо известна и в других (думаю, во всех) СУБД, оптимизатор будет использовать только одно правило для каждого соединения.

Если условие соединения сложное или если он не может распознать известный шаблон для его решения, оптимизация применяться не будет, и будет выполнено полное сканирование таблицы.

В вашем случае условие OR в основном соединении кажется простым, но это не так, потому что вы просите проверить каждый идентификатор пользователя по двум разным столбцам (не постоянным значениям) за раз.

Чтобы решить эту проблему, вы должны разделить условие соединения на несколько подзапросов, чтобы оптимизатор мог использовать лучшее правило для каждого из них.

@Bill Karwin предложил общее решение, и оно хорошо помогает понять проблему.

(Немного) лучший способ решить эту проблему — переместить объединение на один уровень вверх и выполнить соединение с производной таблицей:

EXPLAIN 
SELECT *
FROM test_user AS u
INNER JOIN (
    select id, sender_id as msg_id
    from test_message 
    union all 
    select id, receiver_id 
    from test_message 
    ) AS m 
ON u.id = m.msg_id;

Он не будет использовать TEMPORARY таблиц и выполняет только одно полное сканирование таблицы на test_users вместо двух.

id  select_type table           partitions  type    possible_keys   key             key_len ref         rows    filtered    Extra
1   PRIMARY     u               NULL        ALL     PRIMARY         NULL            NULL    NULL        1       100.00      NULL
1   PRIMARY     <derived2>      NULL        ref     <auto_key0>     <auto_key0>     4       test.u.id   2       100.00      NULL
2   DERIVED     test_message    NULL        index   NULL            idx_sender      4       NULL        1       100.00      "Using index"
3   UNION       test_message    NULL        index   NULL            idx_receiver    4       NULL        1       100.00      "Using index" 
person MtwStark    schedule 03.11.2017
comment
Повторите попытку EXPLAIN после того, как вы добавите в таблицы несколько десятков строк. Вы можете обнаружить, что он не начинается с полного сканирования таблицы u. Но в <derived2> все еще есть таблица tmp. Кроме того, есть полное сканирование таблицы (хорошо, только 2 строки) для построения индекса (<auto_key0>). - person Rick James; 04.11.2017
comment
@RickJames Я провел тесты с 6 пользователями и 50 сообщениями, и план выполнения не изменился. Этот запрос похож на запрос @BillKarwin, но он выполняет только 1 полное сканирование таблицы u и только 1 вложенный цикл (JOIN) вместо 2 и 2. Из MySQL 5.7 построение объединенного индекса <auto_key0> для производной таблицы должно иметь хорошие характеристики. Я имею в виду, что присоединение к союзу должно быть лучше, чем объединение двух объединений. - person MtwStark; 06.11.2017
comment
Вау! Вопрос хотел, чтобы все столбцы из test_message и test_user; запрос здесь предоставляет только столбцы из test_user. Итак, я полагаю, что это неверный ответ. - person Rick James; 06.11.2017
comment
вопрос заключался в том, как это возможно для проверки диапазона для каждой записи в этом сценарии? и мой ответ дает описание проблемы и альтернативное решение, чтобы ее избежать. Если вам не нравится, не волнуйтесь.. Я выживу.. - person MtwStark; 06.11.2017
comment
«Диапазон проверен для каждой записи» настолько загадочен, что я не знаю, что это значит. - person Rick James; 06.11.2017
comment
?!?!? Я думал, что вы эксперт по MySQL... и что вы читали вопрос (по крайней мере, заголовок) - person MtwStark; 06.11.2017