GA

2023/02/20

テンポラリーテーブルがストレージを埋め尽くした時のエラー番号の違い on MySQL 8.0.32

TL;DR

  • TempTableストレージエンジンがDisk落ちした時とInnoDB Temporary tableで「同じストレージあふれ」でも微妙にエラー内容が違う

    • 前者は ERROR 14 (HY000): Can't change size of file (OS errno 28 - No space left on device) , エラーログ出力なし

    • 後者は ERROR 1114 (HY000): The table '#sqlXXXXX' is full でエラーログ出力もあり

    • ちなみにMyISAMは ERROR 126 (HY000): Incorrect key file for table '/mytmp/#sqlXXXXX.MYI'; try to repair it 、これ少なくとも5.7からは変わってない


実験用に100MBくらいのファイルにxfsを作ってマウントする。


$ dd if=/dev/zero of=./mytmp bs=1M count=100

$ mkfs -t xfs ./mytmp

$ sudo mkdir /mytmp
$ sudo mount /home/yoku0825/mytmp /mytmp

$ sudo chown -R yoku0825. /mytmp   ### mysqldをyoku0825ユーザーで動かすので
$ mkdir /mytmp/innodb_temp   ### innodb_temp_tablespaces_dirはあらかじめディレクトリがないと転ける

my.cnfで tmpdir (MyISAM, TempTableストレージエンジンが使う)とinnodb_temp_tablespaces_dir/mytmp を割り当てて再起動。

$ vim /etc/my.cnf
[mysqld]
tmpdir= /mytmp
innodb_temp_tablespaces_dir=/mytmp/innodb_temp  ### このディレクトリは自動で作ってくれないので、自分でmkdirしないと起動が転ける

さっさと溢れさせたいのでダミーデータを入れて少しでもテンポラリー領域を使ったらあふれるようにセット。

$ dd if=/dev/zero of=/mytmp/dummy bs=1M count=90   ### さっさと溢れさせたいのでさらにダミー

$ df -h /mytmp
Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0       97M   96M  676K 100% /mytmp

$ du -sh /mytmp/*
90M     /mytmp/dummy
800K    /mytmp/innodb_temp

実験1、TempTable on Disk。Diskに落とすために temptable_max_ram を小さくする。

mysql80 8> SHOW VARIABLES LIKE '%temp%';
+-----------------------------+-----------------------+
| Variable_name               | Value                 |
+-----------------------------+-----------------------+
| avoid_temporal_upgrade      | OFF                   |
| innodb_temp_data_file_path  | ibtmp1:12M:autoextend |
| innodb_temp_tablespaces_dir | /mytmp/innodb_temp    |
| show_old_temporals          | OFF                   |
| temptable_max_mmap          | 1073741824            |
| temptable_max_ram           | 1073741824            |
| temptable_use_mmap          | ON                    |
+-----------------------------+-----------------------+
7 rows in set (0.01 sec)

mysql80 8> SHOW VARIABLES LIKE '%tmp%';
+---------------------------------+-----------+
| Variable_name                   | Value     |
+---------------------------------+-----------+
| default_tmp_storage_engine      | InnoDB    |
| innodb_tmpdir                   |           |
| internal_tmp_mem_storage_engine | TempTable |
| replica_load_tmpdir             | /mytmp    |
| slave_load_tmpdir               | /mytmp    |
| tmp_table_size                  | 16777216  |
| tmpdir                          | /mytmp    |
+---------------------------------+-----------+
7 rows in set (0.01 sec)

mysql80 9> SET GLOBAL temptable_max_ram = 2 * 1024 * 1024;
Query OK, 0 rows affected (0.01 sec)

mysql80 9> SELECT @@temptable_max_ram, @@tmp_table_size;  -- tmp_table_sizeの方が小さいとInnoDBに落ちてしまう
+---------------------+------------------+
| @@temptable_max_ram | @@tmp_table_size |
+---------------------+------------------+
|             2097152 |         16777216 |
+---------------------+------------------+
1 row in set (0.00 sec)

TempTableストレージエンジンはユーザー定義テンポラリーテーブル ( CREATE TEMPORARY TABLE ) としては使えないので、 WITH RECURSIVE (必ずテンポラリーテーブルを作る) で代用。

mysql80 9> SET SESSION cte_max_recursion_depth = 10000000;
Query OK, 0 rows affected (0.00 sec)

mysql80 9> WITH RECURSIVE v AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM v WHERE n <= 1999999), v2 AS (SELECT n, MD5(n) AS m FROM v) SELECT n, m, md5(m) AS o FROM v2 ORDER BY RAND() LIMIT 1;
ERROR 14 (HY000): Can't change size of file (OS errno 28 - No space left on device)

### エラーログには出力なし

実験2、InnoDB Temporary。暗黙の一時テーブル( SELECT がバックグラウンドで作るやつ ) も ユーザー定義テンポラリーテーブル ( CREATE TEMPORARY TABLE ) もどちらも同じ .ibt ファイルを使う。

はずなんだけど、なんかエラーが違った。

### 暗黙のテンポラリーテーブル版
mysql80 7> SET SESSION tmp_table_size = 1024;
Query OK, 0 rows affected (0.00 sec)

mysql80 7> SELECT @@temptable_max_ram, @@tmp_table_size;  -- tmp_table_sizeを超えるとTempTableからInnoDBに落ちる
+---------------------+------------------+
| @@temptable_max_ram | @@tmp_table_size |
+---------------------+------------------+
|             2097152 |             1024 |
+---------------------+------------------+
1 row in set (0.00 sec)

mysql80 7> WITH RECURSIVE v AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM v WHERE n <= 1999999), v2 AS (SELECT n, MD5(n) AS m FROM v) SELECT n, m, md5(m) AS o FROM v2 ORDER BY RAND() LIMIT 1;
ERROR 1146 (42S02): Table './mytmp/#sql45cf_8_0' doesn't exist

### doesn't existって言われた…
### エラーログには出力なし

↑ちなみにdummyファイルを消して、一度実行してからもう一度dummyファイルを作って、もっかいクエリすると↓と同じ table is full になった。テンポラリーテーブルを作る前の下処理っぽいところで失敗してストレージエンジンまで落ちてなかったっぽい。


2023-02-20T14:48:05.879945+09:00 9 [Warning] [MY-012145] [InnoDB] Error while writing 1048576 zeroes to /mytmp/innodb_temp/temp_10.ibt starting at offset 10485760

2023-02-20T14:48:05.880278+09:00 9 [ERROR] [MY-013132] [Server] The table '/mytmp/#sql54e5_9_3' is full!     <--- ここがibtファイルのパスじゃなくて tmpdir/内部名 になるの罠い



### ユーザー定義テンポラリーテーブル版
mysql80 8> CREATE TEMPORARY TABLE d1.tt1 (num int, val varchar(32)) Engine = InnoDB;
Query OK, 0 rows affected (0.00 sec)

mysql80 8> LOAD DATA INFILE '/home/yoku0825/md5' INTO TABLE d1.tt1;
ERROR 1114 (HY000): The table 'tt1' is full

### エラーログ
2023-02-20T14:32:23.733251+09:00 8 [ERROR] [MY-012144] [InnoDB] posix_fallocate(): Failed to preallocate data for file /mytmp/innodb_temp/temp_9.ibt, desired size 425984 bytes. Operating system error number 28. Check that the disk is not full or a disk quota exceeded. Make sure the file system supports this function. Refer to your operating system documentation for operating system error code information.
2023-02-20T14:32:23.733494+09:00 8 [Warning] [MY-012638] [InnoDB] Retry attempts for writing partial data failed.
2023-02-20T14:32:23.733538+09:00 8 [ERROR] [MY-012639] [InnoDB] Write to file /mytmp/innodb_temp/temp_9.ibt failed at offset 622592, 425984 bytes should have been written, only 0 were written. Operating system error number 28. Check that your OS and file system support files of this size. Check also that the disk is not full or a disk quota exceeded.
2023-02-20T14:32:23.733572+09:00 8 [ERROR] [MY-012640] [InnoDB] Error number 28 means 'No space left on device'
2023-02-20T14:32:23.733589+09:00 8 [Note] [MY-012641] [InnoDB] Refer to your operating system documentation for operating system error code information.
2023-02-20T14:32:23.733603+09:00 8 [Warning] [MY-012145] [InnoDB] Error while writing 425984 zeroes to /mytmp/innodb_temp/temp_9.ibt starting at offset 622592
..
2023-02-20T14:32:28.431025+09:00 8 [ERROR] [MY-013132] [Server] The table 'tt1' is full!

実験3、MyISAM版 (8.0.32では既に internal_tmp_disk_storage_engine が選べないから単に実験)

mysql80 9> CREATE TEMPORARY TABLE d1.tt1 (num int, val varchar(32)) Engine = MyISAM;
Query OK, 0 rows affected (0.00 sec)

mysql80 9> LOAD DATA INFILE '/home/yoku0825/md5' INTO TABLE d1.tt1;
ERROR 126 (HY000): Incorrect key file for table '/mytmp/#sql45cf_9_0.MYI'; try to repair it

### エラーログ
2023-02-20T14:36:08.272683+09:00 9 [ERROR] [MY-013134] [Server] Incorrect key file for table '/mytmp/#sql45cf_9_0.MYI'; try to repair it
2023-02-20T14:36:08.272746+09:00 9 [ERROR] [MY-010239] [Server] Got an error from unknown thread, /home/yoku0825/mysql-8.0.32/storage/myisam/mi_write.cc:194

見慣れたやつが出てきた。

2023/02/12

ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed には実は2種類ある

TL;DR

  • SET GLOBAL gtid_purged = '+?' のプラス付きの記法は、既存の gtid_executed と1つたりともカブってはいけない。
    • RESET MASTER ができず、既存の gtid_executed に足したい場合は差分を取って + 記号で足してやらないといけない
  • gtid_executed が空でない時に + 記号なしの SET GLOBAL gtid_purged = ? した時のエラーが5.7と8.0で変わってた

SET GLOBAL gtid_purged = ? した時に出ることがある ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed, よく見たら実は3種類くらいあるっぽかった。

perror で調べてみると、確かに ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: %s なので cannot be changed: までが固定のメッセージで、その理由が後ろに添えられるっぽい。

$ perror 3546
MySQL error code MY-003546 (ER_CANT_SET_GTID_PURGED_DUE_SETS_CONSTRAINTS): @@GLOBAL.GTID_PURGED cannot be changed: %s

my_error (MySQLがエラーパケットを返す時に使われる関数) で ER_CANT_SET_GTID_PURGED_DUE_SETS_CONSTRAINTS を渡しているのはMySQL 8.0.32のコードで3か所。

https://github.com/mysql/mysql-server/blob/mysql-8.0.32/sql/rpl_gtid_state.cc#L622-L646

+ 記号がついてない時は簡単, if文で1つ目と3つ目に当たる。

mysql80 7> SELECT @@gtid_executed;
+----------------------------------------------+
| @@gtid_executed                              |
+----------------------------------------------+
| dddc3a2c-96fe-11ed-ae6d-0201965f8d32:1-53562 |
+----------------------------------------------+
1 row in set (0.01 sec)

mysql80 7> SET GLOBAL gtid_purged = 'dddc3a2c-96fe-11ed-ae6d-0201965f8d32:1';
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the new value must be a superset of the old value

mysql80 7> SET GLOBAL gtid_purged = 'dddc3a2c-96fe-11ed-ae6d-0201965f8d32:1-53562';
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the added gtid set must not overlap with @@GLOBAL.GTID_EXECUTED

mysql80 8> SET GLOBAL gtid_purged = 'dddc3a2c-96fe-11ed-ae6d-0201965f8d32:53563';
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the new value must be a superset of the old value

mysql80 8> SET GLOBAL gtid_purged = 'dddc3a2c-96fe-11ed-ae6d-0201965f8d31:1';  -- server_uuid部分が違ってもダメ
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the new value must be a superset of the old value

これは5.7とそれ以前の ERROR 1840 (HY000): @@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_EXECUTED is empty. に対応していて、「既存の gtid_executed よりも小さく、かつ大きくなければならない」と言っているのでどう考えても無理。 RESET MASTER して新たに設定するしかないはず。

ちなみに5.7とそれ以前の MySQL error code MY-001840 (ER_CANT_SET_GTID_PURGED_WHEN_GTID_EXECUTED_IS_NOT_EMPTY): @@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_EXECUTED is empty. は8.0ではどこからも呼び出されていない。

+ を付けた時でもエラることはある。エラーメッセージを素直に読むと、既存の部分とカブってはいけないらしい。

mysql80 8> SELECT @@gtid_executed;
+----------------------------------------------+
| @@gtid_executed                              |
+----------------------------------------------+
| dddc3a2c-96fe-11ed-ae6d-0201965f8d32:1-53562 |
+----------------------------------------------+
1 row in set (0.00 sec)

mysql80 8> SET GLOBAL gtid_purged = '+dddc3a2c-96fe-11ed-ae6d-0201965f8d32:1-53563';
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the added gtid set must not overlap with @@GLOBAL.GTID_EXECUTED

既存の部分とカブらせないようにした↓は通る。

mysql80 8> SET GLOBAL gtid_purged = '+dddc3a2c-96fe-11ed-ae6d-0201965f8d32:53563';
Query OK, 0 rows affected (0.06 sec)

mysql80 8> SELECT @@gtid_executed;
+----------------------------------------------+
| @@gtid_executed                              |
+----------------------------------------------+
| dddc3a2c-96fe-11ed-ae6d-0201965f8d32:1-53563 |
+----------------------------------------------+
1 row in set (0.00 sec)

mysql80 8> SET GLOBAL gtid_purged = '+dddc3a2c-96fe-11ed-ae6d-0201965f8d31:1';  -- server_uuid部分を変えたものは別gtidなのでもちろん通る
Query OK, 0 rows affected (0.01 sec)

mysql80 8> SELECT @@gtid_executed;
+--------------------------------------------------------------------------------------+
| @@gtid_executed                                                                      |
+--------------------------------------------------------------------------------------+
| dddc3a2c-96fe-11ed-ae6d-0201965f8d31:1,
dddc3a2c-96fe-11ed-ae6d-0201965f8d32:1-53563 |
+--------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

これに当たるのはたぶん、

  • 「レプリカがしばらく止まっててI/Oスレッドが 'Cannot replicate because the master purged required binary logs. Replicate the missing transactions from elsewhere, or provision a new slave from backup. Consider increasing the master's binary log expiration period. The GTID set sent by the slave is '', and the missing transactions are 'xxx:yyy で再開できなくなったから、インスタンスはそのままにmysqldumpで上書きして再開させてやろう」と思った時
  • マルチソースレプリケーションを組んでて↑になった時
    のパターンだと思う。

前者はインスタンスをつぶしてきれいに再構築するか、 RESET MASTER してから通せば良いけれど、後者の時は差分を取って綺麗に + で当ててやらないといけないので、mysqldumpに書いてある SET GLOBAL gtid_purged=? の部分から

SET GLOBAL gtid_purged= CONCAT('+', GTID_SUBTRACT(/* GTID_SUBTRACTの1つ目の引数がmysqldumpから確認したgtid_purgedにセットしたい値 */ 'dddc3a2c-96fe-11ed-ae6d-0201965f8d31:1,dddc3a2c-96fe-11ed-ae6d-0201965f8d32:1-53563', /* 第2引数をMySQLが認識しているgtid_executedにすると差分がとれる */ @@gtid_executed));

と、差分だけ追加する形でgtid_purgedをセットできた。

しかし、 + 記号がついてない時のエラーは MySQL error code MY-001840 (ER_CANT_SET_GTID_PURGED_WHEN_GTID_EXECUTED_IS_NOT_EMPTY): @@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_EXECUTED is empty. で良かったんじゃないかなと思う。

2023/02/11

複合プライマリーキー vs サロゲートキーでInnoDB読み取りページ数の比較をもうちょっと機械的にやりたい

日々の覚書: 複合プライマリーキー vs サロゲートキーでInnoDB読み取りページ数の比較 の続き。

毎回innodb_metricsを眺める方法だとサンプル数が余程限られている状態でもない限りスケールしないので、もうちょっと機械的にやる方法を考える。

思いついたのはPercona Serverのスローログでやる方法。

取り敢えずオリジナル側でサンプルにするためにスローログを取っておく。本番から期間を絞って取れれば取り敢えず良いだろうか。


mysql> SET GLOBAL slow_query_log = ON, long_query_time = 0;

/* サンプルクエリを取る */

mysql> SET GLOBAL long_query_time = ?;  -- もとの値に戻す

データ元も取っておく。細かく気にするならxtrabackupでフルバックアップか、FLUSH TABLES FOR EXPORTでibdファイルをまるまる取ってきた方が断片化具合そのままで計測ができるけれど、気にしなければmysqldumpでも良いと思う。

$ bin/mysqldump d1 > /tmp/dump.sql

MySQL 8.0.23互換のPercona Server 8.0.23を手元にインストール。


$ wget https://downloads.percona.com/downloads/Percona-Server-8.0/Percona-Server-8.0.23-14/binary/tarball/Percona-Server-8.0.23-14-Linux.x86_64.glibc2.12.tar.gz

$ tar xf Percona-Server-8.0.23-14-Linux.x86_64.glibc2.12.tar.gz

$ cd Percona-Server-8.0.23-14-Linux.x86_64.glibc2.12/

$ bin/mysqld --no-defaults --initialize-insecure --datadir=/tmp/data_percona --lower_case_table_names=1 --log-error-verbosity=3

$ bin/mysqld --no-defaults --datadir=/tmp/data_percona --lower_case_table_names=1 --log-error-verbosity=3 --daemonize
mysqld will log errors to /tmp/data_percona/hostname.err
mysqld is running as pid 10782

$ bin/mysql -e "CREATE DATABASE d1"
$ bin/mysql d1 < /tmp/dump.sql

スローログの設定とlog_slow_verbosityの設定をしてから、サンプルのクエリを食わせる。余計なヘッダや実行されたくないクエリは抜いておく必要がありそう。
ヘッダの処理がめんどいのと期間指定もしたかったのでpt-query-digest —output=slowlog —no-reportを間に挟んで整形する

$ bin/mysql -e "SET GLOBAL slow_query_log = ON, long_query_time = 0, log_slow_verbosity= full"

$ pt-query-digest --since="2023-02-11 00:00:00" --output=slowlog --no-report /tmp/data/hostname-slow.log > /tmp/slow.log

$ bin/mysql < /tmp/slow.log > /dev/null

これで測定の諸元になるPercona版のスロークエリが手に入る。

$ less /tmp/data_percona/hostname-slow.log
..
# Time: 2023-02-11T06:49:28.072174Z
# User@Host: root[root] @ localhost []  Id:    12
# Schema:   Last_errno: 0  Killed: 0
# Query_time: 0.068346  Lock_time: 0.000063  Rows_sent: 100000  Rows_examined: 100000  Rows_affected: 0  Bytes_sent: 3870459
# Tmp_tables: 0  Tmp_disk_tables: 0  Tmp_table_sizes: 0
# InnoDB_trx_id: 0
# Full_scan: Yes  Full_join: No  Tmp_table: No  Tmp_table_on_disk: No
# Filesort: No  Filesort_on_disk: No  Merge_passes: 0
#   InnoDB_IO_r_ops: 0  InnoDB_IO_r_bytes: 0  InnoDB_IO_r_wait: 0.000000
#   InnoDB_rec_lock_wait: 0.000000  InnoDB_queue_wait: 0.000000
#   InnoDB_pages_distinct: 294
SET timestamp=1676098168;
SELECT * FROM d1.t5;
# Time: 2023-02-11T06:49:28.135320Z
# User@Host: root[root] @ localhost []  Id:    12
# Schema:   Last_errno: 0  Killed: 0
# Query_time: 0.000275  Lock_time: 0.000088  Rows_sent: 100  Rows_examined: 100  Rows_affected: 0  Bytes_sent: 4145
# Tmp_tables: 0  Tmp_disk_tables: 0  Tmp_table_sizes: 0
# InnoDB_trx_id: 0
# Full_scan: No  Full_join: No  Tmp_table: No  Tmp_table_on_disk: No
# Filesort: No  Filesort_on_disk: No  Merge_passes: 0
#   InnoDB_IO_r_ops: 0  InnoDB_IO_r_bytes: 0  InnoDB_IO_r_wait: 0.000000
#   InnoDB_rec_lock_wait: 0.000000  InnoDB_queue_wait: 0.000000
#   InnoDB_pages_distinct: 2
SET timestamp=1676098168;
SELECT * FROM d1.t5 WHERE user_id = 0825;
# Time: 2023-02-11T06:49:28.137490Z
# User@Host: root[root] @ localhost []  Id:    12
# Schema:   Last_errno: 0  Killed: 0
# Query_time: 0.001976  Lock_time: 0.000060  Rows_sent: 567  Rows_examined: 567  Rows_affected: 0  Bytes_sent: 22339
# Tmp_tables: 0  Tmp_disk_tables: 0  Tmp_table_sizes: 0
# InnoDB_trx_id: 0
# Full_scan: No  Full_join: No  Tmp_table: No  Tmp_table_on_disk: No
# Filesort: No  Filesort_on_disk: No  Merge_passes: 0
#   InnoDB_IO_r_ops: 0  InnoDB_IO_r_bytes: 0  InnoDB_IO_r_wait: 0.000000
#   InnoDB_rec_lock_wait: 0.000000  InnoDB_queue_wait: 0.000000
#   InnoDB_pages_distinct: 171
SET timestamp=1676098168;
SELECT * FROM d1.t5 WHERE updated = '2023-02-08 00:57:38';

..

うーんちょっと 前回 測ったのと結構結果が違ってどっから引いてるんだろうこれ。

Percona Serverのlog_slow_verbosityで追加した情報はpt-query-digestでそのまま食わせられるので(そりゃそうだ) ページ数に着目してプロファイルを見た後に、複合プライマリーキーに組み替えた後で同じスローログ (/tmp/slow.log) を流してページ数を比較するなんてことができるはず。

$ pt-query-digest /tmp/data_percona/hostname-slow.log --order-by="InnoDB_pages_distinct:sum"

# 250ms user time, 20ms system time, 27.57M rss, 120.81M vsz
# Current date: Sat Feb 11 15:55:43 2023
# Hostname: hostname
# Files: /tmp/data_percona/hostname-slow.log
# Overall: 60 total, 9 unique, 0.13 QPS, 24.78x concurrency ______________
# Time range: 2023-02-11T06:41:48 to 2023-02-11T06:49:28
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time             1s     4us    76ms    19ms    65ms    23ms     7ms
# Lock time           15ms       0     6ms   215us   273us   708us    89us
# Rows sent        983.11k       0  97.66k  13.85k  97.04k  33.72k    0.99
# Rows examine       2.49M       0  97.66k  35.85k  97.04k  46.69k   97.36
# Rows affecte           0       0       0       0       0       0       0
# Bytes sent        37.17M       0   3.69M 536.05k   3.68M   1.28M  112.70
# Merge passes           0       0       0       0       0       0       0
# Tmp tables             0       0       0       0       0       0       0
# Tmp disk tbl           0       0       0       0       0       0       0
# Tmp tbl size           0       0       0       0       0       0       0
# Query size         2.79k      19      64   40.31   62.76   15.33   38.53
# InnoDB:
# IO r bytes             0       0       0       0       0       0       0
# IO r ops               0       0       0       0       0       0       0
# IO r wait              0       0       0       0       0       0       0
# pages distin      11.13k       2     297  189.98  284.79  128.16  284.79
# queue wait             0       0       0       0       0       0       0
# rec lock wai           0       0       0       0       0       0       0
# Boolean:
# Full scan     50% yes,  49% no

..

これならまあ、ひたすら手計測よりはいくらかマシであろう。。

2023/02/09

複合プライマリーキー vs サロゲートキーでInnoDB読み取りページ数の比較

TL;DR

この考察の単位は ページ数 であって パフォーマンスじゃない 。パフォーマンスを考える時はこれにさらにバッファプールヒット率が関連するはず

  • COUNT(*)に関してはページあたりのインデックスレコードの充填数が多いサロゲートキーの方が読むページが少ない
  • カバリングインデックスの効くCOUNT(*)に関してはナチュラルキーのサイズの大きさはほとんど関係なさそう

  • 行本体を読まなければいけない(=カバリングインデックスで済まない)select_lsitになるとサロゲートキーの充填数の差は目立たなくなる

  • ナチュラルキーの左端でレンジスキャンする時がページ読み取り数は最小

  • 読みだすページの数だけで比べると , セカンダリキー引きはテーブルスキャンよりも読むページ数は多いことがある

日々の覚書: インデックス vs InnoDBログ書き込みのサイズ観測 の続き。

今度は読み取るページ数にのみ着目。

計測方法。

### Before, Afterの累計ページ読み取り数
### After - Beforeがそのクエリで読み取ったページ数になる
SELECT name, count, status FROM information_schema.innodb_metrics WHERE name = 'buffer_pool_read_requests';

## 計測対象クエリ
### COUNT(*) -> リーフページだけ読めば足りるやつ
SELECT COUNT(*) FROM <table>;
SELECT COUNT(*) FROM <table> WHERE user_id = 0825;
SELECT COUNT(*) FROM <table> WHERE updated = ?;

### 全フィールド。クラスターインデックスに触らないといけないやつ
SELECT * FROM <table>;
SELECT * FROM <table> WHEER user_id = 0825;
SELECT * FROM <table> WHEER updated = ?;

## updatedは深く考えずにNOW()で降ってたのでバラツキがあって、
## 結果セットの行数だけ合わせるためにCOUNTしてJOINして値を決めた
mysql> WITH t1_c AS (SELECT updated, COUNT(*) AS c FROM d1.t1 GROUP BY 1),
    -> t2_c AS (SELECT updated, COUNT(*) AS c FROM d1.t2 GROUP BY 1),
    -> t3_c AS (SELECT updated, COUNT(*) AS c FROM d1.t3 GROUP BY 1),
    -> t4_c AS (SELECT updated, COUNT(*) AS c FROM d1.t4 GROUP BY 1),
    -> t5_c AS (SELECT updated, COUNT(*) AS c FROM d1.t5 GROUP BY 1)
    ->
    ->
    -> SELECT c, t1_c.updated AS t1, t2_c.updated AS t2, t3_c.updated AS t3, t4_c.updated AS t4, t5_c.updated AS t5
    -> FROM t1_c JOIN t2_c USING(c) JOIN t3_c USING(c) JOIN t4_c USING(c) JOIN t5_c USING(c);

 c: 567
t1: 2023-02-08 00:32:39
t2: 2023-02-08 00:37:34
t3: 2023-02-08 00:44:08
t4: 2023-02-08 00:51:12
t5: 2023-02-08 00:57:38

結果。単位は「ページリクエスト」

COUNT(*)

table without_WHERE without_WHERE_key without_WHERE_vs_t1 WHERE_userid WHERE_userid_key WHERE_userid_vs_t1 WHERE_updated WHERE_updated_key WHERE_updated_vs_t1 comment
t1 881 PRIMARY 1.00 1286 N/A(type:ALL) 1.00 1286 N/A(type:ALL) 1.00 auto_increment PK + 0 key
t2 881 idx_userid 1.00 7 idx_userid 0.01 1286 N/A(type:ALL) 1.00 auto_increment PK + 1 key
t3 881 idx_userid 1.00 7 idx_userid 0.01 13 idx_updated 0.01 auto_increment PK + 2 key
t4 1140 PRIMARY 1.29 8 PRIMARY 0.01 1373 N/A(type:ALL) 1.07 natural PK + 0 key
t5 1140 idx_updated 1.29 8 PRIMARY 0.01 12 idx_updated 0.01 natural PK + 1 key

all_fields

table without_WHERE without_WHERE_key without_WHERE_vs_t1 WHERE_userid WHERE_userid_key WHERE_userid_vs_t1 WHERE_updated WHERE_updated_key WHERE_updated_vs_t1 comment
t1 1286 N/A(type:ALL) 1.00 1286 N/A(type:ALL) 1.00 1286 N/A(type:ALL) 1.00 auto_increment PK + 0 key
t2 1286 N/A(type:ALL) 1.00 308 idx_userid 0.24 1286 N/A(type:ALL) 1.00 auto_increment PK + 1 key
t3 1286 N/A(type:ALL) 1.00 308 idx_userid 0.24 1710 idx_updated 1.33 auto_increment PK + 2 key
t4 1373 N/A(type:ALL) 1.07 8 PRIMARY 0.01 1373 N/A(type:ALL) 1.07 natural PK + 0 key
t5 1373 N/A(type:ALL) 1.07 8 PRIMARY 0.01 1708 idx_updated 1.33 natural PK + 1 key

まとめ

この考察の単位は ページ数 であって パフォーマンスじゃない 。パフォーマンスを考える時はこれにさらにバッファプールヒット率が関連するはず

  • COUNT(*)に関してはページあたりのインデックスレコードの充填数が多いサロゲートキーの方が読むページが少ない
  • カバリングインデックスの効くCOUNT(*)に関してはナチュラルキーのサイズの大きさはほとんど関係なさそう

  • 行本体を読まなければいけない(=カバリングインデックスで済まない)select_lsitになるとサロゲートキーの充填数の差は目立たなくなる

  • ナチュラルキーの左端でレンジスキャンする時がページ読み取り数は最小
  • 読みだすページの数だけで比べると , セカンダリキー引きはテーブルスキャンよりも読むページ数は多いことがある