MySQLチートシート

忘れやすいことを中心に書いてます。

# ユーザ管理
該当するドキュメント : https://dev.mysql.com/doc/refman/8.0/en/access-control.html

## ユーザを追加・削除
```sql
-- 現在のユーザ
SELECT USER(), CURRENT_USER();
-- 一覧
SELECT User, Host, plugin, ssl_type, account_locked
FROM mysql.user
ORDER BY User, Host;
-- 追加
CREATE USER
    'user_name'@'host' IDENTIFIED BY 'password',
    'user_name'@'host' IDENTIFIED BY 'password', ...;
-- 削除
DROP USER
    'user1'@'host2',
    'user2'@'host2', ...;
-- パスワード設定
ALTER USER 'user_name'@'host' IDENTIFIED BY 'password';
ALTER USER 'user_name'@'host' IDENTIFIED BY RANDOM PASSWORD;  -- MySQL 8.0以降
-- オプション
ALTER USER 'user_name'@'host' REQUIRE SSL;
ALTER USER 'user_name'@'host' WITH MAX_USER_CONNECTIONS 100;
ALTER USER 'user_name'@'host' ACCOUNT LOCK;
```

## ロール
```sql
CREATE ROLE 'admin', 'developer';
GRANT ALL ON *.* TO 'admin';
GRANT SELECT ON app_db.* TO 'developer';
SET DEFAULT ROLE 'admin', 'developer' TO 'joe'@'10.0.0.1';

-- 現在のセッションでのアクティブにするロールを変更
SET ROLE DEFAULT;
SET ROLE NONE;
SET ROLE ALL;
```
> SET DEFAULT ROLE and SET ROLE DEFAULT are different statements:
>
> * SET DEFAULT ROLE defines which account roles to activate by default within account sessions.
> * SET ROLE DEFAULT sets the active roles within the current session to the current account default roles. 
>
> https://dev.mysql.com/doc/refman/8.0/en/set-default-role.html

https://dev.mysql.com/doc/refman/8.0/en/roles.html

## 権限を付与・剥奪
```sql
-- 付与
GRANT '権限' ON 'レベル' TO 'ユーザ';
-- 確認
SHOW GRANTS FOR 'ユーザ';
SHOW GRANTS;  -- 現在のユーザが持っている権限を表示
-- 剥奪
REVOKE '権限' ON 'レベル' FROM 'ユーザ';
```
権限はSELECT, INSERT, UPDATE, DELETE, CREATE, DROPなど
権限の一覧: https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html

# プロセス操作
```sql
-- 一覧
SHOW PROCESSLIST;
-- こっちの方がおすすめ。WHERE句で絞り込みができる。
SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST;
-- 問題を起こしているクエリを探す
SELECT COMMAND,
       STATE,
       COUNT(1) THREADS,
       MAX(TIME) MAX_TIME
FROM INFORMATION_SCHEMA.PROCESSLIST
GROUP BY COMMAND, STATE
ORDER BY THREADS DESC, COMMAND, STATE;

-- 強制停止
KILL QUERY ID;
KILL [CONNECTION] ID
```
参考: https://dev.mysql.com/doc/refman/8.0/en/kill.html

# パーティション
## 既存のテーブルにパーティションを作成・追加・削除
```sql
ALTER TABLE 'table_name'
PARTITION BY RANGE COLUMNS(timestamp) (
    PARTITION p_201501 VALUES LESS THAN ('2015-01-01 00:00:00'),
    PARTITION p_201502 VALUES LESS THAN ('2015-02-01 00:00:00')
);

ALTER TABLE 'table_name' ADD PARTITION (
    PARTITION p_future VALUES LESS THAN (MAXVALUE)
);

ALTER TABLE 'table_name' DROP PARTITION p_201501, p_201502;
```
https://dev.mysql.com/doc/refman/5.5/en/alter-table.html
https://dev.mysql.com/doc/refman/5.5/en/alter-table-partition-operations.html

### 参考、及び注意点
> REORGANIZE PARTITION するにも ADD PARTITION するにもテーブルロックが避けられないので、事前にまとめてガガッとパーティション作っちゃう場合が多いみたいですね。
> http://blog.livedoor.jp/sasata299/archives/51882315.html

削除対象のパーティションの前後のパーティションが存在すると、遅くなるケースがある。
http://lab.adn-mobasia.net/?p=182

## パーティションの使用状況を確認
```sql
SELECT TABLE_SCHEMA, TABLE_NAME, PARTITION_NAME, PARTITION_ORDINAL_POSITION, TABLE_ROWS
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_NAME='table_name';
```

# テーブルの列
```sql
DESC 'table_name';
```
https://dev.mysql.com/doc/refman/5.6/ja/describe.html

# テーブルの詳細情報を得る
```sql
-- 現在のデータベース内の全てのテーブルについて表示
SHOW TABLE STATUS \G
-- 指定したテーブルのみ表示
SHOW TABLE STATUS LIKE 'table_name' \G
```
デーブルのサイズや、auto_inclimentの現在の値などが調べられる。
## 注意点
InnoDBでは行数に関するデータはかなりアバウトな値なのであまり信用しないほうがいい。


# SSL

## 鍵を作る
**注意:**

 - 鍵のパーミッションには注意してください。mysqlと管理者以外のユーザから鍵にアクセスできる状態は好ましくありません。
 - 証明書を作成するときCNに、一意なホスト名を指定してください。MySQLはCAと同じCNの証明書では、SSL接続が出来ません。

**コンピュータ名:**

 - ca: 認証局の役割をするコンピュータ。手元のPCのことだと思ってください
 - db: MySQLが稼働するサーバ
 - client: MySQLへアクセスするサーバ

**変数:**

 - CA_HOST: CA(認証局)のCommon Name
 - DB_HOST: DBサーバのCommon Name
 - CLIENT_HOST: MySQLクライアントのCommon Name

```bash
# ca -- CA(認証局)の鍵ペアを作る
$ openssl genrsa 2048 > ca-key.pem
$ openssl req -new -x509 -nodes -days 3600 \
    -key ca-key.pem -out ca-cert.pem \
    -subj "/CN=$CA_HOST"
# ca -- ca-cert.pemをdbとclientにコピー

# db -- DBサーバの鍵ペアを作る
$ openssl req -newkey rsa:2048 -days 3600  \
    -nodes -keyout server-key.pem -out server-req.pem \
    -subj "/CN=$DB_HOST"
$ openssl rsa -in server-key.pem -out server-key.pem

# クライアント証明書を利用しないなら、この作業は必要ない。
# client -- クライアントの鍵ペアを作る
$ openssl req -newkey rsa:2048 -days 3600 \
    -nodes -keyout client-key.pem -out client-req.pem \
    -subj "/CN=$CLIENT_HOST"
$ openssl rsa -in client-key.pem -out client-key.pem

# ca -- server-req.pemをdbからコピー
# ca -- CAの鍵ペアを使ってDBサーバの鍵に署名する
$ openssl x509 -req -in server-req.pem -days 3600 \
    -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 \
    -out server-cert.pem
# ca -- 署名済みのDBサーバの鍵(server-cert.pem)をdbにコピー

# クライアント証明書を利用しないなら、この作業は必要ない。
# ca -- client-req.pemをclientからコピー
# ca -- CAの鍵ペアを使ってクライアントの鍵に署名する
$ openssl x509 -req -in client-req.pem -days 3600 \
    -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 \
    -out client-cert.pem
# ca -- 署名済みのクライアントの鍵(client-cert.pem)をclientにコピー
```

## MySQLサーバの設定
```bash
$ mkdir /etc/mysql/ssl
$ chown root:mysql /etc/mysql/ssl
$ chmod 750 /etc/mysql/ssl

# MySQL 5.6.3以降を使用するなら、証明書失効リストも作っておく
touch /etc/mysql/ssl/crl.pem
```

```ini
# /etc/mysql/conf.d/ssl.cnf
[mysqld]
# SSL接続の設定
ssl-key  = /etc/mysql/ssl/server-key.pem
ssl-cert = /etc/mysql/ssl/server-cert.pem

# (MySQL 5.6.3以降) 証明書失効リスト
# (MySQL 8.0.16以降) 動的に別のファイルに変更できる。定期的に更新が出来る。
ssl-crl  = /etc/mysql/ssl/crl-YYYYMMDD.pem

# クライアント証明書を利用するならこれも指定する
ssl-ca   = /etc/mysql/ssl/ca-cert.pem

# (8.0.27以前) デフォルトではTLSv1やTLSv1.1が有効になっている。古いTLSプロトコルを使用する必要がない場合は、無効化しておく。
# tls_version = TLSv1.2,TLSv1.3
# admin_tls_version = TLSv1.2,TLSv1.3
```


## mysqlコマンドでSSL接続をする
**引数:**
 - --ssl-ca: SSL接続を使用します。`ca-cert.pem`を指定
 - --ssl-verify-server-cert: CNと接続先ホスト名を比較し、一致しない場合は接続しない
 - --ssl-cert: クライアント証明書を使用する。`client-cert.pem`を指定
 - --ssl-key: クライアント証明書を使用する。`client-key.pem`を指定
 - --ssl-crl: 証明書失効リストを指定 (MySQL 5.6.3以降)

## SSLを使用して接続しているか確認
```bash
$ mysql -h example.com -utest -p --ssl-ca /path/to/ca-cert.pem
Enter password:
mysql> \s
(前略)
SSL: Cipher in use in DHE-RSA-AES256-SHA
(後略)
```

**SSLが有効にならないときに確認すること:**

 - localhostへ接続する場合、通常はSSLが有効になりません。
   引数に`-h 127.0.0.1`を追加するとSSLが有効になります。
 - mysqldのerror.logに`SSL error: Unable to get private key from '/etc/mysql/ssl/server-key.pem'`がでていたら、秘密鍵のフォーマットが古いのが原因かも。  
   秘密鍵の最初の行が`-----BEGIN PRIVATE KEY-----`から始まっていたら古いフォーマット(PKCS#1)です。`openssl rsa -in old-server-key.pem -out new-server-key.pem`でフォーマットを変換してみる。`-----BEGIN RSA PRIVATE KEY-----`から始まるPKCS#8の秘密鍵が出力されればOK。
 - mysqldのerror.logに`[Warning] SSL error: SSL_CTX_set_default_verify_paths failed`がでていたら、MySQLが鍵に証明書や秘密鍵にアクセスできるか確認する。ファイルパーミッションやSELinuxの設定など。

## SSL接続を強制する
```sql
-- test@127.0.0.1はSSL接続を必須にする
GRANT USAGE ON test.* TO test@127.0.0.1 IDENTIFIED BY ${password} REQUIRE SSL;

-- test@127.0.0.1はクライアント証明書を必須にする
GRANT USAGE ON test.* to test@127.0.0.1 IDENTIFIED BY ${password} REQUIRE X509;
```

# SlaveをMasterに昇格する
データロストをすること無く、安全にslaveをmasterに昇格する方法を解説する。

この手順は、既にGTIDベースのレプリケーションがされているシステムで、masterサーバのメンテをする場面を想定している。masterで障害が起きたときのフェイルオーバーについては考慮していない。

```sql
-- 実行中のクエリを確認。
-- 重たいクエリが流れてないかなどを確認。
srv1> SHOW PROCESSLIST;

-- 念のため、slave遅延が起きてないか確認。
-- Seconds_Behind_Master が大きくなっているなら、色々まずい。
srv2> SHOW SLAVE STATUS \G

--------------------------------------------
-- ↓　これ以降はDBがダウンするので、テキパキ作業する。

-- masterへの書き込みをブロック
srv1> FLUSH TABLES WITH READ LOCK;
srv1> SET GLOBAL read_only = ON;

-- masterに追いついたか確認
-- Executed_Gtid_Set がmasterと一致するか確認
srv2> SHOW SLAVE STATUS \G

-- slaveをmasterに昇格する
-- これ以降は
--   srv1: 引退
--   srv2: master
srv2> STOP SLAVE;
srv2> RESET SLAVE ALL;
srv2> RESET MASTER;

-- * HAProxyの設定を変更し、masterサーバの向き先をsrv1からsrv2に変更。
-- * srv1のmysqlプロセスを停止し、強制的に既存のセッションを切断。

-- masterへ書き込み可能にする。
-- この作業は、全てのセッションが新しいサーバに向いた後にすること！
-- srv1へのセッションが残っている状況で書き込み可能にすると、commitしたあとにselectしても更新した内容が見えないなど、思わぬトラブルが発生する恐れがある。
srv2> SET GLOBAL read_only = OFF;

-- ↑　DBが復帰しているはず
-- アプリケーションの動作状況を確認する。

-- mysqld.cnfを修正
-- srv1とsrv2のmysqld.cnfを修正し、read_onlyの値を正しい値に書き換えておく。
-- これ忘れると、サーバが再起動したときに事故ります...
```

# バックアップとリストア
[Percona XtraBackup](https://www.percona.com/software/mysql-database/percona-xtrabackup)を使ったオンラインバックアップ取得方法とリストア方法の解説。[公式の解説](https://www.percona.com/doc/percona-xtrabackup/LATEST/backup_scenarios/full_backup.html)も読んでおくとよい。

## オンラインバックアップ
`--stream tar`オプションを指定すると、標準出力にtarballが出力される。サイズが大きいのでgzipやlzoで圧縮すると良いだろう。バックアップの負荷が気になるなら、`pv`コマンドなどで転送レート制限をする。

```sh
xtrabackup --backup --stream tar -uroot |pigz --fast >mysql-backup.tar.gz
```

## リストア
注意: もしslaveを構築するなら、`xtrabackup_binlog_pos_innodb`ファイルを残しておく。

```sh
# MySQLの停止 & データディレクトリ削除
systemctl stop mysql
mv /var/lib/mysql{,.old}

# tarballを展開
mkdir /var/lib/mysql
tar xvf mysql-backup.tar.gz -C /var/lib/mysql

# データ修正 & MySQL起動
xtrabackup --prepare --target-dir /var/lib/mysql
chown -R mysql:mysql /var/lib/mysql
systemctl start mysql
```

## Slave構築
バックアップをリストアして、`change master to`文をすれば動きます。

注意: XtraBackupで取得したバックアップデータを利用してslaveを構築する場合、決してautopositionを使ってはいけない。`xtrabackup_binlog_pos_innodb`に書いてあるpositionにすること。`xtrabackup --prepare`してもGTIDが正しい位置に更新されない(?)ようで、autopositionを使うとレプリケーションに失敗します。

```sh
$ cat /var/lib/mysql/xtrabackup_binlog_pos_innodb
mysql-bin.025018        43974570
```

```sql
mysql> change master to master_host='xxx',
    master_port=3306,
    master_user='foo',
    master_password='bar',
    master_log_file='mysql-bin.025018',
    master_log_pos=43974570,
    master_ssl=1;
mysql> start slave;
mysql> show slave status;
-- masterに追いつくまで待ちましょう
```

# Binary Log Transaction Compression 
https://dev.mysql.com/doc/refman/8.0/en/binary-log-transaction-compression.html

8.0.20以降で使用可能。binlogをzstdで可逆圧縮できる。一つのトランザクションで発生した複数のbinary log eventsを一つのイベント ( `Transaction_payload_event`) として扱い、スレーブに送信する。

注意点: masterで巨大な変更が発生するトランザクションを実行すると、スレーブが壊れるようです...

# 大量のデータを本番影響を出すことなく削除する

適切な粒度でパーティションを分けて、適切な頻度でパーティションをdropしていくのが理想。とはいえ、色々な制約があって、大量のデータを一気に削除していかないといけないケースはある。そのような状況下での対処法を記しておく。

## 巨大なデータベース/テーブル/パーティションをDROPする

下記のようなケースでは、unlink(2)の遅さに注意！

- HDDで数百GBやTB級のファイルを削除する
- HDDで数百ファイルを削除する

巨大ファイルに対するunlink(2)や、多数のファイルをunlinkするときは処理時間が長くなる。unlinkは同期的に行うため、削除処理中は他のトランザクションに影響を及ぼす可能性がある。
別プロセスでopen(2)するかhard linkを作っておき、mysqldがunlink(2)したタイミングでファイルの実態の削除が始まるのを防ぐと、意図せぬブロッキングを防げる。dropが終わったら、バックグラウンドで様子をうかがいながら少しずつ削除できる。


```bash
# お手軽なやり方。
# I/O負荷が低いDBサーバでおすすめ。
sleep 10m <large_table.idb
# 10分以内に DROP TABLE large_table;
# 10分経過したら、バックグラウンドで巨大ファイルの削除が始まる。
# sleepコマンドがしばらくDステートで固まる。削除処理の中断はできません...
```

```bash
# 少しずつ消していくパターン。この例では50MB/sのペースで領域を開放していく。
# ちょっと手間はかかるけど、作業の中断やI/O負荷の調整は出来る。
# I/O負荷の高いDBサーバでおすすめ。今どきのSSDが搭載されたサーバでは、通常はここまで手間をかける必要ない。
tmp_file=large_table.idb.tmp
ln large_table.idb $tmp_file
# DROP TABLE large_table;
while [ -s $tmp_file ] && truncate -s -50M $tmp_file; do sleep 1; done
rm $tmp_file
```

## 大量の行をDELETEする

本番トラフィックを捌きながら、何十億行をDELETEしたいですか？　...数週間の作業時間を確保しておくべきです。
DELETEは遅い。マジで遅い。DELETEクエリをしくじるとDBが落ちる。調子こいて勢い良くDELETEしていくと、レプリカが遅延する。

1つのトランザクションで消すのは無理です。多数のトランザクションに細切れに分割して、時間をかけて少しずつ消していくのがいいでしょう。

実際に実行するクエリのサンプルは下記の通り。下記のクエリを`@offset`の値をずらしながら実行していく。

```sql
SET @offset = 1000000; -- この部分の値を増やしながら、この一連のクエリを繰り返し実行する。
SET @batch_size = 10000;

BEGIN;
DELETE /*+ INDEX(large_table PRIMARY) */  -- ポイント1: Optimizer Hintsを指定
  FROM large_table
  WHERE id BETWEEN @offset AND @offset + @batch_size AND  -- ポイント2: idで範囲指定
        created_at < '2020-01-01';  -- ポイント3: 削除条件の指定
SET @deleted_rows = ROW_COUNT();
COMMIT;

-- ポイント4: sleepする
-- DBのレプリケーション遅延に寛容なシステムであれば、レプリカの様子を見ながら動的にsleep時間を調整するアプローチが良い。
-- システムの負荷に応じて自動的に調整されるので、人間が張り付いてsleep時間を調整する必要が無くなる。
-- 
-- mysqlの拡張コマンド systemコマンドを使う。ローカルで任意のスクリプトを実行できる機能。
-- 行の分布が偏っている場合、このスクリプトは高頻度で実行される可能性がある。TTL数秒のキャッシュがあるといいだろう。
system wait_until_replication_lag_is_less_than_10_sec.sh

-- レプリケーション遅延に非寛容なシステムであれば、長めのsleepを付けていくしか無い。
-- SELECT SLEEP(1);
--
-- 行の分布が偏っている場合は、削除した行数に応じて動的にsleep時間を調整するといい。
-- SET @sleep_time = CASE
--   WHEN @deleted_rows < 100 THEN 0
--   WHEN @deleted_rows < 5000 THEN 0.5
--   ELSE 1
-- END;
-- SELECT SLEEP(@sleep_time);
```

- ポイント1: Optimizer Hintsを指定する。Optimizerが誤った判断をしてしまうと途端にパフォーマンス劣化し、障害を引き起こす可能性がある。
- ポイント2: PRIMARY KEYで削除範囲を限定する。DELETE対象行が非常に多い場合は、lockの獲得に時間がかかる。他のクエリが詰まってDB障害になる...。
- ポイント3: 削除対象外の行を誤って削除しないよう、削除条件を入れておく。
- ポイント4: sleepする。全速力でDELETEしていると、レプリケーション遅延を引き起こすかもしれません。