web-dev-qa-db-ja.com

ネストされたCTEを使用した条件付きINSERT?

この特定のケースでネストされたCTEを機能させる方法があるかどうかを確認しようとしています。

実際のアプリケーションに基づいた次の(高度に考案された)シナリオを考えてみます。従業員IDの単一の列テーブルがあります。次に、すべての詳細が記載された従業員プロパティテーブルがあります。 (単一のcolテーブルの背後にある主な理由は、多くの場合、実際のスタッフの詳細がわかる前に、新しい従業員IDをロットで作成して割り当てる必要があることが多いためです。)

今度は手元のタスクに、新しい従業員の詳細(つまり名前)を挿入しますが、最初にその名前の従業員が既に存在するかどうかを確認する必要があります。含まれている場合は、単にIDを返します。含まれていない場合は、新しい従業員レコードを作成して詳細を挿入し、最終的に新しく作成されたIDを返します。

このテストシナリオを再作成するには:

_CREATE TABLE public.employee (
    id text DEFAULT gen_random_uuid(),
    PRIMARY KEY (id)
);

CREATE TABLE public.employee_details (
    employee_id text,
    name text,
    PRIMARY KEY (employee_id),
    FOREIGN KEY (employee_id) REFERENCES public.employee(id)
);
_

私が形にしようとしているクエリは、次のようになります。

_with 
e as 
    (select name, employee_id from employee_details where name = 'jack bauer'), 

i as (insert into employee_details (name, employee_id) 
    select 'jack bauer', 
        (with a as (insert into employee values(default) RETURNING id) select a.id from a)
    where not exists (select 1 from e) returning name, employee_id) 

select employee_id, name from e
union all 
select employee_id, name from i; 
_

ネストされたCTEを既に作成されたIDで置き換えると(ネストされたCTEを個別に実行)、機能します(ただし、余分なIDが作成される可能性があります)。ネストされたCTEを最上位に移動することも可能です(全体がwith e as (..), i as (..), a as (..) select .. where not exists...のようになりますが、これは、詳細を挿入する必要がない余分な従業員IDが作成されることも意味します。 「インライン」で実行する方法を見つけたいので、_not exists_句がtrueを返した場合にのみ新しいIDが作成されます。

エラーが発生し続けます:

データ変更ステートメントを含むWITH句は、最上位にある必要があります。

問題は、ネストされたCTEが「列」を返すのに対し、代わりに「値」を取得した場合にクエリ全体が機能することです(CTEの代わりに単にテキスト値をコピーする場合)。 この質問でやや関連のある議論 9.3以降修正された明らかなバグについて言及しました。これが私のトラブルに関連しているかどうかはわかりません。リンクされたディスカッションから引用するには:

解析コードは、WITHは集合演算ツリー内のトップレベルまたはリーフレベルのSELECTにのみアタッチできると考えているようです。しかし文法はそのようなことはないと言っているSQL標準に従います

Postgres 10.3を使用しています。

5
Yogesch

この質問では、_employee_details.name_をUNIQUEと定義するとします。そうでなければ、操作全体が意味をなさないでしょう。

試したようにデータ変更CTEをネストすることはできません(すでに難しい方法がわかっているため)。また、その必要はありません。このクエリはあなたの目的を達成します:

_WITH e AS (
   SELECT name, employee_id
   FROM   employee_details
   WHERE  name = 'jack bauer'
   )
 , i1 AS (
   INSERT INTO employee             -- no target columns!
   SELECT                           -- empty SELECT list!
   WHERE NOT EXISTS (SELECT FROM e)
   RETURNING id
   )
 , i2 AS (
   INSERT INTO employee_details (name, employee_id) 
   SELECT 'jack bauer', id
   FROM   i1
   RETURNING name, employee_id
   )
SELECT employee_id, name FROM e
UNION ALL 
SELECT employee_id, name FROM i2;
_

コア機能は、ターゲット列がなく空のINSERTがあるSELECTです。 PostgresはSELECTにリストされていないすべての列をデフォルト値で埋めます。このようにして、無条件のVALUES (default)conditional INSERTに置き換えることができます。 CTE _i1_は、指定された名前が見つからなかった場合にのみ行を挿入します。

マニュアル:

[target]列名のリストがまったく指定されていない場合、デフォルトは、宣言された順序でのテーブルのすべての列です。 [...]

明示的または暗黙的な列リストに存在しない各列には、宣言されたデフォルト値またはデフォルト値がない場合はnullのいずれかのデフォルト値が入力されます。

これは、標準のPostgres固有の拡張です。

また、列名リストが省略されているが、すべての列がVALUES句またはqueryから入力されていない場合は、標準では許可されていません。

最後のCTE _i2_は、_i1_が行を返した場合にのみ行を挿入します。ボイラ。

これはであり、同じテーブルへの同時書き込み負荷での競合状態の影響を受けます。それを除外する必要がある場合は、さらに行う必要があります。関連:

2番目の表の条件付きINSERTによる複雑化がなければ、これはSELECTまたはINSERTの一般的なケースに要約されます。

さておき

_"id" text DEFAULT gen_random_uuid()
_

UUIDを格納するには、データタイプuuidを使用することを強くお勧めします。

7