Эффективный поиск во многих связанных таблицах - PullRequest
1 голос
/ 11 февраля 2012

У меня есть две таблицы, относящиеся ко многим ко многим через третью соединительную таблицу: продукты и категории .Каждый товар может быть в нескольких категориях.Это типичная реализация «многие ко многим»:

products
-------------
id
product_name


categories
-------------
id
category_name


products_to_categories
-------------
product_id
caregory_id

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

Пример: найти все продукты, которые находятся в категориях "Компьютеры" и "Программное обеспечение", но не в категориях "Игры", "Программирование" и "Образование".

Здесьзапрос, который я разработал для этого:

SELECT product_name
FROM products
WHERE
    EXISTS (SELECT product_id FROM products_to_categories WHERE category_id = 1 AND product_id = products.id) 
    AND EXISTS (SELECT product_id FROM products_to_categories WHERE category_id = 2 AND product_id = products.id) 
    AND NOT EXISTS (SELECT product_id FROM products_to_categories WHERE category_id = 3 AND product_id = products.id)
    AND NOT EXISTS (SELECT product_id FROM products_to_categories WHERE category_id = 4 AND product_id = products.id) 
    AND NOT EXISTS (SELECT product_id FROM products_to_categories WHERE category_id = 5 AND product_id = products.id)
ORDER BY id

Это работает.Но это так невероятно медленно, что я просто не могу использовать его в производстве.Все idex на месте, но этот запрос приводит к 5 зависимым подзапросам, и таблицы огромны.

Есть ли способ решить ту же задачу без зависимых подзапросов или оптимизировать этот запрос каким-либо другим способом?

ОБНОВЛЕНИЕ

Индексы:

products: PRIMARY KEY (id)
categories: PRIMARY KEY (id)
products_to_categories: PRIMARY KEY (product_id, caregory_id)

Все таблицы InnoDB

Ответы [ 4 ]

2 голосов
/ 11 февраля 2012

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

Вы также можете опубликовать план выполнения вашего запроса (используя оператор EXPLAIN).

Вы также можете попробовать переписать запрос различными способами. Вот один из них:

SELECT p.product_name
FROM products  AS p
  JOIN products_to_categories  AS pc1
    ON pc1.category_id = 1 
    AND pc1.product_id = p.id
  JOIN products_to_categories  AS pc2
    ON  pc2.category_id = 2 
    AND pc2.product_id = p.id
WHERE
    NOT EXISTS 
    ( SELECT * 
      FROM products_to_categories  AS pc 
      WHERE pc.category_id IN (3, 4, 5)
        AND pc.product_id = p.id
    )

Обновление: у вас нет индекса (category_id, product_id). Попробуйте добавить его.

0 голосов
/ 11 февраля 2012

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

where category_id IN(1,2)

или

where category_id NOT IN(1,2)
0 голосов
/ 11 февраля 2012

Я думаю, что вы хотите избежать предложений in, потому что SQL-сервер будет делать несколько запросов или делать "или", что будет менее эффективно, чем то, что я вставляю ниже, потому что он может не иметь возможности воспользоватьсяиндексов.

Вы также можете избавиться от временной таблицы #product_categories_filtered и сделать все это в одном большом запросе, а также использовать псевдонимы, если хотите.Возможно, вы захотите поиграть с разными конфигурациями и посмотреть, какая из них лучше, но временные таблицы никогда не были проблемой производительности в моем приложении, если кто-то не попытался сделать запрос с десятками миллионов записей.Я использовал #product_categories_filtered, потому что в некоторых случаях запросы к SQL-серверу выполняются лучше, когда вы разбиваете запросы на меньшее количество объединений, особенно в больших таблицах, таких как product.

create table #includes (category_id int not null primary key)
create table #excludes (category_id int not null primary key)

insert #includes (category_id) 
    select 1
    union all select 2
insert #excludes (category_id) 
    select 3
    union all select 4
    union all select 5

select 
  pc.product_id
into #product_catories_filtered
from 
  product_categories pc
  join #includes i 
    on pc.category_id = i.category_id
  left join #excludes e 
    on pc.category_id = i.category_id
where 
  e.category_id is null


select distinct
  p.product_name
from 
  #product_categories_filtered pc
  join products p
    on pc.product_id = p.id
order by 
  p.id
0 голосов
/ 11 февраля 2012
SELECT product_name
FROM products
-- we can use an inner join as an optimization, as some categories MUST exist
INNER JOIN products_to_categories ON products.product_id=products_to_categories.product_id
WHERE 
  products_to_categories.category_id NOT IN (3,4,5) -- substitute unwanted category IDs
  AND EXISTS (SELECT product_id FROM products_to_categories WHERE category_id = 1 AND product_id = products.id) 
  AND EXISTS (SELECT product_id FROM products_to_categories WHERE category_id = 2 AND product_id = products.id) 
...