【问题标题】:How to estimate a wide row's size (1000 columns) in MySQL InnoDB?如何估计 MySQL InnoDB 中的宽行大小(1000 列)?
【发布时间】:2021-07-07 14:05:02
【问题描述】:

我想解决的特殊问题是一个列非常宽(有时 > 1000 列)的表,这需要我拆分表。为了巧妙地做到这一点,我想在 Row size too large (> 8126)所用表类型的最大行大小(不包括 BLOB)之前进行拆分,是 65535。.

为此,我试图找出每列的实际大小。有些很简单(如 BOOLEAN、INT、DATE、TEXT 等)。我想弄清楚VARCHAR。因此,创建具有许多 VARCHAR 列的表的实验。

但是,我无法真正理解它。我尝试使用仅包含 VARCHAR(217) 的表进行试验,我能够创建 100 列。下一列(第 101 列)的最大大小是 VARCHAR(78)。

CREATE TABLE IF NOT EXISTS test
(
col1    VARCHAR(14) NOT NULL,
col2    VARCHAR(14) NOT NULL,
...
col100  VARCHAR(217) NOT NULL,
col101  VARCHAR(78) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

我发现原因如下:

  1. 2 个字节用于存储长度值且 NOT NULL
  2. 217x3 = 651 字节用于页外存储
  3. 所以 100 列 = 2x100 + 651x100 = 65300
  4. 最大值为 65535 - 65300 = 235 个字节
  5. 因此,我们可以再容纳 1 列的最大值 (235 - 2) / 3 = 77.666666666666667 … 或 VARCHAR(78)

将上述内容应用于 VARCHAR(128),我预计 65535 / (128x3+2) 或 169 列,最后一列可以适合 VARCHAR(99)。这确实是真的。这有效:

CREATE TABLE IF NOT EXISTS test
(
col1    VARCHAR(128) NOT NULL,
col2    VARCHAR(128) NOT NULL,
...
col169  VARCHAR(128) NOT NULL,
col170  VARCHAR(99) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

但是,对 VARCHAR(64) 应用相同的逻辑是行不通的。应用上面相同的逻辑,我希望有 337 列。但是,我只能得到 197 列。除此之外,我得到 Row size too large (> 8126) 错误。

CREATE TABLE IF NOT EXISTS test
(
col1    VARCHAR(64) NOT NULL,
col2    VARCHAR(64) NOT NULL,
...
col196  VARCHAR(64) NOT NULL,
col197  VARCHAR(64) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

对于 VARCHAR(14) 也是如此。在遇到 Row size too large (> 8126) 错误之前,我还可以创建最多 197 列。我进一步体验并发现直到 VARCHAR(109),我会得到 Row size too large (> 8126) 错误。但是对于 VARCHAR(110),我会得到一个不同的错误使用的表类型的最大行大小,不包括 BLOB,是 65535。

MySQL documentation

当使用 ROW_FORMAT=DYNAMIC 创建表时,InnoDB 可以完全离页存储长可变长度列值(对于 VARCHAR、VARBINARY 以及 BLOB 和 TEXT 类型),聚集索引记录仅包含 20 字节指向溢出页面的指针。

我怀疑这是这里发生的事情,InnoDB 会将一些值完全存储在页面之外,因为 64 长度的 utf8 字符串中的 197 个不可能适合 8126。如果所有 64 长度的 utf8 字符串都存储在行内,我预计最多 41 列( 8126 / (64x3+2) )。

任何帮助表示赞赏。我正在使用 MySQL 5.7.26-29,梭子鱼文件格式和 16K 页面大小。

【问题讨论】:

  • 如果你有 1000 列的表,我建议你从基本的关系数据库设计课程开始
  • 不幸的是,考虑到这个特定的用例,规范化并不可行。这就是为什么它必须以扁平的方式完成,这迫使我发现这一点。
  • 公式以不同的方式混乱。

标签: mysql innodb


【解决方案1】:

即使 InnoDB 在页面外“完全”存储 varchar/blob/text 列,主页上仍然有一个 20 字节的指针。所以这不像你可以制作一个包含无限多列的表格。甚至 1000 列。

元数据文件 (.frm) 对行大小也有限制。这很复杂。见https://www.percona.com/blog/2013/04/08/understanding-the-maximum-number-of-columns-in-a-mysql-table/

这在 MySQL 8.0 中完全改变了,因为 .frm 文件已被新的数据字典实现所取代。我对此的研究还不够深入,无法知道限制。

话虽如此,您还没有描述为什么您有一个包含这么多 varchar 列的表,但我认为任何包含数百列的表都是 Code Smell。也就是说,不能保证它是一个糟糕的设计,但它确实闻起来像一个。这就像当您编写一个 1000 多行长的单一代码方法时,您的 IDE 向您抱怨。一个方法可能需要那么长吗?当然。这通常是一个坏主意吗?肯定的。

您应该注意的另一个软件工程隐喻是XY Problem。不要让自己过于执着于使一种解决方案发挥作用,而忽略了可能更容易的替代解决方案。

【讨论】:

  • 感谢您的回复比尔。用例相当独特,最好将东西平放。在这种特定情况下,规范化没有意义。因此问题。
  • 也许关系数据库不是您所需要的?听起来你需要一个文件
【解决方案2】:

最大值刚刚超过 8KB记录在案

正在使用什么ROW_FORMAT

长字符串可能在 8KB 限制和几乎无限的“非记录”存储空间之间分割。

VARCHARsTEXTsVARBINARYsBLOBs 以下列方式之一分配:

  • 完全记录在案。这适用于
  • 记录中最多 767 个字节;其余的在“溢出块”中。这适用于 row_format=COMPACT。
  • 完全在溢出块中。这适用于动态和压缩。它留下一个 20 字节的“指针”。所以算作 20 个字节。

我不知道:

  • 我是否对你提到的“2”感到失望。
  • 在未提及的情况下会发生什么。

至于做“垂直分区”,我有这样的想法:

  • 有跨列分布的“数组”。而是将它们分布在另一个表中的中。
  • 将宽表拆分为 2 个以上的表。
  • 根据相关信息进行拆分。
  • 拆分大部分为NULL 的列。然后在将它们重新绑定在一起时使用LEFT JOIN,因为新表中可能缺少行。
  • 规范化重复的字符串。例如:country 的两个字母 country_code。 (但不要规范化短字符串。)
  • 在可行的情况下,请使用较小的非字符串列类型(例如,1 字节 TINYTINT 与 8 字节 BIGINT
  • 考虑在客户端中“压缩”大文本列,并将它们存储在BLOB 中。典型的文本缩小 3:1。这种缩减有助于改善磁盘空间、行大小限制,或许还有助于提高性能。
  • ROW_FORMAT=COMPRESSED 缩小了大约 2:1,但开销很大,实际上可能对您的问题没有帮助。
  • 如果所有文本都仅使用英文字符,CHARACTER SET 可能没有影响(甚至没有您提到的 3x)。
  • 有几列用于搜索/排序,再加上一个大的 JSON 列,是否可行?该 JSON 列的大小可能是千兆字节,主要位于非记录存储中。这将很快解决 8KB 的限制。
  • 我见过没有人真正改变块大小。但是,理论上,一个 32KB 的块(对于您服务器上的所有表)将为您提供每条记录的最大 16KB。还有一个 64KB 选项,但它也将行大小限制为 16KB。

更多关于尺寸:

  • 我认为NULL 列需要 3 个字节。
  • 一行有大约 20-30 字节的开销。
  • 一个块的填充不超过 15/16(希望允许添加一个额外的行)。 (我对逻辑有疑问。)

【讨论】:

    猜你喜欢
    • 2014-09-12
    • 1970-01-01
    • 2013-08-21
    • 2011-02-13
    • 1970-01-01
    • 1970-01-01
    • 2017-03-24
    • 1970-01-01
    • 2016-11-09
    相关资源
    最近更新 更多