GA

2018/09/27

「max_allowed_packetの基本的な動き」がどうしてそうなるのかのはなし

TL;DR

  • 1SQLのサイズ上限は、max_allowed_packetとnet_buffer_lengthで制御される。
  • max_allowed_packetはグローバルにデフォルト値を持ち、コネクション確立時にセッションにコピーされ、セッション変数は読み込み専用である。(セッションに適用するにはconnectなど再接続が必要)
  • net_buffer_lengthはグローバルにデフォルト値を持ち、コネクション確立時にセッションにコピーされ、セッション変数も変更可能である。
  • net_buffer_length => max_allowed_packetの場合、net_buffer_lengthが1SQLの上限値になる。
  • net_buffer_length < max_allowed_packetの場合、max_allowed_packetが1SQLの上限値になる。
とまとめられているが、これは

ということ。

まず、 max_allowed_packet の上限に引っかかった時に出るエラーである、 “Got a packet bigger than ‘max_allowed_packet’ bytes” からたどっていくことにする。
(See also Dive into MySQL Error - Speaker Deck
ソースコードは5.7.23のものにしていますよん。
$ grep "max_allowed_packet" include/mysqld_ername.h
{ "ER_NET_PACKET_TOO_LARGE", 1153, "Got a packet bigger than \'max_allowed_packet\' bytes" },
{ "ER_TOO_LONG_STRING", 1162, "Result string is longer than \'max_allowed_packet\' bytes" },
{ "ER_WARN_ALLOWED_PACKET_OVERFLOWED", 1301, "Result of %s() was larger than max_allowed_packet (%ld) - truncated" },
MySQLへのJDBC接続で、とあるバグを踏むまでの話 -(1)「max_allowed_packet」の基本的な働き - なからなLife ではバルクインサートだったので、 ER_NET_PACKET_TOO_LARGE の方だと思う。
$ grep -r ER_NET_PACKET_TOO_LARGE . | grep -v mysql-test
./include/sql_state.h:{ ER_NET_PACKET_TOO_LARGE                 ,"08S01", "" },
./include/mysqld_ername.h:{ "ER_NET_PACKET_TOO_LARGE", 1153, "Got a packet bigger than \'max_allowed_packet\' bytes" },
./include/mysqld_error.h:#define ER_NET_PACKET_TOO_LARGE 1153
./libmysql/libmysql.c:      else if (net->last_errno == ER_NET_PACKET_TOO_LARGE)
./sql-common/client.c:    set_mysql_error(mysql, net->last_errno == ER_NET_PACKET_TOO_LARGE ?
./sql-common/client.c:    if (net->last_errno == ER_NET_PACKET_TOO_LARGE)
./sql/net_serv.cc:    net->last_errno= ER_NET_PACKET_TOO_LARGE;
./sql/net_serv.cc:    my_error(ER_NET_PACKET_TOO_LARGE, MYF(0));
./sql/rpl_rli_pdb.cc:    with error ER_NET_PACKET_TOO_LARGE.
./sql/rpl_slave.cc:          mi->report(ERROR_LEVEL, ER_NET_PACKET_TOO_LARGE,
./sql/share/errmsg-utf8.txt:ER_NET_PACKET_TOO_LARGE 08S01
./include のやつは定義系だし、 ./libmysql, ./sql-common のやつは == で比較しているのでこのエラーを受け取った後にどうするかの処理だろう。
./sql/rpl_* はレプリケーション関連なので取り敢えずパスして、 ./sql/net_serv.cc の中で代入しているからここが本丸のような気がする。
grepで引っかかった2行は net_realloc という関数の中に入っていて、名前から察するにここが
パケットメッセージバッファーは net_buffer_length バイトに初期化されますが、必要に応じて max_allowed_packet バイトまで大きくできます。
のパケットメッセージバッファーを大きくする処理なのであろう。
コイツが呼ばれているところを探しに行く。
$ grep -r net_realloc .
./include/mysql.h.pp:my_bool net_realloc(NET *net, size_t length);
./include/mysql_com.h:my_bool net_realloc(NET *net, size_t length);
./libmysql/libmysql.c:    res= net_realloc(net, buf_length + length);
./sql/net_serv.cc:my_bool net_realloc(NET *net, size_t length)
./sql/net_serv.cc:  DBUG_ENTER("net_realloc");
./sql/net_serv.cc:      must match the size of the buffer allocated in net_realloc().
./sql/net_serv.cc:  if ((pkt_data_len >= net->max_packet) && net_realloc(net, pkt_data_len))
./libmysql とはlibmysqlclient.so(Connector/C)のコードなので外すとするとまだ同じ ./sql/net_serv.cc の行。開いてみると net_read_packet の中で、「今のパケットメッセージバッファーよりもデータが大きければ net_realloc 」な記述に当たる。
というわけで、
  • net_buffer_length => max_allowed_packetの場合、net_buffer_lengthが1SQLの上限値になる。
の理由は、 net_buffer_length のサイズでアロケートされたパケットメッセージバッファが足りなくなって拡張しようとした時に初めて max_allowed_packet と比較されてエラーに分岐するから、でした。
Cこわくないよ!
(あっdbts2018のブログがまだ下書きn

2018/08/27

Club MySQL #3 ~ yoku0825 のつくりかた に登壇してきました(?)

去る8/20、Club MySQLの第3回「yoku0825のつくりかた」に登壇してきました。
Club MySQLなのに MySQLではなく ピンクのかわいいおとうふ のことを語ってしまって恐縮です。
主催の 坂井さん 、会場をお貸しいただいた サイボウズさん 、ご来場いただいたみなさま、どうもありがとうございました。
当日のハッシュタグまとめました(ただよう自作自演風味)


まずは坂井さんから、 “Club MySQLは日本MySQLユーザ会の一形態であり、スピンアウトじゃないよ” とクギを刺されます :D
  • 日本MySQLユーザ会 の副代表、 @sakaik さんによるスピンアウト勉強会です
    • Club MySQL は、ひとりの講演者の話をじっくりと聞こう、という趣向の、日本MySQLユーザ会の新しいイベントシリーズです だそう
( ´-`).oO(ここに訂正してお詫び申し上げます…

俺とそーだいさんの馴れ初めは多分↓これで、


その後、中国地方DB勉強会 in 東京(何を言っているのかわからねーと思うが略)で仲良くなって一緒にヤパチーでキャッキャウフフしたりYAPC::Hokkaido逝ったりともう3~4年の付き合いになるようです。
物理そーだいさん(?)から何度か「僕 弟キャラなので色んなこと教えてもらえるんですよねー」(リアル弟なのかは忘れましたが)って聞いてて、なるほど確かにそーだいさん可愛がられキャラだし聞き方も上手いよな、とか思ってたんですが、スライドの「あじぇんだ」にガリっと「利用手順」「活用事例」とか紹介されてやっぱり上手いなと思いました。
そーだいさんのLTが思いのほか面白く、本当はその5分でちゃちゃっと追加しようと思っていた第4部が未完(ただし5分作業できたとしても完成していたかどうかは定かではない)のまま、「yoku0825のつくりかた」のセッションに入ります。

前日まで何を話すか全然決まらなくて書いちゃ破き書いちゃ破き状態で、上手くまとめられなかったような気がしたんですが、楽しんでいただけたなら何よりでした!
ありがとうございました!
最後になりますが、 SH2さん 愛してますよ ;D

2018/08/22

innodb_ft_enable_stopword が無効にできなかったはなし

TL;DR

  • innodb_ft_enable_stopword は2018/08/22現在ドキュメントの記載が “GLOBAL” のみになっているけど、実際は “GLOBAL, SESSION” で実効値はセッション側
  • このパラメーターでストップワードを判定させるか否かは CREATE TABLE または ALTER TABLE した時点の session.innodb_ft_enable_stopword に依存する

元ネタはMySQL CasualのSlackでのこの発言。
(文中の引用元リンクがたどれない方は是非とも MySQL CasualのSlackへ!
innodb_ft_enable_stopwordパラメータですが、set globalでON/OFFが切り替えられると思っていたのですが、
実際に試してみたところOFFになりません。
(コマンド自体はエラーにならない)
マニュアルには Global って書いてあるんですけど、set コマンドでは Global と Session の両方あるような雰囲気ですね…。
むむむと思って調べてみた。
まず、 innodb_ft_enable_stopwordMYSQL_THDVAR_BOOL で定義されているので、グローバルオンリーではなくてグローバルとセッションの両方持っているのはそれで合っていそう(グローバルオンリーだと MYSQL_SYSVAR_* 型)
この値(ft_enable_stopword)を唯一引きまわしているinnobase_fts_load_stopwordの中でも、 THDVAR で呼ばれているので、グローバルとセッションと両方ある時はセッション側の値が実効値、という定石の通りになっていると思われる。
innobase_fts_load_stopwordが呼ばれるのはcreate_table_info_t::create_table_update_dictとprepare_inplace_alter_table_dictの中。
というところで、ドキュメントに以下の記述を見つけた。
Specifies that a set of stopwords is associated with an InnoDB FULLTEXT index at the time the index is created.
なるほど、フルテキストインデックスが生成される時に決まるのね。

じゃあ実験。
ストップワード関連のオプションをデフォルトのままで CREATE TABLE, INSERT INTO すると、フルテキストインデックスの中味に “one” を “on”, “ne” に分割したもののうちストップワードの “on” を除いた “ne” だけが記録される。
mysql57 2> SELECT @@session.innodb_ft_enable_stopword, @@global.innodb_ft_enable_stopword;
+-------------------------------------+------------------------------------+
| @@session.innodb_ft_enable_stopword | @@global.innodb_ft_enable_stopword |
+-------------------------------------+------------------------------------+
|                                   1 |                                  1 |
+-------------------------------------+------------------------------------+
1 row in set (0.02 sec)

mysql57 2> create table t1 (val varchar(32), fulltext key (val) with parser ngram);
Query OK, 0 rows affected (0.15 sec)

mysql57 2> INSERT INTO t1 VALUES ('one');
Query OK, 1 row affected (0.23 sec)

mysql57 2> SET GLOBAL innodb_ft_aux_table = 'd1/t1';
Query OK, 0 rows affected (1.59 sec)

mysql57 2> SELECT * FROM i_s.innodb_ft_index_cache;
+------+--------------+-------------+-----------+--------+----------+
| WORD | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+------+--------------+-------------+-----------+--------+----------+
| ne   |            2 |           2 |         1 |      2 |        1 |
+------+--------------+-------------+-----------+--------+----------+
1 row in set (0.01 sec)
次、まず SET SESSION innodb_ft_enable_stopword= 0 でセッション側の設定をOFFにしてもう一度 INSERT してみる
mysql57 2> SET SESSION innodb_ft_enable_stopword= 0;
Query OK, 0 rows affected (0.00 sec)

mysql57 2> SELECT @@session.innodb_ft_enable_stopword, @@global.innodb_ft_enable_stopword;
+-------------------------------------+------------------------------------+
| @@session.innodb_ft_enable_stopword | @@global.innodb_ft_enable_stopword |
+-------------------------------------+------------------------------------+
|                                   0 |                                  1 |
+-------------------------------------+------------------------------------+
1 row in set (0.00 sec)

mysql57 2> INSERT INTO t1 VALUES ('one');
Query OK, 1 row affected (0.01 sec)

mysql57 2> SELECT * FROM i_s.innodb_ft_index_cache;
+------+--------------+-------------+-----------+--------+----------+
| WORD | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+------+--------------+-------------+-----------+--------+----------+
| ne   |            2 |           3 |         2 |      2 |        1 |
| ne   |            2 |           3 |         2 |      3 |        1 |
+------+--------------+-------------+-----------+--------+----------+
2 rows in set (0.00 sec)
セッション値を変えても INSERT した時にストップワードの “on” が取り除かれている。
じゃあグローバル値はどうかな。
mysql57 2> SET GLOBAL innodb_ft_enable_stopword= 0;
Query OK, 0 rows affected (0.00 sec)

mysql57 2> SELECT @@session.innodb_ft_enable_stopword, @@global.innodb_ft_enable_stopword;
+-------------------------------------+------------------------------------+
| @@session.innodb_ft_enable_stopword | @@global.innodb_ft_enable_stopword |
+-------------------------------------+------------------------------------+
|                                   0 |                                  0 |
+-------------------------------------+------------------------------------+
1 row in set (0.05 sec)

mysql57 2> INSERT INTO t1 VALUES ('one');
Query OK, 1 row affected (0.00 sec)

mysql57 2> SELECT * FROM i_s.innodb_ft_index_cache;
+------+--------------+-------------+-----------+--------+----------+
| WORD | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+------+--------------+-------------+-----------+--------+----------+
| ne   |            2 |           4 |         3 |      2 |        1 |
| ne   |            2 |           4 |         3 |      3 |        1 |
| ne   |            2 |           4 |         3 |      4 |        1 |
+------+--------------+-------------+-----------+--------+----------+
3 rows in set (0.00 sec)
変わらない。フルテキストインデックスが生成されるタイミングで有効なのは本当らしい。
じゃあこの状態(グローバルOFF、セッションOFF)の状態でもういっこテーブルを作ってみる。
mysql57 2> CREATE TABLE t2 (val varchar(32), FULLTEXT KEY (val) WITH PARSER ngram);
Query OK, 0 rows affected (0.08 sec)

mysql57 2> INSERT INTO t2 VALUES ('one');
Query OK, 1 row affected (0.01 sec)

mysql57 2> SET GLOBAL innodb_ft_aux_table = 'd1/t2';
Query OK, 0 rows affected (0.01 sec)

mysql57 2> SELECT * FROM i_s.innodb_ft_index_cache;
+------+--------------+-------------+-----------+--------+----------+
| WORD | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+------+--------------+-------------+-----------+--------+----------+
| ne   |            2 |           2 |         1 |      2 |        1 |
| on   |            2 |           2 |         1 |      2 |        0 |
+------+--------------+-------------+-----------+--------+----------+
2 rows in set (0.00 sec)
ストップワードが判定されなくなって “on” もフルテキストインデックスの中に入ってくるようになった。
その後、セッション値とグローバル値とパターンを変えていくつか試してみると、
  • CREATE TABLE または ALTER TABLE の時点で指定していた innodb_ft_enable_stopword のセッション値で判断される
    • innodb_optimize_fulltext_only= 1ALTER TABLE してもその時点の値が反映される
  • INSERT, SELECT の時の innodb_ft_enable_stopword は関係ない
    • SET SESSION innodb_ft_enable_stopword だけで結果が変わったらどうしようかと思っちゃった
  • ということは単にドキュメントの間違いで、本当は “GLOBAL, SESSION” である
ごちそうさまでした!

【2018/08/22 17:22】
ばぐれぽしておいた!

MySQL Bugs: #92118: innodb_ft_enable_stopword is wrongly described as "GLOBAL" scope in Docs



『』

2018/07/25

MySQL 8.0でLOAD DATA LOCAL INFILEが "ERROR 1148 (42000): The used command is not allowed with this MySQL version" または "Error: 3948. Loading local data is disabled; this must be enabled on both the client and server sides" で失敗する時

TL;DR

【2020/02/18 14:23】
MySQL 8.0.19とそれ以降ではエラーメッセージが "Error Code: 3948. Loading local data is disabled; this must be enabled on both the client and server sides" に変わりました(それ以前は "ERROR 1148 (42000): The used command is not allowed with this MySQL version" )

エラーメッセージの出力例以外はこの記事の内容そのままで対処できると思います。たぶん。


吊るしのMySQL 8.0で mysql コマンドラインクライアントから LOAD DATA LOCAL INFILE を実行すると転けます。
mysql80 125> LOAD DATA LOCAL INFILE '/tmp/aaa' INTO TABLE t1;
ERROR 1148 (42000): The used command is not allowed with this MySQL version
LODA DATA LOCAL INFILE を実行するには2つの条件が必要で、
  1. LOAD DATA LOCAL INFILE を実行するコネクションに CLIENT_LOCAL_FILES ケーパビリティー(オプションだと思って)が設定されていること
  2. サーバー側で opt_local_infile が設定されていること
2.mysqldlocal_infile オプションなのでわかりやすい。単にデフォルトが5.7とそれ以前の “1” から8.0では “0” に変わったというだけ。
再起動しなくても SET GLOBALSET PERSIST で設定できる。
mysql80 125> SELECT @@local_infile;
+----------------+
| @@local_infile |
+----------------+
|              0 |
+----------------+
1 row in set (0.00 sec)

mysql80 125> SET PERSIST local_infile= 1;
Query OK, 0 rows affected (0.00 sec)

mysql80 125> SELECT @@local_infile;
+----------------+
| @@local_infile |
+----------------+
|              1 |
+----------------+
1 row in set (0.00 sec)
サーバー側( 2. )だけ満たされていても、コネクションに CLIENT_LOCAL_FILES ケーパビリティー( 1. )はついてないのでやっぱり転ける。
同じエラーなので見分けにくい。
mysql80 125> LOAD DATA LOCAL INFILE '/tmp/aaa' INTO TABLE t1;
ERROR 1148 (42000): The used command is not allowed with this MySQL version
1. が満たされているかどうかをgdbを使わずに確かめる方法が見当たらなかったのだけれど、 mysql コマンドラインクライアントであれば —local-infile オプションを有効にするとこのケーパビリティーのフラグが立つ。ただし接続しながら変えることはできないのでこっちは切断してから再接続する。
$ mysql80 --local-infile=1
mysql80> use d1
mysql80 132> LOAD DATA LOCAL INFILE '/tmp/md5' INTO TABLE t1;
サーバーサイドのlocal_infileとクライアントサイドのlocal_infileがそれぞれ別で、それぞれ暗黙のデフォルトが0になったから両方で指定しないといけないよ、というお話でした
ちなみにConnector/Cなら mysql_real_connect を呼ぶときに client_flagCLIENT_LOCAL_FILESを立てるか、 mysql_optionsMYSQL_OPT_LOCAL_INFILE を有効にしてやればおk。

2018/07/12

CREATE USER .. DEFAULT ROLE .. で指定すると一発でROLEも許可される

TL;DR

mysql> CREATE USER hoge@xxx.xxx.xxx.xxx IDENTIFIED BY 'password';
mysql> GRANT hoge_role TO hoge@xxx.xxx.xxx.xxx;
mysql> ALTER USER hoge@xxx.xxx.xxx.xxx DEFAULT ROLE hoge_role;
mysql> CREATE USER hoge@xxx.xxx.xxx.xxx IDENTIFIED BY 'password' DEFAULT ROLE hoge_role;
は同じ状態になる。
当たり前といえば当たり前なんだけど、ちょっと感動したのでメモ。

MySQL 8.0で追加されたROLEの話は↓の記事。
ロールを作ってから
  1. ユーザー作る
  2. ロールを許可する
  3. デフォルトロール設定する
    でやらないといけないのかなと思ってたら一発で指定できた。

5.7とそれ以前

mysql> CREATE USER hoge@xxx.xxx.xxx.xxx IDENTIFIED BY 'password';
mysql> GRANT ALL ON hogedb.* TO hoge@xxx.xxx.xxx.xxx;
mysql> GRANT ALL ON fugadb.* TO hoge@xxx.xxx.xxx.xxx;

mysql> CREATE USER hoge@xxx.xxx.xxx.yyy IDENTIFIED BY 'password';
mysql> GRANT ALL ON hogedb.* TO hoge@xxx.xxx.xxx.yyy;
mysql> GRANT ALL ON fugadb.* TO hoge@xxx.xxx.xxx.yyy;
  • APサーバーが増えるたびに CREATE USER + GRANT * スキーマ数がめんどい
    • スキーマが増減した時とか死にたくなる
      • そして mysql.db の直接変更に手を出す…
  • 「おとなしく hoge@xxx.xxx.% にしたらいいのでは?」「それな」

8.0

mysql> CREATE ROLE hoge_role;
mysql> GRANT ALL ON hogedb.* TO hoge_role;
mysql> GRANT ALL ON fugadb.* TO hoge_role;

mysql> CREATE USER hoge@xxx.xxx.xxx.xxx IDENTIFIED BY 'password' DEFAULT ROLE hoge_role;
mysql> CREATE USER hoge@xxx.xxx.xxx.yyy IDENTIFIED BY 'password' DEFAULT ROLE hoge_role;
  • 楽だ…

【2020/07/30 20:23】
ただし残念ながらこんなバグがあった…8.0.17とそれ以前を使っている場合は注意。。

2018/07/02

MySQL ShellのUpgrade CheckerをPerl 5に書き下してみたけどそんなことする必要はなかったようだ

TL;DR


MySQLの中の人が最近(?)ちょくちょく推してる Upgrade Checker だけど、MySQL Shellの機能なのでX Plugin必須かなと思ってちょっと敬遠していたのだけれど。
checkForServerUpgrade() can use either an X Protocol connection or a classic MySQL protocol connection.
ってなことで実はPerlに打ち直す必要なんてなかったことがこの記事を書いている最中に判明した。かなしい。
$ mysqlsh -S /usr/mysql/5.7.22/data/mysql.sock -uroot --mysql
Creating a Classic session to 'root@/usr%2Fmysql%2F5.7.22%2Fdata%2Fmysql.sock'
Enter password:
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 77
Server version: 5.7.22-log Source distribution
No default schema selected; type \use <schema> to set one.
MySQL Shell 8.0.11

 MySQL  localhost  JS > util.checkForServerUpgrade();
The MySQL server at /usr%2Fmysql%2F5.7.22%2Fdata%2Fmysql.sock will now be checked for compatibility issues for upgrade to MySQL 8.0...
MySQL version: 5.7.22-log - Source distribution
...
よいこはちゃんとドキュメントを読もう、という話で終わっちゃいそうだけれど、折角書き下したのでちょっと紹介。
シンタックスが気にくわなくて直したのはいくつかあるけれど、割と素直にSQLを移植しただけなのでSQLごとにちょっと感じが違うのはある。
あと、本家はワーニングとエラーと分けられているけれど面倒だったので全部 Test::More でOKかNGかしか返さなくしちゃった。
チェック全体の流れとしてはざっとこんな感じ。
  ok(get_reserved_keywords_check(),
     "Usage of db objects with names conflicting with reserved keywords in 8.0");
  ok(get_utf8mb3_check(), "Usage of utf8mb3 charset");
  ok(get_zerofill_check(), "Usage of use ZEROFILL/display length type attributes");
  ok(Check_table_command(), "Issues reported by 'check table x for upgrade' command");
  ok(get_mysql_schema_check(), "Table names in the mysql schema conflicting with new tables in 8.0");
  ok(get_old_temporal_check(), "Usage of old temporal type");
  ok(get_foreign_key_length_check(), "Foreign key constraint names longer than 64 characters");
  ok(get_maxdb_sql_mode_flags_check(), "usage of obsolete MAXDB sql_mode flag");
  ok(get_obsolete_sql_mode_flags_check(), "get_obsolete_sql_mode_flags_check");
  ok(get_partitioned_tables_in_shared_tablespaces_check(), "Usage of partitioned tables in shared tablespaces");
  ok(get_removed_functions_check(), "get_removed_functions_check");
予約語が使われているオブジェクトがないか、utf8mb3(3バイトUTF-8)が使われているオブジェクトはないか、zerofill属性を期待している(ように思われる)データ型はないか、 CHECK TABLE .. FOR UPGRADE は通るか、8.0で新しく追加されたテーブルと競合するオブジェクトはないか、5.5とそれ以前までで使用されていた古い形式のDATETIME/TIMESTAMP型(マイクロ秒非対応の古いデータ型)はないか、64文字を超える外部キー制約はないか、古い複合sql_modeが使われているオブジェクトはないか、ibdata1に格納されているパーティションはないか、削除された関数を使っているストアドやgenerated columnはないか、をチェックしている。
具体的なSQLは、 p5-mysql-upgrade-checker--execute なしで実行するか、general_log = ON にしてMySQL Shellから実行などすると良いと思われる。
あとはREADMEにサンプル( CHECK TABLE .. FOR UPGRADE だけ可変だけど他は固定)が貼ってあるのでそれでも雰囲気は感じ取ってもらえるのではないか。
なおこの get_reserved_keywords_check は「新しく予約語になったもの」が使われていないかを探すけれど、「キーワードから予約語になって俺のスキーマを殺した row 」とかは入っていないので注意。
プルリクチャンス?

【2018/07/06 14:59】
フツーに入ってました。。