web-dev-qa-db-ja.com

MS SQL Serverの動的where句

問題

SQL Serverデータベースには、次のようなビューが含まれています。

+-------------+-----------+------+-------+------+------+
| CAR_ID      | MAKE      |COLOR | Miles | Seats| MODEL|
+-------------+-----------+------+-------+------+------+
| 1           | FORD      | RED  | 100   | 4    | CAR  |
| 2           | TOYOTA    | RED  | 500   | 2    | TRK  |
| 3           | FORD      | BLACK| 10    | 4    | CAR  |
| 4           | HYUNDAI   | BLUE | 150   | 4    | VAN  |
| ...         | ...       | ...  | ...   | ...  | ...  |
| 1000        | TOYOTA    | GRN  | 200   | 8    | CAN  |
+-------------+-----------+------+-------+------+------+

このテーブルからカスタムグループを作成します。私の望ましい出力は次のようになります-

+-------------+-----------+
| Group       | sum(Miles)|
+-------------+-----------+
| Red Cars    | 500       |
| Red Toyota  | 325       |
| Trucks      | 563       | 
| Black Car   | 952       |
| ...         | ...       |
| Black Ford  | 100       |
+-------------+-----------+

実際、これは単純な「GROUP BY」クエリのように見えます。ただし、私の場合、グループは多くの論理ステートメントでかなり複雑になる可能性があり、グループの数も非常に多いため、「group by」クエリを使用するのが難しくなります。私の解決策は、WHERE句を使用してカスタムグループを作成することです。このダイナミックSQLを呼び出す人もいます。

私の解決策

グループ定義とクエリ自体を保存するために使用するC#スクリプトを作成しました。グループ定義は辞書に保存されます。辞書を反復処理し、各グループをselectステートメントのWHERE句に渡してから、SQLステートメントをSQLサーバーに送信します。

サーバーは、カスタムグループを含むDatatableをC#アプリケーションに返します。次に、SQLサーバー上の別のテーブルにデータテーブルを一括挿入します。 50個のグループがある場合、SQLサーバーに50回クエリを実行し、50回一括挿入します。次に例を示します。

Dictonary x = {GRP1, (COLOR = BLACK or Miles <50),
               GRP2, (Seats <5 AND MODEL = CAR)};

FOREACH(key in x)
{
  y = "SELECT * FROM CAR_TABLE WHERE " + x[key]
  Datatable dt = QueryServer(y);
  BulkInsertToDB(dt);
}

質問

これを行うには、より優れた、より効率的な方法が必要です。サーバー上のストアドプロシージャにクエリをスローすることを考えていましたが、それでもグループ化ごとにストアドプロシージャを呼び出す必要がありました。次に、これらのグループ化サーバーサイズを格納し、すべてのクエリをストアドプロシージャで同時に実行する方法があると考えました。どんなガイダンスも役に立ちます。

明確化:

1つの行は多くのグループに属することができます。マイルの合計と車両の年齢の合計があります。辞書のキーをグループ名として使用し、SQLサーバーにキーを渡します。グループ名でc#のデータテーブルを更新します。

3
Tyler

純粋にSQLでこれを行う方法はいくつかあります。論理的には十分ですが、動的SQLも必要です。

まず、辞書をSQL Serverのテーブルにコピーします。これをGroup_Defと呼び、列Group_NameWhere_Clauseを付けます。

オプション1: SQLでロジックを複製します。グループごとに、グループ名と、条件に一致するすべての車のCAR_IDを一時テーブルに追加する一連のSQLステートメント(変数内)を生成します。変数内のSQLステートメントを実行して、一時テーブルにデータを入力します。次に、その一時テーブルをCAR_TABLECAR_IDに結合し、Group_Nameでグループ化して、集計を適用します。

オプション2:移動しながら集計値を取得します。今回は、一致するすべてのCAR_ID値を一時テーブルに書き込む代わりに、実際の集計値を書き込みます。次に、一時テーブルから選択します。

コードが表示されれば、簡単に追跡できます。

/*
 1           | FORD      | RED  | 100   | 4    | CAR  |
| 2           | TOYOTA    | RED  | 500   | 2    | TRK  |
| 3           | FORD      | BLACK| 10    | 4    | CAR  |
| 4           | HYUNDAI   | BLUE | 150   | 4    | VAN  |
| ...         | ...       | ...  | ...   | ...  | ...  |
| 1000        | TOYOTA    | GRN  | 200   | 8    | CAN  |
*/

IF (OBJECT_ID('tempdb..#CAR_TABLE') IS NOT NULL) DROP TABLE #CAR_TABLE;
CREATE TABLE #CAR_TABLE
 ( CAR_ID int IDENTITY(1,1)
  ,Make   varchar(50)
  ,Color  varchar(50)
  ,Miles  int
  ,Seats  int
  ,Model  varchar(50)
);

INSERT INTO #CAR_TABLE (Make, Color, Miles, Seats, Model)
VALUES ('FORD', 'RED', 100, 4, 'CAR')
      ,('TOYOTA', 'RED', 500, 2, 'TRK')
      ,('FORD', 'BLACK', 10, 4, 'CAR')
      ,('HYUNDAI', 'BLUE', 150, 4, 'VAN')
      ,('TOYOTA', 'GRN', 200, 8, 'CAN')
;


/*
Dictonary x = {GRP1, (COLOR = BLACK or Miles <50),
               GRP2, (Seats <5 AND MODEL = CAR)};

*/

IF (OBJECT_ID('tempdb..#Group_Def') IS NOT NULL) DROP TABLE #Group_Def;
CREATE TABLE #Group_Def (Group_Name nvarchar(128), Where_Clause nvarchar(2000));

INSERT INTO #Group_Def
VALUES (N'GRP1', N'(Color = ''BLACK'' OR Miles < 50)')
      ,(N'GRP2', N'(Seats < 5 AND Model = ''CAR'')')
      ,(N'Red Cars', N'(Color = ''RED'' AND Model = ''CAR'')')
      ,(N'Red Toyotas', N'(color = ''RED'' AND Make = ''TOYOTA'')')
      ,(N'All', N'(1 = 1)')
;


--OPTION 1

IF (OBJECT_ID('tempdb..#cars_by_group') IS NOT NULL) DROP TABLE #cars_by_group;
CREATE TABLE #cars_by_group (Group_Name nvarchar(128), CAR_ID int);

DECLARE @sqlcmd nvarchar(MAX); 

SELECT @sqlcmd = N'INSERT INTO #cars_by_group
SELECT NULL, NULL FROM #Group_Def WHERE 1 = 0
';

SELECT @sqlcmd = @sqlcmd + N'UNION ALL
SELECT N''' + Group_Name + N''', CAR_ID FROM #CAR_TABLE WHERE ' + Where_Clause + N'
'
  FROM #Group_Def
;

EXECUTE sp_executesql @sqlcmd;

SELECT t.Group_Name
      ,SUM(c.Miles) as sum_miles
  FROM #cars_by_group t INNER JOIN #CAR_TABLE c ON (t.CAR_ID = c.CAR_ID)
 GROUP BY t.Group_Name
;




--OPTION 2:

IF (OBJECT_ID('tempdb..#group_totals') IS NOT NULL) DROP TABLE #group_totals;
CREATE TABLE #group_totals (Group_Name nvarchar(128), sum_miles int);

DECLARE @sqlcmd2 nvarchar(MAX); 

SELECT @sqlcmd2 = N'INSERT INTO #group_totals
SELECT NULL, NULL FROM #Group_Def WHERE 1 = 0
';

SELECT @sqlcmd2 = @sqlcmd2 + N'UNION ALL
SELECT N''' + Group_Name + N''', SUM(Miles) FROM #CAR_TABLE WHERE ' + Where_Clause + N'
'
  FROM #Group_Def
;

EXECUTE sp_executesql @sqlcmd2;

SELECT *
  FROM #group_totals
;

オプション2は、最後のSELECTで集計を計算する必要がないため、少し高速になる場合があります。ただし、@sqlcmdを単一のSQLステートメントとして作成した場合でも、UNION ALLは機能的に別々のクエリを結合しています。パフォーマンスに関しては、#Group_Defの各行が個別のINSERTステートメントを生成し、パフォーマンスが大幅に変化しないようにこれを書き換えることができます。基本的に、グループごとに1つのステートメントを実行します。

1つのステップですべての集計を収集する3番目のオプションを想像できます。ただし、それほどスケーラブルではなく、すべての集計を合計またはカウントで表すことができる必要があり、最終的には各グループにSELECTが必要です。

単一のSELECTステートメントを作成し、グループごとに記録する値ごとに列を作成します。スペースなしのバージョンのグループ名を追加する必要があります。集計を収集するには、SUMおよびCASEステートメントを使用します。例えば:

SELECT SUM(CASE WHEN (Color = 'RED' and Make = 'TOYOTA' THEN 1 ELSE 0 END) as red_toyotas_count
      ,SUM(CASE WHEN (Color = 'RED' and Make = 'TOYOTA' THEN Miles ELSE 0 END) as red_toyotas_sum_miles
...
  INTO #single_row_total
  FROM CAR_TABLE

GROUP BYは必要ありません-すべての出力列は集約されます)。

次に、SELECTステートメントを作成して、必要なすべての値を#single_row_totalテーブルから引き出します(動的SQLの場合も、Group_Defテーブルを経由してステートメントを作成します)。 UNION ALLを使用してそれらを結合する:

SELECT 'Red Toyotas' as [Group], red_toyotas_sum_miles as sum_miles, red_toyotas_sum_miles * 1.00 / red_toyotas_count as avg_miles FROM #single_row_total
UNION ALL
SELECT 'Red Cars' as [Group], red_cars_sum_miles as sum_miles, red_cars_sum_miles * 1.00 / red_cars_count as avg_miles FROM #single_row_total
UNION ALL
...

これにより、すべてのアグリゲートを1つのセットベースの操作で収集できます。前述のように、単一の結果を別々の行に分割するには、グループごとに1つのSELECTが必要です。ただし、これらの選択は単一行にあるため、非常に高速です。

1つのテーブルに含めることができる列の数に上限があるため、これはスケーラブルではありません。十分な数のグループに、グループごとに必要な十分な数の集計を掛けると、必要な列が多すぎる可能性があります。

また、集計をプルするクエリは単一のSQLクエリですが、各行を評価してCASEステートメントの結果を判断するプロセスにより、実行プランが非常にひどくなり、1つのSELECTグループごとのオプションは、同等以上のパフォーマンスを発揮します。

したがって、私はオプション3の作成(およびより高速なオプションを決定するためのはるかに大きなデータセットでのテスト)をOPまたは他の関係者に任せています。

この動的SQLの側面については、グループをその場で定義できない限り、ユーザー指定の値を使用せずにSQLステートメントを構築しているため、SQLインジェクションのリスクはありません。そして(私が思うに)#Group_Defが変更されない場合、生成された計画は再利用可能になる可能性があります(わからない、チェックを計画していない、元の質問の一部ではなかった)。

2
RDFozz

GROUP BY句では、セット、ロールアップ、キューブをグループ化できます。 こちら を参照してください。これらの中で、CUBEが最も役立つようです。静かな時間にCUBEクエリを実行し、スキップされた列にNULLを含むテーブルに結果を書き込むことができます。フィルター処理されたインデックスを使って自由にインデックスを作成し、読み取りパフォーマンスはおそらく十分でしょう。

0
Michael Green