【问题标题】: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。这个特定的问题大部分由程序员解决。

    标签: mysql query-performance


    【解决方案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 表中,则将其从该索引中删除。

    【讨论】:

      猜你喜欢
      • 2011-06-01
      • 2012-04-02
      • 1970-01-01
      • 2017-07-17
      • 2023-03-05
      • 1970-01-01
      • 1970-01-01
      • 2021-11-07
      • 2015-07-31
      相关资源
      最近更新 更多