Query Go
暗黙の型キャスト — 型が揃わないとインデックスが使われない
チューニング

暗黙の型キャスト — 型が揃わないとインデックスが使われない

列と値の型違いで発生する暗黙キャストがインデックスを無効化する罠と、RDBMS 別の発見方法。

暗黙の型キャストとは

SQL は異なる型を比較するとき、どちらかを自動で変換します。これ自体は便利な仕組みですが、問題はどちら側が変換されるか。変換ルール次第で列側が変換されると、WHERE 句がWHERE CAST(col AS ...) = ? と同等になり、前ページの SARGable ルールによりインデックスが使われなくなります

目に見えない分、同じ条件を書いているのにいつの間にか遅くなっていた、という事故が起きやすいのが厄介なところです。プリペアドステートメント(パラメータバインド)でアプリから型を渡さずに暗黙化した結果、このパターンに落ちることもよくあります。

RDBMS 別・典型ハマりパターン

同じ書き方でも、どちらを変換するかは RDBMS のルールで決まります。「列側が変換されたら負け」と覚えてください。

状況挙動対処
MySQL: VARCHAR 列 = 数値リテラル
WHERE phone = 09012345678
列側を DOUBLE に変換 → インデックス不使用値を文字列で渡す '09012345678'
PostgreSQL: TEXT 列 = 数値
WHERE phone = 12345
型不一致エラー(厳格)エラーで気づけるので早い段階で修正される
MySQL: 数値列 = 文字列
WHERE id = '42'
文字列側を数値化 → インデックス使われる(列側は無変換なので OK)問題なし
PostgreSQL: UUID 列 = 文字列
WHERE id = 'abc-...'
文字列側を UUID 化 → OK問題なし(12.x 以降)
PostgreSQL: TIMESTAMPTZ 列 = TIMESTAMP列側がセッション TZ で変換される場合ありバインド型を TIMESTAMPTZ に揃える
Oracle: VARCHAR2 列 = 数値列側を数値化 → インデックス不使用文字列で渡す
SQL Server: NVARCHAR パラメータ vs VARCHAR
(ORM / JDBC の既定設定)
列側を NVARCHAR に昇格 → インデックス不使用の有名パターンパラメータを VARCHAR に、または sendStringParametersAsUnicode=false

特に SQL Server + Java/.NET ドライバの NVARCHAR 昇格問題は「コードは何も変えていないのに本番だけ突然遅い」の超定番です。

文字セット・照合順序の違い

MySQL で JOIN を書いたら急に遅い、というとき犯人になりやすいのが文字セット / 照合順序の違いです。

  • utf8mb4 vs utf8mb3(旧 utf8): MySQL 5.7 時代のテーブルを 8.0 に移すときに残りがち
  • 同じ utf8mb4 でも COLLATE が違う: utf8mb4_general_ci vs utf8mb4_0900_ai_ci など
  • JOIN ON の両側の列で文字セット / COLLATE が違うと、片側に暗黙の変換がかかり、インデックスが使われなくなる

PostgreSQL / SQL Server / Oracle も同様に COLLATION の不一致でインデックス不使用が起こります。スキーマ全体で揃えるのが一番確実。後から揃えるなら ALTER TABLE ... CONVERT TO CHARACTER SET ... を計画的に。

sql
-- MySQL: JOIN 側の charset/collation を確認
SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
  AND COLUMN_NAME IN ('user_id', 'id');

-- 揃える(計画停止を避けるならオンライン DDL で)
ALTER TABLE orders CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

ORM / ドライバ由来の型ずれ

アプリ側の型ずれは、SQL を見ているだけでは気付けません。代表例:

  • Prisma / Knex / Sequelize: マイグレーションで String と書いていて、DB 側では TEXT / VARCHAR(255) / CHAR(36) のどれにマップされたか曖昧
  • ActiveRecord (Rails): string 型は VARCHAR(255) がデフォルト。UUID を保存して検索すると charset/collation が絡む
  • JDBC / .NET SqlClient: 文字列パラメータが NVARCHAR として送られる(前述の SQL Server 問題)
  • 数値の桁数: JavaScript の number は 64bit 浮動小数。BIGINT の ID を渡すと精度落ち + 暗黙変換

対処は「DB 側の列定義に合わせてドライバの型を明示的に指定する」こと。JDBC なら setString の代わりに型ヒント、Node.js の pg なら $1::text でキャスト、Prisma なら @db.VarChar(255) のようなネイティブ型注釈を使います。

EXPLAIN で暗黙キャストを見つける

EXPLAIN 出力には列側に型変換が入った痕跡が残ります。見つけるコツ:

RDBMS暗黙キャストのサイン
PostgreSQLFilter: ((email)::text = 'x'::text) のように ::type列側に付いている
MySQLEXPLAIN FORMAT=JSONattached_conditionwarningsconvert / castSHOW WARNINGSNote: Cannot use index が出ることも
SQL Server実行プランに CONVERT_IMPLICIT(...) の Compute Scalar。Index Scan + 変換は典型パターン
OracleDBMS_XPLAN.DISPLAYPredicate Information セクションに INTERNAL_FUNCTION(col)TO_NUMBER(col)

特に SQL Server の CONVERT_IMPLICIT は「見つけたら必ず直すべき警告」と考えて良いレベルです。

対処チェックリスト

  • リテラルの型を列に合わせる(数値列に数値、文字列列に文字列)
  • パラメータバインドの型を明示(JDBC 型ヒント、Prisma native types、$1::text キャスト)
  • SQL Server + JDBC/.NET: 文字列パラメータは VARCHAR で送る。sendStringParametersAsUnicode=falseSqlDbType 明示
  • MySQL: テーブル全体の charset/collation を統一。新規は utf8mb4_0900_ai_ci
  • 列型そのものを見直す: 電話番号・郵便番号・社員コードは先頭 0 が消えない文字列型
  • UUID / BIGINT: アプリ側で文字列 / BigInt 型を使い、精度落ちを防ぐ
  • CI で EXPLAIN を流す: 重要クエリだけでも、暗黙キャストが混入したら気付ける仕組みに