InnoDBのレコード長の限界
InnoDBのデフォルトのページサイズは、16Kです。この場合、最大レコード長は約8000バイトです。マニュアルで「約8000バイト」と曖昧な言い回しになっているのは、データの保存に使える領域以外にメタデータを保存する領域が必要だからでしょう。
可変長カラム (VARBINARY、VARCHAR、BLOB、および TEXT) を除き、行の最大長はデータベースページの半分より少し短くなります。つまり、デフォルトページサイズの 16K バイトでは、行の最大長が約 8000 バイトになります。
https://dev.mysql.com/doc/refman/5.6/ja/column-count-limit.html
また、「可変長カラム (VARBINARY、VARCHAR、BLOB、および TEXT) を除き」は可変長カラムはオーバーフローページに分割して保存されることを指し示しています。 可変長カラムは複数のページに分割して保存されるため、可変長カラムを含むテーブルでは、8000バイト以上保存できます。
-- 固定長のCHARだと、250 x 33 = 8250 バイトでエラー mysql> CREATE TABLE table_with_char (pk int primary key, c1 CHAR(250) NOT NULL, c2 CHAR(250) NOT NULL, c3 CHAR(250) NOT NULL, : c33 CHAR(250) NOT NULL ) ROW_FORMAT=DYNAMIC CHARACTER SET 'latin1'; ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help. In current row format, BLOB prefix of 0 bytes is stored inline. -- varchar にすれば、一行8000バイトの壁を越えられる mysql> CREATE TABLE table_with_varchar (pk int primary key, c1 VARCHAR(250) NOT NULL, c2 VARCHAR(250) NOT NULL, c3 VARCHAR(250) NOT NULL, : c33 VARCHAR(250) NOT NULL ) ROW_FORMAT=DYNAMIC CHARACTER SET 'latin1'; Query OK, 0 rows affected (0.02 sec)
では、可変長カラムであれば、いくらでも保存できるかというとそうではありません。 可変長カラムであっても、ポインタ分の 20バイト 消費します。
ROW_FORMAT=DYNAMIC または ROW_FORMAT=COMPRESSED で作成されたテーブルでは、カラムの長さおよび行全体の長さによっては、BLOB、TEXT、または VARCHAR カラムの値が完全にオフページに格納される場合もあります。オフページに格納されるカラムでは、クラスタ化されたインデックスのレコードに、オーバーフローページへの 20 バイトのポインタのみがカラムごとに 1 つずつ含まれます。
https://dev.mysql.com/doc/refman/5.6/ja/innodb-compression-internals.html
また、TEXT/BLOB 型では、実際のデータの長さによっては、20バイトではなく、40バイト消費するケースもあります。
ROW_FORMAT=DYNAMIC または ROW_FORMAT=COMPRESSED で作成されたテーブルでは、40 バイト以下の TEXT および BLOB カラムは、常にインラインに格納されます。
https://dev.mysql.com/doc/refman/5.6/ja/innodb-compression-internals.html
ROW_FORMAT=DYNAMIC で 8000バイトの壁を確認するSQL
さて、上記をふまえて、各テーブルのレコード長(オーバーフローページ分を除く)を表示するSQLを考えてみます。
こちらの記事のSQLを参考にさせて頂きました。この記事のSQLは ROW_FORMAT=COMPACT 用のSQLです。
ROW_FORMAT=DYNAMIC 用に上記の可変長カラムのオーバーフローページを考慮して微修正しました。
SELECT
TABLE_SCHEMA,
TABLE_NAME,
SUM(
CASE
WHEN DATA_TYPE = 'tinyint' then 1
WHEN DATA_TYPE = 'smallint' then 2
WHEN DATA_TYPE = 'mediumint' then 3
WHEN DATA_TYPE = 'int' then 4
WHEN DATA_TYPE = 'bigint' then 8
WHEN DATA_TYPE = 'float' then 4
WHEN DATA_TYPE = 'double' then 8
WHEN DATA_TYPE = 'decimal' then 0
+ FLOOR((NUMERIC_PRECISION - NUMERIC_SCALE) / 9) * 4
+ CEIL((NUMERIC_PRECISION - NUMERIC_SCALE) % 9 / 2)
+ FLOOR((NUMERIC_SCALE) / 9) * 4
+ CEIL((NUMERIC_SCALE) % 9 / 2)
WHEN DATA_TYPE = 'char' then CHARACTER_OCTET_LENGTH
WHEN DATA_TYPE = 'binary' then CHARACTER_OCTET_LENGTH
-- varchar/varbinary ではポインタ分 20BYTE
WHEN DATA_TYPE = 'varchar' then 20
WHEN DATA_TYPE = 'varbinary' then 20
-- text/blob では 40BYTE
WHEN DATA_TYPE REGEXP '^(tiny|medium|long)?text$' then 40
WHEN DATA_TYPE REGEXP '^(tiny|medium|long)?blob$' then 40
WHEN DATA_TYPE = 'datetime' then 8
WHEN DATA_TYPE = 'date' then 3
WHEN DATA_TYPE = 'time' then 3
WHEN DATA_TYPE = 'year' then 1
WHEN DATA_TYPE = 'timestamp' then 4
WHEN DATA_TYPE = 'enum' then IF((CHARACTER_OCTET_LENGTH / CHARACTER_MAXIMUM_LENGTH) < 256, 1, 2)
ELSE NULL
END
) as SIZE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema')
GROUP BY TABLE_SCHEMA, TABLE_NAME ORDER BY TABLE_SCHEMA, TABLE_NAME;
実行結果例)
+--------------+--------------------+------+ | TABLE_SCHEMA | TABLE_NAME | SIZE | +--------------+--------------------+------+ | test | col1017 | 1020 | | test | t2 | 44 | | test | table_with_varchar | 664 | | test | text_x_197 | 7884 | | test | text_x_200 | 7884 | | test | text_x_null197 | 7884 | | test | var1 | 20 | +--------------+--------------------+------+ 7 rows in set (0.03 sec)
ソースコード
MySQL 5.6 では storage/innobase/dict/dict0dict.cc の dict_index_too_big_for_tree でCREATE TABLE 時のレコード長のチェックがされてました。
rec_max_size の値を追いかけると、各型が何バイトで計算されるかわかりました。
/****************************************************************//**
If a record of this index might not fit on a single B-tree page,
return TRUE.
@return TRUE if the index record could become too big */
static
ibool
dict_index_too_big_for_tree(
/*========================*/
const dict_table_t* table, /*!< in: table */
const dict_index_t* new_index) /*!< in: index */
{
ulint zip_size;
ulint comp;
ulint i;
/* maximum possible storage size of a record */
ulint rec_max_size;
/* maximum allowed size of a record on a leaf page */
ulint page_rec_max;
/* maximum allowed size of a node pointer record */
ulint page_ptr_max;
<snip>
add_field_size:
rec_max_size += field_max_size;
/* Check the size limit on leaf pages. */
if (UNIV_UNLIKELY(rec_max_size >= page_rec_max)) {
return(TRUE);
}
/* Check the size limit on non-leaf pages. Records
stored in non-leaf B-tree pages consist of the unique
columns of the record (the key columns of the B-tree)
and a node pointer field. When we have processed the
unique columns, rec_max_size equals the size of the
node pointer record minus the node pointer column. */
if (i + 1 == dict_index_get_n_unique_in_tree(new_index)
&& rec_max_size + REC_NODE_PTR_SIZE >= page_ptr_max) {
return(TRUE);
}