【问题标题】:2 very similar SQL queries have totally different performance2 个非常相似的 SQL 查询具有完全不同的性能
【发布时间】:2017-10-06 19:22:45
【问题描述】:

我有以下 2 个 SQL 查询,95% 相同,但性能明显不同。

SQL 查询 1 (

SELECT CONCAT(a.`report_year`, '-', a. `report_month`) as `yearmonth`,
 AVG(a.cost_leasing/b.rate*IF(`report_year`=2016,0.73235,
       IF(`report_year`=2017,0.83430,1))) as average,
 'current' as `type`
FROM `vehicles` as a, `exchange_rates` as b
WHERE cid='3' AND
 STR_TO_DATE(CONCAT(`report_year`, '-', `report_month`, '-01'),
         '%Y-%m-%d') >= '2016-01-01' AND
 LAST_DAY(STR_TO_DATE(CONCAT(`report_year`, '-', `report_month`,
        '-01'), '%Y-%m-%d')) <= '2017-06-30' AND
 `country` IN ('XX','UK') AND
 a.currency = b.currency AND
 b.`year` = `report_year` AND
 fxid=2
GROUP BY `yearmonth`
ORDER BY `yearmonth`;

解释查询 1:

1   SIMPLE  a   ref new_selectors,...   new_cost_leasing    4   const   10812   Using where; Using index; Using temporary; Using f...   
1   SIMPLE  b   ref PRIMARY,date,fxid   fxid    19  const,c1682fleet.a.report_year,c1682fleet.a.curren...   196 Using where; Using index    

SQL查询2(>3s):

SELECT CONCAT(c.`report_year`, '-', c.`report_month`) as `yearmonth`,
 AVG(c.cost_leasing/d.rate*IF(`report_year`=2016,0.73235,
        IF(`report_year`=2017,0.83430,1))),
 'baseline'
FROM `kpis` as c, `exchange_rates` as d
WHERE cid='3' AND
 STR_TO_DATE(CONCAT(`report_year`, '-', `report_month`, '-01'),
      '%Y-%m-%d') >= '2016-01-01' AND
 LAST_DAY(STR_TO_DATE(CONCAT(`report_year`, '-', `report_month`,
            '-01'), '%Y-%m-%d')) <= '2017-06-30' AND
 `country` IN ('XX','UK') AND
 c.kid=1 AND
 c.currency = d.currency AND
 d.`year` = `report_year` AND
 fxid=2
GROUP BY `yearmonth`
ORDER BY `yearmonth`;

解释查询 2:

1   SIMPLE  c   ref oem_group,...   cost_leasing    8   const,const 30038   Using where; Using index; Using temporary; Using f...   
1   SIMPLE  d   ref PRIMARY,date,fxid   fxid    19  const,c1682fleet.c.report_year,c1682fleet.c.curren...   196 Using where; Using index

显示车辆索引:

vehicles    0   PRIMARY 1   vid A   146068              BTREE           
vehicles    1   new_cost_leasing    1   cid A   12              BTREE           
vehicles    1   new_cost_leasing    2   cost_leasing    A   4564                BTREE           
vehicles    1   new_cost_leasing    3   currency    A   5216                BTREE           
vehicles    1   new_cost_leasing    4   report_month    A   24344               BTREE           
vehicles    1   new_cost_leasing    5   report_year A   29213               BTREE           
vehicles    1   new_cost_leasing    6   country A   36517               BTREE           
vehicles    1   new_cost_leasing    7   supplier    A   29213               BTREE           
vehicles    1   new_cost_leasing    8   jato_segment    A   24344               BTREE           
vehicles    1   new_cost_leasing    9   business_unit   A   36517               BTREE           
vehicles    1   new_cost_leasing    10  entity  A   73034               BTREE           

从 exchange_rates 显示索引:

exchange_rates  0   PRIMARY 1   fxid    A   2               BTREE           
exchange_rates  0   PRIMARY 2   currency    A   160             BTREE           
exchange_rates  0   PRIMARY 3   date    A   569250              BTREE           
exchange_rates  1   date    1   fxid    A   2               BTREE           
exchange_rates  1   date    2   date    A   28462               BTREE           
exchange_rates  1   date    3   currency    A   569250              BTREE           
exchange_rates  1   date    4   rate    A   569250              BTREE           
exchange_rates  1   fxid    1   fxid    A   2               BTREE           
exchange_rates  1   fxid    2   year    A   114             BTREE           
exchange_rates  1   fxid    3   currency    A   2904                BTREE           
exchange_rates  1   fxid    4   rate    A   569250              BTREE           

显示来自 kpis 的索引:

kpis    0   PRIMARY 1   vid A   60308               BTREE                   
kpis    1   cost_leasing    1   cid A   2               BTREE           
kpis    1   cost_leasing    2   kid A   2               BTREE           
kpis    1   cost_leasing    3   cost_leasing    A   78              BTREE           
kpis    1   cost_leasing    4   currency    A   78              BTREE           
kpis    1   cost_leasing    5   report_month    A   1096                BTREE           
kpis    1   cost_leasing    6   report_year A   3350                BTREE           
kpis    1   cost_leasing    7   country A   1884                BTREE           
kpis    1   cost_leasing    8   supplier    A   4020                BTREE           
kpis    1   cost_leasing    9   jato_segment    A   3015                BTREE           
kpis    1   cost_leasing    10  business_unit   A   4307                BTREE           
kpis    1   cost_leasing    11  entity  A   6030                BTREE           
kpis    1   avg_cost    1   cid A   2               BTREE           
kpis    1   avg_cost    2   kid A   2               BTREE           
kpis    1   avg_cost    3   country A   48              BTREE           
kpis    1   avg_cost    4   report_year A   96              BTREE           
kpis    1   avg_cost    5   currency    A   96              BTREE           
kpis    1   avg_cost    6   cost_leasing    A   172             BTREE       

问题: 我的问题是,为什么即使查询 2 (kid) 中只有一个附加条件(甚至是索引的一部分),性能差异也会如此显着(因子 30)。

任何人知道如何优化查询 2?

【问题讨论】:

  • 如果他们使用完全不同的表,我不会说他们 95% 相同。具有可能不同的结构、索引、记录数......需要更多信息。
  • 表是相同的,除了 kpis 还包含字段kid。索引相同,除了 kpi 索引还包含kid 作为列之外,它们是相同的。您可以在解释答案中看到受影响的行。
  • 你能发布你的索引定义吗?
  • 请阅读此内容,并特别注意查询性能部分。 meta.stackoverflow.com/a/271056edit 您的问题提供更多信息。您的一个索引可能缺少一列,但我们不知道是哪一个。
  • 我添加了相关的KPI指标。

标签: mysql sql select indexing query-performance


【解决方案1】:

我发现了问题:exchange_ratesyear 不是唯一的,vehicles 的选择只有 kpis 的选择的一半大小,但是由于非唯一列 @ 的基数很大987654325@ exchange_rateskpis 的连接创建了一个超过 200 万个条目的临时集合,这对于普通操作来说非常大。

解决方案:我没有使用year,而是使用了唯一列date,并将条件更改为

`date` = MAKEDATE(`report_year`, 1)

【讨论】:

  • 如果您在全球范围内知道 ONE SELECT 中使用的行数超过一百万行是没有意义的,那么 my.cnf/ini 中的这一行 ---- sql_select_limit = 1M # 以停止错误的数据量 - -- 在你的任期内为你工作 - 或者直到有人为你的 my.cfg 删除它。
【解决方案2】:

Sargable 是一个强项。更好地处理dates 是一个重点。这里还有一些要点。

11 列的索引几乎可以保证是一种浪费。即使是 6 列索引也不太可能被完全使用。仅使用索引的最左侧列。通常它会到达下一列无用的地步,所以它会停止。

通常,将日期拆分为年、月和日并不是一个好主意。由于您似乎只需要年份和月份,因此建议使用 CHAR(7) CHARSET ascii,其值类似于“2017-06”。或者您是否真的有报告会在月中停止?

请用所涉及的表来限定每个列名。知道在哪个表中是非常重要的,例如fxid

请使用JOIN .. ON 语法:

FROM vehicles AS a
JOIN exchange_rates AS b  ON a.currency = b.currency

(我更喜欢 AS vAS er 作为助记符。)

所需的索引(带有当前年份/月份列):

b,d: INDEX(fxid, currency, year)
a: INDEX(cid, currency, report_year)
c: INDEX(kid, cid, currency, report_year)

更多关于创建索引:http://mysql.rjweb.org/doc.php/index_cookbook_mysql

【讨论】:

    【解决方案3】:

    这些是不可分割的:

     STR_TO_DATE(CONCAT(`report_year`, '-', `report_month`, '-01'), '%Y-%m-%d') >= '2016-01-01' 
    
     LAST_DAY(STR_TO_DATE(CONCAT(`report_year`, '-', `report_month`, '-01'), '%Y-%m-%d')) <= '2017-06-30'
    

    对于每一行,您使用 5 个函数来连接并转换为日期,但只有 2 个日期常量被比较。如果您可以将其反转并将 2 个日期常量转换为适合未更改数据的内容,则会节省大量精力。不仅可以节省函数的计算工作量,还可以使用report_yearreport_month 上的索引。

    我还没来得及测试这么多,而且我猜测所涉及的列是整数,但我认为用于日期范围处理的一组更可预测的谓词将有助于这两个查询。例如

    SQL Fiddle

    MySQL 5.6 架构设置

    CREATE TABLE Table1
        (`Report_Year` int, `Report_Month` int)
    ;
    
    INSERT INTO Table1
        (`Report_Year`, `Report_Month`)
    VALUES
        (2015, 1), (2015, 2), (2015, 3),
        (2015, 4), (2015, 5), (2015, 6),
        (2015, 7), (2015, 8), (2015, 9),
        (2015, 10), (2015, 11), (2015, 12),
        (2016, 1), (2016, 2), (2016, 3),
        (2016, 4), (2016, 5), (2016, 6),
        (2016, 7), (2016, 8), (2016, 9),
        (2016, 10), (2016, 11), (2016, 12),
        (2017, 1), (2017, 2), (2017, 3),
        (2017, 4), (2017, 5), (2017, 6),
        (2017, 7), (2017, 8), (2017, 9),
        (2017, 10), (2017, 11), (2017, 12)
    ;
    

    **查询**:

    set @start := '2016-04-04';
    set @end := '2017-01-30';
    
    select *, @start, @end
    from table1
    where (
           ((year(@start) < year(@end)) AND report_year = year(@start) and report_month >= month(@start)) 
          OR
           ((year(@start) < year(@end)) AND report_year > year(@start) and report_year < year(@end)) 
          OR
           ((year(@start) <= year(@end)) AND report_year = year(@end) and report_month <= month(@end)) 
          )  
    

    [结果]

    | Report_Year | Report_Month |     @start |       @end |
    |-------------|--------------|------------|------------|
    |        2016 |            4 | 2016-04-04 | 2017-01-30 |
    |        2016 |            5 | 2016-04-04 | 2017-01-30 |
    |        2016 |            6 | 2016-04-04 | 2017-01-30 |
    |        2016 |            7 | 2016-04-04 | 2017-01-30 |
    |        2016 |            8 | 2016-04-04 | 2017-01-30 |
    |        2016 |            9 | 2016-04-04 | 2017-01-30 |
    |        2016 |           10 | 2016-04-04 | 2017-01-30 |
    |        2016 |           11 | 2016-04-04 | 2017-01-30 |
    |        2016 |           12 | 2016-04-04 | 2017-01-30 |
    |        2017 |            1 | 2016-04-04 | 2017-01-30 |
    

    [结果]

    set @start := '2016-01-01';
    set @end := '2016-06-30';
    
    | Report_Year | Report_Month |     @start |       @end |
    |-------------|--------------|------------|------------|
    |        2016 |            1 | 2016-01-01 | 2016-06-30 |
    |        2016 |            2 | 2016-01-01 | 2016-06-30 |
    |        2016 |            3 | 2016-01-01 | 2016-06-30 |
    |        2016 |            4 | 2016-01-01 | 2016-06-30 |
    |        2016 |            5 | 2016-01-01 | 2016-06-30 |
    |        2016 |            6 | 2016-01-01 | 2016-06-30 |
    
    set @start := '2016-01-01';
    set @end := '2017-06-30';
    

    [结果]

    | Report_Year | Report_Month |     @start |       @end |
    |-------------|--------------|------------|------------|
    |        2016 |            1 | 2016-01-01 | 2017-06-30 |
    |        2016 |            2 | 2016-01-01 | 2017-06-30 |
    |        2016 |            3 | 2016-01-01 | 2017-06-30 |
    |        2016 |            4 | 2016-01-01 | 2017-06-30 |
    |        2016 |            5 | 2016-01-01 | 2017-06-30 |
    |        2016 |            6 | 2016-01-01 | 2017-06-30 |
    |        2016 |            7 | 2016-01-01 | 2017-06-30 |
    |        2016 |            8 | 2016-01-01 | 2017-06-30 |
    |        2016 |            9 | 2016-01-01 | 2017-06-30 |
    |        2016 |           10 | 2016-01-01 | 2017-06-30 |
    |        2016 |           11 | 2016-01-01 | 2017-06-30 |
    |        2016 |           12 | 2016-01-01 | 2017-06-30 |
    |        2017 |            1 | 2016-01-01 | 2017-06-30 |
    |        2017 |            2 | 2016-01-01 | 2017-06-30 |
    |        2017 |            3 | 2016-01-01 | 2017-06-30 |
    |        2017 |            4 | 2016-01-01 | 2017-06-30 |
    |        2017 |            5 | 2016-01-01 | 2017-06-30 |
    |        2017 |            6 | 2016-01-01 | 2017-06-30 |
    

    【讨论】:

    • 感谢您提供用于“可搜索谓词”的示例。
    • 在 sqlfiddle.com 的查询中缺少分号,再次感谢。
    • 分号在 sqlfiddle 退出,但在选择标记布局时被删除...该布局中可能存在 sqlfiddle 错误。
    • 顺便说一句,如果可以选择更改数据模型,我建议使用日期字段而不是年份和月份列。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-20
    • 2021-11-09
    相关资源
    最近更新 更多