对大型 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 PARTITION
比DELETE
快)不可用
总结 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中可能比这更糟。)
- 将新索引的 BTree 向下钻取到 [3317, '2019-08-07']
- 向前扫描一周(将行收集到临时文件中)
- 互相重复1,2 sensor_id.
- 对temp进行排序table(满足
ORDER BY
)。
- 传送结果行。
这里的关键点是它只准确读取需要传送的行(加上每个传感器额外的一行以实现一周结束)。因为这是一个巨大的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_timestamp
、timestamp
、event_date
、created_at
——都有"same"的值和意义??
关于 DATE
的注释——区分 DATETIME
或 TIMESTAMP
几乎总是比拥有一个额外的列更容易,尤其是比同时拥有两个 [=47] =] 和 TIME
.
没有日期列,检查一天的所有读数需要类似于:
WHERE `dt` >= '2019-08-07'
AND `dt` < '2019-08-07' + INTERVAL 1 DAY
我有一个 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 PARTITION
比DELETE
快)不可用
总结 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中可能比这更糟。)
- 将新索引的 BTree 向下钻取到 [3317, '2019-08-07']
- 向前扫描一周(将行收集到临时文件中)
- 互相重复1,2 sensor_id.
- 对temp进行排序table(满足
ORDER BY
)。 - 传送结果行。
这里的关键点是它只准确读取需要传送的行(加上每个传感器额外的一行以实现一周结束)。因为这是一个巨大的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_timestamp
、timestamp
、event_date
、created_at
——都有"same"的值和意义??
关于 DATE
的注释——区分 DATETIME
或 TIMESTAMP
几乎总是比拥有一个额外的列更容易,尤其是比同时拥有两个 [=47] =] 和 TIME
.
没有日期列,检查一天的所有读数需要类似于:
WHERE `dt` >= '2019-08-07'
AND `dt` < '2019-08-07' + INTERVAL 1 DAY