インデックスショットガンで書き込み性能はどれくらい劣化するのか

この記事は[ミライトデザイン Advent Calendar 2021](https://qiita.com/advent-calendar/2021/miraito-inc) の10日目の記事です。

本日も懲りずに乱入させていただきました。

昨日は@tkek321 さんのDockerについての記事でした。
１つ１つ丁寧に解説してくれている記事なので、ぜひご一読をおすすめします。
読んでね！

[docker-laravel の Docker 構成をちゃんと理解する【docker-compose.yml 編】](https://qiita.com/tkek321/items/9c0c5cec1dc349c67f5f)

本日はSQLアンチパターンのインデックスショットガンについて記事にさせていただきます。
~~名前がカッコイイからこの記事書きました。~~

## インデックスショットガンって何？

インデックスショットガンとは、書籍：「[SQLアンチパターン](https://www.amazon.co.jp/SQL%E3%82%A2%E3%83%B3%E3%83%81%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3-Bill-Karwin/dp/4873115892)」で紹介されているアンチパターンの1つで、闇雲にインデックスを作成することを指しています。

「インデックスって性能改善にいいんでしょ？よくわからないけど、とにかくインデックスをいっぱい作っちゃえ。」というような状態です。

## インデックスショットガンのデメリットって何？

インデックスを作成すると、`INSERT`, `UPDATE`などの書き込み処理が遅くなります。
これはデータを`INSERT`, `UPDATE`する際にインデックスの更新処理が発生するためです。

闇雲にインデックスを作成することで、書き込み処理で無用なオーバーヘッドが発生し、処理性能が劣化する恐れがあります。[^1]

[^1]: 他にも不要なインデックスを格納するためのディスク領域の浪費などもデメリットと言えるでしょう。

## インデックスを作ることでどれくらい書き込み性能が落ちるか測定してみよう

では具体的にインデックスを作ることでどれくらい書き込み性能が落ちるか測定してみようと思います。
今回はMySQLとPostgreSQLをDocker環境で用意して測定を行っていきます。
~~Oracle？そんな金はない~~
~~SQL Server？MariaDB？知らんなあ~~

### 環境

Docker for Windows上でMySQL, PostgreSQLを起動して計測を行います。

MySQL: 8.0.26
PostgreSQL: 14.1

### テーブル

計測に使用するテーブルを準備します。
（下記SQLはMySQL用で、PostgreSQLは`DATETIME`→`TIMESTAMP`にしています。）

```sql
CREATE TABLE test_tables (
  id INT NOT NULL PRIMARY KEY,
  column_01 VARCHAR(20) NOT NULL,
  column_02 INT NOT NULL,
  column_03 DATETIME NOT NULL,
  column_04 VARCHAR(20) NOT NULL,
  column_05 INT NOT NULL,
  column_06 DATETIME NOT NULL,
  column_07 VARCHAR(20) NOT NULL,
  column_08 INT NOT NULL,
  column_09 DATETIME NOT NULL,
  column_10 VARCHAR(20) NOT NULL,
  column_11 INT NOT NULL
);
```

上記のテーブルに対してインデックスを追加していき、インデックスを追加した個数と書き込み処理にかかる時間を見ていきます。

なお、MySQL Innodbの場合、1テーブルに対するセカンダリインデックス数の上限は64個となっています。

[^]:https://dev.mysql.com/doc/refman/8.0/ja/innodb-limits.html

一方、PostgreSQLの場合、1テーブルに対するインデックス数の上限はありません。

[^]:https://www.postgresql.jp/document/12/html/limits.html

今回はMySQLに合わせて、60個まで作成してインデックスを作成して計測をしていこうと思います。

### 計測するSQL

書き込み処理として、今回は`INSERT`, `UPDATE`の実行速度を計測していきます。
100万レコードの`INSERT`, `UPDATE`にかかる時間をそれぞれ見ていこうと思います。

MySQLとPostgreSQLで実行するSQLが多少異なりますが、内容はほぼ同じだと思っていただいて構いません。

#### MySQL

```sql
-- INSERT用のテーブル作成
CREATE TABLE numbers (`no` int DEFAULT NULL);
INSERT INTO numbers VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10);

-- INSERT
INSERT INTO test_tables
SELECT
    CONCAT('C-', LPAD(@rownum := @rownum + 1, 8, '0')),
    @rownum,
    now(),
    CONCAT('C-', LPAD(@rownum, 8, '0')),
    @rownum,
    now(),
    CONCAT('C-', LPAD(@rownum, 8, '0')),
    @rownum,
    now(),
    CONCAT('C-', LPAD(@rownum, 8, '0')),
    @rownum
FROM
    numbers AS s1,
    numbers AS s2,
    numbers AS s3,
    numbers AS s4,
    numbers AS s5,
    numbers AS s6,
    (SELECT @rownum := 0) AS v
;

-- UPDATE
UPDATE test_tables SET
    column_01 = CONCAT('U-', id),
    column_02 = id,
    column_03 = now(),
    column_04 = CONCAT('U-', id),
    column_05 = id,
    column_06 = now(),
    column_07 = CONCAT('U-', id),
    column_08 = id,
    column_09 = now(),
    column_10 = CONCAT('U-', id),
    column_11 = id
;
```

#### PostgreSQL

```sql
-- INSERT用のテーブル作成
CREATE TABLE numbers (no int DEFAULT NULL);
INSERT INTO numbers VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10);

-- INSERT
INSERT INTO test_tables
SELECT
    ROW_NUMBER() OVER (),
    'C-' || LPAD(CAST(ROW_NUMBER() OVER () AS character varying), 8, '0'),
    ROW_NUMBER() OVER (),
    now(),
    'C-' || LPAD(CAST(ROW_NUMBER() OVER () AS character varying), 8, '0'),
    ROW_NUMBER() OVER (),
    now(),
    'C-' || LPAD(CAST(ROW_NUMBER() OVER () AS character varying), 8, '0'),
    ROW_NUMBER() OVER (),
    now(),
    'C-' || LPAD(CAST(ROW_NUMBER() OVER () AS character varying), 8, '0'),
    ROW_NUMBER() OVER ()
FROM
    numbers AS s1,
    numbers AS s2,
    numbers AS s3,
    numbers AS s4,
    numbers AS s5,
    numbers AS s6
;

-- UPDATE
UPDATE test_tables SET
    column_01 = 'U-' || id,
    column_02 = id,
    column_03 = now(),
    column_04 = 'U-' || id,
    column_05 = id,
    column_06 = now(),
    column_07 = 'U-' || id,
    column_08 = id,
    column_09 = now(),
    column_10 = 'U-' || id,
    column_11 = id
;
```

### 計測結果

それでは計測結果の発表です🎺[^2]
計測時間の単位は秒（s）になります。

|           | INDEX  | 0     | 1     | 2     | 3     | 4     | 5     | 10     | 20     | 30     | 40     | 50      | 60      |
|-----------|--------|-------|-------|-------|-------|-------|-------|--------|--------|--------|--------|---------|---------|
| MySQL     | INSERT | 14.32 | 16.51 | 19.6  | 21.28 | 26.36 | 33.99 | 42.59  | 85.54  | 100    | 146.64 | 182.13  | 253.49  |
| MySQL     | UPDATE | 28.63 | 39.96 | 42.98 | 52.79 | 57.35 | 74.4  | 128.94 | 220.62 | 273.34 | 780.03 | 1046.79 | 1253.86 |
| PosgreSQL | INSERT | 4.28  | 5.36  | 5.93  | 6.61  | 8.06  | 8.54  | 13.17  | 26.03  | 35.59  | 45.27  | 56.63   | 68.75   |
| PosgreSQL | UPDATE | 5.71  | 8.68  | 9.63  | 10.11 | 12.81 | 14    | 22.58  | 52.32  | 70.13  | 90.45  | 116.32  | 140.81  |

[^2]: インデックス数はPrimary Keyを含めていません

グラフ化してみると下図のようになりました。

![MySQLグラフ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221479/4d6a9aed-e934-2557-51b7-100f55283aba.png)

![PostgreSQLグラフ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221479/d13198c5-bd80-150b-7e04-721ee4757d1b.png)

## 考察

上記の結果を受けて、自分なりの考察をしたいと思います。

- インデックス数が増えれば`INSERT`, `UPDATE`どちらも実行時間がかかるようになった。
    - この結果は予想通りといえる。
- `INSERT`, `UPDATE`で比較すると、`UPDATE`のほうがインデックスの影響をより多く受けていた。
    - これは`UPDATE`で全カラムを更新していることの影響も大きいと思われる。更新するカラムを絞ればもう少し影響を小さくできそう。
- MySQL, PostgreSQLともにインデックスを4~5個追加すると`INSERT`が2倍, 10個追加すると3倍時間がかかるようになった。
    - 思った以上に影響が大きい印象なので、大量の書き込みをするテーブルに対してはインデックスの追加は慎重になる必要がありそう。[^3]
- PostgreSQL速くね？
    - ~~私はPostgreSQLの回し者ではありません。~~

[^3]: 大量の書き込みを行う場合、インデックスを外した状態でインポートを行い、全件取り込んだ後にインデックスを追加するという技もあります

## まとめ

今回はインデックスショットガンと書き込み性能の関係を、SQLの実行時間を計測することで見てきました。
いかがだったでしょうか。

自分としては思った以上に`INSERT`, `UPDATE`への影響が大きいと感じました。
~~いままで野生のカンでインデックス追加してきたテーブルたちよ、スマヌ。。。~~

とはいえ、登録されたデータの参照を行う際に、インデックスを適切に使用することが非常に重要であることもまた確かです。

インデックスを追加する場合、闇雲に追加するのではなく「MENTOR」の原則[^4]に従って必要最小限のインデックスを追加することが重要です（自分への戒め）。

[^4]: **M**easure（測定）, **E**xplain（解析）, **N**ominate（指名）, **T**est（テスト）, **O**ptimize（最適化）, **R**ebuild（再構築）の頭文字をとったもの。詳しくはSQLアンチパターンを読みましょう。

明日は[ほげさん](https://zenn.dev/suzuki_hoge)が記事を書いてくれるそうです。

いつもほげさんの記事で勉強させてもらっているので、どんな記事を書いてくれるのか自分も楽しみです。


## 参考

- [SQLアンチパターン（インデックスショットガン）](
https://www.slideshare.net/tomoakiuchida311/sql-28561665)
- [SQLで大量のテストデータ作成](https://qiita.com/cobot00/items/8d59e0734314a88d74c7)
- [インデックスを作れば作るほど、INSERTが遅くなる](
https://use-the-index-luke.com/ja/sql/dml/insert)
- [パフォーマンスチューニング9つの技 ～「書き」について～｜PostgreSQLインサイド : 富士通](https://www.fujitsu.com/jp/products/software/resources/feature-stories/postgres/article-index/tuningrule9-write/)