为啥这个 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 不小心丢失了 status
在 f
中的事实(?)。我会假设的。
DISTINCT
不是函数,所以我删除了它后面的括号。
我会选择UNION DISTINCT
(较慢,但与OR
的语义匹配)而不是UNION ALL
(较快,但可能会重复一行)。
我会将children
移到外部SELECT
以避免一些潜在的问题。
当GROUP BY
和ORDER BY
匹配时,查询可以运行得更快。所以,假设id
和name
在逻辑上是联系在一起的,我认为这会给你相同的分组和排序:
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 >='2017-01-01 AND t.date < '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 慢