Как оптимизировать план выполнения запроса с несколькими внешними соединениями с огромными таблицами, предложениями group by и order by?
у меня есть следующая база данных (упрощенная):
CREATE TABLE `tracking` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`manufacture` varchar(100) NOT NULL,
`date_last_activity` datetime NOT NULL,
`date_created` datetime NOT NULL,
`date_updated` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `manufacture` (`manufacture`),
KEY `manufacture_date_last_activity` (`manufacture`, `date_last_activity`),
KEY `date_last_activity` (`date_last_activity`),
) ENGINE=InnoDB AUTO_INCREMENT=401353 DEFAULT CHARSET=utf8
CREATE TABLE `tracking_items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`tracking_id` int(11) NOT NULL,
`tracking_object_id` varchar(100) NOT NULL,
`tracking_type` int(11) NOT NULL COMMENT 'Its used to specify the type of each item, e.g. car, bike, etc',
`date_created` datetime NOT NULL,
`date_updated` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `tracking_id` (`tracking_id`),
KEY `tracking_object_id` (`tracking_object_id`),
KEY `tracking_id_tracking_object_id` (`tracking_id`,`tracking_object_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1299995 DEFAULT CHARSET=utf8
CREATE TABLE `cars` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`car_id` varchar(255) NOT NULL COMMENT 'It must be VARCHAR, because the data is coming from external source.',
`manufacture` varchar(255) NOT NULL,
`car_text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`date_order` datetime NOT NULL,
`date_created` datetime NOT NULL,
`date_updated` datetime NOT NULL,
`deleted` tinyint(4) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `car_id` (`car_id`),
KEY `sort_field` (`date_order`)
) ENGINE=InnoDB AUTO_INCREMENT=150000025 DEFAULT CHARSET=utf8
это мой "проблемный" запрос, который работает очень медленно.
SELECT sql_no_cache `t`.*,
count(`t`.`id`) AS `cnt_filtered_items`
FROM `tracking` AS `t`
INNER JOIN `tracking_items` AS `ti` ON (`ti`.`tracking_id` = `t`.`id`)
LEFT JOIN `cars` AS `c` ON (`c`.`car_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 1)
LEFT JOIN `bikes` AS `b` ON (`b`.`bike_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 2)
LEFT JOIN `trucks` AS `tr` ON (`tr`.`truck_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 3)
WHERE (`t`.`manufacture` IN('1256703406078',
'9600048390403',
'1533405067830'))
AND (`c`.`car_text` LIKE '%europe%'
OR `b`.`bike_text` LIKE '%europe%'
OR `tr`.`truck_text` LIKE '%europe%')
GROUP BY `t`.`id`
ORDER BY `t`.`date_last_activity` ASC,
`t`.`id` ASC
LIMIT 15
это результат EXPLAIN
на вышеуказанный запрос:
+----+-------------+-------+--------+-----------------------------------------------------------------------+-------------+---------+-----------------------------+---------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
+----+-------------+-------+--------+-----------------------------------------------------------------------+-------------+---------+-----------------------------+---------+----------------------------------------------+
| 1 | SIMPLE | t | index | PRIMARY,manufacture,manufacture_date_last_activity,date_last_activity | PRIMARY | 4 | NULL | 400,000 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | ti | ref | tracking_id,tracking_object_id,tracking_id_tracking_object_id | tracking_id | 4 | table.t.id | 1 | NULL |
| 1 | SIMPLE | c | eq_ref | car_id | car_id | 767 | table.ti.tracking_object_id | 1 | Using where |
| 1 | SIMPLE | b | eq_ref | bike_id | bike_id | 767 | table.ti.tracking_object_id | 1 | Using where |
| 1 | SIMPLE | t | eq_ref | truck_id | truck_id | 767 | table.ti.tracking_object_id | 1 | Using where |
+----+-------------+-------+--------+-----------------------------------------------------------------------+-------------+---------+-----------------------------+---------+----------------------------------------------+
в чем проблема, которую пытается решить этот запрос?
в принципе, мне нужно найти все записи в tracking
таблица, которая может быть связана с записями в tracking_items
(1:n), где каждая запись в tracking_items
может быть связано с записью в левые присоединенные таблицы. Критерий фильтрации является важной частью запроса.
в чем проблема, которую я имею с запросом выше?
когда есть order by
и group by
предложения запрос выполняется очень медленно, например 10-15 секунд для завершения вышеуказанной конфигурации. Однако, если я опущу любое из этих предложений, запрос выполняется довольно быстро (~0,2 секунды).
что я уже пытался?
- я пытался использовать
FULLTEXT
индекс, но это не очень помогло, так как результаты оцениваютсяLIKE
statemenet суженыJOINs
использование индексов. - я пытался использовать
WHERE EXISTS (...)
чтобы найти, есть ли записи вleft
присоединился к столам, но, к сожалению, без каких-либо успехов.
несколько заметок об отношениях между этими таблицами:
tracking -> tracking_items (1:n)
tracking_items -> cars (1:1)
tracking_items -> bikes (1:1)
tracking_items -> trucks (1:1)
Итак, я ищу способ оптимизировать этот запрос.
8 ответов
Билл Карвин предполагает, что запрос может работать лучше, если он использует индекс с ведущим столбцом manufacture
. Я поддерживаю это предложение. Особенно если это очень избирательно.
я также отмечаю, что мы делаем GROUP BY t.id
, где id
является первичным ключом таблицы.
нет столбцов из любых таблиц, кроме tracking
ссылка в SELECT
список.
это говорит о том, что мы действительно заинтересованы только в возврате строк из t
, а не при создании дубликатов из-за нескольких внешних соединений.
кажется COUNT()
aggregate имеет потенциал для возврата завышенного количества, если в tracking_item
и bikes
,cars
,trucks
. Если есть три совпадающих ряда от автомобилей и четыре совпадающих ряда от велосипедов, ... агрегат COUNT () возвращает значение 12, а не 7. (Или, может быть, в данных есть какая-то гарантия, что никогда не будет множественного соответствия строки.)
если manufacture
очень избирательно, и это возвращает достаточно небольшой набор строк из tracking
, если запрос может использовать индекс ...
и поскольку мы не возвращаем никаких столбцов из любых таблиц, кроме tracking
, кроме количества или связанных элементов ...
у меня возникнет соблазн проверить коррелированные подзапросы в списке выбора, чтобы получить счетчик и отфильтровать строки с нулевым счетом с помощью предложения HAVING.
что-то вот так:
SELECT SQL_NO_CACHE `t`.*
, ( ( SELECT COUNT(1)
FROM `tracking_items` `tic`
JOIN `cars` `c`
ON `c`.`car_id` = `tic`.`tracking_object_id`
AND `c`.`car_text` LIKE '%europe%'
WHERE `tic`.`tracking_id` = `t`.`id`
AND `tic`.`tracking_type` = 1
)
+ ( SELECT COUNT(1)
FROM `tracking_items` `tib`
JOIN `bikes` `b`
ON `b`.`bike_id` = `tib`.`tracking_object_id`
AND `b`.`bike_text` LIKE '%europe%'
WHERE `tib`.`tracking_id` = `t`.`id`
AND `tib`.`tracking_type` = 2
)
+ ( SELECT COUNT(1)
FROM `tracking_items` `tit`
JOIN `trucks` `tr`
ON `tr`.`truck_id` = `tit`.`tracking_object_id`
AND `tr`.`truck_text` LIKE '%europe%'
WHERE `tit`.`tracking_id` = `t`.`id`
AND `tit`.`tracking_type` = 3
)
) AS cnt_filtered_items
FROM `tracking` `t`
WHERE `t`.`manufacture` IN ('1256703406078', '9600048390403', '1533405067830')
HAVING cnt_filtered_items > 0
ORDER
BY `t`.`date_last_activity` ASC
, `t`.`id` ASC
мы ожидаем, что запрос может эффективно использовать индекс tracking
С ведущей колонкой manufacture
.
и tracking_items
таблица, нам нужен индекс с ведущими столбцами type
и tracking_id
. И в том числе tracking_object_id
в этом индексе будет означать, что запрос может быть удовлетворен из индекса, не посещая базовые страницы.
на cars
, bikes
и trucks
таблицы запрос должен использовать индекс с ведущим столбцом car_id
, bike_id
и truck_id
соответственно. Там нет обойти сканирование car_text
, bike_text
, truck_text
столбцы для соответствующей строки... лучшее, что мы можем сделать, это сузить количество строк, которые должны иметь эту проверку.
этот подход (только tracking
таблица во внешнем запросе) должна устранить необходимость в GROUP BY
, работа, необходимая для идентификации и сворачивания повторяющихся строк.
но этот подход, заменяющий соединения коррелированными подзапросами, лучше всего подходит для запросов, где есть маленький количество строк, возвращаемых внешним запросом. Эти подзапросы выполняются для строка, обработанная внешним запросом. Крайне важно, чтобы эти подзапросы имели подходящие индексы. Даже с настроенными, все еще есть потенциал для ужасной производительности для больших наборов.
это все еще оставляет нас с "использованием filesort" операция ORDER BY
.
если количество связанных элементов должно быть произведением умножения, а не сложения, мы могли бы настроить запрос для достижения этого. (Нам пришлось бы возиться с возвратом нулей, и условие в предложении HAVING необходимо было бы изменить.)
если бы не было требования вернуть COUNT () связанных элементов, то у меня был бы соблазн переместить коррелированные подзапросы из списка выбора вниз в EXISTS
предикаты в WHERE
предложения.
дополнительные примечания: поддержка комментариев Рика Джеймса относительно индексации... по-видимому, определены избыточные индексы. т. е.
KEY `manufacture` (`manufacture`)
KEY `manufacture_date_last_activity` (`manufacture`, `date_last_activity`)
индекс в одноэлементном столбце не нужен, так как есть другой индекс, который имеет столбец в качестве ведущего столбца.
любой запрос, который может эффективно использовать manufacture
индекс сможет эффективно использовать . То есть скажем,manufacture
индекс может быть удален.
то же самое относится к tracking_items
таблица, и эти два индекса:
KEY `tracking_id` (`tracking_id`)
KEY `tracking_id_tracking_object_id` (`tracking_id`,`tracking_object_id`)
на tracking_id
индекс может быть удален, так как он избыточен.
для запроса выше я бы предложил добавить индекс покрытия:
KEY `tracking_items_IX3` (`tracking_id`,`tracking_type`,`tracking_object_id`)
- или-как минимум, индекс без покрытия с этими двумя столбцами, ведущими:
KEY `tracking_items_IX3` (`tracking_id`,`tracking_type`)
объяснение показывает, что вы делаете индекс-сканирование ("индекс" в type
столбец) в таблице отслеживания. Сканирование индекса почти так же дорого, как сканирование таблицы, особенно когда сканируемый индекс является основным индексом.
The также показывает, что это сканирование индекса исследует строки > 355K (поскольку этот показатель является только приблизительной оценкой, он фактически изучает все строки 400K).
у вас есть индекс на t.manufacture
? Я вижу два индекса, названных в possible keys
что может включать этот столбец (я не могу быть уверен только на основе имени индекса), но по какой-то причине оптимизатор не использует их. Возможно, набор значений, который вы ищете, в любом случае соответствует каждой строке в таблице.
если список manufacture
значения предназначены для соответствия подмножеству таблицы, тогда вам может потребоваться дать подсказку оптимизатору, чтобы он использовал лучший индекс. https://dev.mysql.com/doc/refman/5.6/en/index-hints.html
используя LIKE '%word%'
сопоставление шаблонов никогда не может использовать индекс и должно оценивать соответствие шаблонов в каждой строке. Смотрите мою презентацию,Полнотекстовый Поиск Throwdown.
сколько элементов в вашем IN(...)
список? MySQL иногда имеет проблемы с очень длинными списками. Смотри https://dev.mysql.com/doc/refman/5.6/en/range-optimization.html#equality-range-optimization
P. S.: Когда вы задаете вопрос оптимизации запросов, вы всегда должны включать SHOW CREATE TABLE
вывод для каждой таблицы, на которую ссылается запрос, поэтому людям, которые отвечают, не нужно угадывать, какие индексы, типы данных, ограничения у вас есть в настоящее время.
прежде всего: ваш запрос делает предположения о содержимом строки, чего не должно быть. Что может!--3--> указать? Что-то вроде 'Sold in Europe only'
может? Или Sold outside Europe only
? Две возможные строки с противоречивыми значениями. Поэтому, если вы принимаете определенное значение, как только найдете europe
в строке, то вы должны быть в состоянии ввести эти знания в базе данных - с флагом Европы или кодом региона, например.
в любом случае, вы показываете определенные треки с их Европой транспортный отсчет. Итак, выберите трекинги, выберите количество перевозок. Вы можете либо иметь подзапрос агрегации для подсчетов транспортировки в вашем SELECT
пункт или в вашем FROM
предложения.
вложенный запрос в SELECT
статья:
select
t.*,
(
select count(*)
from tracking_items ti
where ti.tracking_id = t.id
and (tracking_type, tracking_object_id) in
(
select 1, car_id from cars where car_text like '%europe%'
union all
select 2, bike_id from bikes where bike_text like '%europe%'
union all
select 3, truck_id from trucks where truck_text like '%europe%'
)
from tracking t
where manufacture in ('1256703406078', '9600048390403', '1533405067830')
order by date_last_activity, id;
вложенный запрос в FROM
статья:
select
t.*, agg.total
from tracking t
left join
(
select tracking_id, count(*) as total
from tracking_items ti
and (tracking_type, tracking_object_id) in
(
select 1, car_id from cars where car_text like '%europe%'
union all
select 2, bike_id from bikes where bike_text like '%europe%'
union all
select 3, truck_id from trucks where truck_text like '%europe%'
)
group by tracking_id
) agg on agg.tracking_id = t.id
where manufacture in ('1256703406078', '9600048390403', '1533405067830')
order by date_last_activity, id;
указатели:
- отслеживание (производство, date_last_activity, id)
- tracking_items(tracking_id, tracking_type, tracking_object_id)
- автомобили(car_text, car_id)
- велосипеды(bike_text, bike_id)
- грузовики(truck_text, truck_id)
иногда MySQL сильнее на простых соединениях, чем на чем-либо еще, поэтому, возможно, стоит попробовать слепо присоединиться к транспортным записям и только позже увидеть, является ли это автомобилем, велосипедом или грузовиком:
select
t.*, agg.total
from tracking t
left join
(
select
tracking_id,
sum((ti.tracking_type = 1 and c.car_text like '%europe%')
or
(ti.tracking_type = 2 and b.bike_text like '%europe%')
or
(ti.tracking_type = 3 and t.truck_text like '%europe%')
) as total
from tracking_items ti
left join cars c on c.car_id = ti.tracking_object_id
left join bikes b on c.bike_id = ti.tracking_object_id
left join trucks t on t.truck_id = ti.tracking_object_id
group by tracking_id
) agg on agg.tracking_id = t.id
where manufacture in ('1256703406078', '9600048390403', '1533405067830')
order by date_last_activity, id;
если моя догадка верна и cars
, bikes
и trucks
независимы друг от друга (т. е. определенный заранее вычислить результат был только у одной из них). Возможно, вам лучше объединить три более простых подзапроса (по одному для каждого).
в то время как вы не можете сделать много индексов о подобных с участием ведущих подстановочных знаков; разделение его на Юнионированные запросы может позволить избежать оценки p.fb_message LIKE '%Europe%' OR p.fb_from_name LIKE '%Europe%
для всех cars
и bikes
матчи, и c
условия для всех b
и t
матчи, и так далее.
когда есть
order by
иgroup by
предложения запрос выполняется очень медленно, например 10-15 секунд для завершения вышеуказанной конфигурации. Однако, если я опущу любое из этих предложений, запрос выполняется довольно быстро (~0,2 секунды).
Это интересно... как правило, лучший метод оптимизации, который я знаю, - это хорошее использование временных таблиц, и похоже, что здесь он будет работать очень хорошо. Таким образом, вы сначала создадите временное таблица:
create temporary table tracking_ungrouped (
key (id)
)
select sql_no_cache `t`.*
from `tracking` as `t`
inner join `tracking_items` as `ti` on (`ti`.`tracking_id` = `t`.`id`)
left join `cars` as `c` on (`c`.`car_id` = `ti`.`tracking_object_id` AND `ti`.`tracking_type` = 1)
left join `bikes` as `b` on (`b`.`bike_id` = `ti`.`tracking_object_id` AND `ti`.`tracking_type` = 2)
left join `trucks` as `tr` on (`tr`.`truck_id` = `ti`.`tracking_object_id` AND `ti`.`tracking_type` = 3)
where
(`t`.`manufacture` in('1256703406078', '9600048390403', '1533405067830')) and
(`c`.`car_text` like '%europe%' or `b`.`bike_text` like '%europe%' or `tr`.`truck_text` like '%europe%');
а затем запросить его для получения необходимых результатов:
select t.*, count(`t`.`id`) as `cnt_filtered_items`
from tracking_ungrouped t
group by `t`.`id`
order by `t`.`date_last_activity` asc, `t`.`id` asc
limit 15;
ALTER TABLE cars ADD FULLTEXT(car_text)
попробовать
select sql_no_cache
`t`.*, -- If you are not using all, spell out the list
count(`t`.`id`) as `cnt_filtered_items` -- This does not make sense
-- and is possibly delivering an inflated value
from `tracking` as `t`
inner join `tracking_items` as `ti` ON (`ti`.`tracking_id` = `t`.`id`)
join -- not LEFT JOIN
`cars` as `c` ON `c`.`car_id` = `ti`.`tracking_object_id`
AND `ti`.`tracking_type` = 1
where `t`.`manufacture` in('1256703406078', '9600048390403', '1533405067830')
AND MATCH(c.car_text) AGAINST('+europe' IN BOOLEAN MODE)
group by `t`.`id` -- I don't know if this is necessary
order by `t`.`date_last_activity` asc, `t`.`id` asc
limit 15;
чтобы увидеть, если он правильно даст вам подходящий 15 - автомобили.
если это выглядит нормально, то объедините три вместе:
SELECT sql_no_cache
t2.*,
-- COUNT(*) -- this is probably broken
FROM (
( SELECT t.id FROM ... cars ... ) -- the query above
UNION ALL -- unless you need UNION DISTINCT
( SELECT t.id FROM ... bikes ... )
UNION ALL
( SELECT t.id FROM ... trucks ... )
) AS u
JOIN tracking AS t2 ON t2.id = u.id
ORDER BY t2.date_last_activity, t2.id
LIMIT 15;
обратите внимание, что внутри SELECTs
только поставить t.id
, а не t.*
.
ti: (tracking_type, tracking_object_id) -- in either order
индексы
когда у вас есть INDEX(a,b)
, вам не нужно INDEX(a)
. (Это не поможет запросу в вопрос, но это поможет дисковое пространство и INSERT
производительность.)
когда я вижу PRIMARY KEY(id), UNIQUE(x)
, Я ищу любую вескую причину, чтобы не избавиться от id
и заменить на PRIMARY KEY(x)
. Если в "упрощении" схемы нет чего-то значительного, такое изменение поможет. Да,car_id
громоздкий и т. д., Но это большая таблица, и дополнительный поиск (от индекса BTree до данных BTree) вредит и т. д.
я думаю, что это очень маловероятно, что KEY
sort_field(date_order)
всегда будет использоваться. Либо отбросьте его (сохранив несколько ГБ), либо объедините его каким-то полезным способом. Давайте посмотрим запрос, в котором вы думаете, что это может быть полезно. (Опять же, предложение, которое не имеет прямого отношения к этому вопросу.)
повторное замечание(с)
я внес некоторые существенные изменения в свою формулировку.
моя формулировка имеет 4 GROUP BYs
, 3 в "производной" таблице (т. е. FROM ( ... UNION ... )
), и один снаружи. Поскольку внешняя часть ограничена строками 3*15, I не беспокойтесь о производительности.
далее обратите внимание, что производная таблица поставляет только t.id
, затем повторно пробники tracking
чтобы получить другие столбцы. Это позволяет производной таблице работать намного быстрее, но за небольшой счет extra JOIN
снаружи.
пожалуйста, подробнее о намерении COUNT(t.id)
; он не будет работать в моей формулировке, и я не знаю, что это подсчет.
я должен был избавиться от ORs
; они вторичное представление убийца. (Первый убийца LIKE '%...'
.)
SELECT t.*
FROM (SELECT * FROM tracking WHERE manufacture
IN('1256703406078','9600048390403','1533405067830')) t
INNER JOIN (SELECT tracking_id, tracking_object_id, tracking_type FROM tracking_items
WHERE tracking_type IN (1,2,3)) ti
ON (ti.tracking_id = t.id)
LEFT JOIN (SELECT car_id, FROM cars WHERE car_text LIKE '%europe%') c
ON (c.car_id = ti.tracking_object_id AND ti.tracking_type = 1)
LEFT JOIN (SELECT bike_id FROM bikes WHERE bike_text LIKE '%europe%') b
ON (b.bike_id = ti.tracking_object_id AND ti.tracking_type = 2)
LEFT JOIN (SELECT truck_id FROM trucks WHERE truck_text LIKE '%europe%') tr
ON (tr.truck_id = ti.tracking_object_id AND ti.tracking_type = 3)
ORDER BY t.date_last_activity ASC, t.id ASC
подзапросы работают быстрее, когда дело доходит до объединения, и если они собираются отфильтровать много записей.
подзапросе отслеживание таблица отфильтрует много других нежелательных производство и приводит к меньшей таблице t присоединиться.
аналогично применяется условие tracking_items таблица, поскольку нас интересует только tracking_types 1,2 и 3; чтобы создать меньшую таблицу ti. Если существует много tracking_objects, вы даже можете добавить фильтр объекта отслеживания в этот подзапрос.
аналогичные подходы к таблицам автомобили, мотоциклы, грузовики с условием их текст, чтобы содержать Европы помогает нам создавать меньшие таблицы c, b, tr соответственно.
также удаление группы по t.id как t.id уникален, и мы выполнение внутреннего соединения и левого соединения на этой или результирующей таблице, так как нет необходимости.
наконец, я только выбираю необходимые столбцы из каждой таблицы это необходимо, что также уменьшит нагрузку на пространство памяти, а также время выполнения.
надеюсь, что это помогает. Пожалуйста, дайте мне знать ваши отзывы и запустить статистику.
Я не уверен, что это сработает, как насчет применения фильтра к каждой таблице (автомобили, велосипеды и грузовики) в предложении ON, прежде чем присоединиться,он должен отфильтровать строки?