在多对多相关表中进行高效搜索

Posted

技术标签:

【中文标题】在多对多相关表中进行高效搜索【英文标题】:Efficient search in many-to-many related tables 【发布时间】:2012-02-11 17:25:53 【问题描述】:

我有两个通过第三个连接表进行多对多关联的表:产品和类别。每个产品可以属于多个类别。这是典型的多对多关系:

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

它有效。但它非常慢,以至于我无法在生产中使用它。所有索引都已就位,但此查询会产生 5 个相关子查询并且表很大。

有没有办法在不依赖子查询的情况下解决相同的任务或以其他方式优化此查询?

更新

索引是:

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

所有表都是 InnoDB

【问题讨论】:

EXISTS 很慢,因为它会查看每个值。尝试限制您获得的结果并添加“更多按钮”或其他技术。 @Silver Light:“索引已到位”。你有什么索引? (尤其是在products_to_categories 表中) 尝试添加(caregory_id, product_id) 索引。它对您查询的任何版本(以及您将来可能进行的其他搜索)都有帮助。 【参考方案1】:

请发布表的定义(以便显示使用的引擎和定义的索引)。

您还可以发布查询的执行计划(使用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) 索引。尝试添加它。

【讨论】:

我认为将 NOT EXISTS 更改为 LEFT JOIN 来尝试此查询是值得的。查看SHOW INDEXES FROM products_to_categories 的输出也会有所帮助。 这是一个很好的优化。正是我要找的,谢谢 @Silver Light:你添加了那个索引吗? @ypercube,是的,还有其他几个以适应更复杂的条件。 中间表中是否还有其他列?【参考方案2】:

我认为您希望避免使用 in 子句,因为 SQL Server 将执行多个查询或执行“或”,这将比我在下面粘贴的效率低,因为它可能无法利用的索引。

您还可以摆脱 #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

【讨论】:

【参考方案3】:

我删除了我的答案,因为其他答案更全面。只是一般提示。要减少语句中 AND 的数量,您可以使用 IN 运算符来检查多个类别

where category_id IN(1,2)

where category_id NOT IN(1,2)

【讨论】:

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 相同【参考方案4】:
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) 

【讨论】:

以上是关于在多对多相关表中进行高效搜索的主要内容,如果未能解决你的问题,请参考以下文章

教义 - 在多对多关系表中插入记录

在多对多 SQL 表中查找数据关系或图形

春季 JPA |在多对多关系中搜索

如何在多对多表中添加列(Django)

我可以在多对多关系的联接表中添加非 Id 字段吗?

在多对多连接表中,如何计算两个“所有者”共享的条目数?