加入两个大表很慢
Joining two large tables very slow
我有两个大table我必须加入
第一个:
CREATE TABLE IF NOT EXISTS `cdr` (
`calldate` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`clid` varchar(80) NOT NULL DEFAULT '',
`src` varchar(80) NOT NULL DEFAULT '',
`dst` varchar(80) NOT NULL DEFAULT '',
`dcontext` varchar(80) NOT NULL DEFAULT '',
`channel` varchar(80) NOT NULL DEFAULT '',
`dstchannel` varchar(80) NOT NULL DEFAULT '',
`lastapp` varchar(80) NOT NULL DEFAULT '',
`lastdata` varchar(80) NOT NULL DEFAULT '',
`duration` decimal(11,6) NOT NULL DEFAULT '0.000000',
`billsec` decimal(11,6) NOT NULL DEFAULT '0.000000',
`disposition` varchar(45) NOT NULL DEFAULT '',
`amaflags` int(11) NOT NULL DEFAULT '0',
`accountcode` varchar(20) NOT NULL DEFAULT '',
`uniqueid` varchar(32) NOT NULL DEFAULT '',
`userfield` varchar(255) NOT NULL DEFAULT '',
`cost` char(20) NOT NULL DEFAULT 'none',
`zone` char(60) NOT NULL DEFAULT 'none',
`profile` char(10) NOT NULL DEFAULT 'none',
`tariff` char(8) NOT NULL DEFAULT 'none',
`status` char(10) NOT NULL DEFAULT 'none',
`answer` datetime NOT NULL,
`end` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Indexes for table `cdr`
--
ALTER TABLE `cdr`
ADD KEY `src` (`src`),
ADD KEY `accountcode` (`accountcode`),
ADD KEY `status` (`status`),
ADD KEY `uniqueid` (`uniqueid`),
ADD KEY `calldate` (`calldate`);
第二个
CREATE TABLE IF NOT EXISTS `routes` (
`id` int(4) NOT NULL,
`route` char(35) NOT NULL,
`zonenum` int(4) NOT NULL,
`comment` char(50) CHARACTER SET latin2 DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT '1',
`wholesaledst` varchar(60) NOT NULL,
`nabava` char(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `routes`
ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `route` (`route`), ADD KEY `zonenum` (`zonenum`);
第一个 table 包含大约 350 万行,第二个包含大约 35 000 行。对于第一个 table 中的每条记录,我必须从第二个 table 中获取 zonenum。
这是我的查询
SELECT src, accountcode, zonenum, calldate, answer, end
FROM cdr
LEFT OUTER JOIN routes ON src LIKE route
WHERE calldate BETWEEN '2015-03-01' AND '2015-04-01' AND status = 'INCOMING' AND accountcode != 110 AND disposition = 'ANSWERED';
例如
src 看起来像 095346435 和 route 看起来像 095%
查询需要大约 7 分钟才能执行。如果我删除加入它只有 1.5 秒。
当检查 mysql 慢速查询日志时,它说查询已经检查了 140 万行,这大约是 where 子句之后的行数 * routes
table 中的行数。我试过使用子查询,临时 table.. 一切,但总是太慢
有什么办法可以加快查询速度吗?还是我错过了一些索引?请帮帮我,我很绝望。
更新
这是EXPLAIN
结果,如果有帮助的话
您的查询未使用任何键在路由 table 中进行查找。 EXPLAIN中的index表示进行了索引扫描,还是不好,因为我们要查看每条记录。
请注意,src LIKE route
不能使用密钥。正如 CindyH 指出的那样,src = route
会更好。当然会给出不同的结果。您的逻辑是否真的需要 src LIKE route
,或者 = 就足够了?
如果您确实需要 LIKE,解决方案将取决于原因,但它将涉及在路由上构建某种形式的 FULLTEXT 索引,使用本机 FULLTEXT 或使用您自己的 table 相关的手动构建子字符串。或者也许是其他一些创造性的解决方案。
编辑:
根据附加信息,假设所有路由都只是前缀,我提出以下 "creative" 解决方案:
- 写一个存储函数(或者只是一个丑陋的大案例...when)从 src 中提取前缀(国家代码?)并向其附加 %。
- 将
src LIKE route
替换为concat(whatever computes the prefix, '%') = route
这将在路线上使用钥匙。
了解如何构建最符合您要求的索引。您需要一个复合索引(有时也包括 COVERING 索引),具体取决于它如何最适合您的 tables / 情况。
由于您正在查看特定日期和状态的通话数据记录,我建议在
上建立索引
cdr table index ( disposition, status, calldate, accountcode )
包含 JOIN 条件的覆盖索引也将包含 SRC 列,例如...
cdr table index ( disposition, status, calldate, accountcode, src )
将索引视为优化排序的一种方式。如果你有两个房间,每个房间都有一份通话记录。
房间A只按1列排序,比如账户代码,你必须去每组账户,然后找到状态,区域,日期范围。
房间 B,根据上面的示例索引对其进行了多重排序。所以,在房间里,假设有文件柜。每个柜子都有自己的配置,所以你直接跳到那些"ANSWERED"而忽略其他的。
在那个柜子里,有两个抽屉...进货和出货...所以现在您只剩下 1 个抽屉了。
从那个抽屉里,它们按日期排序。所以现在您可以直接跳转到您想要合格记录的日期范围。然后它是跳过所有那些帐户代码 110 的问题...
这是思考索引如何工作的前提。永远不要完全依赖单个列,您需要它们根据您的查询与其他列一起工作。
由于您需要从路线 table 获取 ZONE Number,因此应该在
上有一个 COVERING 索引
ROUTES 的索引 table ... ( route, zonenum )
因此引擎不必返回到原始数据页面来获取区域。它是索引的一部分,可以直接 return 。同样,如果您也需要第二部分,请不要依赖单个列作为索引。
由于 routes
小得多,因此可能值得连接到包含它的子查询。在该查询中,您可以 SELECT LEFT(route, 3) AS rtPre, zonenum
并加入 rtPre,如下所示:
SELECT cdr.src, cdr.accountcode, r.zonenum, cdr.calldate, cdr.answer, cdr.end
FROM cdr
LEFT JOIN (
SELECT LEFT(route, 3) AS rtPre, zonenum
FROM routes
) AS r ON LEFT(cdr.src, 3) = r.rtPre
WHERE cdr.calldate BETWEEN '2015-03-01' AND '2015-04-01'
AND cdr.status = 'INCOMING'
AND cdr.accountcode != 110
AND cdr.disposition = 'ANSWERED'
;
如果仍然没有帮助,您可以将该子查询插入临时 table,并在 rtPre
上建立索引;并在 JOIN.
中使用 table
如果这种查询将 运行 频繁出现,您甚至可能需要考虑 routes
中的一个永久性 "prefix" 字段,它可以被索引。
...当然,这做出了一个巨大的假设,即所有 cdr.src 值都是 3 个字符和一个 %。 (如果这是一个错误的假设,前缀类型的解决方案可能仍然可用。LEFT(cdr.src, [standard prefix length]) = r.rtPre AND r.route LIKE cdr.src
可以利用最小前缀来减少所需的 LIKE
比较。
我有两个大table我必须加入
第一个:
CREATE TABLE IF NOT EXISTS `cdr` (
`calldate` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`clid` varchar(80) NOT NULL DEFAULT '',
`src` varchar(80) NOT NULL DEFAULT '',
`dst` varchar(80) NOT NULL DEFAULT '',
`dcontext` varchar(80) NOT NULL DEFAULT '',
`channel` varchar(80) NOT NULL DEFAULT '',
`dstchannel` varchar(80) NOT NULL DEFAULT '',
`lastapp` varchar(80) NOT NULL DEFAULT '',
`lastdata` varchar(80) NOT NULL DEFAULT '',
`duration` decimal(11,6) NOT NULL DEFAULT '0.000000',
`billsec` decimal(11,6) NOT NULL DEFAULT '0.000000',
`disposition` varchar(45) NOT NULL DEFAULT '',
`amaflags` int(11) NOT NULL DEFAULT '0',
`accountcode` varchar(20) NOT NULL DEFAULT '',
`uniqueid` varchar(32) NOT NULL DEFAULT '',
`userfield` varchar(255) NOT NULL DEFAULT '',
`cost` char(20) NOT NULL DEFAULT 'none',
`zone` char(60) NOT NULL DEFAULT 'none',
`profile` char(10) NOT NULL DEFAULT 'none',
`tariff` char(8) NOT NULL DEFAULT 'none',
`status` char(10) NOT NULL DEFAULT 'none',
`answer` datetime NOT NULL,
`end` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Indexes for table `cdr`
--
ALTER TABLE `cdr`
ADD KEY `src` (`src`),
ADD KEY `accountcode` (`accountcode`),
ADD KEY `status` (`status`),
ADD KEY `uniqueid` (`uniqueid`),
ADD KEY `calldate` (`calldate`);
第二个
CREATE TABLE IF NOT EXISTS `routes` (
`id` int(4) NOT NULL,
`route` char(35) NOT NULL,
`zonenum` int(4) NOT NULL,
`comment` char(50) CHARACTER SET latin2 DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT '1',
`wholesaledst` varchar(60) NOT NULL,
`nabava` char(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `routes`
ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `route` (`route`), ADD KEY `zonenum` (`zonenum`);
第一个 table 包含大约 350 万行,第二个包含大约 35 000 行。对于第一个 table 中的每条记录,我必须从第二个 table 中获取 zonenum。
这是我的查询
SELECT src, accountcode, zonenum, calldate, answer, end
FROM cdr
LEFT OUTER JOIN routes ON src LIKE route
WHERE calldate BETWEEN '2015-03-01' AND '2015-04-01' AND status = 'INCOMING' AND accountcode != 110 AND disposition = 'ANSWERED';
例如 src 看起来像 095346435 和 route 看起来像 095%
查询需要大约 7 分钟才能执行。如果我删除加入它只有 1.5 秒。
当检查 mysql 慢速查询日志时,它说查询已经检查了 140 万行,这大约是 where 子句之后的行数 * routes
table 中的行数。我试过使用子查询,临时 table.. 一切,但总是太慢
有什么办法可以加快查询速度吗?还是我错过了一些索引?请帮帮我,我很绝望。
更新
这是EXPLAIN
结果,如果有帮助的话
您的查询未使用任何键在路由 table 中进行查找。 EXPLAIN中的index表示进行了索引扫描,还是不好,因为我们要查看每条记录。
请注意,src LIKE route
不能使用密钥。正如 CindyH 指出的那样,src = route
会更好。当然会给出不同的结果。您的逻辑是否真的需要 src LIKE route
,或者 = 就足够了?
如果您确实需要 LIKE,解决方案将取决于原因,但它将涉及在路由上构建某种形式的 FULLTEXT 索引,使用本机 FULLTEXT 或使用您自己的 table 相关的手动构建子字符串。或者也许是其他一些创造性的解决方案。
编辑:
根据附加信息,假设所有路由都只是前缀,我提出以下 "creative" 解决方案:
- 写一个存储函数(或者只是一个丑陋的大案例...when)从 src 中提取前缀(国家代码?)并向其附加 %。
- 将
src LIKE route
替换为concat(whatever computes the prefix, '%') = route
这将在路线上使用钥匙。
了解如何构建最符合您要求的索引。您需要一个复合索引(有时也包括 COVERING 索引),具体取决于它如何最适合您的 tables / 情况。
由于您正在查看特定日期和状态的通话数据记录,我建议在
上建立索引cdr table index ( disposition, status, calldate, accountcode )
包含 JOIN 条件的覆盖索引也将包含 SRC 列,例如...
cdr table index ( disposition, status, calldate, accountcode, src )
将索引视为优化排序的一种方式。如果你有两个房间,每个房间都有一份通话记录。
房间A只按1列排序,比如账户代码,你必须去每组账户,然后找到状态,区域,日期范围。
房间 B,根据上面的示例索引对其进行了多重排序。所以,在房间里,假设有文件柜。每个柜子都有自己的配置,所以你直接跳到那些"ANSWERED"而忽略其他的。
在那个柜子里,有两个抽屉...进货和出货...所以现在您只剩下 1 个抽屉了。
从那个抽屉里,它们按日期排序。所以现在您可以直接跳转到您想要合格记录的日期范围。然后它是跳过所有那些帐户代码 110 的问题...
这是思考索引如何工作的前提。永远不要完全依赖单个列,您需要它们根据您的查询与其他列一起工作。
由于您需要从路线 table 获取 ZONE Number,因此应该在
上有一个 COVERING 索引ROUTES 的索引 table ... ( route, zonenum )
因此引擎不必返回到原始数据页面来获取区域。它是索引的一部分,可以直接 return 。同样,如果您也需要第二部分,请不要依赖单个列作为索引。
由于 routes
小得多,因此可能值得连接到包含它的子查询。在该查询中,您可以 SELECT LEFT(route, 3) AS rtPre, zonenum
并加入 rtPre,如下所示:
SELECT cdr.src, cdr.accountcode, r.zonenum, cdr.calldate, cdr.answer, cdr.end
FROM cdr
LEFT JOIN (
SELECT LEFT(route, 3) AS rtPre, zonenum
FROM routes
) AS r ON LEFT(cdr.src, 3) = r.rtPre
WHERE cdr.calldate BETWEEN '2015-03-01' AND '2015-04-01'
AND cdr.status = 'INCOMING'
AND cdr.accountcode != 110
AND cdr.disposition = 'ANSWERED'
;
如果仍然没有帮助,您可以将该子查询插入临时 table,并在 rtPre
上建立索引;并在 JOIN.
如果这种查询将 运行 频繁出现,您甚至可能需要考虑 routes
中的一个永久性 "prefix" 字段,它可以被索引。
...当然,这做出了一个巨大的假设,即所有 cdr.src 值都是 3 个字符和一个 %。 (如果这是一个错误的假设,前缀类型的解决方案可能仍然可用。LEFT(cdr.src, [standard prefix length]) = r.rtPre AND r.route LIKE cdr.src
可以利用最小前缀来减少所需的 LIKE
比较。