SQLで特定の日付のデータを抽出するときに、TIMESTAMP型の列に対してto_charを使うのはやめよう

# この記事の要約
- SQLのWHERE節に`to_char(hoge_datetime, 'YYYY-MM-DD') = '2021-09-25'`みたいな抽出条件を書くのはやめよう。性能がヤバくなるので。
- 性能が良いSQLを書くために[Use The Index, Luke](https://use-the-index-luke.com/ja)を読もう。

最低限、↓に書いてある2つのSQLをちょっとでも眺めてからブラウザバックしていただければ私は満足です。

### OK（to_charを使わない）
```sql
SELECT
    id
FROM
    articles
WHERE
    -- 投稿日時が2021-09-25のレコードを抽出
    '2021-09-25T00:00:00' <= post_datetime and post_datetime < '2021-09-26T00:00:00'
;
```

### NG（列に対してto_charを適用してる）
```sql
SELECT
    id
FROM
    articles
WHERE
    -- 投稿日時が2021-09-25のレコードを抽出
    to_char(post_datetime, 'YYYY-MM-DD') = '2021-09-25'
;
```

# 性能を比較してみる
### 環境
PostgreSQL 13.4

### 事前準備
#### テーブル
テーブル名：articles

| No. | 論理名  | 物理名       | 型      | 備考                  | 
|:----|:--------|:-------------|:--------|:----------------------|
|1    |ID       |id            |BIGSERIAL|PK                     |
|2    |投稿日時 |post_datetime |TIMESTAMP|インデックスあり(btree)|

#### DDL
```sql
CREATE TABLE articles (
    id            BIGSERIAL
  , post_datetime TIMESTAMPTZ NOT NULL
  , CONSTRAINT articles_key PRIMARY KEY (id)
);

CREATE INDEX ON articles USING btree (post_datetime);
```

### 手順
#### 投入データ
以下の要領でレコードをテーブルに投入する。

| 条件            | 値                         |
|:----------------|:---------------------------|
|レコード数（概数）| 365 or 36,500 or 3,650,000 or 36,500,000 or 365,000,000 |
|投稿日時の最小値 | 2021-01-01T00:00:00        |
|投稿日時の最大値 | 2022-01-01T00:00:00        |
|投稿日時の分布   | 最小値～最大値の間で一様に分布 |

#### 計測方法
OK/NGのそれぞれのSQLについて、EXPLAINコマンドにANALYZEオプションをつけて実行し、実際のSQLの実行時間を計測する。
[EXPLAINコマンドについては公式ドキュメントを参照すること。](https://www.postgresql.jp/document/13/html/sql-explain.html)

##### OK(to_charを使ってない)
```sql
explain analyze
SELECT
    id
FROM
    articles
WHERE
    -- 投稿日時が2021-09-25のレコードを抽出
    '2021-09-25T00:00:00' <= post_datetime and post_datetime < '2021-09-26T00:00:00'
;
```

##### NG(to_charを使ってる)
```sql
explain analyze
SELECT
    id
FROM
    articles
WHERE
    -- 投稿日時が2021-09-25のレコードを抽出
    to_char(post_datetime, 'YYYY-MM-DD') = '2021-09-25'
;
```


### 計測結果

| レコード数（件） | OK（ミリ秒） | NG（ミリ秒） |
|-----------:|------------------:|------------------:|
|         365|              0.017|              0.151|
|      36,500|              0.032|             14.107|
|   3,650,000|              1.361|            536.992|
|  36,500,000|             18.501|          5,026.952|
| 365,000,000|            117.897|        187,546.418|

レコードが増えれば増えるほどヤバいことになるのがおわかりいただけただろうか。

## どうしてここまで差がつくのか
EXPLAINコマンドで出力される実行計画を比較してみる。
##### OK(to_charを使ってない)
```sql
Index Scan using articles_post_datetime_idx on articles  (cost=0.28..8.48 rows=10 width=8)
  Index Cond: (('2021-09-25 00:00:00+09'::timestamp with time zone <= post_datetime) AND (post_datetime < '2021-09-26 00:00:00+09'::timestamp with time zone))
```
Index Scanをしてる。
（Index Scan……インデックスを使って検索するってこと）

##### NG(to_charを使ってる)
```sql
Seq Scan on articles  (cost=0.00..74.77 rows=18 width=8)
  Filter: (to_char(post_datetime, 'YYYY-MM-DD'::text) = '2021-09-25'::text)
```
Seq Scanをしてる。
（Seq Scan……テーブル内の全てのレコードを読み込んで検索するってこと）

NGパターンのSQLが実行時にインデックスが利用されない理由は[こちら](https://use-the-index-luke.com/ja/sql/where-clause/functions/case-insensitive-search)を参照。
Index ScanとかSeq Scanとかの説明は[こちら](https://use-the-index-luke.com/ja/sql/explain-plan/postgresql/operations)を参照。



# 補足
## to_charを使っても問題ないケース
### 式に対するインデックスが作成されている
[式に対するインデックス](https://www.postgresql.jp/document/13/html/indexes-expressional.html)を作成しておけばNGパターンのSQLでもインデックスを利用して検索できるらしい。（実験はしてない）

### そもそも性能が必要ない
- 頻繁には実行しないとき（保守運用時のデータの調査とかテスト時のデータの検証とか）
- テーブル内のレコードが少ないとき

## ぶっちゃけた話
[こちら](https://use-the-index-luke.com/ja/sql/where-clause/functions/case-insensitive-search)に書いてある内容を理解している人であれば、今回のNGパターンのようなSQLを書いてしまうことは絶対にありえない。

## じゃあなんでこの記事を書いたの？
- 隣のチームが開発したアプリの中で動いてるSQLがこの記事のNGのパターンだった。
- どうやら`SQL 特定の日付 抽出`でググるとこの記事のNGのパターンが結構出てくるらしく、それを真似したっぽい。
- 「こんな書き方をしたらインデックスが利かないじゃん！」と気付いて欲しいけど、そもそもインデックスの存在自体を知らない人もいる。
- そういう人が`SQL 特定の日付 抽出`でググってこの記事にたどり着いて、この記事をきっかけにインデックスを意識してSQLを書くようになってくれたらいいなあ。

# おすすめサイト
- [Use The Index, Luke](https://use-the-index-luke.com/ja)
このサイトを知ってもらうためだけにこの記事は存在すると言っても過言ではない。