【问题标题】:MySql - Handle table size and performanceMySql - 处理表大小和性能
【发布时间】:2016-04-15 12:58:42
【问题描述】:

我们有一个分析产品。我们为每个客户提供一个 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 |

我们没有此表的主键。

MySql 服务器详情

是谷歌云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)

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

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

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

为每个客户创建单独的表

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

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

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

谢谢

【问题讨论】:

    标签: mysql database query-performance


    【解决方案1】:

    首先,如果数据类型过大

      `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个字节。

    country + region + city + (maybe) latlng -- 将其标准化为“位置”。

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

    其他问题...

    要大大加快您的sid 计数器,请更改

    KEY `domain_statstime` (`domain`,`stats_time`),
    

    KEY dss (domain_id,`stats_time`, sid),
    

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

    这对于上面的索引是多余的,DROP 它: 密钥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
    

    建议您查看 pt-online-schema-change 进行所有这些更改。但是,我不知道它是否可以在没有明确的PRIMARY KEY 的情况下工作。

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

    分片

    “分片”是将数据拆分到多台机器

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

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

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

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

    分区

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

    只有limited number of use cases 分区可以为您带来任何性能。您没有表示任何适用于您的用例。阅读该博客,看看您是否认为分区可能有用。

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

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

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

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

    汇总表

    是否有许多查询是对日期范围内的事物进行计数的形式?建议可能基于每小时的汇总表。 More discussion.

    COUNT(DISTINCT sid) 特别难以折叠成汇总表。例如,不能将每小时的唯一计数加在一起以获得当天的唯一计数。但我也有一个technique

    【讨论】:

    • @James 感谢您的详细解释,您能否分享任何解释链接,为什么 100k 表不是一个好的决定。
    • 100K 表是这个论坛上的一个常见问题。以下是一些反对它的论点: 操作系统开销和减速;代码的复杂性;优势很小。可能会有很多小桌子和几张大桌子——每个极端都有其自身的低效率。
    • @cloudpre - 感谢您的评论。这是我喜欢谈论的类型。
    • @RickJames 我将根据您的建议进行架构更正并使用适当的索引优化查询。假设即使在所有这些更改之后假设如果我们有无法放入内存的巨大索引文件,在这种情况下我们是否需要对我们的表进行分片?或者只是范围分区做的工作??。例如,我们有 10 个存储容量。
    • 旋转驱动器?还是固态硬盘? “10个存储容量”是什么意思?
    【解决方案2】:

    如果我是你,我不会这样做。首先想到的是,在收到页面浏览消息时,我将消息发送到队列,以便工作人员稍后可以提取并插入数据库(可能是批量);我也在redis中增加了siteid:date的计数器(例如)。在 sql 中执行 count 对于这种情况来说只是一个坏主意。

    【讨论】:

    • @Tran 嘿,谢谢你的回答,你到底要不要在 MySql 上做呢
    • @Rams 你想在 sql 中做吗?
    • @Tran 我们希望在任何云上可用的解决方案(如 Google cloud sql)上执行此操作。
    • @Rams well rabbitmq 和 redis 都可以在 google cloud 上使用。
    • @Tran 好的,我会调查一下。对 sql 有什么建议吗??
    猜你喜欢
    • 1970-01-01
    • 2010-12-16
    • 1970-01-01
    • 2011-09-12
    • 1970-01-01
    • 2017-02-27
    • 2012-07-28
    • 2013-01-05
    • 2021-09-28
    相关资源
    最近更新 更多