为什么这个 select 语句这么慢?

Why is this select statement so slow?

这个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;

我在 families.companyid、children.familyid、transactions.transactiontype、transactions.taxdeductible 和 transactions.date 上有索引。

有什么理由不顾我的索引而进行完整的 table 扫描吗?或者还有其他原因导致此查询运行缓慢?

编辑:根据以下评论填写一些空白:

  • children table 有 73,000 行的 17MB 数据。
  • 家族 table 在 56,000 行中有 6MB 的数据
  • 交易 table 在 980,000 行中有 83MB 的数据。

    CHILDREN TABLE

    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;
    

    家庭TABLE

    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;
    

    笔交易 TABLE

    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;
    
  • 尝试

    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;
    

    确保加载了正确的索引

    你说你"have indexes on",但你每次查询只能使用1个索引,为你需要的查询制作1索引。

    此外,我建议永远不要使用倍数 from,而是使用 JOIN 语句,以便能够针对连接的 table 索引和索引

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

    同时您可以尝试 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;
    

    最好使用 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.

    无法根据您的问题判断哪个 table 包含 status 列,而且它对性能 非常重要 。如果是 t.status,则尝试在

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

    然后在

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

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

    使用性能最佳的索引。

    如果 status 列不在 transaction table 中,则将其从该索引中取出。

    假设你是这个意思(MySQL 会这样解释它):

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

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

    让我们也使用 'standard' JOIN...ON 而不是 'commajoin'。

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

    您已经因为没有说出哪个 table 包含 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 提供了 2 个索引——同时提供这两个索引,从而让优化器决定是从 f 还是 t 开始。
    • 一些索引是 'covering',从而提供额外的提升。
    • 重新制定后,GROUP_CONCAT中的DISTINCT可能是不必要的。
    • 多个单列索引通常不如 'composite'(多列)索引有益。