【问题标题】:Optimize SQL query with many inner joins on same table在同一张表上使用许多内连接优化 SQL 查询
【发布时间】:2017-07-13 08:45:26
【问题描述】:

我遇到了性能问题:

一家商店有一个商品过滤器,其中包含“颜色”、“尺寸”、“性别”和“特征”类别。所有这些详细信息都存储在article_criterias 表中,如下所示:

article_criterias的表格布局是;该表大约有 36.000 行:

article_id | group    | option | option_val
       100 | "size"   | "35"   |     35.00
       100 | "size"   | "36"   |     36.00
       100 | "size"   | "36½"  |     36.50
       100 | "color"  | "40"   |     40.00
       100 | "color"  | "50"   |     50.00
       100 | "gender" | "1"    |      1.00
       101 | "size"   | "40"   |     40.00
       ...

我们有一个根据当前选择的条件动态构建的 SQL 查询。该查询适用于 2-3 个条件,但在选择超过 5 个选项时会变得非常慢(每个额外的 INNER JOIN 大约会使执行时间增加一倍)

我们怎样才能让这个 SQL 更快,甚至可以用更高效的概念代替内部连接?

这是查询(逻辑正确,只是性能不好):

-- This SQL is generated when the user selected the following criteria
-- gender: 1
-- color: 80 + 30
-- size 36 + 37 + 38 + 39 + 42 + 46
SELECT
    criteria.group AS `key`,
    criteria.option AS `value`
FROM articles
    INNER JOIN article_criterias AS criteria ON articles.id = criteria.article_id
    INNER JOIN article_criterias AS criteria_gender 
        ON criteria_gender.article_id = articles.id AND criteria_gender.group = "gender"
    INNER JOIN article_criterias AS criteria_color1 
        ON criteria_color1.article_id = articles.id AND criteria_color1.group = "color"
    INNER JOIN article_criterias AS criteria_size2 
        ON criteria_size2.article_id = articles.id AND criteria_size2.group = "size"
    INNER JOIN article_criterias AS criteria_size3 
        ON criteria_size3.article_id = articles.id AND criteria_size3.group = "size"
    INNER JOIN article_criterias AS criteria_size4 
        ON criteria_size4.article_id = articles.id AND criteria_size4.group = "size"
    INNER JOIN article_criterias AS criteria_size5 
        ON criteria_size5.article_id = articles.id AND criteria_size5.group = "size"
    INNER JOIN article_criterias AS criteria_size6 
        ON criteria_size6.article_id = articles.id AND criteria_size6.group = "size"
    INNER JOIN article_criterias AS criteria_size7 
        ON criteria_size7.article_id = articles.id AND criteria_size7.group = "size"
WHERE
    AND (criteria_gender.option IN ("1"))
    AND (criteria_color1.option IN ("80", "30"))
    AND (criteria_size2.option_val BETWEEN 35.500000 AND 36.500000)
    AND (criteria_size3.option_val BETWEEN 36.500000 AND 37.500000)
    AND (criteria_size4.option_val BETWEEN 37.500000 AND 38.500000)
    AND (criteria_size5.option_val BETWEEN 38.500000 AND 39.500000)
    AND (criteria_size6.option_val BETWEEN 41.500000 AND 42.500000)
    AND (criteria_size7.option_val BETWEEN 45.500000 AND 46.500000)

【问题讨论】:

  • 检查索引,INNER JOIN 条件中使用的字段,以及 where 条件。索引将使搜索变得快速。
  • 您正在努力解决的问题称为“实体属性值”或“EAV”。就关系数据库而言,您已经从地狱中获得了反模式。您需要不同的数据库或不同的方法。
  • 虽然我同意太空人的观点,但您确定查询真的按预期工作吗?您的查询当前将查找特定性别的鞋子,颜色为 80 OR(!) 30,尺寸为 36 AND (!) 37(以及其他),因此如果您拥有除 39 之外的所有尺寸的鞋子,则没有结果。您如果您也使用or 作为尺寸,已经可以在这里节省很多加入/时间,类似于颜色:只加入一次,例如使用join ... criteria_size2 on ... criteria_size2.group = "size" and (criteria_size2.option_val BETWEEN 35.5 AND 36.5 or criteria_size2.option_val BETWEEN 36.5 AND 37.5 or ...)
  • @LoztInSpace 是的,我知道......但是,现在更改数据库结构不是一种选择。您应该已经看到原始查询是如何工作的(在最低分支中使用 SQL 的 3 个嵌套循环)...
  • 请注意,组、选项和键是保留字,因此它们都比表/列标识符的灵感选择少。

标签: mysql join query-performance entity-attribute-value


【解决方案1】:

键/值表真的很麻烦。但是,为了找到某些条件匹配,请聚合您的数据:

select 
  a.*,
  ac.group AS "key",
  ac.option AS "value"
from articles a
join article_criterias ac on ac.article_id = a.article_id
where a.article_id in
(
  select article_id
  from article_criterias
  group by article_id
  having sum("group" = 'gender' and option = '1') > 0
     and sum("group" = 'color' and option in ('30','80')) > 0
     and sum("group" = 'size' and option_val between 35.5 and 36.5) > 0
     and sum("group" = 'size' and option_val between 36.5 and 37.5) > 0
     and sum("group" = 'size' and option_val between 37.5 and 38.5) > 0
     and sum("group" = 'size' and option_val between 38.5 and 39.5) > 0
     and sum("group" = 'size' and option_val between 41.5 and 42.5) > 0
     and sum("group" = 'size' and option_val between 45.5 and 46.5) > 0
)
order by a.article_id, ac.group, ac.option;

这将为您提供所有适用于性别 1、颜色 30 和/或 80 的文章,以及所有列出的尺寸范围,以及它们的所有选项。 (不过,大小范围有点奇怪;例如,大小为 36.5 会满足两个范围。)您明白了:按 article_id 分组并使用 HAVING 以便只获得符合条件的 article_id。

至于你想要的索引

create index idx on article_criterias(article_id, "group", option, option_val);

【讨论】:

    【解决方案2】:

    正如@affan-pathan 所建议的,添加索引确实解决了这个问题:

    CREATE INDEX text_option 
    ON `article_criterias` (`article_id`, `group`, `option`);
    
    CREATE INDEX numeric_option 
    ON `article_criterias` (`article_id`, `group`, `option_val`);
    

    这两个索引将上述查询表单的执行时间从近 1 分钟缩短到不到 50 毫秒!!

    【讨论】:

      【解决方案3】:

      我了解您创建的索引解决了您的问题, 但只是为了玩一个伪替代品(避免多个INNER JOIN),你可以尝试这样的事情吗? (我只测试了三个条件。您的条件应该插入到内部查询中。要仅选择满足所有条件的记录,您必须更改最后一个 WHERE 条件(WHERE max = 3,使用您上面写的条件数;所以如果你使用 5 个条件,你应该写 WHERE max = 5). (我改变了列组和选项的名称,为了我的易用性)。 这只是一个想法,所以请做一些测试并检查性能,请告诉我...

      CREATE TABLE CRITERIA (ARTICLE_ID INT, GROU VARCHAR(10), OPT VARCHAR(20), OPTION_VAL NUMERIC(12,2));
      CREATE TABLE ARTICLES (ID INT);
      INSERT INTO CRITERIA VALUES (100,'size','35',35);
      INSERT INTO CRITERIA VALUES (100,'size','36',36);
      INSERT INTO CRITERIA VALUES (100,'color','40',40);
      INSERT INTO CRITERIA VALUES (100,'gender','1',1);
      INSERT INTO CRITERIA VALUES (200,'size','36.2',36.2);
      INSERT INTO CRITERIA VALUES (300,'size','36.2',36.2);
      INSERT INTO ARTICLES VALUES (100);
      INSERT INTO ARTICLES VALUES (200);
      INSERT INTO ARTICLES VALUES (300);
      
      -------------------------------------------------------
      
      SELECT D.article_id, D.GROU, D.OPT
      FROM (SELECT C.*
           , @o:=CASE WHEN @h=ARTICLE_ID THEN @o ELSE cumul END max
           , @h:=ARTICLE_ID AS a_id
           FROM (SELECT article_id,
                   B.GROU, B.OPT,             
                   @r:= CASE WHEN @g = B.ARTICLE_ID THEN @r+1 ELSE 1 END cumul,                        
                   @g:= B.ARTICLE_ID g                
                   FROM CRITERIA B
                   CROSS JOIN (SELECT @g:=0, @r:=0) T1
                   WHERE (B.GROU='gender' AND B.OPT IN ('1'))
                          OR  (B.GROU='color'  AND B.OPT IN ('40', '30'))
                          OR  (B.GROU='size'   AND B.OPT BETWEEN 35.500000 AND 36.500000)
                   ORDER BY article_id
          ) C
      CROSS JOIN (SELECT @o:=0, @h:=0) T2
      ORDER BY ARTICLE_ID, CUMUL DESC) D
      WHERE max=3
      ;
      

      输出:

      article_id  GROU    OPT
      100 gender  1
      100 color   40
      100 size    36
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-04-23
        • 2017-04-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-09-26
        • 2016-02-18
        相关资源
        最近更新 更多