Postgres:存在しない場合はINSERT


361

Pythonを使用してpostgresデータベースに書き込みます。

sql_string = "INSERT INTO hundred (name,name_slug,status) VALUES ("
sql_string += hundred + ", '" + hundred_slug + "', " + status + ");"
cursor.execute(sql_string)

しかし、一部の行が同一であるため、次のエラーが発生します。

psycopg2.IntegrityError: duplicate key value  
  violates unique constraint "hundred_pkey"

「この行が既に存在しない限り、INSERT」SQLステートメントをどのように書くことができますか?

私はこれが推奨されるような複雑なステートメントを見てきました:

IF EXISTS (SELECT * FROM invoices WHERE invoiceid = '12345')
UPDATE invoices SET billed = 'TRUE' WHERE invoiceid = '12345'
ELSE
INSERT INTO invoices (invoiceid, billed) VALUES ('12345', 'TRUE')
END IF

しかし、最初に、これは私が必要とするものに対して過剰であり、次に、これらの1つを単純な文字列としてどのように実行できますか?


56
この問題の解決方法に関係なく、そのようなクエリを生成するべきではありません。クエリでパラメーターを使用し、値を個別に渡します。stackoverflow.com/questions/902408/…を
Thomas Wouters、2011年

3
例外をキャッチして無視しないのはなぜですか?
マシュー・ミッチェル

5
Posgres 9.5の通り(現在はベータ2に)機能のような新しいアップサートがあり、以下を参照してください。postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
エセキエル・モレノ

2
これに対する回答を受け入れることを検討しましたか?=]
2015

回答:


513

Postgres 9.5(2016-01-07以降にリリース)は"upsert"コマンドを提供しています。これはINSERTのON CONFLICT句としても知られています:

INSERT ... ON CONFLICT DO NOTHING/UPDATE

他のいくつかの回答が提案する、並行操作を使用するときに遭遇する可能性のある微妙な問題の多くを解決します。


14
9.5がリリースされました。
ラッキードナルド2016年

2
PostgreSQL 9.5より前の@TusharJainでは、「旧式」のUPSERT(CTEを使用)を実行できますが、競合状態で問題が発生する可能性があり、9.5スタイルではパフォーマンスが低下します。詳細については、このブログ(更新された下部の領域)にアップサートに関する詳細情報があり、リンクが含まれています。
スカイガード

17
必要な場合のために、2つの簡単な例を示します。- (1)INSERTは何も存在しない場合はINSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;-他UPDATEが存在しない場合(2)を挿入するINSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;-これらの例は、マニュアルからのものであるpostgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan

13
注意点/副作用が1つあります。シーケンス列(serialまたはbigserial)を持つテーブルでは、行が挿入されていなくても、挿入を試行するたびにシーケンスが増分されます。
Grzegorz Luczywo 2017

2
リリースを指すのではなく、INSERTドキュメントにリンクする方がよいでしょう。ドキュメントリンク:postgresql.org/docs/9.5/static/sql-insert.html
borjagvo 2017年

379

「この行が既に存在しない限り、INSERT」SQLステートメントをどのように書くことができますか?

PostgreSQLで条件付きINSERTを実行する良い方法があります。

INSERT INTO example_table
    (id, name)
SELECT 1, 'John'
WHERE
    NOT EXISTS (
        SELECT id FROM example_table WHERE id = 1
    );

ただし、このアプローチは、同時書き込み操作に対して100%信頼できるわけではありません。非常に小さな競合状態は間があるSELECTNOT EXISTS抗半結合およびINSERTそのもの。このような状況で失敗する可能性があります。


これは、「name」フィールドにUNIQUE制約があると想定するとどの程度安全ですか?一意の違反で失敗することはありますか?
agnsaft 2012

2
これは正常に動作します。唯一の問題は、私が推測するカップリングです。より多くの列が一意になるようにテーブルを変更するとどうなるでしょうか。その場合、すべてのスクリプトを変更する必要があります。これを行うためのより一般的な方法があるとよいでしょう...
Willem Van Onsem 14

1
が挿入されているかどうかRETURNS idを取得するなどのためにそれを使用することは可能idですか?
Olivier Pons 2016年

2
@OlivierPonsはい、それは可能です。RETURNING idクエリのandに追加すると、行が挿入されていない場合、新しい行IDが返されるか、何も返されません。
AlexM 2016

4
これは信頼できないことがわかりました。Postgresはselectを実行する前に挿入を実行することがあり、レコードがまだ挿入されていなくても重複キー違反が発生するようです。ON CONFLICTでバージョン=> 9.5を使用してみてください。
マイケルシルバー

51

1つのアプローチは、すべてのデータを挿入する非制約(一意のインデックスなし)テーブルを作成し、それとは異なる選択を行って100のテーブルに挿入することです。

だから高レベルでしょう。私の例では3つの列がすべて異なると想定しているため、ステップ3で、NOT EXITS結合を100のテーブルの一意の列でのみ結合するように変更します。

  1. 一時テーブルを作成します。こちらのドキュメントをご覧ください

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. 一時テーブルにデータを挿入します。

    INSERT INTO temp_data(name, name_slug, status); 
  3. 一時テーブルにインデックスを追加します。

  4. メインテーブルを挿入します。

    INSERT INTO hundred(name, name_slug, status) 
        SELECT DISTINCT name, name_slug, status
        FROM hundred
        WHERE NOT EXISTS (
            SELECT 'X' 
            FROM temp_data
            WHERE 
                temp_data.name          = hundred.name
                AND temp_data.name_slug = hundred.name_slug
                AND temp_data.status    = status
        );

3
これは、行がすでに存在するかどうかわからないときに大量挿入を行うことがわかった最も速い方法です。
nate c 2010

「X」を選択しますか?誰かが明確にできますか?これは単にselectステートメントの権利です。SELECT name,name_slug,statusまたは*
roberthuttinger

3
相関サブクエリを検索します。「X」は1または「SadClown」に変更することもできます。SQLには何かが必要であり、「X」が一般的に使用されます。小さいので、相関サブクエリが使用されていることがわかり、SQLに必要な要件を満たしています。
Kuberchaun 2014年

「すべてのデータを(一時テーブルを想定して)挿入し、それとは異なる選択を行う」と述べました。その場合、そうではありませんSELECT DISTINCT name, name_slug, status FROM temp_dataか?
gibbz00

17

残念ながら、PostgreSQLももサポートしていMERGEないON DUPLICATE KEY UPDATEため、次の2つのステートメントで行う必要があります。

UPDATE  invoices
SET     billed = 'TRUE'
WHERE   invoices = '12345'

INSERT
INTO    invoices (invoiceid, billed)
SELECT  '12345', 'TRUE'
WHERE   '12345' NOT IN
        (
        SELECT  invoiceid
        FROM    invoices
        )

あなたはそれを関数にラップすることができます:

CREATE OR REPLACE FUNCTION fn_upd_invoices(id VARCHAR(32), billed VARCHAR(32))
RETURNS VOID
AS
$$
        UPDATE  invoices
        SET     billed = $2
        WHERE   invoices = $1;

        INSERT
        INTO    invoices (invoiceid, billed)
        SELECT  $1, $2
        WHERE   $1 NOT IN
                (
                SELECT  invoiceid
                FROM    invoices
                );
$$
LANGUAGE 'sql';

それを呼び出すだけです:

SELECT  fn_upd_invoices('12345', 'TRUE')

1
実際、これは機能しません。INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);何回でも呼び出すことができ、行を挿入し続けます。
AP257、2011年

1
AP257 @: CREATE TABLE hundred (name TEXT, name_slug TEXT, status INT); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); SELECT * FROM hundred。1つのレコードがあります。
Quassnoi

12

VALUESを利用できます-Postgresで利用可能です:

INSERT INTO person (name)
    SELECT name FROM person
    UNION 
    VALUES ('Bob')
    EXCEPT
    SELECT name FROM person;

12
個人から名前を選択<--- 10億行ある場合
Henley Chiu

1
これは問題をすばやく解決する良い方法だと思いますが、ソーステーブルが大きくならないことが確実な場合に限ってください。1000行を超えないテーブルがあるので、このソリューションを使用できます。
Leonard、

うわー、これはまさに私が必要としたものです。関数または一時テーブルを作成する必要があるのではないかと心配していましたが、これによりすべてが除外されます-ありがとう!
アマルゴビナス2016

8

私はこの質問が少し前からであることを知っていますが、これが誰かを助けるかもしれないと思いました。これを行う最も簡単な方法はトリガーを使用することだと思います。例えば:

Create Function ignore_dups() Returns Trigger
As $$
Begin
    If Exists (
        Select
            *
        From
            hundred h
        Where
            -- Assuming all three fields are primary key
            h.name = NEW.name
            And h.hundred_slug = NEW.hundred_slug
            And h.status = NEW.status
    ) Then
        Return NULL;
    End If;
    Return NEW;
End;
$$ Language plpgsql;

Create Trigger ignore_dups
    Before Insert On hundred
    For Each Row
    Execute Procedure ignore_dups();

このコードをpsqlプロンプトから実行します(ただし、データベースでクエリを直接実行したい場合)。次に、Pythonから通常どおり挿入できます。例えば:

sql = "Insert Into hundreds (name, name_slug, status) Values (%s, %s, %s)"
cursor.execute(sql, (hundred, hundred_slug, status))

@Thomas_Woutersがすでに述べたように、上記のコードは文字列を連結するのではなく、パラメーターを利用することに注意してください。


他の誰かもドキュメントから疑問に思っている場合:「BEFOREで起動された行レベルのトリガーはnullを返し、トリガーマネージャーにこの行の残りの操作をスキップするように通知できます(つまり、後続のトリガーは起動されず、INSERT / UPDATE / DELETEはこの行では発生しません。null以外の値が返された場合、操作はその行の値で続行されます。
ピート

まさに私が探していたこの答え。selectステートメントの代わりにfunction + triggerを使用してコードをクリーンアップします。+1
Jacek Krawczyk

私はこの答えが大好きで、関数とトリガーを使用します。ここで、関数とトリガーを使用してデッドロックを解除する別の方法を見つけます...
Sukma Saputra

7

WITHクエリを使用してPostgreSQLで条件付きINSERTを実行する良い方法があります。

WITH a as(
select 
 id 
from 
 schema.table_name 
where 
 column_name = your_identical_column_value
)
INSERT into 
 schema.table_name
(col_name1, col_name2)
SELECT
    (col_name1, col_name2)
WHERE NOT EXISTS (
     SELECT
         id
     FROM
         a
        )
  RETURNING id 

7

これはまさに私が直面している問題であり、私のバージョンは9.5です

そして、私はそれを以下のSQLクエリで解決します。

INSERT INTO example_table (id, name)
SELECT 1 AS id, 'John' AS name FROM example_table
WHERE NOT EXISTS(
            SELECT id FROM example_table WHERE id = 1
    )
LIMIT 1;

バージョン9.5以上で同じ問題を抱えている人に役立つことを願っています。

読んでくれてありがとう。


5

挿入..存在しない場所が良いアプローチです。そして、トランザクション「エンベロープ」によって競合状態を回避できます。

BEGIN;
LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE;
INSERT ... ;
COMMIT;

2

ルールがあれば簡単です。

CREATE RULE file_insert_defer AS ON INSERT TO file
WHERE (EXISTS ( SELECT * FROM file WHERE file.id = new.id)) DO INSTEAD NOTHING

しかし、同時書き込みでは失敗します...


1

(John Doeからの)最も賛成票を使用するアプローチは、何とかしてうまくいきますが、私の場合、予想される422行からは180しか得られません。シンプルなアプローチ。

IF NOT FOUND THEN後に使用SELECTちょうどは私にとって完璧に動作します。

PostgreSQLのドキュメントで説明されています

ドキュメントの例:

SELECT * INTO myrec FROM emp WHERE empname = myname;
IF NOT FOUND THEN
  RAISE EXCEPTION 'employee % not found', myname;
END IF;

1

psycopgsカーソルクラスの属性はrowcountです。

この読み取り専用属性は、最後のexecute *()が生成した(SELECTなどのDQLステートメントの場合)、または影響を受けた(UPDATEまたはINSERTなどのDMLステートメントの場合)行数を指定します。

したがって、行カウントが0の場合にのみ、最初にUPDATEとINSERTを試すことができます。

ただし、データベースのアクティビティレベルによっては、UPDATEとINSERTの間に競合状態が発生し、その間に別のプロセスがそのレコードを作成する場合があります。


おそらくこれらのクエリをトランザクションでラップすることで、競合状態が緩和されるでしょう。
Daniel Lyons

ありがとう、本当にシンプルでクリーンなソリューション
Alexander Malfait

1

あなたの列「百」は主キーとして定義されているようであり、したがって一意ではなく、そうではありません。問題は問題ではなく、データにあります。

主キーを手軽に使用できるように、シリアルタイプとしてIDを挿入することをお勧めします


1

行の多くが同一であると言うと、何度もチェックを終了します。あなたはそれらを送ることができ、データベースはそれを挿入するかどうかをON CONFLICT句で次のように判断します

  INSERT INTO Hundred (name,name_slug,status) VALUES ("sql_string += hundred  
  +",'" + hundred_slug + "', " + status + ") ON CONFLICT ON CONSTRAINT
  hundred_pkey DO NOTHING;" cursor.execute(sql_string);

0

私は、PostgreSQLとHSQLDBで機能するSQLを見つけようとして、同様のソリューションを探していました。(HSQLDBがこれを難しくした理由です。)例としてベースとして使用すると、これは他の場所で見つけた形式です。

sql = "INSERT INTO hundred (name,name_slug,status)"
sql += " ( SELECT " + hundred + ", '" + hundred_slug + "', " + status
sql += " FROM hundred"
sql += " WHERE name = " + hundred + " AND name_slug = '" + hundred_slug + "' AND status = " + status
sql += " HAVING COUNT(*) = 0 );"

-1

以下は、テーブル名、列、値を指定して、postgresqlに相当するupsertを生成する一般的なpython関数です。

jsonをインポートする

def upsert(table_name, id_column, other_columns, values_hash):

    template = """
    WITH new_values ($$ALL_COLUMNS$$) as (
      values
         ($$VALUES_LIST$$)
    ),
    upsert as
    (
        update $$TABLE_NAME$$ m
            set
                $$SET_MAPPINGS$$
        FROM new_values nv
        WHERE m.$$ID_COLUMN$$ = nv.$$ID_COLUMN$$
        RETURNING m.*
    )
    INSERT INTO $$TABLE_NAME$$ ($$ALL_COLUMNS$$)
    SELECT $$ALL_COLUMNS$$
    FROM new_values
    WHERE NOT EXISTS (SELECT 1
                      FROM upsert up
                      WHERE up.$$ID_COLUMN$$ = new_values.$$ID_COLUMN$$)
    """

    all_columns = [id_column] + other_columns
    all_columns_csv = ",".join(all_columns)
    all_values_csv = ','.join([query_value(values_hash[column_name]) for column_name in all_columns])
    set_mappings = ",".join([ c+ " = nv." +c for c in other_columns])

    q = template
    q = q.replace("$$TABLE_NAME$$", table_name)
    q = q.replace("$$ID_COLUMN$$", id_column)
    q = q.replace("$$ALL_COLUMNS$$", all_columns_csv)
    q = q.replace("$$VALUES_LIST$$", all_values_csv)
    q = q.replace("$$SET_MAPPINGS$$", set_mappings)

    return q


def query_value(value):
    if value is None:
        return "NULL"
    if type(value) in [str, unicode]:
        return "'%s'" % value.replace("'", "''")
    if type(value) == dict:
        return "'%s'" % json.dumps(value).replace("'", "''")
    if type(value) == bool:
        return "%s" % value
    if type(value) == int:
        return "%s" % value
    return value


if __name__ == "__main__":

    my_table_name = 'mytable'
    my_id_column = 'id'
    my_other_columns = ['field1', 'field2']
    my_values_hash = {
        'id': 123,
        'field1': "john",
        'field2': "doe"
    }
    print upsert(my_table_name, my_id_column, my_other_columns, my_values_hash)

-8

解決策は単純ですが、すぐにはありません。
この命令を使用する場合は、dbに1つの変更を加える必要があります。

ALTER USER user SET search_path to 'name_of_schema';

これらの変更後、「INSERT」は正しく機能します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.