MySql - 处理表大小和性能

Ram*_*ams 5 mysql database query-performance

我们有一个分析产品。我们为每位客户提供一个 JavaScript 代码,他们将其放入他们的网站中。如果用户访问我们的客户站点,Java 脚本代码就会访问我们的服务器,以便我们代表该客户存储此页面访问。每个客户都包含唯一的域名。

我们将此页面访问存储在 MySql 表中。

以下是表架构。

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 |
Run Code Online (Sandbox Code Playgroud)

我们没有该表的主键。

MySql 服务器详细信息

它是Google云MySql(版本为5.6),存储容量为10TB。

截至目前,我们的表中有 3.5 亿行,表大小为 300 GB。我们将所有客户详细信息存储在同一张表中,即使一个客户与另一个客户之间没有关系。

问题1:对于我们的少数客户来说,表中的行数很大,因此针对这些客户的查询性能非常慢。

示例查询 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)
Run Code Online (Sandbox Code Playgroud)

我将在这里更新更多查询。我们需要在 5-10 秒内得到结果,这可能吗?

问题 2:表大小正在快速增加,到今年年底我们可能会达到表大小 5 TB,因此我们想要对表进行分片。我们希望将与一位客户相关的所有记录保存在一台机器中。这种分片的最佳实践是什么?

我们正在考虑采取以下方法来解决上述问题,请向我们建议解决这些问题的最佳实践。

为每个客户创建单独的表

1)如果我们为每个客户创建单独的表有什么优点和缺点?截至目前,我们拥有 3 万个客户,到今年年底我们可能会达到 10 万个,这意味着数据库中有 10 万个表。我们同时访问所有表以进行读取和写入。

2)我们将使用同一个表并根据日期范围创建分区

更新:“客户”是由域决定的吗?答案是肯定的

谢谢

Ric*_*mes 5

首先,批评是否过大的数据类型

  `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,
Run Code Online (Sandbox Code Playgroud)

对于 IP 地址,使用inet6_aton(),然后存储在 中BINARY(16)

对于country,仅使用CHAR(2) CHARACTER SET ascii- 2 个字节。

国家+地区+城市+(也许)latlng——将其标准化为“位置”。

所有这些更改可能会将磁盘占用空间减少一半。更小 --> 更可缓存 --> 更少的 I/O --> 更快。

其他事宜...

要大大加快sid计数器速度,请更改

KEY `domain_statstime` (`domain`,`stats_time`),
Run Code Online (Sandbox Code Playgroud)

KEY dss (domain_id,`stats_time`, sid),
Run Code Online (Sandbox Code Playgroud)

这将是一个“覆盖索引”,因此不必在索引和数据之间反弹 2713729 次——反弹花费了 13 分钟。(domain_id下面讨论。)

这与上面的索引是多余的,DROP它:KEY domain_index( domain)

“顾客”是由 决定的吗domain

每个 InnoDB 表都必须有一个PRIMARY KEY. 获得PK的方式有3种;你选择了“最差”的一个——由引擎制造的隐藏的 6 字节整数。我认为某些列的组合没有“自然”的 PK 可用?然后,BIGINT UNSIGNED需要显式的。(是的,这将是 8 个字节,但是各种形式的维护都需要显式的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
Run Code Online (Sandbox Code Playgroud)

建议您考虑pt-online-schema-change进行所有这些更改。但是,我不知道如果没有明确的PRIMARY KEY.

“为每个顾客提供单独的桌子”? 。这是一个常见问题;响亮的答案是否定的。我不会重复没有 100K 表的所有原因。

分片

“分片”是将数据分割到多台机器上。

要进行分片,您需要在某处编写代码来查看domain并决定哪个服务器将处理查询,然后将其移交。当您遇到写入扩展问题时,建议使用分片。您没有提到这一点,因此尚不清楚分片是否可取。

当对类似domain(或domain_id) 的内容进行分片时,您可以使用 (1) 哈希来选择服务器,(2) 字典查找(100K 行),或 (3) 混合。

我喜欢混合——哈希到 1024 个值,然后查找 1024 行表以查看哪台机器拥有数据。由于添加新分片并将用户迁移到不同分片是一项重大任务,因此我认为混合是一个合理的折衷方案。查找表需要分发给所有将操作重定向到分片的客户端。

如果您的“写作”已经失去动力,请参阅高速摄取以了解加快速度的可能方法。

分区

PARTITIONing将数据拆分到多个“子表”中。

只有有限数量的用例可以通过分区来获得性能。您没有表明任何适用于您的用例。阅读该博客,看看您是否认为分区可能有用。

您提到“按日期范围分区”。大多数查询都会包含日期范围吗?如果是这样,这种划分可能是可取的。(有关最佳实践,请参阅上面的链接。)我想到了一些其他选项:

计划 A:PRIMARY KEY(domain_id, stats_time, id)但这很庞大,并且每个二级索引都需要更多的开销。(每个二级索引默默地包含 PK 的所有列。)

计划 B:让 stats_time 包含微秒,然后调整值以避免重复。然后使用stats_time代替id. 但这需要增加一些复杂性,特别是当有多个客户端插入数据时。(如果需要的话我可以详细说明。)

计划 C:有一个将 stats_time 值映射到 ids 的表。在进行实际查询之前查找 id 范围,然后使用两者WHERE id BETWEEN ... AND stats_time ...。(再次,混乱的代码。)

汇总表

许多查询都是对日期范围内的事物进行计数的形式吗?建议建立基于每小时的汇总表。 更多讨论

COUNT(DISTINCT sid)折叠到汇总表中尤其困难。例如,每小时的唯一计数无法加在一起以获得当天的唯一计数。但我也有这样的技巧。