根据同一张表的结果优化选择表中的所有行?

Posted

技术标签:

【中文标题】根据同一张表的结果优化选择表中的所有行?【英文标题】:Optimize selecting all rows from a table based on results from the same table? 【发布时间】:2021-11-02 00:39:05 【问题描述】:

我将是第一个承认我不擅长 SQL(而且我可能不应该将它视为滚动日志文件)的人,但我想知道我是否可以得到一些改进一些慢的指针查询...

我有一个 2M 行的大型 mysql 表,我在其中根据最新数据的子集进行两次全表查找。当我加载包含这些查询的页面时,我经常发现它们需要几秒钟才能完成,但里面的查询很快。

PMA 的(据说很糟糕)顾问几乎把整个厨房水槽扔给我,临时表,太多种类,没有索引的连接(我什至没有任何连接?),从固定位置读取,读取下一个位置,写入磁盘的临时表...最后一个特别让我怀疑这是否是配置问题,但我玩弄了所有的旋钮,甚至支付了似乎没有帮助的托管服务。

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 个或更多单独的表,但子查询来自同一个表,所以我不确定这是否也有效。查询应该根据我在过去 24 小时内是否有来自某个 ip 的数据来查找最活跃的 ip/服务...

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 个查询)

我是否注定要在这张桌子上表现糟糕?

希望这里有足够的信息来了解正在发生的事情,但如果有人可以提供帮助,我将不胜感激。我不介意在这个问题上投入更多的硬件,但是当一台 16gb 的 8c/16t 服务器无法处理 150mb 的数据时,我不确定会怎样。提前感谢您阅读我冗长的问题。

【问题讨论】:

explain 显示什么?查询的目标是什么?这与 PHP 无关,除了可能 phpmyadmin 假设这就是 PMA 是什么?即使在那种情况下,它也只是一个用于访问 mysql 数据库的 UI。 也不确定INET_ATON(ip)在group by中的用途。这不会允许将索引用作需要转换的每条记录。 您好 user3783243,我已按要求添加了更多信息。为了回答您关于 INET_ATON 的问题,它会将地址“按字母顺序排列”。谢谢 IN 与子查询一起使用会导致性能下降。你可以JOIN 一个表回到它自己,或者更具体地说,你可以JOIN 你的子查询回到它来自的表。 顺便说一句,您确定将 WHERE 子句放在正确的位置吗?目前,您的查询将查找过去 24 小时内的 IP 地址,然后返回表中针对这些地址的 all 数据。如果您只想要过去 24 小时的摘要,您可以删除子查询并仅在主查询上按日期选择。 【参考方案1】:

您拥有正确的索引(以及许多其他索引),并且您的查询既符合您的规范,又接近最佳运行。您不太可能使这个速度更快:它需要一直追溯到表格的开头。

如果你可以改变你的规范,那么你只需要回顾有限的时间,比如一年,你就会得到一个很好的加速。

一些可能的小调整。

为您的ip 列使用latin1_bin 排序规则。它使用 8 位字符并在不区分大小写的情况下对它们进行整理。这对于 IPv4 dotted-quad 地址(和 IPv6 地址)来说已经足够了。您将摆脱匹配和分组的一些开销。或者,更好的是, 如果您知道除了 IPv4 地址之外什么都没有,请修改您的 ip 列以存储它们的二进制表示(即,INET_ATON() - 每个 IPv4 的生成值)。您可以将它们放入UNSIGNED INT 32 位整数数据类型中,从而使查找、分组和排序更快。

您可以重新设计收集这些数据的方式。例如,您可以安排每天每次服务最多收集一行。这将降低数据的时间序列分辨率,但也会使查询速度更快。像这样定义你的表:

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;

如果ipservicedate 的行已经存在,这将自动增加您的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;

主键的索引会满足这个查询。

【讨论】:

嗨,O,遗憾的是,以这种方式减少数据实际上并不可行,因为如果没有每个事件的行和完整的时间戳,我会丢失很多信息。我在测试中有一个类似的设置,我有一个计数值并且会增加时间戳而不是插入新行。不过感谢您的关注,我可能会在不久的将来尝试更改数据类型,因为我听说 VARCHAR(15) 不适合存储 ips。 更改枚举的字符集/排序规则对存储的内容没有影响(少量),但在运行查询时可能会导致排序规则错误(连接排序规则与列排序规则)。 复制那个,@RickJames。编辑了我的答案。【参考方案2】:

第一次查询

(我不相信它可以做得更快。)

(目前)

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(*) IN (SELECT DISTINCT ...) 中不需要 DISTINCT IN ( SELECT ... ) 通常很慢;使用EXISTS ( SELECT 1 ... )JOIN 重写(见下文) INDEX(date, IP) -- 用于子查询 INDEX(service, IP) -- 用于您的外部查询 INDEX(IP, service) -- 用于我的外部查询 折腾冗余索引;他们可以挡道。 (见下文) 它必须在到达ORDER BYLIMIT 之前收集所有可能的结果。 (也就是说,LIMITthis 查询的性能影响很小。) 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,每年夏令时开始/结束时,您会遇到两次小问题。 我怀疑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)

【讨论】:

嗨 Rick,不幸的是,连接速度慢了一倍,而且您的第二个查询甚至根本不返回任何行。对不起。 好像你的第二个查询也不能产生任何行。

以上是关于根据同一张表的结果优化选择表中的所有行?的主要内容,如果未能解决你的问题,请参考以下文章

同一张表中的 SQL DATE 比较

sql 将多行显示为一行,如果一张表中有几行数据中的同一列的值是相同,那么只显示为一行的数据

如何根据计算值字段选择行[重复]

使用 DB2,您如何为一列选择具有 MAX 的行,然后在同一张表的另一列的结果子集中选择具有 MAX 的行?

左外连接和右外连接的区别

PostgreSQL 统计同一张表的多列