Как ускорить запрос при использовании счетчика и группировать по

У меня есть две таблицы с именем продавец и товар. Они связаны через третью таблицу (seller_item), используя отношение внешнего ключа от "n" к "m".

Теперь я попытаюсь ответить на требование: "Мне, как продавцу, нужен список моих конкурентов с указанием количества товаров, которые я продаю, и они продают также". Итак, список всех продавцов с количеством перекрывающихся товаров по отношению к одному конкретному продавцу. Также я хочу, чтобы это было отсортировано по количеству и ограничено. Но запрос использует временную таблицу и сортировку файлов, что очень медленно. Объясните, говорит:

Используя где; Использование индекса; Используя временные; Использование сортировки файлов

Как я могу ускорить это?

Вот запрос:

SELECT
          COUNT(*) AS itemCount,
          s.sellerName
        FROM
          seller s,
          seller_item si
        WHERE
          si.itemId IN 
           (SELECT itemId FROM seller_item WHERE sellerId = 4711)
        AND
          si.sellerId=s.id
        GROUP BY
          sellerName
        ORDER BY
          itemCount DESC
        LIMIT 50;

таблица определений:

CREATE TABLE `seller` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`sellerName` varchar(50) NOT NULL
PRIMARY KEY (`id`),
UNIQUE KEY `unique_index` (`sellerName`),
) ENGINE=InnoDB 

contains about 200.000 rows

-

CREATE TABLE `item` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`itemName` varchar(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_index` (`itemName`),
) ENGINE=InnoDB

contains about 100.000.000 rows

-

CREATE TABLE `seller_item` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`sellerId` bigint(20) unsigned NOT NULL,
`itemId` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `sellerId` (`sellerId`,`itemId`),
KEY `item_id` (`itemId`),
CONSTRAINT `fk_1` FOREIGN KEY (`sellerId`) REFERENCES `seller` (`id`)            ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT `fk_2` FOREIGN KEY (`itemId`) REFERENCES `item` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION
) ENGINE=InnoDB

contains about 170.000.000 rows

База данных Mysql Percona 5.6

Вывод EXPLAIN:

+----+-------------+-------------+--------+----------------------+-----    ---------+---------+---------------------+------+----------------------------------------------+
| id | select_type | table       | type   | possible_keys        | key                | key_len | ref                 | rows | Extra                                           |
+----+-------------+-------------+--------+----------------------+--------------+---------+---------------------+------+----------------------------------------------+
|  1 | SIMPLE      | s           | index  | PRIMARY,unique_index | unique_index | 152     | NULL                |    1 | Using index; Using temporary; Using filesort |
|  1 | SIMPLE      | si          | ref    | sellerId,item_id     | sellerId     | 8       | tmp.s.id            |    1 | Using index                                  |
|  1 | SIMPLE      | seller_item | eq_ref | sellerId,item_id     |  sellerId     | 16      | const,tmp.si.itemId |    1 | Using where; Using  index                     |
+----+-------------+-------------+--------+----------------------+--------------+---------+---------------------+------+----------------------------------------------+

5 ответов

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

Вы должны это материализовать. Создать такую ​​таблицу

CREATE TABLE
        matches
        (
        seller INT NOT NULL,
        competitor INT NOT NULL,
        matches INT NOT NULL,
        PRIMARY KEY
                (seller, competitor)
        )

и обновите его партиями в cron-скрипте:

DELETE
FROM    matches
WHERE   seller = :seller

INSERT
INTO    matches (seller, competitor, matches)
SELECT  si.seller, sc.seller, COUNT(*) cnt
FROM    seller_item si
JOIN    seller_item sc
ON      sc.item = si.item
        AND sc.seller <> si.seller
WHERE   si.seller = :seller
GROUP BY
        si.seller, sc.seller
ORDER BY
        cnt DESC
LIMIT   50

Вы также должны сделать (seller, item) PRIMARY KEY на seller_item, В настоящее время для поиска продавца по элементу требуется два поиска вместо одного: первый идентификатор по элементу с использованием KEY (item)затем продавец по id используя PRIMARY KEY (id)

Я полагаю, что у вас сложилось неправильное представление о вашей способности устранить временное использование; Использование шагов сортировки файлов для удовлетворения вашего запроса. Запросы формы

 SELECT COUNT(*), grouping_value
   FROM table
  GROUP BY grouping_value
  ORDER BY COUNT(*) 
  LIMIT n

всегда используйте временный набор результатов в памяти и всегда сортируйте этот набор результатов. Это связано с тем, что результирующий набор нигде не существует до тех пор, пока не выполнится запрос, и он должен быть отсортирован до того, как будет выполнено предложение LIMIT.

"Файловая сортировка" несколько неправильно названа. Это не обязательно означает, что происходит сортировка файла в файловой системе, просто сортируется временный набор результатов. Если этот набор результатов массивный, сортировка может вылиться из ОЗУ в файловую систему, но это не обязательно. Пожалуйста прочтите это. https://www.percona.com/blog/2009/03/05/what-does-using-filesort-mean-in-mysql/ Не отвлекайтесь на Using filesort предмет в вашем EXPLAIN Результаты.

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

Но вы все равно можете организовать сортировку меньшего количества материала, сортируя только seller.id и количество, а затем присоединение к (более длинному) sellerName после того, как вы знаете точные пятьдесят строк, которые вам нужны. Это также имеет то преимущество, что вы можете агрегировать только с таблицей seller_item, а не с набором результатов, который получается при объединении этих двух.

Вот что я имею в виду. Этот подзапрос генерирует список из пятидесяти sellerId ценности, которые вам нужны. Все, что нужно отсортировать, это количество и sellerId, Это быстрее, чем сортировка и подсчет sellerName потому что там меньше данных и данных фиксированной длины, чтобы перемешать в операции сортировки.

SELECT COUNT(*) AS itemCount,
       sellerId
  FROM seller_item 
 WHERE itemId IN
        (SELECT itemId FROM seller_item WHERE sellerId = 4711)
 GROUP BY SellerId
 ORDER BY COUNT(*) DESC
 LIMIT 50

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

Вы можете сделать это еще быстрее, отфильтровывая больше строк, добавляя HAVING COUNT(*) > 1 сразу после вашего GROUP BY пункт, но это меняет смысл вашего запроса и может не соответствовать вашим бизнес-требованиям.

Когда у вас есть эти пятьдесят предметов, вы можете получить имена продавцов. Весь запрос выглядит так:

SELECT s.sellerName, c.itemCount
  FROM seller s
  JOIN (
         SELECT COUNT(*) AS itemCount, sellerId
           FROM seller_item 
          WHERE itemId IN
                      (SELECT itemId FROM seller_item WHERE sellerId = 4711)
                GROUP BY SellerId
                ORDER BY COUNT(*) DESC
                LIMIT 50
       ) c ON c.sellerId = s.id
 ORDER BY c.itemCount DESC

Ваши усилия по индексированию должны быть потрачены на быстрые внутренние запросы. Внешний запрос будет быстрым, несмотря ни на что; он обрабатывает только пятьдесят строк и использует индексированный id значение для поиска других значений.

Внутренний запрос SELECT itemId FROM seller_item WHERE sellerId = 4711, Это значительно выиграет от вашего существующего (sellerId, itemId) составной индекс: он может выполнять произвольный доступ, а затем сканировать этот индекс, что очень быстро.

SELECT COUNT(*)... запрос выиграет от (itemId, sellerId) составной индекс. Эта часть вашего запроса является сложной и медленной, но все же этот индекс поможет.

Посмотрите, другие упоминали об этом, и я тоже. Имея оба уникальных составных ключа (sellerId, itemId) и первичный ключ id на что seller_item Стол, с уважением, невероятно расточительный.

  • Это делает ваши обновления и вставляет медленнее.
  • Это означает, что ваша таблица организована в виде дерева на основе бессмысленного id а не значащая пара значений.

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

переформулирование

Я думаю, что это то, что вы действительно хотели:

SELECT  si2.sellerId, COUNT(DISTINCT si2.itemId) AS itemCount
    FROM  seller_item si1
    JOIN  seller_item si2 ON si2.itemId = si1.itemId
    WHERE  si1.sellerId = 4711
    GROUP BY  si2.sellerId
    ORDER BY  itemCount DESC
    LIMIT  50;

(Заметка: DISTINCT вероятно не нужно.)

В словах: для продавца № 4711 найдите товары, которые он продает, а затем найдите, какие продавцы продают почти такой же набор товаров. (Я не пытался отфильтровать #4711 из набора результатов.)

Более эффективный N:M

Но все еще есть неэффективность. Давайте разберем вашу таблицу сопоставления "многие ко многим" (seller_item).

  • Имеет id который, вероятно, не используется ни для чего. Избавиться от этого.
  • Тогда продвигайте UNIQUE(sellerId, itemId) в PRIMARY KEY(sellerId, itemId),
  • Теперь поменяй INDEX(itemId) в INDEX(itemId, sellerId) так что последним этапом запроса может быть "использование индекса".

Блог обсуждает это дальше.

У вас очень большой набор данных; Вы отладили свое приложение. Рассмотрите возможность удаления FOREIGN KEYs; они несколько дорогостоящие.

Получение sellerName

Это может быть возможно JOIN в sellers получить sellerName, Но попробуйте это просто sellerId первый. Затем добавьте имя. Убедитесь, что число не увеличивается (что часто происходит) и что запрос не замедляется.

Если что-то пойдет не так, то сделайте

SELECT s.sellerName, x.itemCount
    FROM ( .. the above query .. ) AS x
    JOIN sellers AS s  USING(sellerId);

(При желании вы можете добавить ORDER BY sellerName.)

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

Во-первых, мы получаем itemcount по seller_id

SELECT so.seller_id
     , COUNT(*) AS itemcount
  FROM seller_item si
  JOIN seller_item so
    ON so.item_id = si.item_id
 WHERE si.seller_id = 4711
 GROUP BY so.seller_id
 ORDER BY COUNT(*) DESC, so.seller_id DESC
 LIMIT 50

Для повышения производительности я бы сделал подходящий индекс покрытия доступным для объединения с so, например

CREATE UNIQUE INDEX seller_item_UX2 ON seller_item(item_id,seller_id) 

Используя "покрывающий индекс", MySQL может полностью удовлетворить запрос со страниц индекса, без необходимости посещать страницы в базовой таблице.

После создания нового индекса я бы отбросил индекс в столбце singleton item_id, так как этот индекс теперь избыточен. (Любой запрос, который мог бы эффективно использовать этот индекс, сможет эффективно использовать составной индекс, который имеет item_id в качестве ведущей колонки.)

Там нет обойти операцию "Использование файловой сортировки". MySQL должен оценить агрегат COUNT() в каждой строке, прежде чем он сможет выполнить сортировку. Нет никакого способа (учитывая текущую схему) для MySQL, чтобы возвратить строки в порядке, используя индекс, чтобы избежать операции сортировки.

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

Чтобы получить имя продавца, мы могли бы использовать коррелированный подзапрос в списке SELECT или операцию соединения.

1) Использование коррелированного подзапроса в списке SELECT, например

SELECT so.seller_id
     , ( SELECT s.sellername
           FROM seller s
          WHERE s.seller_id = so.seller_id
          ORDER BY s.seller_id, s.sellername
          LIMIT 1
       ) AS sellername   
     , COUNT(*) AS itemcount
  FROM seller_item si
  JOIN seller_item so
    ON so.item_id = si.item_id
 WHERE si.seller_id = 4711
 GROUP BY so.seller_id
 ORDER BY COUNT(*) DESC, so.seller_id DESC
 LIMIT 50

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

Или 2) используя операцию соединения, например

SELECT c.seller_id
     , s.sellername
     , c.itemcount
  FROM ( 
         SELECT so.seller_id
              , COUNT(*) AS itemcount
           FROM seller_item si
           JOIN seller_item so
             ON so.item_id = si.item_id
          WHERE si.seller_id = 4711
          GROUP BY so.seller_id
          ORDER BY COUNT(*) DESC, so.seller_id DESC
          LIMIT 50
       ) c
  JOIN seller s
    ON s.seller_id = c.seller_id
 ORDER BY c.itemcount DESC, c.seller_id DESC    

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


ТАБЛИЦА РЕЗУЛЬТАТОВ

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

В качестве иллюстрации того, как это может выглядеть:

CREATE TABLE item_seller_count
( item_id BIGINT NOT NULL PRIMARY KEY
, seller_count BIGINT NOT NULL
) Engine=InnoDB
;

INSERT INTO item_seller_count (item_id, seller_count)
SELECT d.item_id
     , COUNT(*)
  FROM seller_item d
 GROUP BY d.item_id
 ORDER BY d.item_id
;

CREATE UNIQUE INDEX item_seller_count_IX1 
  ON item_seller_count (seller_count, item_id)
;

Новая сводная таблица станет "не синхронизированной", когда строки будут добавлены / обновлены / удалены из таблицы seller_item.

И заполнение этой таблицы потребует ресурсов. Но доступность этого ускорит запросы того типа, над которым мы работаем.

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

    select * from (
        select seller.sellerName,
        count(otherSellersItems.itemId) itemCount from (
            select sellerId, itemId from seller_item where sellerId != 4711
        ) otherSellersItems
        inner join ( 
            select itemId from seller_item where sellerId = 4711
        ) thisSellersItems
        on otherSellersItems.itemId = thisSellersItems.itemId
        inner join seller
        on otherSellersItems.sellerId = seller.id
        group by seller.sellerName
    ) itemsSoldByOtherSellers
    order by itemCount desc
    limit 50 ;
Другие вопросы по тегам