MySql - 处理 table 大小和性能
MySql - Handle table size and performance
我们有一个分析产品。我们为每位客户提供一个 JavaScript 代码,他们将其放在自己的网站上。如果用户访问我们的客户站点,java 脚本代码会访问我们的服务器,以便我们代表该客户存储此页面访问。每个客户都包含唯一的域名。
我们将此页面访问存储在 MySql table。
以下是 table 架构。
CREATE TABLE `page_visits` (
`domain` varchar(50) DEFAULT NULL,
`guid` varchar(100) DEFAULT NULL,
`sid` varchar(100) DEFAULT NULL,
`url` varchar(2500) DEFAULT NULL,
`ip` varchar(20) DEFAULT NULL,
`is_new` varchar(20) DEFAULT NULL,
`ref` varchar(2500) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL,
`stats_time` datetime DEFAULT NULL,
`country` varchar(50) DEFAULT NULL,
`region` varchar(50) DEFAULT NULL,
`city` varchar(50) DEFAULT NULL,
`city_lat_long` varchar(50) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
KEY `sid_index` (`sid`) USING BTREE,
KEY `domain_index` (`domain`),
KEY `email_index` (`email`),
KEY `stats_time_index` (`stats_time`),
KEY `domain_statstime` (`domain`,`stats_time`),
KEY `domain_email` (`domain`,`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
我们没有此 table 的主键。
MySql 服务器详情
是Google云MySql(版本是5.6)存储容量是10TB
截至目前,我们的 table 中有 3.5 亿行,table 大小为 300 GB。我们将所有客户详细信息存储在同一个 table 中,即使一个客户与另一个客户之间没有任何关系。
问题 1:我们的少数客户在 table 中拥有大量行,因此针对这些客户的查询性能非常慢。
示例查询 1:
SELECT count(DISTINCT sid) AS count,count(sid) AS total FROM page_views WHERE domain = 'aaa' AND stats_time BETWEEN CONVERT_TZ('2015-02-05 00:00:00','+05:30','+00:00') AND CONVERT_TZ('2016-01-01 23:59:59','+05:30','+00:00');
+---------+---------+
| count | total |
+---------+---------+
| 1056546 | 2713729 |
+---------+---------+
1 row in set (13 min 19.71 sec)
我会在这里更新更多查询。我们需要在 5-10 秒内得到结果,这可能吗?
问题 2:table 大小正在迅速增加,我们可能会在今年年底达到 table 5 TB 的大小,因此我们希望对我们的数据进行分片table。我们想在一台机器上保存与一个客户相关的所有记录。此分片的最佳实践是什么。
我们正在考虑以下解决上述问题的方法,请建议我们解决这些问题的最佳做法。
为每个客户创建单独的 table
1) 如果我们为每个客户创建单独的 table 有什么优点和缺点。截至目前,我们有 3 万客户,到今年年底可能会达到 10 万,这意味着数据库中有 10 万 table。我们同时访问所有 tables 以进行读取和写入。
2) 我们将使用相同的 table 并将根据日期范围创建分区
UPDATE : "customer" 是由域决定的吗? 答案是肯定的
谢谢
如果我是你,我不会这样做。首先想到的是,在收到页面浏览消息时,我将消息发送到队列,以便工作人员稍后可以提取并插入数据库(可能是批量);我还增加了 redis 中 siteid:date
的计数器(例如)。在 sql 中执行 count
对于这种情况来说只是一个坏主意。
首先,批评数据类型过大:
`domain` varchar(50) DEFAULT NULL, -- normalize to MEDIUMINT UNSIGNED (3 bytes)
`guid` varchar(100) DEFAULT NULL, -- what is this for?
`sid` varchar(100) DEFAULT NULL, -- varchar?
`url` varchar(2500) DEFAULT NULL,
`ip` varchar(20) DEFAULT NULL, -- too big for IPv4, too small for IPv6; see below
`is_new` varchar(20) DEFAULT NULL, -- flag? Consider `TINYINT` or `ENUM`
`ref` varchar(2500) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL, -- normalize! (add new rows as new agents are created)
`stats_time` datetime DEFAULT NULL,
`country` varchar(50) DEFAULT NULL, -- use standard 2-letter code (see below)
`region` varchar(50) DEFAULT NULL, -- see below
`city` varchar(50) DEFAULT NULL, -- see below
`city_lat_long` varchar(50) DEFAULT NULL, -- unusable in current format; toss?
`email` varchar(100) DEFAULT NULL,
对于 IP 地址,使用 inet6_aton()
,然后存储在 BINARY(16)
。
对于 country
,使用 CHAR(2) CHARACTER SET ascii
-- 仅 2 个字节。
国家+地区+城市+(也许)latlng——将其标准化为"location"。
所有这些更改可能会将磁盘占用空间减少一半。更小 --> 更可缓存 --> 更少 I/O --> 更快。
其他问题...
要大大加快 sid
计数器的速度,请更改
KEY `domain_statstime` (`domain`,`stats_time`),
到
KEY dss (domain_id,`stats_time`, sid),
那将是 "covering index",因此不必在索引和数据之间跳动 2713729 次——跳动耗时 13 分钟。 (domain_id
将在下面讨论。)
这与上面的索引是多余的,DROP
它:
键 domain_index
(domain
)
是"customer"由domain
决定的吗?
每个 InnoDB table 必须有一个 PRIMARY KEY
。获得PK的途径有3种;您选择了 'worst' 一个——一个由引擎制造的隐藏的 6 字节整数。我假设某些列组合没有可用的 'natural' PK?然后,显式 BIGINT UNSIGNED
被调用。 (是的,那将是 8 个字节,但是各种形式的维护需要一个 explicit PK。)
如果 大多数 查询包含 WHERE domain = '...'
,那么我推荐以下内容。 (这将大大改善所有此类查询。)
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
domain_id MEDIUMINT UNSIGNED NOT NULL, -- normalized to `Domains`
PRIMARY KEY(domain_id, id), -- clustering on customer gives you the speedup
INDEX(id) -- this keeps AUTO_INCREMENT happy
建议您查看 pt-online-schema-change
以进行所有这些更改。但是,我不知道它是否可以在没有显式 PRIMARY KEY
.
的情况下工作
"Separate table for each customer"? 没有。这是一个常见的问题;响亮的答案是否定的。我不会重复所有没有 100K 的原因 tables.
分片
"Sharding" 正在将数据拆分到多个 机器 。
要进行分片,您需要在某个地方使用代码来查看 domain
并决定哪个服务器将处理查询,然后将其传递出去。当您遇到 写入缩放 问题时,建议进行分片。您没有提到这一点,因此不清楚分片是否可取。
当对 domain
(或 domain_id
)之类的东西进行分片时,您可以使用 (1) 哈希来选择服务器,(2) 字典查找(10 万行),或者(3) 混合体。
我喜欢这种混合方式——散列到 1024 个值,然后查找 1024 行 table 以查看哪台机器具有数据。由于添加新分片和将用户迁移到不同的分片是一项重大任务,因此我认为混合是一种合理的折衷方案。查找 table 需要分发给所有将操作重定向到分片的客户端。
如果您的 'writing' 运行 失去动力,请参阅 high speed ingestion 以了解可能的加速方法。
分区
PARTITIONing
将数据拆分到多个 "sub-tables".
只有 limited number of use cases 分区可以为您带来任何性能。您没有表示任何适用于您的用例。阅读该博客,看看您是否认为分区可能有用。
您提到了 "partition by date range"。大多数查询都会包含日期范围吗?如果是这样,这样的分区 可能 是可取的。 (有关最佳实践,请参阅上面的 link。)想到其他一些选项:
A 计划:PRIMARY KEY(domain_id, stats_time, id)
但这很笨重,每个二级索引需要更多的开销。 (每个二级索引默默地包含 PK 的所有列。)
B 计划:让 stats_time 包括微秒,然后调整值以避免重复。然后使用 stats_time
而不是 id
。但这需要增加一些复杂性,尤其是在有多个客户端插入数据的情况下。 (如果需要我可以详细说明。)
计划 C:有一个 table 将 stats_time 值映射到 ID。在进行真正的查询之前查找 id 范围,然后使用两者 WHERE id BETWEEN ... AND stats_time ...
。 (又是乱码。)
总结tables
是否有许多查询都是在日期范围内对事物进行计数?建议使用可能基于 per-hour 的汇总表。 More discussion.
COUNT(DISTINCT sid)
折叠成摘要 table 尤其困难。例如,不能将每小时的唯一计数相加以获得当天的唯一计数。但我也有一个 technique。
我们有一个分析产品。我们为每位客户提供一个 JavaScript 代码,他们将其放在自己的网站上。如果用户访问我们的客户站点,java 脚本代码会访问我们的服务器,以便我们代表该客户存储此页面访问。每个客户都包含唯一的域名。
我们将此页面访问存储在 MySql table。
以下是 table 架构。
CREATE TABLE `page_visits` (
`domain` varchar(50) DEFAULT NULL,
`guid` varchar(100) DEFAULT NULL,
`sid` varchar(100) DEFAULT NULL,
`url` varchar(2500) DEFAULT NULL,
`ip` varchar(20) DEFAULT NULL,
`is_new` varchar(20) DEFAULT NULL,
`ref` varchar(2500) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL,
`stats_time` datetime DEFAULT NULL,
`country` varchar(50) DEFAULT NULL,
`region` varchar(50) DEFAULT NULL,
`city` varchar(50) DEFAULT NULL,
`city_lat_long` varchar(50) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
KEY `sid_index` (`sid`) USING BTREE,
KEY `domain_index` (`domain`),
KEY `email_index` (`email`),
KEY `stats_time_index` (`stats_time`),
KEY `domain_statstime` (`domain`,`stats_time`),
KEY `domain_email` (`domain`,`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
我们没有此 table 的主键。
MySql 服务器详情
是Google云MySql(版本是5.6)存储容量是10TB
截至目前,我们的 table 中有 3.5 亿行,table 大小为 300 GB。我们将所有客户详细信息存储在同一个 table 中,即使一个客户与另一个客户之间没有任何关系。
问题 1:我们的少数客户在 table 中拥有大量行,因此针对这些客户的查询性能非常慢。
示例查询 1:
SELECT count(DISTINCT sid) AS count,count(sid) AS total FROM page_views WHERE domain = 'aaa' AND stats_time BETWEEN CONVERT_TZ('2015-02-05 00:00:00','+05:30','+00:00') AND CONVERT_TZ('2016-01-01 23:59:59','+05:30','+00:00');
+---------+---------+
| count | total |
+---------+---------+
| 1056546 | 2713729 |
+---------+---------+
1 row in set (13 min 19.71 sec)
我会在这里更新更多查询。我们需要在 5-10 秒内得到结果,这可能吗?
问题 2:table 大小正在迅速增加,我们可能会在今年年底达到 table 5 TB 的大小,因此我们希望对我们的数据进行分片table。我们想在一台机器上保存与一个客户相关的所有记录。此分片的最佳实践是什么。
我们正在考虑以下解决上述问题的方法,请建议我们解决这些问题的最佳做法。
为每个客户创建单独的 table
1) 如果我们为每个客户创建单独的 table 有什么优点和缺点。截至目前,我们有 3 万客户,到今年年底可能会达到 10 万,这意味着数据库中有 10 万 table。我们同时访问所有 tables 以进行读取和写入。
2) 我们将使用相同的 table 并将根据日期范围创建分区
UPDATE : "customer" 是由域决定的吗? 答案是肯定的
谢谢
如果我是你,我不会这样做。首先想到的是,在收到页面浏览消息时,我将消息发送到队列,以便工作人员稍后可以提取并插入数据库(可能是批量);我还增加了 redis 中 siteid:date
的计数器(例如)。在 sql 中执行 count
对于这种情况来说只是一个坏主意。
首先,批评数据类型过大:
`domain` varchar(50) DEFAULT NULL, -- normalize to MEDIUMINT UNSIGNED (3 bytes)
`guid` varchar(100) DEFAULT NULL, -- what is this for?
`sid` varchar(100) DEFAULT NULL, -- varchar?
`url` varchar(2500) DEFAULT NULL,
`ip` varchar(20) DEFAULT NULL, -- too big for IPv4, too small for IPv6; see below
`is_new` varchar(20) DEFAULT NULL, -- flag? Consider `TINYINT` or `ENUM`
`ref` varchar(2500) DEFAULT NULL,
`user_agent` varchar(255) DEFAULT NULL, -- normalize! (add new rows as new agents are created)
`stats_time` datetime DEFAULT NULL,
`country` varchar(50) DEFAULT NULL, -- use standard 2-letter code (see below)
`region` varchar(50) DEFAULT NULL, -- see below
`city` varchar(50) DEFAULT NULL, -- see below
`city_lat_long` varchar(50) DEFAULT NULL, -- unusable in current format; toss?
`email` varchar(100) DEFAULT NULL,
对于 IP 地址,使用 inet6_aton()
,然后存储在 BINARY(16)
。
对于 country
,使用 CHAR(2) CHARACTER SET ascii
-- 仅 2 个字节。
国家+地区+城市+(也许)latlng——将其标准化为"location"。
所有这些更改可能会将磁盘占用空间减少一半。更小 --> 更可缓存 --> 更少 I/O --> 更快。
其他问题...
要大大加快 sid
计数器的速度,请更改
KEY `domain_statstime` (`domain`,`stats_time`),
到
KEY dss (domain_id,`stats_time`, sid),
那将是 "covering index",因此不必在索引和数据之间跳动 2713729 次——跳动耗时 13 分钟。 (domain_id
将在下面讨论。)
这与上面的索引是多余的,DROP
它:
键 domain_index
(domain
)
是"customer"由domain
决定的吗?
每个 InnoDB table 必须有一个 PRIMARY KEY
。获得PK的途径有3种;您选择了 'worst' 一个——一个由引擎制造的隐藏的 6 字节整数。我假设某些列组合没有可用的 'natural' PK?然后,显式 BIGINT UNSIGNED
被调用。 (是的,那将是 8 个字节,但是各种形式的维护需要一个 explicit PK。)
如果 大多数 查询包含 WHERE domain = '...'
,那么我推荐以下内容。 (这将大大改善所有此类查询。)
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
domain_id MEDIUMINT UNSIGNED NOT NULL, -- normalized to `Domains`
PRIMARY KEY(domain_id, id), -- clustering on customer gives you the speedup
INDEX(id) -- this keeps AUTO_INCREMENT happy
建议您查看 pt-online-schema-change
以进行所有这些更改。但是,我不知道它是否可以在没有显式 PRIMARY KEY
.
"Separate table for each customer"? 没有。这是一个常见的问题;响亮的答案是否定的。我不会重复所有没有 100K 的原因 tables.
分片
"Sharding" 正在将数据拆分到多个 机器 。
要进行分片,您需要在某个地方使用代码来查看 domain
并决定哪个服务器将处理查询,然后将其传递出去。当您遇到 写入缩放 问题时,建议进行分片。您没有提到这一点,因此不清楚分片是否可取。
当对 domain
(或 domain_id
)之类的东西进行分片时,您可以使用 (1) 哈希来选择服务器,(2) 字典查找(10 万行),或者(3) 混合体。
我喜欢这种混合方式——散列到 1024 个值,然后查找 1024 行 table 以查看哪台机器具有数据。由于添加新分片和将用户迁移到不同的分片是一项重大任务,因此我认为混合是一种合理的折衷方案。查找 table 需要分发给所有将操作重定向到分片的客户端。
如果您的 'writing' 运行 失去动力,请参阅 high speed ingestion 以了解可能的加速方法。
分区
PARTITIONing
将数据拆分到多个 "sub-tables".
只有 limited number of use cases 分区可以为您带来任何性能。您没有表示任何适用于您的用例。阅读该博客,看看您是否认为分区可能有用。
您提到了 "partition by date range"。大多数查询都会包含日期范围吗?如果是这样,这样的分区 可能 是可取的。 (有关最佳实践,请参阅上面的 link。)想到其他一些选项:
A 计划:PRIMARY KEY(domain_id, stats_time, id)
但这很笨重,每个二级索引需要更多的开销。 (每个二级索引默默地包含 PK 的所有列。)
B 计划:让 stats_time 包括微秒,然后调整值以避免重复。然后使用 stats_time
而不是 id
。但这需要增加一些复杂性,尤其是在有多个客户端插入数据的情况下。 (如果需要我可以详细说明。)
计划 C:有一个 table 将 stats_time 值映射到 ID。在进行真正的查询之前查找 id 范围,然后使用两者 WHERE id BETWEEN ... AND stats_time ...
。 (又是乱码。)
总结tables
是否有许多查询都是在日期范围内对事物进行计数?建议使用可能基于 per-hour 的汇总表。 More discussion.
COUNT(DISTINCT sid)
折叠成摘要 table 尤其困难。例如,不能将每小时的唯一计数相加以获得当天的唯一计数。但我也有一个 technique。