Databricks SQL Warehouse に Simba Spark ODBC ドライバー(v2.8.0)で接続し、.NET 8 / ASP.NET Web API(System.Data.Odbc)からパラメータクエリを実装すると、WHERE 句は問題なくパラメータ化できる一方で、LIMIT / OFFSET を 同じ感覚で「?」にするとエラーになることがあります。この記事では、再現しやすい落とし穴(特に OFFSET)と、SQL インジェクションを避けつつページングを成立させる現実的な実装を整理します。
結論:WHERE は「?」で安全にパラメータ化できるが、LIMIT / OFFSET は制約を前提に設計する
先に要点だけ押さえると、Simba Spark ODBC v2.8.0 + Databricks SQL Warehouse の組み合わせでは、次のように考えておくのが安全です。
- WHERE 句の値(検索条件)は「?」+ OdbcParameter でパラメータ化できる
- OFFSET に「?」を置くと UNBOUND_SQL_PARAMETER 等で失敗するケースが再現する
- 結果として、LIMIT / OFFSET は SQL 文字列内に数値として埋め込む(ただし C# 側で int 化・範囲検証を必ず行う)
- Dapper を使っても、根本の制約(OFFSET を ? で渡せない)は解消しない
| SQL の部位 | 例 | 「?」でのパラメータ化 | 実運用の考え方 |
|---|---|---|---|
| WHERE(値) | WHERE col = ? | 概ね OK | 基本はすべてここでパラメータ化する |
| LIMIT | LIMIT ? | 環境依存(不安定になりがち) | 固定値か、検証済みの int を埋め込むのが堅い |
| OFFSET | OFFSET ? | NG(エラーになりやすい) | 検証済みの int を埋め込む / そもそも OFFSET 依存を減らす |
| ORDER BY(列名) | ORDER BY ? | NG | 列名はホワイトリストで組み立てる(値は ?) |
前提:利用環境と「やりたいこと」を具体化する
この記事の前提となる構成は次のとおりです。
- Databricks SQL Warehouse
- Simba Spark ODBC ドライバー v2.8.0
- .NET 8 / ASP.NET Web API(
System.Data.Odbc経由で接続)
やりたいことは「検索条件(WHERE)だけでなく、ページング用の LIMIT / OFFSET もパラメータで渡して SQL インジェクションを避けたい」というものです。典型例は以下です。
SELECT *
FROM MyTable
WHERE param1 = ?
AND param2 = ?
LIMIT ? OFFSET ?;
ところが、上記のように LIMIT / OFFSET にも「?」を使うと、実行時に次のようなエラーが出ることがあります。
[UNBOUND_SQL_PARAMETER] Found the unbound parameter: _119 ...
特に OFFSET ? を含むと失敗し、OFFSET を外すと通る、という挙動がポイントです。
ODBC の「?」は“名前付き”ではなく“位置”でバインドされる
まず大事な前提として、System.Data.Odbc のパラメータは多くのケースで位置パラメータとして扱われます。つまり、OdbcParameter の名前(”param1″ など)を付けても、実際には SQL 内の「?」の登場順で紐づきます。
この性質を理解していないと、LIMIT/OFFSET の問題以前に、WHERE 条件が意図と違う値で評価される事故が起きます。
| SQL 内の「?」の順序 | 意味 | C# 側で Add する順序 |
|---|---|---|
| 1番目 | param1 | cmd.Parameters.Add(… param1 …) |
| 2番目 | param2 | cmd.Parameters.Add(… param2 …) |
| 3番目 | limit | cmd.Parameters.Add(… limit …) |
| 4番目 | offset | cmd.Parameters.Add(… offset …) |
今回のケースでは、WHERE の「?」は問題なくバインドできているのに、OFFSET 位置の「?」が原因でエラーになる、という切り分けができています。
再現パターン:OFFSET に「?」を置くと失敗しやすい
質問内容の挙動を整理すると、次のようなパターンになります。
LIMIT 100のようにリテラル指定:成功LIMIT ? OFFSET ?のように「?」で渡す:失敗(UNBOUND_SQL_PARAMETER、構文エラー等)- OFFSET を外すと同じ書き方でも成功することがある
- 文字列補間などで
LIMIT 100 OFFSET 0のように数値を直書きすると成功する
この傾向から、少なくとも Simba Spark ODBC v2.8.0 + Databricks SQL Warehouse の組み合わせでは、OFFSET の位置にパラメータマーカー(?)を置くこと自体がサポートされていない(または極めて不安定)と捉えるのが実務的です。
「なぜ?」の理由はドライバー実装や SQL 方言の都合が絡みますが、現場で重要なのは原因究明よりも安全に回避できる設計です。OFFSET はクエリプラン生成や解析段階で定数扱いになりやすく、ドライバー側でバインド変数として通しにくい、というのはよくある落とし穴です。
WHERE 句の値は「?」でパラメータ化する(基本形)
まずは成功する側の実装を、確実に押さえます。WHERE の値は「?」でパラメータ化できるので、ここは王道どおりに行きます。
using System;
using System.Data;
using System.Data.Odbc;
public sealed class Sample
{
public void Run(string connectionString, string param1value, int param2value)
{
using (var connection = new OdbcConnection(connectionString))
{
connection.Open();
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = @"
SELECT *
FROM MyTable
WHERE param1 = ?
AND param2 = ?
LIMIT 100"; // LIMIT は固定値(この形は通りやすい)
// ODBC は位置パラメータ。Add する順序が重要。
cmd.Parameters.Add(new OdbcParameter("param1", OdbcType.Text)).Value = param1value;
cmd.Parameters.Add(new OdbcParameter("param2", OdbcType.Int)).Value = param2value;
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
// 結果読み取り
}
}
}
}
}
}
ポイントは次のとおりです。
- SQL 内の「?」の登場順と、
Parameters.Addの順序を一致させる OdbcTypeはできるだけ実データ型に合わせる(Text / Int / BigInt など)- まずは LIMIT を固定して、WHERE のみパラメータ化した形で動作を確定させる
LIMIT / OFFSET を“安全に”埋め込む実装(実務で一番安定する)
OFFSET を「?」で渡せない以上、ページングを実現するにはLIMIT / OFFSET は SQL に直書きする必要があります。ただし「直書き=危険」ではありません。埋め込む値を“整数として確定”し、範囲を縛ることで、SQL インジェクションの現実的なリスクは大幅に下げられます。
実装の考え方はシンプルです。
- クライアント入力(クエリ文字列、JSON、フォーム等)を int としてパースする
- 上限・下限を固定し、範囲外は丸める(または 400 を返す)
- 最終的に得られた int 値だけを SQL に埋め込む
using System;
using System.Data.Odbc;
public sealed class PagingQuery
{
public void Run(string connectionString, string param1value, int param2value, int requestedLimit, int requestedOffset)
{
// 例:API のページング仕様として上限を決める(無制限は避ける)
int limit = Math.Clamp(requestedLimit, 1, 1000);
int offset = Math.Max(requestedOffset, 0);
// OFFSET が 0 のときは省略すると、SQL が読みやすくなりトラブルシュートも楽
string pagingClause = offset == 0
? $"LIMIT {limit}"
: $"LIMIT {limit} OFFSET {offset}";
string sql = $@"
SELECT *
FROM MyTable
WHERE param1 = ?
AND param2 = ?
{pagingClause}";
using (var connection = new OdbcConnection(connectionString))
{
connection.Open();
using (var cmd = new OdbcCommand(sql, connection))
{
// WHERE は引き続きパラメータ化
cmd.Parameters.Add(new OdbcParameter("param1", OdbcType.Text)).Value = param1value;
cmd.Parameters.Add(new OdbcParameter("param2", OdbcType.Int)).Value = param2value;
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
// ...
}
}
}
}
}
}
この方式が安定する理由は、埋め込む値が 「SQL の構文を壊せない純粋な数値」に固定されるからです。例えば、攻撃者が 0; DROP TABLE ... のような文字列を送ってきても、C# 側で int にパースできない時点で弾けます。パース後の値は「0」や「100」などの数値にしかなりません。
入力検証のルールを“仕様”として固定する
ページングは「柔軟にしたい」ほど事故が起きやすい領域です。あらかじめ仕様として制約を決めておくと、セキュリティとパフォーマンスの両方が安定します。
| 項目 | 推奨ルール例 | 理由 | 実装の方向性 |
|---|---|---|---|
| limit | 1〜1000(固定上限) | 過大な取得で Warehouse を圧迫しない | Math.Clamp で丸める/範囲外は 400 |
| offset | 0 以上(必要なら上限も) | 負数は意味がない。巨大 offset は遅くなりやすい | Math.Max/上限を決める |
| order | ホワイトリスト | ORDER BY の列名はパラメータ化できない | 許可した列だけ文字列連結 |
質問者が LightVx のような入力検証ライブラリを併用しているのも、考え方としては同じです。重要なのは「ページング用の値は“整数として確定してから” SQL に入れる」という手順を崩さないことです。
SQL インジェクション対策として「パラメータ化できない部分」をどう守るか
パラメータ化は万能ではなく、SQL の世界では「そもそもパラメータ化できないもの」があります。代表例が LIMIT/OFFSET や ORDER BY の列名です。ここを守るには、次の2系統で対策を考えるのが実務的です。
- 値(データ):可能な限り「?」でパラメータ化
- 構文(句の形):アプリ側でルール化して安全な文字列を生成
危険な例と安全な例を、はっきり分けておきます。
| パターン | 例 | 評価 | なぜ |
|---|---|---|---|
| 危険 | LIMIT {request.Query["limit"]} | NG | 文字列がそのまま SQL として解釈される |
| 安全(推奨) | int limit = Math.Clamp(parsed, 1, 1000); | OK | SQL に入るのは数値リテラルのみ |
| 危険 | ORDER BY {sort} | NG | 列名・式はパラメータ化できず注入が成立しやすい |
| 安全(推奨) | 列名ホワイトリスト+固定方向(ASC/DESC) | OK | 許可したトークンのみで構文を作れる |
ページングは ORDER BY とセットで設計する
LIMIT/OFFSET の話題になると見落としがちですが、実務ではページングが安定していること(同じページで同じ結果が返ること)がとても重要です。Databricks 側は分散実行されるため、ORDER BY がない LIMIT/OFFSETは結果順序が安定しないことがあります。
少なくとも次のルールを推奨します。
- ページングするクエリには ORDER BY を必ず付ける
- ORDER BY に使う列は、できれば一意(または複合で一意)になるようにする
SELECT *は避け、必要列のみ取得する(転送量とコストを下げる)
「OFFSET をどうしてもパラメータ化したい」場合の代替案
少なくとも本構成では OFFSET の「?」が難しいため、設計を少し変えて「OFFSET を使わない」方向に寄せるのが現実的です。ここでは代表的な2案を紹介します。
キーセットページング(Seek Method)で OFFSET を捨てる
大量データで OFFSET を大きくすると、スキップ分も処理対象になりがちで、パフォーマンスが落ちやすくなります。そこで「最後に見たキー以降を取る」方式(キーセットページング)にすると、ページングが軽くなり、OFFSET も不要になります。
SELECT col1, col2, created_at, id
FROM MyTable
WHERE param1 = ?
AND param2 = ?
AND (created_at > ? OR (created_at = ? AND id > ?))
ORDER BY created_at, id
LIMIT 100;
この方式のポイントは、境界条件が WHERE に落ちることです。つまり ODBC の「?」で素直にパラメータ化できます。アプリ側は「次ページ用に lastCreatedAt / lastId を返す」設計にするだけで済みます。
| 観点 | キーセットページング | LIMIT/OFFSET |
|---|---|---|
| 速度(大きいページ) | 速い傾向 | 遅くなりやすい |
| 実装の単純さ | 境界キーの設計が必要 | ページ番号だけで済む |
| 並び順の安定 | 安定させやすい(複合キー推奨) | ORDER BY がないと不安定 |
ROW_NUMBER() を使って「範囲指定」を WHERE に寄せる
「どうしてもページ番号(page=3 のような UI)を維持したい」場合、ROW_NUMBER() で行番号を付け、範囲を WHERE で絞る設計もあります。OFFSET を直接書かずに済み、パラメータは WHERE 側に置けます。
WITH base AS (
SELECT
col1, col2, created_at, id,
ROW_NUMBER() OVER (ORDER BY created_at, id) AS rn
FROM MyTable
WHERE param1 = ?
AND param2 = ?
)
SELECT col1, col2, created_at, id
FROM base
WHERE rn BETWEEN ? AND ?;
ただし、ROW_NUMBER() は計算コストが増える場合があるため、データ量や Warehouse サイズ次第ではキーセットのほうが有利です。
Dapper を使う場合の注意点(ODBC でも使えるが制約は残る)
Dapper は ODBC 接続でも便利に使えますが、重要なのはドライバーが OFFSET の「?」を受け付けない問題は、Dapper を挟んでも解決しないという点です。つまり方針は同じで、WHERE はパラメータ、LIMIT/OFFSET は検証した数値の埋め込みになります。
また、ODBC は位置パラメータになりやすいので、Dapper を使う場合でも「SQL 内の ? の順序」と「渡すパラメータの順序」を意識してください。実装スタイルとしては次のいずれかが堅いです。
- 確実性重視:OdbcCommand を自分で組み立てて Execute(本記事の基本形)
- Dapper 利便性重視:パラメータ順序が崩れない形(順序を固定できる仕組み)で渡す
「とにかく事故を避けたい」なら、ページング周りだけは Dapper ではなく OdbcCommand を直に書く、という折衷も現場ではよく採られます。
トラブルシュート:UNBOUND_SQL_PARAMETER を最短で切り分けるチェックリスト
同じエラーでも原因が複数あり得るため、以下の順番で切り分けると迷いません。
- SQL 内の「?」の数と、Parameters.Add の数が一致しているか
- Parameters.Add の順序が「?」の登場順と一致しているか(名前は基本当てにしない)
- まずは OFFSET を外して動作確認し、WHERE のみのパラメータ化が安定しているか
- LIMIT/OFFSET を 数値リテラルにしたら動くか(ドライバーの制約かどうかを切り分ける)
OdbcTypeを明示しているか(Text / Int / BigInt など)- ORDER BY の有無を確認(ページングの順序が意図通りか)
ログに残すときは、SQL にユーザー入力を埋め込んだ“完成形”をそのまま出すのは避け、SQL テンプレート(? を含むもの)とパラメータ一覧(値と型)を分けて記録すると安全です。LIMIT/OFFSET を埋め込む場合でも、埋め込んだ limit/offset の値は別途ログフィールドとして出すと追跡しやすくなります。
運用上のポイント:ページングは「安全」だけでなく「コスト」も設計する
Databricks SQL Warehouse は、クエリの形やデータ量によってコストとレイテンシが変わります。LIMIT/OFFSET を安全に扱うだけでなく、次の点も意識すると運用が安定します。
- limit に上限を必ず設ける(例:最大 1000)
- 巨大 offset を許さない(UI 仕様として「先頭から数十ページまで」など制約を持たせる)
- 可能ならキーセットページングへ(大規模データほど効果が出る)
- ページング対象は必ず ORDER BY し、できれば一意キーを含める
まとめ
Databricks SQL Warehouse に対して Simba Spark ODBC v2.8.0 を使い、.NET 8(System.Data.Odbc)からパラメータクエリを実装する場合、WHERE 句の値は「?」+ OdbcParameter で安全にパラメータ化できる一方で、OFFSET を「?」で渡すとエラーになりやすいという制約に直面しがちです。
現実的な落としどころは、検索条件(WHERE)はパラメータ化し、LIMIT / OFFSET は int として確定・範囲検証してから SQL 文字列に埋め込むことです。これにより、SQL インジェクションのリスクを抑えつつ、ドライバーの制約を回避できます。さらに規模が大きくなってきたら、OFFSET 依存を減らすためにキーセットページングや ROW_NUMBER() を検討すると、パフォーマンス面でも安定します。

コメント