需要帮助优化外连接 SQL 查询

Posted

技术标签:

【中文标题】需要帮助优化外连接 SQL 查询【英文标题】:Need help optimizing outer join SQL query 【发布时间】:2014-08-03 01:40:42 【问题描述】:

我希望就如何使用外连接优化此查询的性能获得一些建议。首先我会解释我想要做什么,然后我会展示代码和结果。

我有一个包含所有客户帐户列表的帐户表。我有一个数据使用表,可以跟踪每个客户使用了多少数据。在多台服务器上运行的后端进程每天将记录插入到 datausage 表中,以跟踪该服务器上每个客户当天发生的使用量。

后端流程是这样工作的 - 如果当天该服务器上没有针对某个帐户的活动,则不会为该帐户写入任何记录。如果有活动,则用当天的"LogDate" 写入一条记录。这发生在多台服务器上。因此,总的来说,datausage 表最终没有行(该客户每天根本没有活动)、一行(当天活动仅在一台服务器上)或多行(当天活动在多台服务器上)。

我们需要生成一份报告,列出所有客户以及他们在特定日期范围内的使用情况。一些客户可能根本没有使用(datausage 表中没有任何内容)。一些客户可能在当前期间完全没有使用(但在其他期间使用)。

无论是否有任何使用情况(曾经,或在选定的时间段内),我们都需要将帐户表中的每个客户都列在报告中,即使他们没有显示使用情况。因此,这似乎需要外部连接。

这是我正在使用的查询:

SELECT
   Accounts.accountID as AccountID,
   IFNULL(Accounts.name,Accounts.accountID) as AccountName,
   AccountPlans.plantype as AccountType,
   Accounts.status as AccountStatus,
   date(Accounts.created_at) as Created,
   sum(IFNULL(datausage.Core,0) + (IFNULL(datausage.CoreDeluxe,0) * 3)) as 'CoreData'
FROM `Accounts` 
 LEFT JOIN `datausage` on `Accounts`.`accountID` = `datausage`.`accountID`
 LEFT JOIN `AccountPlans` on `AccountPlans`.`PlanID` = `Accounts`.`PlanID`
WHERE
(
   (`datausage`.`LogDate` >= '2014-06-01' and `datausage`.`LogDate` < '2014-07-01') 
   or `datausage`.`LogDate` is null
) 
GROUP BY Accounts.accountID 
ORDER BY `AccountName` asc 

此查询大约需要 2 秒才能运行。 但是,如果“or datausage.LogDate is NULL”被删除,运行只需要 0.3 秒。 但是,我似乎必须在其中包含该子句,因为没有使用的帐户被排除在结果之外如果不出现则设置。

这是表格数据:

| id | select_type | table        | type   | possible_keys                                           | key     | key_len | ref                  | rows  | Extra                                                  |
+----+-------------+--------------+--------+---------------------------------------------------------+---------+---------+----------------------+-------    +----------------------------------------------------+
|  1 | SIMPLE      | Accounts     | ALL    | PRIMARY,accounts_planid_foreign,accounts_cardid_foreign | NULL    | NULL    | NULL                 |    57 | Using     temporary; Using filesort                    |
|  1 | SIMPLE      | datausage   | ALL    | NULL                                                    | NULL    | NULL    | NULL                 | 96805 | Using where;     Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | AccountPlans | eq_ref | PRIMARY                                                 | PRIMARY | 4       | mydb.Accounts.planID |     1 | NULL                                                   |

Accounts 表的索引如下:

| Table    | Non_unique | Key_name                | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+----------+------------+-------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Accounts |          0 | PRIMARY                 |            1 | accountID   | A         |          57 |     NULL | NULL   |      | BTREE      |         |               |
| Accounts |          1 | accounts_planid_foreign |            1 | planID      | A         |           5 |     NULL | NULL   |      | BTREE      |         |               |
| Accounts |          1 | accounts_cardid_foreign |            1 | cardID      | A         |           0 |     NULL | NULL   | YES  | BTREE      |         |               |

datausage表上的索引如下:

| Table      | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| datausage |          0 | PRIMARY  |            1 | UsageID     | A         |       96805 |     NULL | NULL   |      | BTREE      |         |               |

我尝试在 datausage 上创建不同的索引以查看是否有帮助,但没有任何帮助。我尝试了AccountID 上的索引、AccountIDLogData 上的索引和LogDataAccountID 上的索引以及LogData 上的索引。这些都没有任何区别。

我还尝试将UNION ALL 与其中一个查询与 logdata 范围一起使用,而另一个查询恰好在 logdata 为空的地方,但结果大致相同(实际上有点糟糕)。

有人可以帮助我了解可能发生的情况以及我可以优化查询执行时间的方法吗?谢谢!!

更新:应 Philipxy 的要求,这里是表定义。请注意,我删除了一些与此查询无关的列和约束,以帮助保持尽可能紧凑和干净。

CREATE TABLE `Accounts` (
   `accountID` varchar(25) NOT NULL,
   `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
   `status` int(11) NOT NULL,
   `planID` int(10) unsigned NOT NULL DEFAULT '1',
   `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
   PRIMARY KEY (`accountID`),
   KEY `accounts_planid_foreign` (`planID`),
   KEY `acctname_id_ndx` (`name`,`accountID`),
   CONSTRAINT `accounts_planid_foreign` FOREIGN KEY (`planID`) REFERENCES `AccountPlans` (`planID`)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 


CREATE TABLE `datausage` (
   `UsageID` int(11) NOT NULL AUTO_INCREMENT,
   `Core` int(11) DEFAULT NULL,
   `CoreDelux` int(11) DEFAULT NULL,
   `AccountID` varchar(25) DEFAULT NULL,
   `LogDate` date DEFAULT NULL
   PRIMARY KEY (`UsageID`),
   KEY `acctusage` (`AccountID`,`LogDate`)
   ) ENGINE=MyISAM AUTO_INCREMENT=104303 DEFAULT CHARSET=latin1 


CREATE TABLE `AccountPlans` (
   `planID` int(10) unsigned NOT NULL AUTO_INCREMENT,
   `name` varchar(150) COLLATE utf8_unicode_ci NOT NULL,
   `params` text COLLATE utf8_unicode_ci NOT NULL,
   `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
   `plantype` varchar(25) COLLATE utf8_unicode_ci NOT NULL,
   PRIMARY KEY (`planID`),
   KEY `acctplans_id_type_ndx` (`planID`,`plantype`)
 ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 

【问题讨论】:

表格定义是什么?特别是,哪些列可以为空,在您计时的情况下为空? 发布足以让 sqlfiddle.com 供我们测试的 SQL 表定义会很有帮助。 请为您的查询和一些答案查询更新您对当前键的解释。如果您的定义以 sqlfiddle(错别字和顺序)加上示例数据值运行,这样回答者可以生成解释,这很有帮助。重新引擎:对于基线,尝试使用 FK datausage accountid 到帐户的所有 innodb。 (没有 mysql innodb-isam fks。)FK 可能非常重要。 嗨,philipxy - 您想要哪个查询的 EXPLAIN 输出?我原来的一个,还是这里发布的其他一个(如果是,是哪一个)?我需要一个多星期才能更新系统以将表从 myISAM 转换为 InnoDB。我认为这可能是主要问题。并且将 LogData 定义为 NOT NULL 可能会有所帮助。当我能够在系统上试用时,我将在月底将这些结果发回这里。 嗨,philipxy 和所有 - 我终于能够继续工作了。我尝试的第一件事是将数据使用表从 myIASM 转换为 InnoDB。令人惊讶的是,它没有任何用途。然后我将 LogDate 列更改为 NOT NULL,这也没有帮助。我创建了您要求的 SQL 小提琴:sqlfiddle.com/#!9/f3259/4/0。请注意,此处提供的所有版本的查询(包括我自己的)都具有相同的 EXPLAIN 计划。请注意,虽然这个查询速度很快,但它的数据使用量只有十几行。在我们的真实系统中,它有近 100,000 行。谢谢! 【参考方案1】:

首先,您可以通过将where 子句移至on 子句来简化查询:

SELECT a.accountID as AccountID, coalesce(a.name, a.accountID) as AccountName,
       ap.plantype as AccountType, a.status as AccountStatus,
       date(a.created_at) as Created,
       sum(coalesce(du.Core, 0) + (coalesce(du.CoreDeluxe, 0) * 3)) as CoreData
FROM Accounts a LEFT JOIN 
     datausage du
     on a.accountID = du.`accountID` AND
        du.`LogDate` >= '2014-06-01' and du.`LogDate` < '2014-07-01'
LEFT JOIN 
     AccountPlans ap
     on ap.`PlanID` = a.`PlanID`
GROUP BY a.accountID 
ORDER BY AccountName asc ;

(我还引入了表别名以使查询更易于阅读。)

这个版本应该更好地利用索引,因为它消除了where 子句中的or。但是,它仍然不会使用外部排序的索引。以下可能会更好:

SELECT a.accountID as AccountID, coalesce(a.name, a.accountID) as AccountName,
       ap.plantype as AccountType, a.status as AccountStatus,
       date(a.created_at) as Created,
       sum(coalesce(du.Core, 0) + (coalesce(du.CoreDeluxe, 0) * 3)) as CoreData
FROM Accounts a LEFT JOIN 
     datausage du
     on a.accountID = du.`accountID` AND
        du.LogDate >= '2014-06-01' and du.LogDate < '2014-07-01'LEFT JOIN 
     AccountPlans ap
     on ap.PlanID = a.PlanID
GROUP BY a.accountID 
ORDER BY a.name, a.accountID ;

为此,我会推荐以下索引:

Accounts(name, AccountId)
Datausage(AccountId, LogDate)
AccountPlans(PlanId, PlanType)

【讨论】:

@user2838966 请注意,通过移动 WHERE 约束,Gordon 保留了您的联接的“外部性”,这在您的查询版本中丢失了。 谢谢草莓!我刚刚尝试了您提供的最后一条 SQL 语句以及索引,但查询仍然需要 2 秒才能运行。和我之前的没什么不同。乍一看,EXPLAIN 输出也是相同的。还有其他想法吗? 另外 - 我刚刚尝试了您编写的第一个查询,它的执行与第一个没有什么不同。总而言之,您的查询和我的查询都需要 2 秒。我们还能尝试什么?谢谢。 戈登 - 上面的意思是说谢谢,但写了“草莓”。我也很感谢他的帮助。如果您认为有什么可以改进的,请告诉我。 这两个查询仅在 ORDER BY 上有所不同。【参考方案2】:

当您使用 datausage 离开 join 时,您应该尽可能地限制输出。 (JOIN 意味着 AND 意味着 WHERE 意味着 ON。基本上以任何顺序放置条件,以便在必要时清楚和/或优化。)当没有使用时,结果将是一个空扩展行;您想保留该行。

当您加入 AccountPlans 时,您不想引入空行(无论如何都不会发生),所以这只是一个内部联接。

以下版本将 AccountPlan 联接作为内部联接并放在首位。 (索引)Accounts FK PlanID 到 AccountPlan 意味着 DBMS 知道内部连接只会为每个 Accounts PK 生成一行。所以输出有关键的AccountId。该行可以立即内部连接到数据使用。 (其 AccountID 上的索引应该会有所帮助,例如对于合并连接。)相反,外部连接结果上没有 PlanID 键/索引来与 AccountPlan 连接。

SELECT
   a.accountID as AccountID,
   IFNULL(a.name,a.accountID) as AccountName,
   ap.plantype as AccountType,
   a.status as AccountStatus,
   date(a.created_at) as Created,
   sum(IFNULL(du.Core,0) + (IFNULL(du.CoreDeluxe,0) * 3)) as CoreData
FROM Accounts a
 JOIN AccountPlans ap ON ap.PlanID = a.PlanID
 LEFT JOIN datausage du ON a.accountID = du.accountID AND du.LogDate >= '2014-06-01' AND du.LogDate < '2014-07-01'
GROUP BY a.accountID

【讨论】:

谢谢philipxy。不幸的是,您的查询运行速度较慢(2.4 秒),然后是我的原始查询和 Gordon 建议的两个查询所用的 2.0 秒(也是 2.0 秒)。到目前为止,这些查询的所有版本中的 EXPLAIN 输出都是相同的。我拥有的索引与 Gordon 建议的一样。从这里到哪里?谢谢! 我还要补充一点,AccountPlans 表和plantype 的包含对此处提供的任何查询(包括我自己的)的查询速度没有影响。为了简单起见,我打算在示例中省略它,但我认为我最好将其包含在内,因为一旦提出了成功的查询,我可能不知道如何重新处理查询的那部分。 有人有建议我可以试试吗?到目前为止,这些查询的性能与我原来的没有什么不同。谢谢。 正如我刚才问的,请发送您的表定义,因为它们会影响优化,尤其是列是否可以为空。 感谢您的留言 philipxy!抱歉,我没有看到您之前对表定义的请求。我只是将它们添加到上面的原始帖子中。请注意,我删除了查询中未使用的一些列名,以帮助保持简短。关于 NULL 列,如果重要的话,是的,LogDate 肯定可以重新定义为 NOT NULL,因为 datausage 中的所有行都将始终具有 LogDate。当然,当从没有匹配的外部连接返回时,它总是为 NULL。另外我刚刚注意到 datausage 表是 myISAM 而其他表是 InnoDB 以防万一?

以上是关于需要帮助优化外连接 SQL 查询的主要内容,如果未能解决你的问题,请参考以下文章

对两个 MySQL 查询执行左外连接?

删除重复的左外连接

具有许多表、左外连接和 where 子句的 LINQ 查询

SQL查询,外连接,cte?需要使用“左”值修复运行总计

MySQLDQL之连接查询

SQL的连接(外连接内连接交叉连接和自连接)