暗黙の型キャスト — 型が揃わないとインデックスが使われない
列と値の型違いで発生する暗黙キャストがインデックスを無効化する罠と、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_civsutf8mb4_0900_ai_ciなど - JOIN ON の両側の列で文字セット / COLLATE が違うと、片側に暗黙の変換がかかり、インデックスが使われなくなる
PostgreSQL / SQL Server / Oracle も同様に COLLATION の不一致でインデックス不使用が起こります。スキーマ全体で揃えるのが一番確実。後から揃えるなら ALTER TABLE ... CONVERT TO CHARACTER SET ... を計画的に。
-- 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 | 暗黙キャストのサイン |
|---|---|
| PostgreSQL | Filter: ((email)::text = 'x'::text) のように ::type が列側に付いている |
| MySQL | EXPLAIN FORMAT=JSON の attached_condition や warnings に convert / cast。SHOW WARNINGS で Note: Cannot use index が出ることも |
| SQL Server | 実行プランに CONVERT_IMPLICIT(...) の Compute Scalar。Index Scan + 変換は典型パターン |
| Oracle | DBMS_XPLAN.DISPLAY の Predicate Information セクションに INTERNAL_FUNCTION(col) や TO_NUMBER(col) |
特に SQL Server の CONVERT_IMPLICIT は「見つけたら必ず直すべき警告」と考えて良いレベルです。
対処チェックリスト
- リテラルの型を列に合わせる(数値列に数値、文字列列に文字列)
- パラメータバインドの型を明示(JDBC 型ヒント、Prisma native types、
$1::textキャスト) - SQL Server + JDBC/.NET: 文字列パラメータは
VARCHARで送る。sendStringParametersAsUnicode=falseやSqlDbType明示 - MySQL: テーブル全体の charset/collation を統一。新規は
utf8mb4_0900_ai_ci - 列型そのものを見直す: 電話番号・郵便番号・社員コードは先頭 0 が消えない文字列型で
- UUID / BIGINT: アプリ側で文字列 / BigInt 型を使い、精度落ちを防ぐ
- CI で EXPLAIN を流す: 重要クエリだけでも、暗黙キャストが混入したら気付ける仕組みに
