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