对大型 MySQL InnoDB table 进行分区的方法

Approach to partitioning a large MySQL InnoDB table

我有一个 table 每年将接收 45-60 百万行 IOT 类型数据。最初的愿望是 从不 删除数据,因为我们可能会将其用于不同类型的 "big data analysis"。今天这个 table 需要支持我们的在线申请。该应用程序需要快速查询通常在过去 30 或 90 天内的数据。所以我在想分区可能是个好主意。

我们目前的想法是使用 'aging' 列,在本例中称为 partition_id。最近 30 天内的记录是 partition_id = 0。记录 31 天到 90 天是 partition_id = 1,其他所有内容都在 partition_id = 2。

所有查询将 'know' 他们想要使用的partition_id。其中,查询总是由 sensor_id、badge_id 等(参见索引)组内的所有 sensor_id 或 badge_id,即 sensor_id in ( 3, 15, 35, 100, 1024)

这是 table 定义

    CREATE TABLE 'device_messages' (
    'id' int(10) unsigned NOT NULL AUTO_INCREMENT,
    'partition_id' tinyint(3) unsigned NOT NULL DEFAULT '0',
    'customer_id' int(10) unsigned NOT NULL,
    'unix_timestamp' double(12, 2) NOT NULL,
    'timestamp' timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    'timezone_id' smallint(5) unsigned NOT NULL,
    'event_date' date NOT NULL,
    'is_day_shift' tinyint(1) unsigned NOT NULL,
    'msg_id' tinyint(3) unsigned NOT NULL,
    'sensor_id' int(10) unsigned NOT NULL,
    'sensor_role_id' int(10) unsigned NOT NULL,
    'sensor_box_build_id' int(10) unsigned NOT NULL,
    'gateway_id' int(10) unsigned NOT NULL,
    'location_hierarchy_id' int(10) unsigned NOT NULL,
    'group_hierarchy_id' int(10) unsigned DEFAULT NULL,
    'badge_id' int(10) unsigned NOT NULL,
    'is_badge_deleted' tinyint(1) DEFAULT NULL,
    'user_id' int(10) unsigned DEFAULT NULL,
    'is_user_deleted' tinyint(1) DEFAULT NULL,
    'badge_battery' double unsigned DEFAULT NULL,
    'scan_duration' int(10) unsigned DEFAULT NULL,
    'reading_count' tinyint(3) unsigned DEFAULT NULL,
    'median_rssi_reading' tinyint(4) DEFAULT NULL,
    'powerup_counter' int(10) unsigned DEFAULT NULL,
    'tx_counter' int(10) unsigned DEFAULT NULL,
    'activity_counter' int(10) unsigned DEFAULT NULL,
    'still_counter' int(10) unsigned DEFAULT NULL,
    'created_at' timestamp NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY ('id', 'partition_id', 'sensor_id', 'event_date'),
    KEY 'sensor_id_query_index' ('partition_id', 'sensor_id', 'event_date'),
    KEY 'badge_id_query_index' ('partition_id', 'badge_id', 'event_date'),
    KEY 'location_hierarchy_id_query_index' ('partition_id', 'location_hierarchy_id', 'event_date'),
    KEY 'group_hierarchy_id_query_index' ('partition_id', 'group_hierarchy_id', 'event_date')
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci
    PARTITION BY RANGE (partition_id)
    SUBPARTITION BY HASH (sensor_id)
    (PARTITION fresh VALUES LESS THAN (1)
    (SUBPARTITION f0 ENGINE = InnoDB,
    SUBPARTITION f1 ENGINE = InnoDB,
    SUBPARTITION f2 ENGINE = InnoDB,
    SUBPARTITION f3 ENGINE = InnoDB,
    SUBPARTITION f4 ENGINE = InnoDB,
    SUBPARTITION f5 ENGINE = InnoDB,
    SUBPARTITION f6 ENGINE = InnoDB,
    SUBPARTITION f7 ENGINE = InnoDB,
    SUBPARTITION f8 ENGINE = InnoDB,
    SUBPARTITION f9 ENGINE = InnoDB),
    PARTITION archive VALUES LESS THAN (2)
    (SUBPARTITION a0 ENGINE = InnoDB,
    SUBPARTITION a1 ENGINE = InnoDB,
    SUBPARTITION a2 ENGINE = InnoDB,
    SUBPARTITION a3 ENGINE = InnoDB,
    SUBPARTITION a4 ENGINE = InnoDB,
    SUBPARTITION a5 ENGINE = InnoDB,
    SUBPARTITION a6 ENGINE = InnoDB,
    SUBPARTITION a7 ENGINE = InnoDB,
    SUBPARTITION a8 ENGINE = InnoDB,
    SUBPARTITION a9 ENGINE = InnoDB),
    PARTITION deep_archive VALUES LESS THAN MAXVALUE
    (SUBPARTITION C0 ENGINE = InnoDB,
    SUBPARTITION C1 ENGINE = InnoDB,
    SUBPARTITION C2 ENGINE = InnoDB,
    SUBPARTITION C3 ENGINE = InnoDB,
    SUBPARTITION C4 ENGINE = InnoDB,
    SUBPARTITION C5 ENGINE = InnoDB,
    SUBPARTITION C6 ENGINE = InnoDB,
    SUBPARTITION C7 ENGINE = InnoDB,
    SUBPARTITION C8 ENGINE = InnoDB,
    SUBPARTITION C9 ENGINE = InnoDB)) ;

此 table 定义当前处理 1600 万行数据,查询速度似乎很快。但是,我担心这种实施的长期可持续性。另外,我现在看到我们在 'age' 记录时通过每周更新 10 万条记录的 partition_id 来对分区进行大量改动。

查询几乎总是这样的变体:

    SELECT * FROM device_messages
    WHERE partition_id = 0
      AND 'event_date' BETWEEN '2019-08-07' AND '2019-08-13'
      AND 'sensor_id' in ( 3317, 3322, 3323, 3327, 3328, 3329, 3331, 3332, 3333, 3334, 3335, 3336, 3337, 3338, 3339, 3340, 3341, 3342 )
      ORDER BY 'unix_timestamp' asc

列表中可能少至一个 sensor_id,但通常会有多个。

我花了数小时研究分区,但没有找到针对此用例的分区示例或讨论。因为,我们以这种方式使用 partition_id 的人工老化列,我也意识到我无法对分区进行任何真正的操作,所以我认为我至少失去了一些分区的价值.

非常感谢有关分区方案甚至替代方法的建议。

PARTITIONing 并不是性能的灵丹妙药。

不删除? OK,主要用途(DROP PARTITIONDELETE快)不可用

总结 Tables 是数据仓库性能问题的答案。参见 http://mysql.rjweb.org/doc.php/summarytables

(现在我将详细阅读问题和任何答案;也许我会回来改变一些东西。)

模式批评

由于您预计会有数百万行,因此缩小数据类型非常重要。

customer_id 是一个 4 字节整数。如果您预计不会超过几千,请使用 2 字节 SMALLINT UNSIGNED。另见 MEDIUMINT UNSIGNED。同上所有其他 INTs.

'unix_timestamp' double(12, 2) 很奇怪。 TIMESTAMP(2)有什么问题,哪个会更小?

'badge_battery' double -- 分辨率过高? DOUBLE为8个字节; FLOAT 是 4,有 ~7 位有效数字。

大多数列是 NULLable。他们真的是可选的吗? (NULL 开销很小;在可行的情况下使用 NOT NULL。)

当行不再 "fresh" 时,您会做大量的工作 UPDATE 来更改该列吗?请考虑该声明将产生的巨大影响。最好创建新分区并更改查询。如果您有 AND some_date > some_column 并且该列是 PARTITION BY RANGE(TO_DAYS(..)).

,则此方法特别有效

我还没有看到 SUBPARTITIONing 的理由。

非分区

鉴于这是典型的:

SELECT * FROM device_messages
WHERE partition_id = 0
  AND 'event_date' BETWEEN '2019-08-07' AND '2019-08-13'
  AND 'sensor_id' in ( 3317, 3322, 3323, 3327, 3328, 3329, 3331, 3332,
                       3333, 3334, 3335, 3336, 3337, 3338, 3339, 3340, 3341, 3342 )
  ORDER BY 'unix_timestamp' asc

我建议如下:

  • 没有分区(也没有 partition_key
  • event_date;使用 unix_timestamp 代替
  • 改变select如下:

...

SELECT * FROM device_messages
WHERE `unix_timestamp` >= '2019-08-07'
  AND `unix_timestamp`  < '2019-08-07' + INTERVAL 1 WEEK
  AND sensor_id in ( 3317, 3322, 3323, 3327, 3328, 3329, 3331, 3332,
                     3333, 3334, 3335, 3336, 3337, 3338, 3339, 3340, 3341, 3342 )
  ORDER BY `unix_timestamp` asc

并添加

INDEX(sensor_id, `unix_timestamp`)

那个,我想下面会进行处理。 (注意:在一些老版本的MySQL/MariaDB中可能比这更糟。)

  1. 将新索引的 BTree 向下钻取到 [3317, '2019-08-07']
  2. 向前扫描一周(将行收集到临时文件中)
  3. 互相重复1,2 sensor_id.
  4. 对temp进行排序table(满足ORDER BY)。
  5. 传送结果行。

这里的关键点是它只准确读取需要传送的行(加上每个传感器额外的一行以实现一周结束)。因为这是一个巨大的table,所以它已经很好了

额外的排序(参见 Explain 的 "filesort")是必要的,因为无法按 ORDER BY 顺序获取行。

还有一个优化...

在上面,index是有序的,但是data不是。我们可以按如下方式解决:

PRIMARY KEY(sensor_id, `unix_timestamp`, id),  -- (`id` adds uniqueness)
INDEX(id),   -- to keep AUTO_INCREMENT happy

(并跳过我之前的索引建议)

如果 table 变得比 buffer_pool 大,此修改将变得特别有益。这是因为修改后的PK提供的"clustering"

更多标准化

我怀疑这 ~30 列中的许多列在行与行之间是相同的,尤其是对于相同的传感器(又名 'device'?)。如果我是正确的,那么你 'should' 从这个巨大的 table 中删除这些列并将它们放入另一个 table,去重复。

这比调整 INT 等 space 节省更多

总结Table

同样,使用您的查询,让我们讨论一下什么摘要 table 会有用。但首先,我看不出总结什么有用。我希望看到 device_value FLOAT 或类似的东西。我会用它作为一个假设的例子:

CREATE TABLE Summary (
        event_date DATE NOT NULL, -- reconstructed from `unix_timestamp`
        sensor_id ...,
        ct SMALLINT UNSIGNED,  -- number of readings for the day
        sum_value FLOAT NOT NULL,  -- SUM(device_value)
        sum2  -- if you need standard deviation
        min_value, etc   -- if you want those
        PRIMARY KEY(sensor_id, event_date)
    ) ENGINE=InnoDB;

一天一次:

INSERT INTO Summary (sensor_id, event_date, ct, sum_value, ...)
        SELECT sensor_id, DATE(`unix_timestamp`),
                          COUNT(*), SUM(device_value), ...
            FROM device_messages
            WHERE `unix_timestamp` >= CURDATE() - INTERVAL 1 DAY
             AND `unix_timestamp`  < CURDATE()
           GROUP BY sensor_id;

(有更稳健的方法;有更及时的方法;等)或者您可能希望按小时而不是天进行汇总。无论如何,您可以通过对每日摘要中的总和求和来获得任意日期范围。

 Average:  SUM(sum_value) / SUM(ct)

冗余?

unix_timestamptimestampevent_datecreated_at——都有"same"的值和意义??

关于 DATE 的注释——区分 DATETIMETIMESTAMP 几乎总是比拥有一个额外的列更容易,尤其是比同时拥有两个 [=47] =] 和 TIME.

没有日期列,检查一天的所有读数需要类似于:

    WHERE `dt` >= '2019-08-07'
      AND `dt`  < '2019-08-07' + INTERVAL 1 DAY