3

My problem is that my query is very slow when use JOIN on the same table twice.

I want to retrieve all the products from a given category. But since the product can be in multiple categories I also want to get the (c.canonical) category that should provide the URL base. Therefore I have 2 extra JOIN on categories AS c and categories_products AS cp2.

Original query

SELECT p.product_id
FROM products AS p
JOIN categories_products AS cp
    ON p.product_id = cp.product_id
JOIN product_variants AS pv
    ON pv.product_id = p.product_id
WHERE cp.category_id = 2
    AND p.status = 2
GROUP BY p.product_id
ORDER BY cp.product_sortorder ASC
LIMIT 0, 40

EXPLAIN

| id | select_type | table |   type |          possible_keys |                    key | key_len |                     ref | rows |                                        extra |
|----|-------------|-------|--------|------------------------|------------------------|---------|-------------------------|------|----------------------------------------------|
|  1 |      SIMPLE |    cp |    ref | FK_categories_products | FK_categories_products |       4 |                   const | 1074 | Using where; Using temporary; Using filesort |
|  1 |      SIMPLE |     p | eq_ref |                PRIMARY |                PRIMARY |       4 | superlove.cp.product_id |    1 |                                    Using where |
|  1 |      SIMPLE |    pv |    ref |    FK_product_variants |    FK_product_variants |       4 |  superlove.p.product_id |    1 |                                    Using where |    

Slow query

SELECT p.product_id, c.category_id
FROM products AS p
JOIN categories_products AS cp
    ON p.product_id = cp.product_id
JOIN categories_products AS cp2        // Extra line
    ON p.product_id = cp2.product_id   // Extra line
JOIN categories AS c                   // Extra line
    ON cp2.category_id = c.category_id // Extra line
JOIN product_variants AS pv
    ON pv.product_id = p.product_id
WHERE cp.category_id = 2
    AND p.status = 2
    AND c.canonical = 1                // Extra line
GROUP BY p.product_id
ORDER BY cp.product_sortorder ASC
LIMIT 0, 40

EXPLAIN

| id | select_type | table |   type |          possible_keys |                    key | key_len |                      ref | rows |                                        extra |
|----|-------------|-------|--------|------------------------|------------------------|---------|--------------------------|------|----------------------------------------------|
|  1 |      SIMPLE |     c |    ALL |                PRIMARY |                 (null) |  (null) |                   (null) |  221 | Using where; Using temporary; Using filesort |
|  1 |      SIMPLE |   cp2 |    ref | FK_categories_products | FK_categories_products |       4 |  superlove.c.category_id |   33 |                                              |
|  1 |      SIMPLE |     p | eq_ref |                PRIMARY |                PRIMARY |       4 | superlove.cp2.product_id |    1 |                                  Using where |
|  1 |      SIMPLE |    pv |    ref |    FK_product_variants |    FK_product_variants |       4 |   superlove.p.product_id |    1 |                                  Using where |
|  1 |      SIMPLE |    cp |    ref | FK_categories_products | FK_categories_products |       4 |                    const | 1074 |                                  Using where |
4

1 回答 1

1

MySQL 优化器似乎对这个查询有问题。我的印象是只有很少的产品会在请求的类别中,但可能会有很多规范的类别。但是,优化器显然无法判断 thatcp.category_id = 2是比 更强的条件,因此它使用而不是c.canonical = 1开始新查询,从而导致沿途有很多多余的行。ccp

向优化器提供数据

您的第一次尝试应该是尝试为优化器提供所需的数据:使用该ANALYZE TABLE命令,您可以收集有关密钥分配的信息。为此,您必须准备好合适的密钥。所以也许你应该在categories.canonical. 然后 MySQL 会知道(如果我理解正确的话)该列只有两个不同的值,甚至可能每个中有多少行。运气好的话,这会告诉它使用c.canonical = 1作为起点将是一个糟糕的选择。

强制加入顺序

如果这没有帮助,那么我建议您使用STRAIGHT_JOIN. 特别是,您可能希望强制cp作为第一个表,就像您的原始(和快速)查询一样。如果这样可以解决问题,您可以坚持使用该解决方案。如果没有,那么您应该提供一个新的EXPLAIN输出,以便我们可以看到该方法失败的地方。

架构注意事项

还有一件事要考虑:您的问题意味着对于每种产品,都有一个与之相关的规范类别。但是您的数据库架构并未反映这一事实。您可能需要考虑修改架构以反映这一事实的方法。例如,您可以有一个名为canonical_category_idinproducts表的列,并且categories_products仅用于非规范类别。如果您使用这样的设置,您可能希望创建一个VIEW将产品连接到所有类别的产品,包括规范和非规范类别,使用UNION如下:

CREATE VIEW products_all_categories AS
SELECT product_id, canonical_category_id AS category_id
FROM products
UNION ALL
SELECT product_id, category_id
FROM categories_products

您可以使用它而不是categories_products在那些您不关心类别是否规范的地方。您甚至可以重命名表并命名视图categories_products,以便您现有的查询像以前一样工作。products您应该在此查询中使用的两列上添加索引。甚至可能有两个索引,一个用于这些列的任一顺序。

不确定整个设置在您的应用程序中是否可以接受。不确定它是否真的会带来预期的速度增益。最后,您可能会被迫维护冗余数据,例如products.canonical除了对表中规范类别的引用之外的列categories_products。我知道从设计的角度来看冗余数据是丑陋的,但为了性能,可能有必要避免长时间的计算。至少在不支持物化视图的 RDBMS 上。尽管我没有实际经验,但您可能可以使用触发器来保持数据一致。

于 2013-10-23T17:00:54.263 回答