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

时间:2014-06-12 16:23:05

标签: mysql sql database query-optimization outer-join

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

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

后端进程的工作方式如下 - 如果当天该帐户的该服务器上没有活动,则不会为该帐户写入任何记录。如果有活动,则会在当天"LogDate"写一条记录。这发生在多个服务器上。因此,数据管理表总体上没有行(每天对该客户没有任何活动),一行(活动当天只在一台服务器上)或多行(当天活动在多台服务器上)。

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

无论是否有任何使用(或曾经或在选定的时间段内),我们都需要在Accounts表中列出每个客户,即使它们没有显示用途。因此,这似乎需要一个外部联接。

以下是我正在使用的查询:

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秒钟才能运行。 但是,如果删除“或datausage.LogDate为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上的索引,AccountID上的索引,LogDataLogData上的索引,AccountID以及LogData上的索引。这些都没有任何区别。

我还尝试使用UNION ALL一个带有logdata范围的查询和另一个查询,其中logdata为null,但结果差不多(实际上有点差)。

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

更新:在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 

2 个答案:

答案 0 :(得分: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 ;

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

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

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)

答案 1 :(得分:0)

当您使用datausage离开联接时,您应该尽可能地限制输出。 (JOIN表示AND表示WHERE表示ON。将条件基本上放在任何明确的顺序和/或必要时进行优化。)当没有使用时,结果将是一个空的扩展行;你想留下那一行。

当您使用AccountPlans加入时,您不希望引入空行(无论如何都不会发生),因此这只是一个内部联接。

以下版本将AccountPlan连接作为内连接并放在第一位。 (索引)帐户FK PlanID到AccountPlan意味着DBMS知道内部联接只会为每个帐户PK生成一行。所以输出有关键的AccountId。该行可以立即内部连接到datausage。 (其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