SQLite UPSERT /更新または挿入


102

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    |

回答:


51

これは遅い答えです。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)。

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


現時点では、このリリースはUbuntuリポジトリにはまだありません。
bl79

これをAndroidで使用できないのはなぜですか?試しましたdb.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?")。「オン」という単語の構文エラー
Bastian Voigt

1
@BastianVoigt AndroidのさまざまなバージョンにインストールされているSQLite3ライブラリは3.24.0より古いため。参照:developer.android.com/reference/android/database/sqlite/…悲しいことに、AndroidまたはiOSでSQLite3(またはその他のシステムライブラリ)の新機能が必要な場合は、特定のバージョンのSQLiteをバンドルする必要があります。インストールされているシステムに依存する代わりにアプリケーション。
プラピン2018年

UPSERTではなく、最初に挿入を試みるので、これはINDATEではありませんか?;)
Mark A. Donohoe

@ BastianVoigt、3.24.0より前のバージョン用の以下の私の回答(上記の質問にリンクされています)を参照してください。
Mark A. Donohoe

105

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を使用して(または少なくとも管理しなかった)、EXISTS(select id from player where user_name = 'steven')の場合は1つのUPDATEクエリを実行できず、そうでない場合は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 age=32 WHERE user_name='steven';

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

10
同上...オプション2は素晴らしいです。例外として、私はそれを逆に行いました:更新を試み、rowsAffected> 0かどうかを確認し、そうでない場合は挿入を行います。
トムスペンサー

これもかなり良いアプローチです。唯一の小さな欠点は、「upsert」用のSQLが1つしかないことです。
bgusach 2013

2
最後のコードサンプルのupdateステートメントでuser_nameを再設定する必要はありません。年齢を設定するのに十分です。
Serg Stetsuk 2018年

72

ここでは、ブルートフォースの「無視」を必要としないアプローチがあり、キー違反があった場合にのみ機能します。このように基づいて動作するすべてのアップデートで指定した条件。

これを試して...

-- 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);

使い方

ここの「マジックソース」はChanges()Where節で使用しています。 Changes()最後の操作の影響を受ける行の数を表します。この場合は更新です。

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

場合はUpdate なかった既存の行を更新し、その後Changes()で「どこ句はので、= 1(またはより正確に、ゼロではない複数の行が更新された場合)Insertになりましfalseと評価され、したがって、何の挿入は行われません。

これの利点は、ブルートフォースが不要で、不必要に削除してからデータを再挿入する必要がないため、外部キーの関係でダウンストリームキーがめちゃくちゃになることです。

さらに、これは単なる標準的なWhere条項であるため、キー違反だけでなく、定義したあらゆるものに基づくことができます。同様に、式を使用できるChanges()場所ならどこでも、必要なものと組み合わせて使用できます。


1
これは私にとってはうまくいきました。INSERT OR REPLACEの例と並んで、このソリューションを他のどこでも見たことはありません。私のユースケースでは、はるかに柔軟です。
csab 2017年

@MarqueIVと、2つのアイテムを更新または挿入する必要がある場合はどうでしょうか?たとえば、もみが更新され、2番目のは存在しません。このような場合、falseChanges() = 0が返され、2つの行がINSERT OR REPLACEを実行します
Andriy Antonov

通常UPSERTは1つのレコードに基づいて動作することになっています。複数のレコードに確実に作用していることがわかっている場合は、それに応じてカウントチェックを変更してください。
Mark A. Donohoe

悪い点は、行が存在する場合、行が変更されたかどうかに関係なくupdateメソッドを実行する必要があることです。
ジミ

1
なぜそれが悪いのですか?そして、データが変更されていない場合、なぜUPSERT最初に呼び出すのですか?しかし、そうであっても、それはだ良い設定、更新は起こるものChanges=1か、他INSERTの文だろうあなたはそれをしたくない、誤って火、。
Mark A. Donohoe

25

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

INSERT OR IGNORE ...
UPDATE ...

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

適切なソリューションは

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

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


1
あなたの言ってる事がわかります。質問を更新します。ちなみに、UPDATE OR IGNORE行が見つからなくても更新がクラッシュしないため、なぜ必要なのかはわかりません。
bgusach

1
読みやすさ?アンディのコードが何をしているかが一目でわかります。あなたのbgusach私は理解するために少し勉強しなければならなかった。
Brandan

6

一意のキーやその他のキーを中継しない穴(プログラマー用)のない純粋なUPSERTを使用するには、次のようにします。

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

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

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

これは、@ fiznoolが彼のコメントで提案したものに相当します(ただし、彼の解決策を模索します)。大丈夫ですが、実際には問題なく動作しますが、独自のSQLステートメントはありません。UPSERTがPKまたは他の一意のキーに基づいていない場合、私にはほとんど意味がありません。
bgusach

4

ON CONFLICT REPLACE句をuser_name一意制約に追加し、INSERTを実行するだけで、競合が発生した場合の対処方法をSQLiteに任せることもできます。https://sqlite.org/lang_conflict.htmlを参照してください

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


1

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

両方を避けたい場合、changes()=0およびINSERT 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';

トリガーについて

通知:どのトリガーが呼び出されているかを確認するためにテストしていませんが、以下を想定しています。

行が存在しない場合

  • 挿入前
  • 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を追加しました。

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