Databricks Simba Spark ODBCで.NET 8パラメータクエリを実装する方法:LIMIT/OFFSETの制約と安全なページング対策

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基本はすべてここでパラメータ化する
LIMITLIMIT ?環境依存(不安定になりがち)固定値か、検証済みの int を埋め込むのが堅い
OFFSETOFFSET ?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番目param1cmd.Parameters.Add(… param1 …)
2番目param2cmd.Parameters.Add(… param2 …)
3番目limitcmd.Parameters.Add(… limit …)
4番目offsetcmd.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」などの数値にしかなりません。

入力検証のルールを“仕様”として固定する

ページングは「柔軟にしたい」ほど事故が起きやすい領域です。あらかじめ仕様として制約を決めておくと、セキュリティとパフォーマンスの両方が安定します。

項目推奨ルール例理由実装の方向性
limit1〜1000(固定上限)過大な取得で Warehouse を圧迫しないMath.Clamp で丸める/範囲外は 400
offset0 以上(必要なら上限も)負数は意味がない。巨大 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);OKSQL に入るのは数値リテラルのみ
危険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() を検討すると、パフォーマンス面でも安定します。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次