为什么在此MySQL查询中添加特定的where子句会导致性能查杀瓶颈?

时间:2012-04-09 01:19:23

标签: mysql sql performance optimization

对不起,我想给出一个完整的描述!我需要显示一个报告,其中显示有关来自另一个表格的ID的一些信息,以及有人在一个国家/地区内更改国家/地区的时间。请注意我如何在表中多次为同一个国家/地区输入一个id(因为信息会定期多次查询,但在此期间可能没有移动),并且还可以有不同的国家/地区条目(因为它们改变国家)。

快速解释数据: 我有下表:

CREATE TABLE IF NOT EXISTS `country` (
`id` mediumint(8) unsigned NOT NULL,
`timestamp` datetime NOT NULL,
`country` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`,`timestamp`),
KEY `country` (`country`),
KEY `timestamp` (`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

并且这些声明是这样的:

41352   2012-03-26 15:46:01     Jamaica
41352   2012-03-05 22:49:41     Jamaican Applicant
41352   2012-02-26 15:46:01     Jamaica
41352   2012-02-16 12:11:19     Jamaica
41352   2012-02-05 23:00:30     Jamaican Applicant

此表目前总共约有214,590行,但一旦测试数据被实际数据替换,就会有数百万。

我想要的是有关从y时间起离开x国家的所有人的一些信息。假设它是在上面的数据上运行的,我希望它输出的方式如下:

id  name    last    country     TIMESTAMP   o_timestamp
41352 Sweet Mercy   Jamaica     2012-03-26 15:46:01     2012-03-05 22:49:41
41352 Sweet Mercy   Jamaica     2012-02-16 12:11:19     2012-02-05 23:00:30

如果o_timestamp比某个日期(比如说100)更新,那么国家就是他们搬到的地方,他们来自的旧国家(未显示)是我传入查询的任何内容(牙买加申请人基于以上数据)

我开发了以下查询以满足要求并使用某个id进行测试:

SELECT a.id,
       c.name,
       c.last,
       a.country,
       a.timestamp,
       b.timestamp AS o_timestamp
FROM   country a
       INNER JOIN user_info c
         ON ( a.id = c.id )
       LEFT JOIN country AS b
         ON ( a.id = b.id
              AND a.timestamp != b.timestamp
              AND a.country != b.country )
WHERE  b.timestamp = (SELECT c.timestamp
                      FROM   country c
                      WHERE  a.id = c.id
                             AND a.timestamp > c.timestamp
                      ORDER  BY c.timestamp DESC
                      LIMIT  1) 
       AND a.id = 965

我完成了这个(总共7个,查询耗时0.0050秒)

并且扩展的解释揭示了以下内容:

id  select_type     table   type    possible_keys   key     key_len     ref     rows    filtered    Extra
1   PRIMARY     c   const   PRIMARY     PRIMARY     3   const   1   100.00  
1   PRIMARY     a   ref     PRIMARY     PRIMARY     3   const   16  100.00  
1   PRIMARY     b   eq_ref  PRIMARY,timestamp   PRIMARY     11  const,func  1   100.00  Using where
2   DEPENDENT SUBQUERY  c   index   PRIMARY,timestamp   timestamp   8   NULL    1   700.00  Using where; Using index

所以我认为我非常好并且突然出现:

SELECT a.id,
       c.name,
       c.last,
       a.country,
       a.timestamp,
       b.timestamp AS o_timestamp
FROM   country a
       INNER JOIN user_info c
         ON ( a.id = c.id )
       LEFT JOIN country AS b
         ON ( a.id = b.id
              AND a.timestamp != b.timestamp
              AND a.country != b.country )
WHERE  b.timestamp = (SELECT c.timestamp
                      FROM   country c
                      WHERE  a.id = c.id
                             AND a.timestamp > c.timestamp
                      ORDER  BY c.timestamp DESC
                      LIMIT  1) 
       AND b.country = "whatever" AND timestamp > DATE_SUB(NOW(), INTERVAL 7 DAY)

这个查询在一个有200条记录并且从未完成的国家(在下午和晚上外出后

完成了惊人的6分54秒完成 对于一个在数据库中有9000条记录的国家来说,回家一共大约8小时。在实际数据中,一个国家可以容易10000次。 100k不会是不合理的。

所以我解释扩展,得到这个:

id  select_type     table   type    possible_keys   key     key_len     ref     rows    filtered    Extra
1   PRIMARY     <derived2>  ALL     NULL    NULL    NULL    NULL    3003    100.00  
1   PRIMARY     c   eq_ref  PRIMARY     PRIMARY     3   b.id    1   100.00  
1   PRIMARY     a   ref     PRIMARY     PRIMARY     3   b.id    7   100.00  Using where
3   DEPENDENT SUBQUERY  c   index   PRIMARY,timestamp   timestamp   8   NULL    1   700.00  Using where; Using index
2   DERIVED     country     range   country,timestamp   country     195     NULL    474     100.00  Using where; Using index

所以它看起来更大,但并非不合理。

[删除了空间的配置变量,如果需要,请告诉我,以及自问题查询以来的性能信息。]

如果我错过了什么,请告诉我。

3 个答案:

答案 0 :(得分:2)

问题不是添加标准;它正在放弃一个正在造成伤害的人。在原始查询中,您有:

AND a.id = 965

这意味着查询执行不需要读取整个acountry)表。在您的第二个遇到性能的查询中,您将该标准更改为:

AND b.country = "whatever"
AND timestamp > DATE_SUB(NOW(), INTERVAL 7 DAY)

您在a上不再有严格限制标准,因此工作起来要慢得多。

当意识到bcountry的另一个引用时,事情变得更加复杂。然而,从a上的条件到b(其中b位于外连接的外侧)的变化并非微不足道;处理查询条件需要更长的时间。


  

这是否意味着因为我不是在寻找具体的身份证,我运气不好?

使用给定的查询结构,答案似乎是“是”,但是,我们说,给定的查询结构可能是次优的。

“处理一个ID”查询的“足够快”是:

SELECT a.id,
       c.name,
       c.last,
       a.country,
       a.timestamp,
       b.timestamp AS o_timestamp
FROM   country a
       INNER JOIN user_info c
         ON ( a.id = c.id )
       LEFT JOIN country AS b
         ON ( a.id = b.id
              AND a.timestamp != b.timestamp
              AND a.country != b.country )
WHERE  b.timestamp = (SELECT c.timestamp
                      FROM   country c
                      WHERE  a.id = c.id
                             AND a.timestamp > c.timestamp
                      ORDER  BY c.timestamp DESC
                      LIMIT  1) 
       AND a.id = 965

我不完全理解这个查询以及它正在尝试做什么。您需要知道外连接比内连接更昂贵,并且外连接表上的条件如

b.timestamp = (...correlated sub-query...)

非常昂贵。一个问题是b列中可能存在NULL,包括timestamp,但子查询被浪费在那,因为除非值非空,否则不满足条件,所以我们最终想知道'为什么要加入'?

当您添加修订后的条件时,您应该收到“模糊列名称”错误,因为该时间戳可能来自ac。此外,b.country = "whatever"条件是仅在b值不为空时才有意义的情况,因此OUTER连接也是可疑的。

据我了解,country表包含有关谁进入哪个国家/地区以及何时进入的记录。另外,FWIW,我可以肯定地确定与user_info表的连接是一个可以忽略的性能问题;问题全部归结为country表的三个引用。


从一些澄清中判断,你可以逐步建立查询,也许就是这样。

  1. 查找记录在时间顺序上相同的id的每对国家/地区记录,其中较旧的一对是针对给定国家/地区(“牙买加申请人”),较新的是对于另一个国家。

    这很简单:

    SELECT a.id, a.country, a.timestamp, b.country, b.timestamp
      FROM country AS a
      JOIN country AS b
        ON a.id = b.id
       AND b.timestamp > a.timestamp
       AND a.country = 'Jamaica Applicant'
       AND b.country != a.country
    

    这可以完成大部分工作,但不能确保条目的相邻性。要做到这一点,我们必须坚持country表中没有记录两个时间戳ida.timestamp之间(但不包括)b.timestamp的相同SELECT a.id, a.country AS o_country, a.timestamp AS o_timestamp, b.country AS n_country, b.timestamp AS n_timestamp FROM country AS a JOIN country AS b ON a.id = b.id AND b.timestamp > a.timestamp AND a.country = 'Jamaica Applicant' AND b.country != a.country WHERE NOT EXISTS (SELECT * FROM country AS c WHERE c.timestamp > a.timestamp AND c.timestamp < b.timestamp AND c.id = a.id ) 。这是一个额外的NOT EXISTS条件:

    user_info

    请注意,BETWEEN和符号不合适。它包括范围内的终点,但我们明确需要排除终点。

  2. 鉴于上面的国家/地区条目列表,我们现在需要选择那些......嗯,那么,标准是什么?我想您可以选择,但结果可以轻松地与SELECT e.id, u.name, u.last, e.o_country, e.o_timestamp, e.n_country, e_n_timestamp FROM (SELECT a.id, a.country AS o_country, a.timestamp AS o_timestamp, b.country AS n_country, b.timestamp AS n_timestamp FROM country AS a JOIN country AS b ON a.id = b.id AND b.timestamp > a.timestamp AND a.country = 'Jamaica Applicant' AND b.country != a.country WHERE NOT EXISTS (SELECT * FROM country AS c WHERE c.timestamp > a.timestamp AND c.timestamp < b.timestamp AND c.id = a.id ) ) AS e JOIN user_info AS u ON e.id = u.id WHERE e.o_timestamp > DATE_SUB(NOW(), INTERVAL 7 DAY); 表结合使用:

    {{1}}
  3. 我不打算保证性能会更好(甚至它在语法上是正确的;它还没有超过SQL DBMS)。但我认为获取相邻日期的复杂查询结构更整洁,可能比原始代码更好。特别要注意,它不使用任何外连接,(显式)排序或限制子句。这应该会有所帮助。

答案 1 :(得分:0)

您应该查看此参考:http://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html#function_now

http://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html#function_date-add

它说的是,NOW()函数可以返回一个字符串(取决于上下文),而date_add可以返回一个字符串(取决于参数)。我的猜测是,你正在获取字符串,然后只在比较中投射到日期(在每条记录上都会发生)。你能试试AND时间戳&gt;施放(DATE_SUB(NOW(),INTERVAL 7 DAY)作为日期时间),这可能会提高性能。

答案 2 :(得分:0)

我并不是说这是一个完成的解决方案,但这是一个我将回归的开始。请告诉我这对您的测试数据集的效果如何 -

SELECT ui.*, c1.*, MAX(c2.timestamp)
FROM country c1
INNER JOIN user_info ui
    ON c1.id = ui.id
INNER JOIN country c2
    ON c1.id = c2.id
    AND c1.timestamp > c2.timestamp
    AND c1.country <> c2.country
WHERE c2.timestamp > DATE_SUB(NOW(), INTERVAL 7 DAY)
AND c2.country = 'somewhere'
GROUP BY c1.id

下一步是添加LEFT JOIN以确保其间没有其他记录 -

SELECT ui.*, c1.*, c2.timestamp
FROM country c1
INNER JOIN user_info ui
    ON c1.id = ui.id
INNER JOIN country c2
    ON c1.id = c2.id
    AND c1.timestamp > c2.timestamp
    AND c1.country <> c2.country

LEFT JOIN country c3
    ON c1.id = c3.id
    AND c1.timetsamp > c3.timestamp
    AND c2.timestamp < c2.timetsamp

WHERE c2.timestamp > DATE_SUB(NOW(), INTERVAL 7 DAY)
AND c2.country = 'somewhere'
AND c3.id IS NULL