使用SELECT进入SELECT,MySQL将CPU 100%加载一分钟

时间:2015-12-13 11:54:37

标签: mysql

情况

我的SQL查询包含两部分。第一个很简单:

SELECT * FROM `clients` WHERE id IN (...)

这个将返回有关具有特定ID的客户的所有信息

第二个查询要大得多,它应该为之前的查询准备ID列表(为了更好的可读性,我省略了几个UNION,你可以在最后看到完整的查询):

SELECT client_id
  FROM `contact_persons`
  WHERE id IN (
              SELECT owner_id
                FROM `contacts`
                WHERE contact_info LIKE '%keyword%' AND company_or_person = 'person'
  )
UNION
SELECT owner_id
  FROM `contacts`
  WHERE contact_info LIKE '%keyword%' AND company_or_person = 'company'
UNION
SELECT id
  FROM `clients`
  WHERE client_name LIKE '%keyword%'

每个查询都需要不到0.5秒的时间来单独执行。但是,当我将第二个查询放入第一个查询时,它使两个查询完成时间超过一分钟,此时CPU负载跳跃到100%。

我尝试了什么

  • 分别运行此查询的两个部分。结果:一切都足够快(少于0.5秒)
  • SELECT * ...更改为SELECT id ...(尽管这会使整个查询无效)。结果:没有改变
  • 使用少数不同的关键字以避免缓存
  • 使内部部分(大部分)返回0行。结果:没有改变。完整的查询仍然非常慢

看起来如果我单独运行查询的这些部分并将ID列表存储在php变量中它会正常工作,但即使对于我作为初学者来说也是如此错误。

完成查询

SELECT * FROM `clients` WHERE deleted = 0 AND id IN (
SELECT client_id as found
  FROM `contact_persons`
  WHERE id IN (
              SELECT owner_id
                FROM `contacts`
                WHERE contact_info LIKE '%keyword%' AND company_or_person = 'person'
 )
UNION
SELECT owner_id as found
  FROM `contacts`
  WHERE contact_info LIKE '%keyword%' AND company_or_person = 'company'
UNION
SELECT id as found
  FROM `clients`
  WHERE client_name LIKE '%keyword%'
UNION
SELECT client_id as found
  FROM `cargo`
  WHERE cargo_name LIKE '%keyword%'
UNION
SELECT page_id as found
  FROM `comments`
  WHERE message LIKE '%keyword%' AND page_type = 'client' AND deleted = 0
UNION
SELECT client_id as found
  FROM `contact_persons`
  WHERE person_name LIKE '%keyword%')

我的数据库非常小(160 Kb),特别是表clients只有160行和5列。我尝试了所有我想出的但仍然无法解决问题。

更新1. EXPLAIN

我得到EXPLAIN的结果 enter image description here

更新2.这不是关于UNION

我刚刚运行了这个最小的查询,我用UNION切断了每个部分。结果:几乎相同的时间 - 57秒执行

SELECT SQL_NO_CACHE id FROM `clients` WHERE id IN (
 SELECT client_id
   FROM `contact_persons`
   WHERE id IN (
               SELECT owner_id
                 FROM `contacts`
                 WHERE contact_info LIKE '%keyword%'
             )
)

更新3.解决方案

正如所建议的那样,我试图用IN替换INNER JOIN中的一个,这就是诀窍,约60秒现在转向~0.4s

所以而不是

SELECT .. WHERE .. IN (SELECT .. WHERE .. IN (SELECT ..))

我写的就像

SELECT .. AS t1 INNER JOIN (SELECT .. WHERE .. IN (SELECT ..)) AS t2 WHERE ti.id = t2.id

对于人类来说,这是完全相同的事情,但看起来它不适用于MySQL服务器。

如果有人好奇,我会在此处发布EXPLAIN输出作为最终查询:

enter image description here

2 个答案:

答案 0 :(得分:3)

您的查询可能过于复杂,查询优化器会出现错误的优化。正如@huhushow建议的那样,针对您的查询运行EXPLAIN可能会显示此信息。

您可以尝试几种方法。

  • 更简单的方法是单独运行查询,并以编程方式累积所有ID,然后使用重复数据删除的ID列表构建最后一个查询。这对你来说是“错误的”。实际上,随着更多ID的检索,这种方法可能会变得尴尬。

  • 类似的解决方案是使用INSERT IGNOREfound上的唯一索引将查询结果累积到临时表中,以确保重复数据删除。然后,您将在clients表和临时表之间运行JOIN。

  • 一种完全不同的方法是为SQL查询优化器提供更多内存和时间限制,以鼓励它找到更快的查询计划。这里的困难在于干预mysqld的参数(您可能没有被授权这样做)以及稍后更改查询可能导致优化器再次失败的事实;换句话说,这个解决方案不一定稳定。

  • 另一种方法是将外部SELECT扩散到UNION中,从而将UNION带出来;即,而不是做

    SELECT a.* WHERE id IN ( select1 UNION select2 UNION select3 )
    

    由于生成id的位置与使用位置之间的“分离度”太多而无法轻易优化,您可以先做

    SELECT a.* WHERE id IN ( select1 )
    UNION
    SELECT a.* WHERE id IN ( select2 )
    UNION
    SELECT a.* WHERE id IN ( select3 )
    

    然后,反过来,您将合并IN将其转换为JOIN。例如第一组:

    SELECT * FROM `clients` WHERE deleted = 0 AND id IN (
        SELECT client_id as found
        FROM `contact_persons`
        WHERE id IN (
          SELECT owner_id
            FROM `contacts`
            WHERE contact_info LIKE '%keyword%' AND company_or_person
            = 'person' )
    

    将成为第一个:

    SELECT * FROM `clients` WHERE deleted = 0 AND id IN (
        SELECT cp.client_id as found
        FROM `contact_persons` AS cp
        JOIN `contacts` AS c ON (cp.id = c.owner_id)
            WHERE c.contact_info LIKE '%keyword%' AND c.company_or_person
            = 'person' )
    )
    

    然后最后是一个在优化器上更容易的表单:

    SELECT [DISTINCT] clients.* FROM `clients`
        JOIN `contact_persons` AS cp ON (clients.id = cp.client_id)
        JOIN `contacts` AS c ON (cp.id = c.owner_id)
             WHERE clients.deleted = 0 
             AND c.contact_info LIKE '%keyword%'
             AND c.company_or_person = 'person'
    

    作为最后一个阶段,您将添加索引:例如,在上面的案例中,您基于owner_id({= 1}}从contacts选择company_or_person并且{ {1}}。你可以因此

    contact_info

    这也允许完整地执行所有子查询,并查看是否有其中一个特别减慢了事情。

答案 1 :(得分:0)

首先检查您的查询的解释查询计划。像这样

EXPLAIN SELECT * FROM `clients` WHERE deleted = 0 AND id IN (
SELECT client_id as found
  FROM `contact_persons`
  WHERE id IN (
              SELECT owner_id
                FROM `contacts`
                WHERE contact_info LIKE '%keyword%' AND company_or_person = 'person'
 )
UNION
SELECT owner_id as found
  FROM `contacts`
  WHERE contact_info LIKE '%keyword%' AND company_or_person = 'company'
UNION
SELECT id as found
  FROM `clients`
  WHERE client_name LIKE '%keyword%'
UNION
SELECT client_id as found
  FROM `cargo`
  WHERE cargo_name LIKE '%keyword%'
UNION
SELECT page_id as found
  FROM `comments`
  WHERE message LIKE '%keyword%' AND page_type = 'client' AND deleted = 0
UNION
SELECT client_id as found
  FROM `contact_persons`
  WHERE person_name LIKE '%keyword%')

这可以找出查询的哪个部分很慢。 mysql official manual对于理解解释结果非常有帮助。