为啥这个 select 语句这么慢?

Posted

技术标签:

【中文标题】为啥这个 select 语句这么慢?【英文标题】:Why is this select statement so slow?为什么这个 select 语句这么慢? 【发布时间】:2018-05-13 23:06:20 【问题描述】:

此 select 语句运行速度非常慢。完成执行需要 10 多秒。可能会更长,但我不知道,因为与 mysql 的连接超时。这是一个单独的问题。

代码如下:

SELECT 
    f.id, f.name, GROUP_CONCAT(DISTINCT (c.firstname)) children
FROM
    families f,
    children c,
    transactions t
WHERE
    f.companyid = 1170 AND f.id = t.familyid
        AND f.id = c.familyid
        AND t.transactiontype = 'P'
        AND t.taxdeductible = 'Y'
        AND YEAR(t.date) = 2017
        AND status = 'A'
        OR f.id = 9779432
GROUP BY f.id
ORDER BY name;

我确实有有关family.companyid、children.familyid、transactions.transactiontype、transactions.taxdeductible 和transactions.date 的索引。

尽管有我的索引,它是否有任何理由进行全表扫描?还是有其他原因导致此查询运行缓慢?

编辑:按照以下 cmets 填写一些空白:

children 表在 73,000 行中有 17MB 的数据。 family 表在 56,000 行中有 6MB 的数据 事务表在 980,000 行中有 83MB 的数据。

儿童桌

CREATE TABLE `children` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `familyid` int(10) unsigned DEFAULT '0',
  `companyid` int(11) DEFAULT '0',
  `picture` varchar(250) DEFAULT NULL,
  `stockpicture` varchar(1) DEFAULT 'N',
  `firstname` varchar(250) DEFAULT NULL,
  `lastname` varchar(250) DEFAULT NULL,
  `nickname` varbinary(250) DEFAULT NULL,
  `birthdate` date NOT NULL DEFAULT '0000-00-00',
  `usecustomfee` varchar(1) NOT NULL DEFAULT 'N',
  `usecustomproviderfee` varchar(1) NOT NULL DEFAULT 'N',
  `customfee` decimal(10,2) DEFAULT '0.00',
  `customfeetypecode` varchar(45) DEFAULT 'MONTH',
  `customproviderfee` decimal(10,2) DEFAULT '0.00',
  `customproviderfeetypecode` varchar(45) DEFAULT 'MONTH',
  `usecustomchargeitem` varchar(1) DEFAULT 'N',
  `customchargeitem` int(11) DEFAULT '0',
  `dailyrate` decimal(10,2) DEFAULT '55.00',
  `startdate` date DEFAULT NULL,
  `enddate` date DEFAULT NULL,
  `subsidynotrequired` char(1) NOT NULL DEFAULT 'Y',
  `subsidychildid` varchar(250) DEFAULT NULL,
  `subsidyapplicantid` varchar(250) DEFAULT NULL,
  `subsidynote` text,
  `waitingsince` date DEFAULT NULL,
  `waitingroom` int(11) DEFAULT NULL,
  `waitingtype` varchar(1) DEFAULT 'F',
  `preferredstart` date DEFAULT NULL,
  `registrationdate` date DEFAULT NULL,
  `groupid` int(11) NOT NULL DEFAULT '0',
  `providerisparent` varchar(1) NOT NULL DEFAULT 'N',
  `attendingschool` char(1) NOT NULL DEFAULT 'N',
  `schoolname` varchar(250) DEFAULT NULL,
  `liveswithmother` char(1) NOT NULL DEFAULT 'Y',
  `liveswithfather` char(1) NOT NULL DEFAULT 'Y',
  `liveswithother` char(1) NOT NULL DEFAULT 'N',
  `otherguardian` varchar(250) DEFAULT NULL,
  `sex` char(1) NOT NULL DEFAULT 'M',
  `note` text,
  `archived` char(1) NOT NULL DEFAULT 'N',
  `priorityid` int(11) DEFAULT '0',
  `onlineregistration` varchar(1) NOT NULL DEFAULT 'N',
  `onlineregistrationaccept` varchar(1) NOT NULL DEFAULT 'N',
  `registrationconfirmed` varchar(1) NOT NULL DEFAULT 'N',
  `registrationconfirmeddate` datetime DEFAULT NULL,
  `createddate` datetime DEFAULT NULL,
  `modifieddate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `fullpart` varchar(1) DEFAULT 'F',
  `parttimedays` int(11) DEFAULT '10',
  `parttimedaystype` varchar(45) DEFAULT 'D',
  `parttimedaystypecode` varchar(45) DEFAULT 'MONTH',
  `program` varchar(45) DEFAULT 'daycare',
  `registrationnote` varchar(2000) DEFAULT NULL,
  `registrationnoteread` varchar(1) DEFAULT 'N',
  `registrationsubsidy` varchar(45) DEFAULT 'noplan',
  `registrationsubsidydate` datetime DEFAULT NULL,
  `registrationsubsidyamount` decimal(10,2) DEFAULT '0.00',
  PRIMARY KEY (`id`),
  KEY `Familyid` (`familyid`),
  KEY `companyid` (`companyid`),
  KEY `startdate` (`startdate`),
  KEY `enddate` (`enddate`),
  KEY `roomid` (`groupid`),
  KEY `providerisparent` (`providerisparent`)
) ENGINE=InnoDB AUTO_INCREMENT=93685 DEFAULT CHARSET=latin1;

家庭表

CREATE TABLE `families` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `accountnumber` varchar(100) DEFAULT NULL,
  `name` varchar(245) NOT NULL COMMENT 'The account name will typically be the name of the parent responsible for payment',
  `motherid` int(10) unsigned NOT NULL,
  `fatherid` int(10) unsigned NOT NULL,
  `balance` decimal(10,2) NOT NULL DEFAULT '0.00',
  `notes` varchar(2000) DEFAULT NULL,
  `companyid` int(10) unsigned NOT NULL,
  `status` varchar(1) NOT NULL DEFAULT 'A',
  `financialaidrequired` char(1) NOT NULL DEFAULT 'N',
  `intakesurveyid` int(10) unsigned DEFAULT NULL,
  `referralid` int(10) unsigned NOT NULL DEFAULT '0',
  `registrationemailrequired` varchar(1) DEFAULT 'N',
  `registrationemailsent` varchar(1) DEFAULT 'N',
  `registrationemaildate` date DEFAULT NULL,
  `registrationemailaddressfound` varchar(1) DEFAULT NULL,
  `waitinglistemailrequired` varchar(1) DEFAULT 'N',
  `waitinglistemailsent` varchar(1) DEFAULT 'N',
  `waitinglistemaildate` date DEFAULT NULL,
  `waitinglistemailaddressfound` varchar(1) DEFAULT NULL,
  `activationemailrequired` varchar(1) DEFAULT 'N',
  `activationemailsent` varchar(1) DEFAULT 'N',
  `activationemaildate` date DEFAULT NULL,
  `activationemailaddressfound` varchar(1) DEFAULT NULL,
  `createddate` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `companyid` (`companyid`),
  KEY `intakesurveyid` (`intakesurveyid`),
  KEY `status` (`status`)
) ENGINE=InnoDB AUTO_INCREMENT=9803007 DEFAULT CHARSET=latin1;

交易表

CREATE TABLE `transactions` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `familyid` int(10) unsigned NOT NULL,
  `date` datetime NOT NULL,
  `transactiontype` varchar(1) NOT NULL DEFAULT 'C' COMMENT '''C'' = Charge, ''P'' = Payment',
  `paymenttype` varchar(3) DEFAULT NULL COMMENT '''DBT'' = Debit, ''CSH'' = Cash, ''CRE'' = Credit Card, ''CHQ'' = Cheque, ''MNY'' = Money Order,''EFT'' = Electronic Funds Transfer',
  `comment` varchar(500) DEFAULT NULL,
  `amount` decimal(10,2) NOT NULL DEFAULT '0.00',
  `reference` varchar(45) DEFAULT NULL,
  `chargeitem` int(10) unsigned DEFAULT '0',
  `taxdeductible` varchar(1) NOT NULL DEFAULT 'Y',
  `payer` varchar(1) DEFAULT 'M',
  `createddate` datetime DEFAULT NULL,
  `modifieddate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `Familyid` (`familyid`),
  KEY `Transaction Type` (`transactiontype`),
  KEY `Tax Deductible` (`taxdeductible`),
  KEY `date` (`date`)
) ENGINE=InnoDB AUTO_INCREMENT=1013472 DEFAULT CHARSET=latin1 ROW_FORMAT=DYNAMIC;

【问题讨论】:

1) 你在这里跑题了;请参阅 DBA 网站 2)您没有提供足够的信息;例如你的表的体积(对于小表,从不使用索引) 3)“我失去了我的连接 mysql”看起来是一个完全不同的问题,应该首先解决 4)加上括号,因为 X AND Y OR Z 读起来不明确,确保它做你想做的事。 4) 在表之间使用正确的 SQL 语法和 INNER JOIN 关于查询性能的问题总是需要针对所有相关的 SHOW CREATE TABLE 语句以及 EXPLAIN 的结果。另外,请注意YEAR(t.date) = 2017 不能使用索引,但t.date BETWEEN '2017-01-01' AND '2017-12-31' 可以。 您没有提供足够的信息让我们帮助您。请read this note about asking good SQL questions,并关注查询性能部分。那么请edit你的问题。 @PatrickMevzek - “失去连接”通常意味着查询运行时间过长。因此,这个正确的论坛。 (当然,OP 不知道这一点。) @PatrickMevzek - 我将优化视为 SE 和 SO 之间的灰色区域。我说“正确”的解决方案不会通过配置。我对程序员编写草率代码的公司感到恼火,然后将其扔给因需要重新编写查询而流泪的DBA。这个特定的问题大部分由程序员解决。 【参考方案1】:

假设你的意思是这个(这就是 MySQL 将如何解释它):

(this AND that ...) OR (f.id=...)

让我们使用UNION 而不是OR。 (OR 优化不佳。)

让我们也使用“标准”JOIN...ON 而不是“逗号连接”。

我们不要在函数中隐藏列 (YEAR);它禁止使用索引。

你已经因为没有说哪个表包含status而受到指责。我看到 Hamoon 不小心丢失了 statusf 中的事实(?)。我会假设的。

DISTINCT 不是函数,所以我删除了它后面的括号。

我会选择UNION DISTINCT(较慢,但与OR 的语义匹配)而不是UNION ALL(较快,但可能会重复一行)。

我会将children 移到外部SELECT 以避免一些潜在的问题。

GROUP BYORDER BY 匹配时,查询可以运行得更快。所以,假设idname 在逻辑上是联系在一起的,我认为这会给你相同的分组和排序:

GROUP BY name, id
ORDER BY name, id

把我所有的建议放在一起:

SELECT  x.id, x.name,
        GROUP_CONCAT(DISTINCT c.firstname) children
    FROM (
           ( SELECT  f.id, f.name,
                FROM  families f
                JOIN  transactions t  ON f.id = t.familyid
                WHERE  f.companyid = 1170
                  AND  t.transactiontype = 'P'
                  AND  t.taxdeductible = 'Y'
                  AND  t.date >= '2017-01-01'
                  AND  t.date <  '2017-01-01' + INTERVAL 1 YEAR
                  AND  f.status = 'A'
           )
           UNION DISTINCT
           ( SELECT   f.id, f.name
                FROM  families f
                WHERE  f.id = 9779432
           ) 
         ) AS x
    JOIN  children c  ON x.id = c.familyid
    GROUP BY  x.name, x.id
    ORDER BY  x.name, x.id 

您将需要这些索引。列顺序通常很重要。

f:  I assume it has PRIMARY KEY(id)
f:  (companyid, status)   -- in either order
t:  (familyid, transactiontype, taxdeductible, date)
t:  (transactiontype, taxdeductible, date, familyid)
c:  (familyid, firstname)

一些注意事项:

我为t 提供了两个索引——同时提供这两个索引,从而让优化器决定是从f 还是t 开始。 一些索引是“覆盖”的,从而提供了额外的提升。 重新表述后,GROUP_CONCAT 中的DISTINCT 可能不需要了。 多个单列索引通常不如像“复合”(多列)索引那样有益。

【讨论】:

是的,它是 f.status。不知道为什么在编辑中删除它,虽然我最初没有它,所以也许这就是他删除它的原因? 哇!与我的版本在 10 秒后超时相比,您的查询在 2.2 秒内返回结果。虽然 2.2 秒还是有点长,但比我的要好很多,谢谢! @Vincent - 嗯... 2.2 看起来确实很高。结果集中有多少行? GROUP BY 生效前多少行?桌子有多大?提供EXPLAIN SELECT ... 我是否正确解释了AND/OR 致其他阅读此问答的人 -- 这是一个相当极端的例子,说明重新编写查询对性能至关重要。 @Vincent - 我的第 5 段简要解释了 YEAR。我在t 上的两个索引都利用了重新表述。【参考方案2】:

试试

EXPLAIN
SELECT 
    f.id, f.name, GROUP_CONCAT(DISTINCT (c.firstname)) children
FROM
    families f,
    children c,
    transactions t
WHERE
    f.companyid = 1170 AND f.id = t.familyid
        AND f.id = c.familyid
        AND t.transactiontype = 'P'
        AND t.taxdeductible = 'Y'
        AND YEAR(t.date) = 2017
        AND f.status = 'A'
        OR f.id = 9779432
GROUP BY f.id
ORDER BY name;

确保加载正确的索引

您说您“有索引”,但每个查询只能使用 1 个索引,为您需要的查询创建 1 索引。

另外我建议不要使用多个from,而是使用JOIN 语句,而不是能够针对连接表索引和索引

【讨论】:

每个查询只有 1 个索引?我不确定我是否理解。请您进一步解释该评论。 @Vincent - 有一种很少使用的技术,称为“索引合并相交”,可以使用 2 个索引。但是构建等效的“复合”索引效率较低(就像我在答案中所做的那样)。【参考方案3】:

请提供您的表格架构。我们需要检查您有哪些索引。

同时您可以尝试JOIN 表并删除ORDER BY。 从我看你只有一个f.id = 9779432,那你为什么要订购相同的价值呢?

检查您的OR 条件,我已将其转换为对我有意义的东西。您最初的陈述具有广泛的 OR 意味着您需要任何东西 YEAR(t.date) OR f.id = 9779432 这对您有任何意义吗?

SELECT 
    f.id, f.name, GROUP_CONCAT(DISTINCT (c.firstname)) children
FROM
    families f
INNER JOIN children c
ON f.id = c.familyid
INNER JOIN transactions t
ON f.id = t.familyid
   AND t.transactiontype = 'P'
   AND t.taxdeductible = 'Y'
   AND YEAR(t.date) = 2017
WHERE
    (f.companyid = 1170 OR f.id = 9779432)
    AND f.status = 'A'

GROUP BY f.id;

【讨论】:

请使用ON 说明表格之间的关系,使用WHERE 进行过滤。 按要求添加了表架构。【参考方案4】:

最好使用 21 世纪的 JOIN 语法。

SELECT f.id, f.name, GROUP_CONCAT(DISTINCT (c.firstname)) children
  FROM families f
  JOIN children c ON f.id = c.familyid
  JOIN transactions t ON f.id = t.familyid
 WHERE f.companyid = 1170 
   AND t.transactiontype = 'P'
   AND t.taxdeductible = 'Y'
   AND YEAR(t.date) = 2017
   AND status = 'A'
    OR f.id = 9779432
 GROUP BY f.id
 ORDER BY name;

AND YEAR(t.date) = 2017 更改为AND t.date &gt;='2017-01-01 AND t.date &lt; '2018-01-01'。为什么?该过滤子句的YEAR() 形式不是sargeable。

从您的问题中无法判断哪个表包含 status 列,这对性能非常重要。如果是t.status,则尝试在

上创建复合索引
 transaction(status, transactiontype, taxdeductible, date, familyid)

然后在

上尝试复合索引
 transaction(familyid, status, transactiontype, taxdeductible, date)

其中一个应该会有很大帮助。为什么?当满足您对transaction 表的查询时,MySQL 可以随机访问第一个符合条件的记录的索引:匹配所有= 条件并具有最低值date 的记录。然后它可以顺序扫描索引,直到找到最后一个符合条件的日期。

使用表现最好的索引。

如果status 列不在transaction 表中,则将其从该索引中删除。

【讨论】:

以上是关于为啥这个 select 语句这么慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥select count(_) from t,在InnoDB引擎中比MyISAM 慢

sqlserver2000,为啥执行时很慢?仅仅是300条数据。求大神帮助

为啥我们要合并几个select语句

MYSQL数据库性能调优之三:explain分析慢查询

为啥打印到标准输出这么慢?可以提速吗?

MySQL - SELECT WHERE field IN(子查询) - 为啥非常慢?