GROUP BY с ORDER BY очень медленно работает с базой данных (кажется, создает временную таблицу)

Я создаю приложение для твиттера, которое отображает опубликованные ссылки в твиттере, но у меня возникла проблема при сортировке таблицы по времени.

tweet
+----------------------------------------+
| tweet_id | [...] | created_at          |
+----------------------------------------+  
| 123456   | [...] | 2012-06-11 11:31:28 |
| 234567   | [...] | 2012-06-11 11:32:55 |
| 345678   | [...] | 2012-06-11 11:33:22 |
+----------------------------------------+

tweets_url
+---------------------+
| tweet_id | url      |
+---------------------+
| 123456   | cnn.com  |
| 123456   | fox.com  |
| 234567   | abc.com  |
| 345678   | abc.com  |
+---------------------+

Вот мой SQL (я использую GROUP для возврата только уникальных URL-адресов):

SELECT tweet_urls.url,
    FROM  `tweets` 
    LEFT JOIN tweet_urls ON tweet_urls.tweet_id = tweets.tweet_id 
    WHERE tweet_urls.url LIKE '%cnn.com%'
    GROUP BY tweet_urls.url 
    ORDER BY tweets.created_at DESC LIMIT 0 , 20

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

Изменить: я провел дополнительное тестирование. Кажется, что Mysql создает временную таблицу на основе GROUP BY tweet_urls.url, а затем упорядочивает результаты без использования указанного индекса, потому что он запускается на временной таблице.

Вот вывод EXPLAIN:

+----+-------------+------------+--------+---------------+---------+---------+-----+----------------------+----------------------------------------------+
| id | select_type | table      | type   | possible_keys | key     | key_len | ref |                rows  | Extra                                        |
+---------------------------------------------------------------------------------------------------------+----------------------------------------------+
| 1  | SIMPLE      | tweet_urls | index  | tweet_id      | url     | 422     | NULL                 86783 | Using where; Using temporary; Using filesort 
| 1  | SIMPLE      | tweets     | eq_ref | PRIMARY       | PRIMARY | 8       | tweet_urls.tweet_id        |
+----+-------------+------------+--------+---------------+---------+---------+-----+----------------------+----------------------------------------------+

person Joey    schedule 18.06.2012    source источник
comment
Можно ли не начинать выражение LIKE с подстановочного знака? В противном случае потребуется сканирование таблицы.   -  person Martin Wilson    schedule 18.06.2012
comment
Я поставлю индекс как для tweet_id, так и для полнотекстового индекса для tweet_urls.url, и если вам не нужен порядок, удалите его.   -  person jcho360    schedule 18.06.2012
comment
Вы знаете, что LIKE '%cnn.com%' также будет соответствовать abcnn.comp.co.uk? Это действительно то, чего вы хотите?   -  person Mark Byers    schedule 18.06.2012
comment
Вы уверены, что именно ORDER BY вносит наибольший вклад в низкую производительность? Какова производительность по сравнению с запросом с опущенным предложением ORDER BY? (Для тестирования вы хотите убедиться, что кеш запросов не отбрасывает ваши результаты: SET SESSION query_cache_type = OFF;   -  person spencer7593    schedule 19.06.2012
comment
Да, я уверен. Время запроса составляет 0,0021 с против 0,4639. Тесты выполняются на сервере разработки с небольшим набором данных, на рабочем сервере время запроса составляет 0,01 с против 2,5 с.   -  person Joey    schedule 19.06.2012


Ответы (3)


Я думаю, что настоящая проблема здесь:

WHERE tweet_urls.url LIKE '%cnn.com%'

Этот тип запроса (LIKE без постоянного префикса) не может эффективно использовать индекс.

Вы можете решить эту проблему, добавив в таблицу дополнительный столбец с именем domain и проиндексировав его. Затем вы можете изменить свой запрос на:

WHERE tweet_urls.domain = 'cnn.com'
person Mark Byers    schedule 18.06.2012
comment
что изменится, если добавить еще один столбец, а не просто индексировать столбец URL? - person jcho360; 18.06.2012
comment
Спасибо за ответ, но это не решило проблему длительного времени запроса, хотя и дает небольшой прирост производительности. - person Joey; 19.06.2012
comment
@Джоуи: Странно. Можете ли вы опубликовать ОБЪЯСНЕНИЕ? - person Mark Byers; 19.06.2012
comment
Формат @MarkByers Hope не взломан: id select_type тип таблицы возможных_ключей ключ key_len ref строк Extra 1 SIMPLE tweet_urls index tweet_id url 422 NULL 86783 Использование where; Использование временного; Используя filesort 1 ПРОСТЫЕ твиты eq_ref PRIMARY PRIMARY 8 tweet_urls.tweet_id 1 Используя where edit: взломан. Извините, я здесь новенький. Форматирование ответов кажется хлопотным;) - person Joey; 19.06.2012
comment
@MarkByers внес некоторые изменения в начальный пост, чтобы правильно отображать EXPLAIN. - person Joey; 19.06.2012
comment
@joey: я заметил, что он использует неправильный индекс. Он должен использовать индекс в домене. Я предполагаю, что ваш запрос неверен. Можешь и это опубликовать? - person Mark Byers; 19.06.2012
comment
@MarkByers Я изменил строки URL, чтобы они содержали только доменное имя, как вы предложили. Я в основном обрезал все до и после домена, чтобы убедиться, что индекс можно использовать. Запрос: ВЫБЕРИТЕ tweet_urls.url, ОТ tweets ОСТАВШЕГО СОЕДИНЕНИЯ tweet_urls ON tweet_urls.tweet_id = tweets.tweet_id ГДЕ tweet_urls.url = 'cnn.com' СГРУППИРОВАТЬ ПО tweet_urls.url ПОРЯДОК ПО tweets.created_at DESC LIMIT 0, 20 - person Joey; 19.06.2012
comment
@Joey: я более внимательно изучил ваш запрос, и кажется, что он не совсем четко определен. У вас есть GROUP BY tweet_urls.url, но затем вы используете ORDER BY tweets.created_at. Если вы используете столбец в ORDER BY, если он не должен либо содержаться в агрегатной функции, либо функционально зависеть от столбцов в GROUP BY, что, я думаю, здесь не так. Я предлагаю вам сначала попытаться сделать ваш запрос правильным, прежде чем пытаться его оптимизировать. - person Mark Byers; 20.06.2012

Поместите индекс в столбец tweets.created_at

person John Conde    schedule 18.06.2012
comment
извините, забыл упомянуть: индексируется. - person Joey; 18.06.2012
comment
Индексируется ли файл tweet_url.url? Вы выполняете поиск по подстановочным знакам в столбце varchar, что может быть довольно утомительно, если совпадений много. Вы сохраняете только домен, с которого пришел твит? Возможно, стоит сделать ключ к этому, если вы видите много повторений. - person Mike; 18.06.2012
comment
@Mike: Это может быть еще больше обременительно, если не так много совпадений. Если у вас менее 20 совпадений, необходимо проверить каждую строку в таблице. - person Mark Byers; 18.06.2012
comment
Как только я это написал, я понял, что имел в виду не матчи, а много записей. Я проголосовал за ваш ответ ниже, так как мы опубликовали в одно и то же время и имели одну и ту же идею. Спасибо за исправление этой ошибки - я оставлю это как есть, чтобы показать ошибку моего пути. - person Mike; 18.06.2012
comment
Спасибо за ответ. Даже если я полностью заменю GROUP BY, это не повлияет на производительность запроса. На самом деле это становится еще медленнее, потому что есть больше результатов для ORDER BY;) - person Joey; 19.06.2012

Прежде чем настраивать оператор, убедитесь, что оператор гарантированно возвращает правильный набор результатов, который вы ожидаете. (увидеть ниже)

Что касается производительности, то предикат LIKE '%foo' (с начальным подстановочным знаком) не подлежит анализу. (То есть механизм запросов не может использовать индекс для ограничения количества проверяемых строк. Механизм запросов должен будет проверять КАЖДУЮ строку в таблице.

Я подозреваю, что это, наряду с операцией JOIN, может быть основным фактором низкой производительности. (Я не вижу, чтобы в вашем запросе требовалось ВНЕШНЕЕ соединение, оно похоже на ВНУТРЕННЕЕ соединение, учитывая предикат в tweet_urls.url.

В идеале вам не нужен подстановочный знак % в начале, и вместо этого вы можете проверить url LIKE 'cnn.com%' без начального подстановочного знака, что может заставить механизм запросов использовать индекс (по столбцу url).

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

Итак, я обращаюсь только к вашему запросу и не предлагаю никаких изменений схемы. (ЕСЛИ бы я столкнулся с таким требованием, как ваше, я бы подумал о внесении изменений в таблицы и индексы, возможно, даже в полнотекстовый индекс.)

Но вы спрашивали об изменении запроса, поэтому я собираюсь только обратиться к запросу.


Похоже, вы хотите вернуть разные URL-адреса, упорядоченные по created_at самого ПОСЛЕДНЕГО твита, а не только по created_at одного (не обязательно самого последнего) твита.

Если это так, я не верю, что ваш запрос возвращает строки в том порядке, в котором вы ожидаете, учитывая, что вы ссылаетесь на неагрегатное выражение в предложении ORDER BY, которое не включено в предложение GROUP BY.

ПРИМЕЧАНИЕ. Другие реляционные базы данных будут генерировать исключение с таким оператором, например. Oracle выбросит ORA-00979: not a GROUP BY expression, а SQL Server выбросит Msg 8127 Column "tweets.created_at" is invalid in the ORDER BY clause because it is not contained in either an aggregate function or the GROUP BY clause. MySQL более либерален, что не всегда хорошо.

Если ваш запрос возвращает строки в том порядке, в котором вы ожидаете, это происходит по счастливой случайности, а не из-за какого-то гарантированного поведения.

Чтобы получить строки, упорядоченные по created_at самого ПОСЛЕДНЕГО твита, вам понадобится запрос другой формы, который указывает, что вы хотите сортировать на основе максимального created_at. В качестве примеров:

SELECT tweet_urls.url
  FROM tweet_urls
 WHERE tweet_urls.url LIKE '%cnn.com%'
 GROUP BY tweet_urls.url
 ORDER BY MAX((SELECT MAX(tweets.created_at) FROM tweets WHERE tweets.tweet_id = tweet_urls.tweet_id)) DESC LIMIT 0, 20

-- or

SELECT t.url
  FROM ( SELECT tweet_urls.url, MAX(tweets.created_at) AS max_created_at
           FROM tweets 
           JOIN tweet_urls ON tweet_urls.tweet_id = tweets.tweet_id 
          WHERE tweet_urls.url LIKE '%cnn.com%'
          GROUP BY tweet_urls.url
       ) t
 ORDER BY t.max_created_at DESC LIMIT 0, 20

Настройте тестовый пример, который вы показали:

CREATE TABLE tweets (tweet_id INT UNSIGNED NOT NULL, created_at DATETIME) ENGINE=MyISAM; 
CREATE TABLE tweet_urls (tweet_id INT UNSIGNED NOT NULL, url VARCHAR(20) NOT NULL) ENGINE=MyISAM;
INSERT INTO tweets VALUES (123456, '2012-06-11 11:31:28'),(234567,'2012-06-11 11:32:55'),(345678,'2012-06-11 11:33:22');
INSERT INTO tweet_urls VALUES (123456,'cnn.com'),(123456,'fox.com'),(234567,'abc.com'),(345678,'abc.com');

И добавьте еще пару строк:

INSERT INTO tweets VALUES (1, '2012-06-10'),(2,'2012-06-12'); 
INSERT INTO tweet_urls VALUES (1,'Xcnn.com'),(2,'Xcnn.com');

Когда я запускаю ваш запрос, он возвращает строки в порядке НЕКОТОРЫХ created_at, но не обязательно created_at САМОГО ПОСЛЕДНЕГО твита.

person spencer7593    schedule 18.06.2012
comment
Спасибо за этот отличный ответ, он мне очень помог, хотя ваши запросы выполняются в 5 раз медленнее, чем мои, на сервере разработки с уменьшенным набором данных. Выбор последнего твита — это следующая проблема, с которой я сталкиваюсь, и я думаю, что вся проблема заключается в предложении GROUP BY. GROUP BY возвращает несортированные данные, которые должны быть отсортированы с помощью ORDER BY. Я относительно новичок в разработке баз данных, поэтому не знаю, как этого избежать. - person Joey; 19.06.2012