根据来自相同 table 的结果优化从 table 中选择所有行?
Optimize selecting all rows from a table based on results from the same table?
我会第一个承认我不擅长SQL(我可能不应该把它当作滚动日志文件),但我想知道我是否可以得到一些改进一些慢速查询的建议...
我有一个很大的 mysql table,有 200 万行,我根据最新数据的一个子集进行了两次完整的 table 查找。当我加载包含这些查询的页面时,我经常发现它们需要几秒钟才能完成,但里面的查询非常快。
PMA 的(据说很糟糕)顾问几乎把整个厨房水槽都扔给我,临时 tables,种类太多,没有索引的连接(我什至没有任何连接?),阅读固定位置,读取下一个位置,临时 tables 写入磁盘...最后一个特别让我想知道它是否是配置问题,但我尝试了所有旋钮,甚至支付了没有托管服务的费用' 似乎有帮助。
CREATE TABLE `archive` (
`id` bigint UNSIGNED NOT NULL,
`ip` varchar(15) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`service` enum('ssh','telnet','ftp','pop3','imap','rdp','vnc','sql','http','smb','smtp','dns','sip','ldap') CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`hostid` bigint UNSIGNED NOT NULL,
`date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
ALTER TABLE `archive`
ADD PRIMARY KEY (`id`),
ADD KEY `service` (`service`),
ADD KEY `date` (`date`),
ADD KEY `ip` (`ip`),
ADD KEY `date-ip` (`date`,`ip`),
ADD KEY `date-service` (`date`,`service`),
ADD KEY `ip-date` (`ip`,`date`),
ADD KEY `ip-service` (`ip`,`service`),
ADD KEY `service-date` (`service`,`date`),
ADD KEY `service-ip` (`service`,`ip`);
添加索引肯定有帮助(即使它们是实际数据大小的 4 倍),但我有点不知所措,我可以进一步优化。最初我想在 php 中缓存子查询结果并在主查询中使用它两次,但我认为一旦关闭子查询我就无法访问结果。我研究了连接,但它们看起来像是用于 2 个或更多单独的 table,但子查询来自同一个 table,所以我不确定这是否有效任何一个。查询应该根据我在过去 24 小时内是否有来自 ip 的数据来找到最活跃的 ip/services...
SELECT service, COUNT(service) AS total FROM `archive`
WHERE ip IN
(SELECT DISTINCT ip FROM `archive` WHERE date > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR))
GROUP BY service HAVING total > 1
ORDER BY total DESC, service ASC LIMIT 10
+----+--------------+-----------------+------------+-------+----------------------------------------------------------------------------+------------+---------+------------------------+-------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+-----------------+------------+-------+----------------------------------------------------------------------------+------------+---------+------------------------+-------+----------+---------------------------------+
| 1 | SIMPLE | <subquery2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | archive | NULL | ref | service,ip,date-service,ip-date,ip-service,service-date,service-ip | ip-service | 47 | <subquery2>.ip | 5 | 100.00 | Using index |
| 2 | MATERIALIZED | archive | NULL | range | date,ip,date-ip,date-service,ip-date,ip-service | date-ip | 5 | NULL | 44246 | 100.00 | Using where; Using index |
+----+--------------+-----------------+------------+-------+----------------------------------------------------------------------------+------------+---------+------------------------+-------+----------+---------------------------------+
SELECT ip, COUNT(ip) AS total FROM `archive`
WHERE ip IN
(SELECT DISTINCT ip FROM `archive` WHERE date > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR))
GROUP BY ip HAVING total > 1
ORDER BY total DESC, INET_ATON(ip) ASC LIMIT 10
+----+--------------+-----------------+------------+-------+---------------------------------------------------------------+---------+---------+------------------------+-------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+-----------------+------------+-------+---------------------------------------------------------------+---------+---------+------------------------+-------+----------+---------------------------------+
| 1 | SIMPLE | <subquery2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | archive | NULL | ref | ip,date-ip,ip-date,ip-service,service-ip | ip-date | 47 | <subquery2>.ip | 5 | 100.00 | Using index |
| 2 | MATERIALIZED | archive | NULL | range | date,ip,date-ip,date-service,ip-date,ip-service | date-ip | 5 | NULL | 44168 | 100.00 | Using where; Using index |
+----+--------------+-----------------+------------+-------+---------------------------------------------------------------+---------+---------+------------------------+-------+----------+---------------------------------+
普通子查询:0.0351s
整个查询 1:1.4270s
整个查询 2:1.5601s
页面总加载时间:3.050 秒(总共 7 个查询)
我是不是注定要用这个 table 表现糟糕?
希望这里有足够的信息来了解发生了什么,但如果有人能提供帮助,我将不胜感激。我不介意在这个问题上投入更多的硬件,但是当一个 16gb 的 8c/16t 服务器无法处理 150mb 的数据时,我不确定会怎样。预先感谢您阅读我冗长的问题。
您拥有正确的索引(以及许多其他索引)并且您的查询既符合您的规范又接近最佳运行。您不太可能使它变得更快:它需要一直查看到 table.
的开头
如果您可以更改您的规范,那么您只需回顾有限的时间(例如一年),您将获得很好的加速。
一些可能的小调整。
- 为您的
ip
列使用 latin1_bin
归类。它使用 8 位字符并在不区分大小写的情况下整理它们。这对于 IPv4 点分四组地址(和 IPv6 地址)来说已经足够了。您将摆脱一些匹配和分组的开销。或者,更好的是,
- 如果您知道除了 IPv4 地址之外什么都没有,请修改您的
ip
列以存储它们的二进制表示(即 INET_ATON()
- 每个 IPv4 的生成值)。您可以将它们放入 UNSIGNED INT
32 位整数数据类型,使查找、分组和排序更快。
您可以重新设计收集这些数据的方式。例如,您可以安排每天每项服务最多收集一行。这会降低数据的时间序列分辨率,但也会使查询速度更快。像这样定义你的table:
CREATE TABLE archive2 (
ip VARCHAR(15) COLLATE latin1_bin NOT NULL,
service ENUM ('ssh','telnet','ftp',
'pop3','imap','rdp',
'vnc','sql','http','smb',
'smtp','dns','sip','ldap') COLLATE NOT NULL,
`date` DATE NOT NULL,
`count` INT NOT NULL,
hostid bigint UNSIGNED NOT NULL,
PRIMARY KEY (`date`, ip, service)
) ENGINE=InnoDB;
然后,当您插入一行时,使用此查询:
INSERT INTO archive2 (`date`, ip, service, `count`, hostid)
VALUES (CURDATE(), ?ip, ?service, 1, ?hostid)
ON DUPLICATE KEY UPDATE
SET count = count + 1;
如果 ip
、service
和 date
的行已经存在,这将自动增加您的 count
列。
那么您的第二个查询将如下所示:
SELECT ip, SUM(`count`) AS total
FROM archive
WHERE ip IN (
SELECT ip FROM archive
WHERE `date` > CURDATE() - INTERVAL 1 DAY
GROUP BY ip
HAVING total > 1
)
ORDER BY total DESC, INET_ATON(ip) ASC LIMIT 10;
主键的索引将满足此查询。
第一次查询
(我不相信它可以做得更快。)
(目前)
SELECT service, COUNT(service) AS total
FROM `archive`
WHERE ip IN (
SELECT DISTINCT ip
FROM `archive`
WHERE date > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR)
)
GROUP BY service
HAVING total > 1
ORDER BY total DESC, service ASC
LIMIT 10
备注:
COUNT(service)
--> COUNT(*)
DISTINCT
在 IN (SELECT DISTINCT ...)
中不需要
IN ( SELECT ... )
通常很慢;使用 EXISTS ( SELECT 1 ... )
或 JOIN
重写(见下文)
INDEX(date, IP)
-- 对于子查询
INDEX(service, IP)
-- 对于你的外部查询
INDEX(IP, service)
-- 对于我的外部查询
- 折腾冗余索引;他们会挡路。 (见下文)
- 它 将 必须在到达
ORDER BY
和 LIMIT
之前收集所有可能的结果。 (也就是说,LIMIT
对 this 查询的性能影响很小。)
CHARACTER SET utf8 COLLATE utf8_unicode_ci
是 IP 地址的严重矫枉过正;切换到 CHARACTER SET ascii COLLATE ascii_bin
.
- 如果你是运行 MySQL 8.0(或MariaDB 10.2),一个
WITH
计算一次子查询,连同一个UNION
计算两个外层查询,可能提供一些额外的速度。
- MariaDB 有一个“子查询缓存”可能具有跳过第二个子查询评估的效果。
- 通过使用
DATETIME
而不是 TIMESTAMP
,您每年会在夏令时开始时出现两次小问题 in/out。
- 我怀疑
hostid
是否需要成为 BIGINT
(8 字节)。
要切换到 JOIN
,首先考虑获取候选行:
SELECT service, COUNT(*) AS total
FROM ( SELECT DISTINCT IP
FROM archive
WHERE `date` > NOW() - INTERVAL 24 HOUR
) AS x
JOIN archive USING(IP)
GROUP BY service
HAVING total > 1
ORDER BY total DESC, service ASC
LIMIT 10
如需进一步讨论任何缓慢(但有效)的查询,请提供两种形式的 EXPLAIN
:
EXPLAIN SELECT ...
EXPLAIN FORMAT=JSON SELECT ...
删除这些索引:
ADD KEY `service` (`service`),
ADD KEY `date` (`date`),
ADD KEY `ip` (`ip`),
只推荐
ADD PRIMARY KEY (`id`),
-- as discussed:
ADD KEY `date-ip` (`date`,`ip`),
ADD KEY `ip-service` (`ip`,`service`),
ADD KEY `service-ip` (`service`,`ip`),
-- maybe other queries need these:
ADD KEY `date-service` (`date`,`service`),
ADD KEY `ip-date` (`ip`,`date`),
ADD KEY `service-date` (`service`,`date`),
这里的一般规则是当您还有 INDEX(a,b)
时,您不需要 INDEX(a)
。特别是,他们可能会阻止使用更好的索引;见 EXPLAINs
.
第二次查询
重写
SELECT ip, COUNT(DISTINCT ip) AS total
FROM `archive`
WHERE date > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR)
GROUP BY ip
HAVING total > 1
ORDER BY total DESC, INET_ATON(ip) ASC
LIMIT 10
它将仅使用 INDEX(date, ip)
。
我会第一个承认我不擅长SQL(我可能不应该把它当作滚动日志文件),但我想知道我是否可以得到一些改进一些慢速查询的建议...
我有一个很大的 mysql table,有 200 万行,我根据最新数据的一个子集进行了两次完整的 table 查找。当我加载包含这些查询的页面时,我经常发现它们需要几秒钟才能完成,但里面的查询非常快。
PMA 的(据说很糟糕)顾问几乎把整个厨房水槽都扔给我,临时 tables,种类太多,没有索引的连接(我什至没有任何连接?),阅读固定位置,读取下一个位置,临时 tables 写入磁盘...最后一个特别让我想知道它是否是配置问题,但我尝试了所有旋钮,甚至支付了没有托管服务的费用' 似乎有帮助。
CREATE TABLE `archive` (
`id` bigint UNSIGNED NOT NULL,
`ip` varchar(15) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`service` enum('ssh','telnet','ftp','pop3','imap','rdp','vnc','sql','http','smb','smtp','dns','sip','ldap') CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`hostid` bigint UNSIGNED NOT NULL,
`date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
ALTER TABLE `archive`
ADD PRIMARY KEY (`id`),
ADD KEY `service` (`service`),
ADD KEY `date` (`date`),
ADD KEY `ip` (`ip`),
ADD KEY `date-ip` (`date`,`ip`),
ADD KEY `date-service` (`date`,`service`),
ADD KEY `ip-date` (`ip`,`date`),
ADD KEY `ip-service` (`ip`,`service`),
ADD KEY `service-date` (`service`,`date`),
ADD KEY `service-ip` (`service`,`ip`);
添加索引肯定有帮助(即使它们是实际数据大小的 4 倍),但我有点不知所措,我可以进一步优化。最初我想在 php 中缓存子查询结果并在主查询中使用它两次,但我认为一旦关闭子查询我就无法访问结果。我研究了连接,但它们看起来像是用于 2 个或更多单独的 table,但子查询来自同一个 table,所以我不确定这是否有效任何一个。查询应该根据我在过去 24 小时内是否有来自 ip 的数据来找到最活跃的 ip/services...
SELECT service, COUNT(service) AS total FROM `archive`
WHERE ip IN
(SELECT DISTINCT ip FROM `archive` WHERE date > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR))
GROUP BY service HAVING total > 1
ORDER BY total DESC, service ASC LIMIT 10
+----+--------------+-----------------+------------+-------+----------------------------------------------------------------------------+------------+---------+------------------------+-------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+-----------------+------------+-------+----------------------------------------------------------------------------+------------+---------+------------------------+-------+----------+---------------------------------+
| 1 | SIMPLE | <subquery2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | archive | NULL | ref | service,ip,date-service,ip-date,ip-service,service-date,service-ip | ip-service | 47 | <subquery2>.ip | 5 | 100.00 | Using index |
| 2 | MATERIALIZED | archive | NULL | range | date,ip,date-ip,date-service,ip-date,ip-service | date-ip | 5 | NULL | 44246 | 100.00 | Using where; Using index |
+----+--------------+-----------------+------------+-------+----------------------------------------------------------------------------+------------+---------+------------------------+-------+----------+---------------------------------+
SELECT ip, COUNT(ip) AS total FROM `archive`
WHERE ip IN
(SELECT DISTINCT ip FROM `archive` WHERE date > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR))
GROUP BY ip HAVING total > 1
ORDER BY total DESC, INET_ATON(ip) ASC LIMIT 10
+----+--------------+-----------------+------------+-------+---------------------------------------------------------------+---------+---------+------------------------+-------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+-----------------+------------+-------+---------------------------------------------------------------+---------+---------+------------------------+-------+----------+---------------------------------+
| 1 | SIMPLE | <subquery2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | archive | NULL | ref | ip,date-ip,ip-date,ip-service,service-ip | ip-date | 47 | <subquery2>.ip | 5 | 100.00 | Using index |
| 2 | MATERIALIZED | archive | NULL | range | date,ip,date-ip,date-service,ip-date,ip-service | date-ip | 5 | NULL | 44168 | 100.00 | Using where; Using index |
+----+--------------+-----------------+------------+-------+---------------------------------------------------------------+---------+---------+------------------------+-------+----------+---------------------------------+
普通子查询:0.0351s
整个查询 1:1.4270s
整个查询 2:1.5601s
页面总加载时间:3.050 秒(总共 7 个查询)
我是不是注定要用这个 table 表现糟糕?
希望这里有足够的信息来了解发生了什么,但如果有人能提供帮助,我将不胜感激。我不介意在这个问题上投入更多的硬件,但是当一个 16gb 的 8c/16t 服务器无法处理 150mb 的数据时,我不确定会怎样。预先感谢您阅读我冗长的问题。
您拥有正确的索引(以及许多其他索引)并且您的查询既符合您的规范又接近最佳运行。您不太可能使它变得更快:它需要一直查看到 table.
的开头如果您可以更改您的规范,那么您只需回顾有限的时间(例如一年),您将获得很好的加速。
一些可能的小调整。
- 为您的
ip
列使用latin1_bin
归类。它使用 8 位字符并在不区分大小写的情况下整理它们。这对于 IPv4 点分四组地址(和 IPv6 地址)来说已经足够了。您将摆脱一些匹配和分组的开销。或者,更好的是, - 如果您知道除了 IPv4 地址之外什么都没有,请修改您的
ip
列以存储它们的二进制表示(即INET_ATON()
- 每个 IPv4 的生成值)。您可以将它们放入UNSIGNED INT
32 位整数数据类型,使查找、分组和排序更快。
您可以重新设计收集这些数据的方式。例如,您可以安排每天每项服务最多收集一行。这会降低数据的时间序列分辨率,但也会使查询速度更快。像这样定义你的table:
CREATE TABLE archive2 (
ip VARCHAR(15) COLLATE latin1_bin NOT NULL,
service ENUM ('ssh','telnet','ftp',
'pop3','imap','rdp',
'vnc','sql','http','smb',
'smtp','dns','sip','ldap') COLLATE NOT NULL,
`date` DATE NOT NULL,
`count` INT NOT NULL,
hostid bigint UNSIGNED NOT NULL,
PRIMARY KEY (`date`, ip, service)
) ENGINE=InnoDB;
然后,当您插入一行时,使用此查询:
INSERT INTO archive2 (`date`, ip, service, `count`, hostid)
VALUES (CURDATE(), ?ip, ?service, 1, ?hostid)
ON DUPLICATE KEY UPDATE
SET count = count + 1;
如果 ip
、service
和 date
的行已经存在,这将自动增加您的 count
列。
那么您的第二个查询将如下所示:
SELECT ip, SUM(`count`) AS total
FROM archive
WHERE ip IN (
SELECT ip FROM archive
WHERE `date` > CURDATE() - INTERVAL 1 DAY
GROUP BY ip
HAVING total > 1
)
ORDER BY total DESC, INET_ATON(ip) ASC LIMIT 10;
主键的索引将满足此查询。
第一次查询
(我不相信它可以做得更快。)
(目前)
SELECT service, COUNT(service) AS total
FROM `archive`
WHERE ip IN (
SELECT DISTINCT ip
FROM `archive`
WHERE date > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR)
)
GROUP BY service
HAVING total > 1
ORDER BY total DESC, service ASC
LIMIT 10
备注:
COUNT(service)
-->COUNT(*)
DISTINCT
在IN (SELECT DISTINCT ...)
中不需要
IN ( SELECT ... )
通常很慢;使用EXISTS ( SELECT 1 ... )
或JOIN
重写(见下文)INDEX(date, IP)
-- 对于子查询INDEX(service, IP)
-- 对于你的外部查询INDEX(IP, service)
-- 对于我的外部查询- 折腾冗余索引;他们会挡路。 (见下文)
- 它 将 必须在到达
ORDER BY
和LIMIT
之前收集所有可能的结果。 (也就是说,LIMIT
对 this 查询的性能影响很小。) CHARACTER SET utf8 COLLATE utf8_unicode_ci
是 IP 地址的严重矫枉过正;切换到CHARACTER SET ascii COLLATE ascii_bin
.- 如果你是运行 MySQL 8.0(或MariaDB 10.2),一个
WITH
计算一次子查询,连同一个UNION
计算两个外层查询,可能提供一些额外的速度。 - MariaDB 有一个“子查询缓存”可能具有跳过第二个子查询评估的效果。
- 通过使用
DATETIME
而不是TIMESTAMP
,您每年会在夏令时开始时出现两次小问题 in/out。 - 我怀疑
hostid
是否需要成为BIGINT
(8 字节)。
要切换到 JOIN
,首先考虑获取候选行:
SELECT service, COUNT(*) AS total
FROM ( SELECT DISTINCT IP
FROM archive
WHERE `date` > NOW() - INTERVAL 24 HOUR
) AS x
JOIN archive USING(IP)
GROUP BY service
HAVING total > 1
ORDER BY total DESC, service ASC
LIMIT 10
如需进一步讨论任何缓慢(但有效)的查询,请提供两种形式的 EXPLAIN
:
EXPLAIN SELECT ...
EXPLAIN FORMAT=JSON SELECT ...
删除这些索引:
ADD KEY `service` (`service`),
ADD KEY `date` (`date`),
ADD KEY `ip` (`ip`),
只推荐
ADD PRIMARY KEY (`id`),
-- as discussed:
ADD KEY `date-ip` (`date`,`ip`),
ADD KEY `ip-service` (`ip`,`service`),
ADD KEY `service-ip` (`service`,`ip`),
-- maybe other queries need these:
ADD KEY `date-service` (`date`,`service`),
ADD KEY `ip-date` (`ip`,`date`),
ADD KEY `service-date` (`service`,`date`),
这里的一般规则是当您还有 INDEX(a,b)
时,您不需要 INDEX(a)
。特别是,他们可能会阻止使用更好的索引;见 EXPLAINs
.
第二次查询
重写
SELECT ip, COUNT(DISTINCT ip) AS total
FROM `archive`
WHERE date > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR)
GROUP BY ip
HAVING total > 1
ORDER BY total DESC, INET_ATON(ip) ASC
LIMIT 10
它将仅使用 INDEX(date, ip)
。