Подзапрос, который должен быть независимым, не является. Зачем?

У меня есть стол files с файлами и таблицей reades с доступом для чтения к этим файлам. В таблице reades есть колонка file_id где относится к соответствующему столбцу в files,

Теперь я хотел бы перечислить все файлы, к которым не обращались и пробовал это:

SELECT * FROM files WHERE file_id NOT IN (SELECT file_id FROM reades)

Это ужасно медленно. Причина в том, что mySQL считает, что подзапрос зависит от запроса:

+----+--------------------+--------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type        | table  | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+--------------------+--------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | PRIMARY            | files  | ALL  | NULL          | NULL | NULL    | NULL | 1053 |   100.00 | Using where |
|  2 | DEPENDENT SUBQUERY | reades | ALL  | NULL          | NULL | NULL    | NULL | 3242 |   100.00 | Using where |
+----+--------------------+--------+------+---------------+------+---------+------+------+----------+-------------+

Но почему? Подзапрос является полностью независимым и более или менее просто предназначен для возврата списка идентификаторов.

(Чтобы быть точным: каждый file_id может появляться несколько раз в readesКонечно, поскольку для каждого файла может быть сколь угодно много операций чтения.)

6 ответов

Решение

Попробуйте заменить подзапрос соединением:

SELECT * 
FROM files f
LEFT OUTER JOIN reades r on r.file_id = f.file_id
WHERE r.file_id IS NULL

Вот ссылка на статью об этой проблеме. Автор этой статьи написал хранимую процедуру, чтобы заставить MySQL оценивать подзапросы как независимые. Я сомневаюсь, что это необходимо в этом случае, хотя.

Я видел это раньше. это ошибка в MySQL. попробуй это:

SELECT * FROM files WHERE file_id NOT IN (SELECT * FROM (SELECT file_id FROM reades))

там отчет об ошибке есть здесь: http://bugs.mysql.com/bug.php?id=25926

Пытаться:

SELECT * FROM files WHERE file_id NOT IN (SELECT reades.file_id FROM reades)

То есть: если это становится зависимым, возможно, это из-за двусмысленности в том, что file_id относится к, так что давайте попробуем полностью квалифицировать его.

Если это не сработает, просто сделайте:

SELECT files.*
FROM files
LEFT JOIN reades
USING (file_id)
WHERE reades.file_id IS NULL

MySQL поддерживает EXISTS так же, как MSSQL? Если это так, вы можете переписать запрос как

ВЫБРАТЬ * ИЗ файлов как f ГДЕ file_id НЕ СУЩЕСТВУЕТ (ВЫБЕРИТЕ 1 ИЗ ПРОЧИТАЕТ r ГДЕ r.file_id = f.file_id)

Использование IN ужасно неэффективно, поскольку он выполняет этот подзапрос для каждой строки в родительском запросе.

Глядя на эту страницу, я нашел два возможных решения, которые оба работают. Просто для полноты я добавляю один из них, аналогично ответам с JOIN, показанными выше, но это быстро даже без использования внешних ключей:

  SELECT * FROM files AS f 
    INNER JOIN (SELECT DISTINCT file_id FROM reades) AS r 
    ON f.file_id = r.file_id

Это решает проблему, но все же это не отвечает на мой вопрос:)

РЕДАКТИРОВАТЬ: Если я правильно интерпретировать вывод EXPLAIN, это быстро, потому что интерпретатор генерирует временный индекс:

+----+-------------+------------+--------+---------------+---------+---------+-----------+------+--------------------------+
| id | select_type | table      | type   | possible_keys | key     | key_len | ref       | rows | Extra                    |
+----+-------------+------------+--------+---------------+---------+---------+-----------+------+--------------------------+
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL    | NULL    | NULL      |  843 |                          |
|  1 | PRIMARY     | f          | eq_ref | PRIMARY       | PRIMARY | 4       | r.file_id |    1 |                          |
|  2 | DERIVED     | reades     | range  | NULL          | file_id | 5       | NULL      |  811 | Using index for group-by |
+----+-------------+------------+--------+---------------+---------+---------+-----------+------+--------------------------+

IN-подзапросы в MySQL 5.5 и более ранних преобразованы в подзапросы EXIST. Данный запрос будет преобразован в следующий запрос:

SELECT * FROM файлов, ГДЕ НЕ СУЩЕСТВУЕТ (ВЫБЕРИТЕ 1 ИЗ ПРОЧИТАНИЙ, ГДЕ reades.filed_id = files.file_id)

Как видите, подзапрос на самом деле зависим.

MySQL 5.6 может выбрать материализацию подзапроса. То есть сначала запустите внутренний запрос и сохраните результат во временной таблице (удалив дубликаты). Затем он может использовать операцию, подобную соединению между внешней таблицей (то есть файлами) и временной таблицей, чтобы найти строки без совпадения. Этот способ выполнения запроса, вероятно, будет более оптимальным, если reades.file_id не проиндексирован.

Однако, если reades.file_id проиндексирован, традиционная стратегия выполнения IN-to-EXISTS на самом деле довольно эффективна. В этом случае я не ожидаю какого-либо существенного улучшения производительности от преобразования запроса в объединение, как это предлагается в других ответах. Оптимизатор MySQL 5.6 делает выбор на основе стоимости между материализацией и выполнением IN-to-EXISTS.

Другие вопросы по тегам