【问题标题】:Optimize SQL Queries on Big Table优化大表SQL查询
【发布时间】:2021-04-20 05:49:40
【问题描述】:

我正在处理一个非常大的表 - 目前我的任务是将所有设备的日志读入数据库并运行 SELECT 来执行指标。当前表定义如下:

mysql> describe device_events;
+-------------+---------------------+------+-----+-------------------+-----------------------------+
| Field       | Type                | Null | Key | Default           | Extra                       |
+-------------+---------------------+------+-----+-------------------+-----------------------------+
| id          | bigint(20) unsigned | NO   | PRI | NULL              | auto_increment              |
| device_type | varchar(255)        | NO   | MUL | NULL              |                             |
| device_id   | bigint(20) unsigned | NO   | MUL | NULL              |                             |
| message     | json                | NO   |     | NULL              |                             |
| source      | text                | NO   | MUL | NULL              |                             |
| created_at  | timestamp           | NO   | MUL | CURRENT_TIMESTAMP |                             |
| updated_at  | timestamp           | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
| file_date   | date                | YES  | MUL | NULL              |                             |
+-------------+---------------------+------+-----+-------------------+-----------------------------+```


Indexes:
+---------------+------------+---------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table         | Non_unique | Key_name                        | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------------+------------+---------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| device_events |          0 | PRIMARY                         |            1 | id          | A         |    40932772 |     NULL | NULL   |      | BTREE      |         |               |
| device_events |          1 | device_events_device_id_index   |            1 | device_id   | A         |       44021 |     NULL | NULL   |      | BTREE      |         |               |
| device_events |          1 | device_events_device_type_index |            1 | device_type | A         |         621 |     NULL | NULL   |      | BTREE      |         |               |
| device_events |          1 | device_events_source_index      |            1 | source      | A         |        3085 |      255 | NULL   |      | BTREE      |         |               |
| device_events |          1 | device_events_created_at_index  |            1 | created_at  | A         |     2846551 |     NULL | NULL   |      | BTREE      |         |               |
| device_events |          1 | device_events_file_date_index   |            1 | file_date   | A         |       25017 |     NULL | NULL   | YES  | BTREE      |         |               |
+---------------+------------+---------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

我正在研究分区,理想情况下希望为每个设备和每个日志源创建一个分区,但由于 MySQL 的固定分区范围,我认为我无法这样做。目前,SELECT 大约需要一分钟和大约 15 分钟来生成所有指标,但希望加快速度。有人对如何优化大型选择有任何其他想法吗?我预计数据库中有超过一万亿条记录。大多数 SELECT 将针对过去 30 天内的事件完成,并在 message 上使用 JSON_EXTRACT。请注意,我已经在时间戳上使用 BETWEEN 以避免月份(created_at)计算,并且很可能已尽可能优化查询 - 我主要是针对这个问题寻找结构优化。

--
-- Table structure for table `device_events`
--

DROP TABLE IF EXISTS `device_events`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `device_events` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `device_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `device_id` bigint(20) unsigned NOT NULL,
  `message` json NOT NULL,
  `source` text COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `file_date` date DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `device_events_device_id_index` (`device_id`),
  KEY `device_events_device_type_index` (`device_type`),
  KEY `device_events_source_index` (`source`(255)),
  KEY `device_events_created_at_index` (`created_at`),
  KEY `device_events_file_date_index` (`file_date`)
) ENGINE=InnoDB AUTO_INCREMENT=42771939 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2021-04-19 17:55:35

雄辩的 ORM 查询


    public function scopeLastMonth($query) {
        $lastMonth = Carbon::now()->subMonth();
        $start = $lastMonth->firstOfMonth()->startOfDay()->toDateTimeString();
        $end = $lastMonth->lastOfMonth()->endOfDay()->toDateTimeString();
        return $query->whereBetween("created_at", [$start, $end]);
    }

$topIdentitiesQuery = (clone $lastMonthEvents)->selectRaw("JSON_EXTRACT(message, '$.policy_identity') as identity")->selectRaw("count(*) as aggregate")->groupBy("identity")->orderBy("aggregate", "desc");
$topIdentities = [];
foreach($topIdentitiesQuery->take(self::NUM_TOP_IDENTITIES)->get() as $topIdentity) { 
    array_push($topIdentities, $topIdentity->identity);
}
$topIdentities = array_pad($topIdentities, self::NUM_TOP_IDENTITIES, "");

【问题讨论】:

  • 提供表定义作为完整的 CREATE TABLE 脚本 - 当前信息没有用。还要提供最常见或最关键的要优化的查询,包括 EXPLAIN。
  • 大多数 SELECT 将在过去 30 天内的事件上完成,并在消息上使用 JSON_EXTRACT 显示 JSON 值(2-3 个值)和最常见的 JSON_EXTRACT 表达式的示例(2-3 个变体)。 PS。按日期分区现在看起来像是一个选项..
  • @Akina 添加了 MySQL 转储并使用 Laravel Eloquent ORM 进行查询。上面是一个例子。我也在考虑为每个 JSON 值使用生成的列,但是日志消息会有所不同,最终可能会在每条记录上产生大约 20 个额外的空白列。非常感谢您的建设性回应
  • 我正在使用 Laravel Eloquent ORM 进行查询 只有纯 SQL 查询可以刻意优化,而不是意外。因此,获取由您的代码生成的 SQL 文本并显示它。当然,还要加上 EXPLAIN。
  • 请提供为SELECT生成的SQL。

标签: mysql optimization


【解决方案1】:

大多数查询是针对给定月份的吗?还是星期或日期范围?然后,我强烈建议构建和维护汇总表,将每天的事情汇总到少量行中。从这样的表中,构建“报告”应该需要几秒钟,而不是几分钟。

当您达到一万亿行时,报告可能需要几天时间,因为它会受 I/O 限制!使用汇总表,可能只需几分钟。

在达到一万亿行之前,您需要弄清楚将 200TB 的原始数据存储在哪里。 或者,你需要完善汇总表,折腾原始数据。或者,作为一种折衷方案,您可以保留最后一个月的原始数据,同时“永远”保留汇总表。如果您需要“最近”活动的“详细信息”,但可以只接受“较旧”数据的每日摘要,这种折衷方案可能会很有用。

如果你选择妥协,那么PARTITION BY RANGE(TO_DAYS(..));这可以快速清除“旧”数据。如果您不妥协,那么分区的使用可能为零。

将汇总表作为主要信息来源的另一个好处是,您现在在“事实”表上拥有的大多数索引都可以删除,从而节省空间并加快插入速度。

您似乎正在从 JSON 中提取内容以生成报告?好吧,这可以在您构建汇总表时完成,并且可以将字段放入带有索引的真实列中。

您是否估计过插入数万亿行的速度?这可能是个问题;如果您需要一些技巧来帮助您,请告诉我。

哦,去掉中间的任何 3rd 方包;您需要直接使用 MySQL 语法来做事,而不受他们实现的功能的限制。

为了节省一些空间,规范化device_type。如果有数百个,那么 SMALLINT UNSIGNED 只有 2 个字节,可以让您拥有 64K 值。

你有数十亿的device_id吗?如果不是,请不要使用 8 字节的BIGINT

为了节省一些空间,请考虑(在客户端)压缩 json 和源代码。顺便问一下,这些通常有多大?

“前缀”索引 (INDEX(source(255))) 很少有任何用处。

您可能不需要created_atupdated_at。请记住,对于您的终极表来说,一个不必要的时间戳列会花费超过 5TB!

Data Warehouse .. Summary Tables .. High speed ingestion .. Bulk normalization .. Partitioning

【讨论】:

  • 很荣幸有像你这样的MySQL大神回答我的问题。我在谷歌搜索的汇总表上找到了您的博客,然后意识到您在答案中发布了它,但汇总表正是我正在寻找的 - 而不是对可能具有 JSON where 子句的“事实”表运行查询,而是处理摄取期间的指标。非常感谢您对社区的支持,并且您提供了惊人的帮助
  • @ShawnShroyer - 感谢您的好评。是的,在摄取期间进行处理——这是一项一次性任务(每行),而“报告”端可能会多次接触该行。此外,用户正在等待生成报告。
  • @ShawnShroyer - 我已经为您提供了一些处理 200TB 的技巧,但可能还不够。我建议您在应用我的建议后再次检查数字。计算最终行数、摄取率(行/秒)、所需磁盘空间。最大表的大小(行;TB)。频率或SELECTs 以及他们需要触摸的行数。等等。
猜你喜欢
  • 2017-07-22
  • 2010-09-11
  • 1970-01-01
  • 2017-12-25
  • 2016-04-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多