web-dev-qa-db-ja.com

SQLite UPSERT / UPDATE OR INSERT

SQLiteデータベースに対してUPSERT/INSERT OR UPDATEを実行する必要があります。

INSERT OR REPLACEコマンドがあり、多くの場合に役立ちます。ただし、外部キーのために自動インクリメントを使用してIDを保持したい場合、行を削除し、新しい行を作成し、その結果、この新しい行には新しいIDが設定されるため機能しません。

これはテーブルになります:

プレーヤー-(IDの主キー、一意のuser_name)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |
90
bgusach

これは遅い回答です。 2018年6月4日にリリースされたSQLIte 3.24.0以降、PostgreSQL構文に続く UPSERT 句が最終的にサポートされます。

INSERT INTO players (user_name, age)
  VALUES('steven', 32) 
  ON CONFLICT(user_name) 
  DO UPDATE SET age=excluded.age;

注:3.24.0より前のバージョンのSQLiteを使用する必要がある場合は、 この回答 を参照してください(投稿者:@MarqueIV)。

ただし、アップグレードするオプションがある場合は、強くお勧めします、私のソリューションとは異なり、ここに投稿されたものは、単一のステートメント。さらに、通常は最新リリースに付属する他のすべての機能、改善、およびバグ修正を取得できます。

41
prapin

Q&Aスタイル

さて、何時間も問題を調査し、戦った後、テーブルの構造と、整合性を維持するために外部キーの制限を有効にするかどうかに応じて、これを達成する2つの方法があることがわかりました。私の状況にいる人々に時間を節約するために、これをきれいな形式で共有したいと思います。


オプション1:行を削除する余裕がある

つまり、外部キーがないか、または外部キーがある場合、SQLiteエンジンは整合性例外がないように構成されます。方法はINSERT OR REPLACEです。 IDが既に存在するプレーヤーを挿入/更新しようとすると、SQLiteエンジンはその行を削除し、提供しているデータを挿入します。ここで質問が来ます:古いIDを関連付けたままにするために何をすべきか?

データuser_name = 'steven'およびage = 32でUPSERTにしたいとしましょう。

このコードを見てください:

INSERT INTO players (id, name, age)

VALUES (
    coalesce((select id from players where user_name='steven'),
             (select max(id) from drawings) + 1),
    32)

トリックは合体です。ユーザー 'steven'のIDを返します(存在しない場合)。それ以外の場合は、新しい新しいIDを返します。


オプション2:行を削除する余裕がない

前のソリューションを試した後、この場合、このIDは他のテーブルの外部キーとして機能するため、データが破壊される可能性があることに気付きました。また、ON DELETE CASCADE句を使用してテーブルを作成しました。これは、データをサイレントに削除することを意味します。危険な。

したがって、私は最初にIF句を考えましたが、SQLiteにはCASEしかありません。そして、これはCASEを使用して1つ実行することはできません(または少なくとも管理していません)UPDATE EXISTS(user_name = 'steven')、およびINSERT含まれていない場合。立ち入り禁止。

そして、ついに私はブルートフォースを使用して成功しました。ロジックは、実行する各UPSERTに対して、最初にINSERT OR IGNOREを実行して、行があることを確認しますユーザー、そして挿入しようとしたのとまったく同じデータでUPDATEクエリを実行します。

前と同じデータ:user_name = 'steven'およびage = 32。

-- make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

-- make sure it has the right data
UPDATE players SET user_name='steven', age=32 WHERE user_name='steven'; 

そして、それだけです!

編集

Andyがコメントしたように、最初に挿入してから更新しようとすると、予想よりも頻繁にトリガーが起動される可能性があります。私の意見ではこれはデータの安全性の問題ではありませんが、不必要なイベントの発生はほとんど意味がないことは事実です。したがって、改善されたソリューションは次のようになります。

-- Try to update any existing row
UPDATE players SET user_name='steven', age=32 WHERE user_name='steven';

-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 
102
bgusach

これは、キー違反があった場合にのみ機能するブルートフォースの「無視」を必要としないアプローチです。この方法は、更新で指定するany条件に基づいて機能します。

これを試して...

-- Try to update any existing row
UPDATE players
SET age=32
WHERE user_name='steven';

-- If no update happened (i.e. the row didn't exist) then insert one
INSERT INTO players (user_name, age)
SELECT 'steven', 32
WHERE (Select Changes() = 0);

使い方

ここでの「魔法」は、挿入する行があるかどうかを判断するためにWhere (Select Changes() = 0)句を使用することです。これは独自のWhere句に基づいているため、定義するものではなく、キー違反のみ。

上記の例では、更新による変更がない場合(つまり、レコードが存在しない場合)Changes() = 0であるため、WhereステートメントのInsert句はtrueを返し、指定したデータで新しい行が挿入されます。

Updatedidが既存の行を更新する場合、Changes() = 1になるので、Insertの 'Where'句はfalseになり、挿入されません。開催されます。

総当たり攻撃は必要ありません。

66
MarqueIV

提示されたすべての答えの問題は、トリガー(およびおそらく他の副作用)を考慮に入れていないという完全な欠如です。のようなソリューション

INSERT OR IGNORE ...
UPDATE ...

行が存在しない場合、両方のトリガーが実行されます(挿入および更新)。

適切なソリューションは

UPDATE OR IGNORE ...
INSERT OR IGNORE ...

その場合、1つのステートメントのみが実行されます(行が存在するかどうか)。

24
Andy

一意のキーや他のキーで中継しない(プログラマ向けの)穴のない純粋なUPSERTを作成するには:

UPDATE players SET user_name="gil", age=32 WHERE user_name='george'; 
SELECT changes();

SELECT changes()は、最後の問い合わせで行われた更新の数を返します。次に、changes()からの戻り値が0であるかどうかを確認し、そうであれば実行します。

INSERT INTO players (user_name, age) VALUES ('gil', 32); 
4
Gilco

また、ON CONFLICT REPLACE句をuser_name一意制約に追加してから、挿入するだけでSQLiteに残して、競合が発生した場合の対処方法を把握することもできます。参照: https://sqlite.org/lang_conflict.html .

削除トリガーに関する文にも注意してください。REPLACE競合解決戦略が制約を満たすために行を削除する場合、再帰トリガーが有効な場合にのみ削除トリガーが起動します。

2

オプション1:挿入->更新

行を削除する余裕がない場合でも、changes()=0INSERT OR IGNOREの両方を避けたい場合-このロジックを使用できます。

最初に挿入(存在しない場合)、次に更新一意のキーでフィルタリングします。

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Insert if NOT exists
INSERT INTO players (user_name, age)
SELECT 'johnny', 20
WHERE NOT EXISTS (SELECT 1 FROM players WHERE user_name='johnny' AND age=20);

-- Update (will affect row, only if found)
-- no point to update user_name to 'johnny' since it's unique, and we filter by it as well
UPDATE players 
SET age=20 
WHERE user_name='johnny';

トリガーについて

注意:どのトリガーが呼び出されているかを確認するためにテストしていませんが、私はassume次のようにします:

行が存在しない場合

  • 挿入する前に
  • INSTEAD OFを使用した挿入
  • 挿入後
  • 更新前
  • INSTEAD OFを使用した更新
  • 更新後

行が存在する場合

  • 更新前
  • INSTEAD OFを使用した更新
  • 更新後

オプション2:挿入または置換-独自のIDを保持

このようにして、単一のSQLコマンドを使用できます

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Single command to insert or update
INSERT OR REPLACE INTO players 
(id, user_name, age) 
VALUES ((SELECT id from players WHERE user_name='johnny' AND age=20),
        'johnny',
        20);

編集:オプション2を追加しました。

1
itsho