Как оптимизировать план выполнения запроса с несколькими внешними соединениями с огромными таблицами, предложениями 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 секунды).

что я уже пытался?

  1. я пытался использовать FULLTEXT индекс, но это не очень помогло, так как результаты оцениваются LIKE statemenet сужены JOINs использование индексов.
  2. я пытался использовать 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) вредит и т. д.

я думаю, что это очень маловероятно, что KEYsort_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, прежде чем присоединиться,он должен отфильтровать строки?