2012-02-11 4 views
1

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

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 

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

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

UPDATE

Индексы:

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

Все таблицы InnoDB

+0

EXISTS медленный, потому что он смотрит на каждое значение. Попытайтесь ограничить полученные результаты и поместите «больше кнопки» или другую технику. – AlfredoVR

+0

@Silver Light: «индексы на месте». Какие индексы у вас есть? (особенно в таблице 'products_to_categories') –

+0

Попробуйте добавить индекс' (caregory_id, product_id) '. Это действительно поможет с любой версией вашего запроса (и с другими поисковыми запросами, которые могут возникнуть у вас). –

ответ

2

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

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

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

+0

Я думаю, что стоит попробовать этот запрос с заменой' NOT EXISTS' для 'LEFT JOIN'. Это также поможет увидеть вывод из «ПОКАЖИТЕ ИНДЕКСЫ ИЗ ТОВАРОВ». – nnichols

+0

Это отличная оптомизация. Именно то, что я искал, спасибо –

+0

@Silver Light: Вы добавили этот индекс? –

0
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) 
0

я удалил мой ответ, потому что другие ответы были более всеобъемлющим. Просто общий совет. Для того, чтобы сократить число И это находится в ваших заявлениях вы можете использовать оператор IN, чтобы проверить на несколько категорий

where category_id IN(1,2) 

или

where category_id NOT IN(1,2) 
+0

'NOT IN (1,2)' такой же, как запрос OP. 'a NOT IN (1,2)' означает 'NOT a = 1 AND NOT a = 2'. Но 'IN (1,2)' не то же самое. 'x IN (1,2)' такой же, как 'x = 1 OR x = 2' –

0

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

Вы также можете избавиться от таблицы temp #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 
Смежные вопросы