web-dev-qa-db-ja.com

1つのテーブルから多対多の3つのテーブルに挿入する

Postgres 9.5では、簡単な例を使用しています。私がこのデータを持っているとすると:

テーブルeverything

| fruit  | country |
|--------|---------|
| Banana | USA     |
| Banana | Panama  |

これらのテーブルをどのように作成しますか?:

テーブルfruit

| fruit_id | name   |
|----------|--------|
| 1        | Banana |

テーブルcountry

| country_id | name   |
|------------|--------|
| 1          | USA    |
| 2          | Panama |

テーブル fruit_country

| fruit_id | country_id |
|----------|------------|
| 1        | 1          |
| 1        | 2          |

最初のパスとして、私は次のようなことができることを望みました:

WITH fruit_ids AS (
  INSERT INTO fruit(name) (
    SELECT (fruit) FROM everything
  )
  ON CONFLICT DO NOTHING
  RETURNING fruit_id
),
country_ids AS (
  INSERT INTO country(name) (
    SELECT(country) FROM everything
  )
    RETURNING country_id
)
INSERT INTO country_fruit(fruit_id, country_id) (
  SELECT
  fruit_id,
  (SELECT country_id FROM country_ids)
  FROM fruit_ids
);

しかし、サイコロはありません:

ERROR:  more than one row returned by a subquery used as an expression
2
Coffee

エラーメッセージの直接の原因は、括弧の乱用です。しかし、もっとあります:

データ変更CTEは、ユースケースに適したツールではありません。 [〜#〜] upsert [〜#〜]ON CONFLICT DO NOTHING)は、果物の一部がテーブルfruitにすでに存在している可能性があることを示します(またはその使用は意味がありません)。したがって、一部の果物はRETURNINGで返されるセットに含まれていない可能性があります。

代わりに、単純な個別のINSERTコマンドを使用してください。操作全体をアトミック(すべてまたはなし)にする必要がある場合は、それをトランザクションにラップします。

また、 name は列に適した名前ではありません。代わりに、果物にはfruitを、国にはcountryを使用します。名前の競合を回避し、結合条件に(完全にオプションの)USING句を許可することで、クエリを少し単純化します。

テーブルは次のようになります:(それが問題の一部だった場合でも?)

CREATE TABLE fruit (
   fruit_id serial PRIMARY KEY
 , fruit    text UNIQUE
);

CREATE TABLE country (
   country_id serial PRIMARY KEY
 , country    text UNIQUE
);

CREATE TABLE fruit_country (
   fruit_id   int REFERENCES fruit
 , country_id int REFERENCES country
 , PRIMARY KEY (fruit_id, country_id)
);

詳細な手順:

INSERTからeverythingデータへ:

BEGIN;

INSERT INTO fruit (fruit)
SELECT DISTINCT fruit              -- must be distinct!
FROM   everything
ON     CONFLICT DO NOTHING;

INSERT INTO country (country)
SELECT DISTINCT country            -- must be distinct!
FROM   everything
ON     CONFLICT DO NOTHING;        -- UPSERT here as well

INSERT INTO fruit_country(fruit_id, country_id)
SELECT f.fruit_id, c.country_id
FROM   everything
JOIN   fruit   f USING (fruit)     -- matching row guaranteed now
JOIN   country c USING (country);  -- matching row guaranteed now

COMMIT;
3