web-dev-qa-db-ja.com

CASE .. ENDをORDER BYに含めることは意味がありますか?

SELECT * FROM t ORDER BY case when _parameter='a' then column_a end, case when _parameter='b' then column_b endのようなクエリは可能ですが、これは良い方法ですか?

クエリのWHERE部分でparametersを使用し、SELECT部分​​にいくつかの計算列を含めることは一般的ですが、ORDERをパラメーター化することは一般的ではありませんBY句。

中古車をリストするアプリケーションがあるとします(àCraigsList)。車のリストは、価格または色で並べ替えることができます。特定の量のパラメーター(たとえば、価格範囲、色、並べ替え基準など)を指定すると、結果を含むレコードのセットを返す関数が1つあります。

具体的に説明するために、carsがすべて次の表にあるとします。

CREATE TABLE cars
(
  car_id serial NOT NULL PRIMARY KEY,  /* arbitrary anonymous key */
  make text NOT NULL,       /* unnormalized, for the sake of simplicity */
  model text NOT NULL,      /* unnormalized, for the sake of simplicity */
  year integer,             /* may be null, meaning unknown */
  euro_price numeric(12,2), /* may be null, meaning seller did not disclose */
  colour text               /* may be null, meaning unknown */
) ;

テーブルにはほとんどの列のインデックスがあります...

CREATE INDEX cars_colour_idx
  ON cars (colour);
CREATE INDEX cars_price_idx
  ON cars (price);
/* etc. */

そしていくつかの商品の列挙があります:

CREATE TYPE car_sorting_criteria AS ENUM
   ('price',
    'colour');

...そしていくつかのサンプルデータ

INSERT INTO cars.cars (make, model, year, euro_price, colour)

VALUES 
    ('Ford',   'Mondeo',   1990,  2000.00, 'green'),
    ('Audi',   'A3',       2005,  2500.00, 'golden Magenta'),
    ('Seat',   'Ibiza',    2012, 12500.00, 'dark blue'),
    ('Fiat',   'Punto',    2014,     NULL, 'yellow'),
    ('Fiat',   '500',      2010,  7500.00, 'blueish'),
    ('Toyota', 'Avensis',  NULL,  9500.00, 'brown'), 
    ('Lexus',  'CT200h',   2012, 12500.00, 'dark whitish'), 
    ('Lexus',  'NX300h',   2013, 22500.00, NULL) ;

これから行うクエリの種類は次のとおりです。

SELECT
    make, model, year, euro_price, colour
FROM
    cars.cars
WHERE
    euro_price between 7500 and 9500 
ORDER BY
    colour ;

このスタイルのクエリを関数に含めたいと思います:

CREATE or REPLACE FUNCTION get_car_list
   (IN _colour    text, 
    IN _min_price numeric, 
    IN _max_price numeric, 
    IN _sorting_criterium car_sorting_criteria) 
RETURNS record AS
$BODY$
      SELECT
          make, model, year, euro_price, colour
      FROM
          cars
      WHERE
           euro_price between _min_price and _max_price
           AND colour = _colour
      ORDER BY
          CASE WHEN _sorting_criterium = 'colour' THEN
            colour
          END,
          CASE WHEN _sorting_criterium = 'price' THEN
            euro_price
          END 
$BODY$
LANGUAGE SQL ;

このアプローチの代わりに、この関数のSQLは(PL/pgSQLで)文字列として動的に生成され、実行されます。

どちらのアプローチでも、いくつかの制限、長所、短所を感じることができます。

  1. 関数内で、特定のステートメントのクエリプランを見つけるのは困難です(可能な場合)。しかし、何かを十分に頻繁に使用しようとするときは、主に関数を使用する傾向があります。
  2. 静的SQLのエラーは、関数がコンパイルされたとき、または最初に呼び出されたときに(ほとんど)キャッチされます。
  3. 動的SQLのエラーは、関数がコンパイルされ、すべての実行パスがチェックされた後でのみキャッチされます(つまり、関数に対して実行するテストの数が非常に多くなる可能性があります)。
  4. 公開されているようなパラメトリッククエリは、動的に生成されたクエリよりも効率が悪いでしょう。それでも、エグゼキュータは、解析/クエリツリーの作成/毎回の決定がより困難になります(逆方向の効率に影響する可能性があります)。

質問:

「両方の世界を最大限に活用する」方法(可能な場合)? [効率+コンパイラチェック+デバッグが簡単+最適化が簡単]

注:これはPostgreSQL 9.6で実行されます。

5
joanolo

一般的な答え

まず、前提のあいまいさについて説明します。

クエリのWHERE部分でパラメーターを使用し、SELECT部分​​にいくつかの計算列を含めることは一般的ですが、ORDER BY句をパラメーター化することは一般的ではありません。

SELECT部分の計算された列は、クエリプランやパフォーマンスにほとんど関係がありません。しかし、"WHERE部分]はあいまいです。

準備されたステートメントで機能するWHERE句でvaluesをパラメーター化するのが一般的です。 (そしてPL/pgSQLは内部的に準備されたステートメントで動作します。)一般的なクエリプランは、提供されたvaluesに関係なく、多くの場合意味があります。つまり、テーブルに非常に不均一なデータ分布がない限り、ただしPostgres 9.2以降、PL/pgSQLはクエリを数回再計画して、一般的な計画が十分であるかどうかをテストします。

しかし、全体をパラメータ化することは一般的ではありません述語識別子を含むWHERE句で使用します。これは、準備されたステートメントでは最初から不可能です。 EXECUTEを使用した動的SQLが必要であるか、クライアントでクエリ文字列を組み立てます。

動的ORDER BY式は、両方の中間にあります。あなたはcanCASE式で実行しますが、一般的に最適化するのは非常に困難です。 PostgresはプレーンなORDER BYのインデックスを使用する場合がありますが、最終的な並べ替え順序を隠すCASE式では使用しません。プランナーは賢いですが、AIではありません。クエリの残りの部分に応じて(ORDER BYはプランに関連するかどうかにかかわらず、例では関連があります)、最終的にsub-常に最適なクエリプラン
さらに、CASE式のマイナーコストを追加します。また、特定の例ではmultiple役に立たないORDER BY列も含まれます。

通常、EXECUTEを使用した動的SQLの方が高速または高速です。

関数本体で明確で読み取り可能なコード形式を維持する場合、保守性は問題になりません。

デモ機能を修正

問題の関数はbrokenです。戻り値の型は、匿名のレコードを返すように定義されています。

RETURNS record AS

しかし、クエリは実際にはset ofレコードを返すため、次のようになります。

RETURNS SETOF record AS

しかし、それでもまだ役に立ちません。すべての呼び出しで列定義リストを提供する必要があります。クエリは、既知のタイプの列を返します。それに応じて戻り値の型を宣言してください!私はここで推測している、返された列/式の実際のデータ型を使用します:

RETURNS TABLE (make text, model text, year int, euro_price int, colour text) AS

便宜上、同じ列名を使用しています。 RETURNS TABLE句の列は、実質的にOUTパラメータであり、本文のすべてのSQLステートメントで表示されます(EXECUTE内では表示されません)。したがって、名前の競合の可能性を回避するために、関数本体のクエリで列をテーブル修飾します。デモ関数は次のように機能します。

CREATE or REPLACE FUNCTION get_car_list (
    _colour            text, 
    _min_price         numeric, 
    _max_price         numeric, 
    _sorting_criterium car_sorting_criteria) 
  RETURNS TABLE (make text, model text, year int, euro_price numeric, colour text) AS
$func$
      SELECT c.make, c.model, c.year, c.euro_price, c.colour
      FROM   cars c
      WHERE  c.euro_price BETWEEN _min_price AND _max_price
      AND    c.colour = _colour
      ORDER  BY CASE WHEN _sorting_criterium = 'colour' THEN c.colour     END
              , CASE WHEN _sorting_criterium = 'price'  THEN c.euro_price END;
$func$  LANGUAGE sql;

関数宣言のRETURNSキーワードをplpgsql RETURNコマンドと混同しないでください Evanは彼の答えで行いました 。詳細:

クエリ例の一般的な難しさ

some列の述語(さらに悪いことにはrange述語)、_otherORDER BYの列、これはすでに最適化が困難です。しかし コメントで言及した

実際の結果セットは、約1.000行程度になる可能性があります(したがって、サーバー側の小さなチャンクでソートおよびページ付けされます)

したがって、これらのクエリにLIMITOFFSETを追加して、最初にn「最良の」一致を返します。または、よりスマートなページネーション手法:

あなた必要これを高速にするための一致するインデックス。これがORDER BYCASE式でどのように機能するかはわかりません。

考慮してください:

3

私が提起する3つのポイント、

  1. これは、適用されたバージョンでも、非常に基本的なクエリです。 VIEWを作成します。ユーザーにWHEREを使用してVIEW条件をカスタマイズしてもらいます。関数は、クエリプランナーにとってブラックボックスです。他の関数の中でそれらを使用するのは恐ろしく、インライン化されるのはSQLだけです。また、動的関数はキャッシュされたプランを取得しません。
  2. 引き続きplpgsqlを使用する場合は、RETURNS QUERYではなくRETURNS QUERY EXECUTE(またはRETURNS SETOF)を使用してください。ソートでRETURNS SETOFを使用する理由はありません。とにかくそれは緩衝されなければならない、アファイク。結果セットがwork_memより大きい場合、いずれかで問題が発生します。
  3. あなたのアプリが何で書かれているのかわかりません。私はウェブを想定しています。私は10年間自動車業界にいましたが、CraigslistやCraigslist用の投稿ツールのようなものをたくさん作りました「データベース内のユーザーのデータを並べ替えないでください」が一般的な経験則です。その理由はありません。それをJSONにドロップして、ブラウザーで処理できるようにします。 1000行を超える行を表示している場合を除いて、考える価値はありません。携帯電話塔からの往復時間を考慮してください。 thisの問題について不思議に思うことはありません。

今後は、完全に任意の順序を処理する PostgREST のようなサービスをラップすることも検討します。

If you care where nulls are sorted, add nullsfirst or nullslast:

GET /people?order=age.nullsfirst
GET /people?order=age.desc.nullslast
2
Evan Carroll

この場合、動的SQLを使用します。
動的クエリはそれほど複雑ではなく、より優れた実行計画をもたらす可能性があります。
私の経験から、プランのキャッシュまたは解析時間の短縮による利益は、その隣では無視できます。

私は「静的SQLが機能しない場合にのみ動的SQLを使用する」というルールに従っていました。今日の経験則として、私は通常、次の点で柔軟性が必要なSQLクエリの句に従ってそれを選択します。

  • WHERE-静的SQL
  • FROM、ORDER BY、GROUP BY-動的SQL
  • SELECT-代替

確かに、デバッグは少し難しいですが、いくつかの優れたプラクティスを使用すると、ギャップを減らすのに役立ちます。
たとえば、plpgsqlでは、$表記+ REPLACE + RAISE NOTICEを使用できます。

-- Write the entire query using $ for replacements.
-- Don't use || operator.
-- This makes the dynamic query clearer to read and easier to maintain and debug.
v_sql:= 'SELECT <some columns> FROM tbl ORDER BY $sorting$';

v_sql:= REPLACE(v_sql,
  '$sorting$',
  CASE condition
    WHEN value1 THEN 'colX'
    WHEN value2 THEN 'colY'
  END);

-- Debug the query when running
RAISE NOTICE '%', v_sql;
RETURN QUERY EXECUTE v_sql;  

最初の実行で構文エラーをトラップできるはずです。
論理SQLは、静的SQLの場合と同じように簡単/簡単に見つけることができます。

1
bentaly

説明されているケースで関数の順序にケースがあることは意味がありません

インデックスは、order by句ではそのように使用されません。データベースエンジンは、各行のケース式を計算してからソートする必要があります。また、この手法は動的な結合とフィルタに拡張できません。

私は動的SQL生成で行きます...

PostgreSQL関数での動的SQLの生成

あなたはこのようなことをすることができます:

  create or replace function fn_test(car_sort_option) returns text as $$
  declare
      xsql text;
      xresult text;
  begin

    xsql = 'select array_to_json(array_agg(c)) from cars';

    if $1 = 'colour' then 
      xsql = xsql || ' order by colour';
    elsif $1 = 'model' then  
      xsql = xsql || ' order by model';
    else 
      raise exception 'invalid parameter: sort option=% is invalid', $1; 
    end if;

    execute xsql into xresult;

    return xresult;
  end

  $$ language 'plpgsql';

私は通常、PL/pgSQL内のプレゼンテーション層に固有の動的SQLを作成しません。私は通常、このSQLビルドをPHPまたはJavaに残しておきます。しかし、他の多くのことについては、PL/pgSQL内で多くの動的SQLを実行します。主にパーティション化、データベースメンテナンス、ワークフローのために実装とデータ整合性制御。

このポリシーにより、コードがよりクリーンになり、インデックスの使用が改善され、より複雑な動的SQLで使用できることがわかりました。インデックスの使用は私の世界では非常に重要です。なぜなら、私は数十億のレコードを持つデータベースで分析を行っており、高速な応答が必要だからです。

データベースの外部で動的SQLを生成する方が一般的である理由に関する追加の注釈(以下のコメント)

これは、ほとんどの動的SQLが必要なのは、テーブル名または列名が動的である場合のみであるためです。それ以外の場合はすべて、パラメータ化されたクエリで問題ありません。ほとんどのレポートには動的SQLが必要です。並べ替え順序の列を選択する必要があるだけでなく、ほとんどの場合、フィルターと列を動的に含める必要があります。多くの開発者は、プレゼンテーション層のレポートプログラムまたはSQLを生成するバッチでSQLを報告し続けます。その場合、動的SQLもデータベースの外部で生成されます。

1
Lucas