web-dev-qa-db-ja.com

SQLite - UPSERTは* INSERTやREPLACEではない*

http://en.wikipedia.org/wiki/Upsert

SQL Server上の挿入更新ストアドプロシージャ

私が考えていなかったSQLiteでこれをする賢い方法がありますか?

基本的に、レコードが存在する場合は4つのカラムのうち3つを更新します。それが存在しない場合は、4番目のカラムのデフォルト(NUL)値でレコードを挿入します。

IDは主キーなので、UPSERTへのレコードは1つだけです。

(明らかにUPDATEまたはINSERTが必要かどうかを判断するために、SELECTのオーバーヘッドを避けようとしています)

提案?


SQLiteサイトのTABLE CREATEの構文は確認できません。私はそれをテストするデモを作成していませんが、それはサポートされていないようです..

もしそうなら、私は3つの列を持っているので、実際には次のようになります。

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    Blob1 BLOB ON CONFLICT REPLACE, 
    Blob2 BLOB ON CONFLICT REPLACE, 
    Blob3 BLOB 
);

しかし、最初の2つのBLOBは衝突を起こさないでしょう、IDだけがそうするでしょう。だから私はBlob1とBlob2は(望まれるように)置き換えられないでしょう


バインドデータが完全なトランザクションである場合のSQLiteでの更新、つまり更新される送信された各行には以下が必要です。reset関数を使用できるINSERTとは異なり、Prepare/Bind/Step/Finalizeステートメント

ステートメントオブジェクトの寿命は次のようになります。

  1. Sqlite3_prepare_v2()を使用してオブジェクトを作成します
  2. Sqlite3_bind_インターフェースを使用して、値をHostパラメーターにバインドします。
  3. Sqlite3_step()を呼び出してSQLを実行します。
  4. Sqlite3_reset()を使用してステートメントをリセットしてからステップ2に戻り、繰り返してください。
  5. Sqlite3_finalize()を使用してステートメントオブジェクトを破棄します。

更新私はINSERTに比べて遅いと思いますが、それはどのように主キーを使用してSELECTと比較するのですか?

たぶん私は4列目(Blob 3)を読むためにselectを使い、そして最初の3列のための新しいデータと元の4列目を混合する新しいレコードを書くためにREPLACEを使うべきですか?

495
Mike Trader

表の3列を想定しています.. ID、NAME、ROLE


BAD:これは、ID = 1のすべての列を新しい値で挿入または置換します。

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (1, 'John Foo', 'CEO');

BAD:これは、2つの列を挿入または置換します... NAME列はNULLまたはデフォルト値に設定されます。

INSERT OR REPLACE INTO Employee (id, role) 
  VALUES (1, 'code monkey');

GOOD:これにより、2つの列が更新されます。 ID = 1が存在する場合、NAMEは影響を受けません。 ID = 1が存在しない場合、名前はデフォルト(NULL)になります。

INSERT OR REPLACE INTO Employee (id, role, name) 
  VALUES (  1, 
            'code monkey',
            (SELECT name FROM Employee WHERE id = 1)
          );

これで2列が更新されます。 ID = 1が存在する場合、ロールは影響を受けません。 ID = 1が存在しない場合、役割はデフォルト値ではなく 'Benchwarmer'に設定されます。

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (  1, 
            'Susan Bar',
            COALESCE((SELECT role FROM Employee WHERE id = 1), 'Benchwarmer')
          );
812
Eric B

INSERT OR REPLACEは、 "UPSERT"と同等のNOTです。

テーブルEmployeeにid、name、およびroleの各フィールドがあるとします。

INSERT OR REPLACE INTO Employee ("id", "name", "role") VALUES (1, "John Foo", "CEO")
INSERT OR REPLACE INTO Employee ("id", "role") VALUES (1, "code monkey")

ブーム、あなたは従業員番号1の名前をなくしました。SQLiteはそれをデフォルト値に置き換えました。

UPSERTの予想される出力は、役割を変更して名前を維持することです。

125
gregschlom

Eric Bの答え は、既存の行から1つか2つの列だけを保存したい場合は問題ありません。たくさんの列を保存したい場合は、面倒になりがちです。

これは、どちらの側の列にも対応できるアプローチです。それを説明するために、私は以下のスキーマを仮定します:

 CREATE TABLE page (
     id      INTEGER PRIMARY KEY,
     name    TEXT UNIQUE,
     title   TEXT,
     content TEXT,
     author  INTEGER NOT NULL REFERENCES user (id),
     ts      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );

特にnameが行の自然キーであることに注意してください - idは外部キーにのみ使用されるので、新しい行を挿入するときにSQLiteがID値自体を選択することが重要です。しかし、そのnameに基づいて既存の行を更新するとき、私はそれが古いID値を持ち続けることを望みます(明らかに!)。

私は、次の構文で本当のUPSERTを実現します。

 WITH new (name, title, author) AS ( VALUES('about', 'About this site', 42) )
 INSERT OR REPLACE INTO page (id, name, title, content, author)
 SELECT old.id, new.name, new.title, old.content, new.author
 FROM new LEFT JOIN page AS old ON new.name = old.name;

このクエリの正確な形式は多少異なる場合があります。重要なのは、既存の行を新しい値に結合するための、左外部結合を伴うINSERT SELECTの使用です。

ここで、行が以前に存在しなかった場合、old.idNULLとなり、SQLiteは自動的にIDを割り当てますが、すでにそのような行が存在する場合、old.idは実際の値を持ち、これは再利用されます。まさに私が欲しかったものです。

実際、これは非常に柔軟です。 tsカラムがすべての面で完全に欠落していることに注意してください - それはDEFAULT値を持っているので、SQLiteはどんな場合でもちょうど正しいことをするので、私は自分でそれを気にする必要はありません。

newoldの両サイドに列を含めることもできます。 「新しいコンテンツがあれば挿入し、そうでなければ古いコンテンツを保持する」と言うための、外側のSELECT内のCOALESCE(new.content, old.content) - 例えば固定クエリを使用していて、新しい値をプレースホルダでバインドしている場合。

106

あなたが一般的に更新をしているならば私は..

  1. トランザクションを開始する
  2. 更新する
  3. 行数をチェックする
  4. それが0なら挿入をする
  5. コミット

あなたが一般的に挿入をしているならば私は

  1. トランザクションを開始する
  2. インサートを試す
  3. 主キー違反エラーをチェックします
  4. エラーが発生した場合は更新を行います
  5. コミット

この方法であなたは選択を避け、あなたはSqlite上でトランザクション的に健全です。

79
Sam Saffron

この回答は更新されたので、以下のコメントは適用されません。

2018-05-18プレスを停止します。

SQLiteでのUPSERTのサポート! バージョン3.24.0(保留中)のUPSite構文がSQLiteに追加されました。

UPSERTはINSERTに特殊な構文追加機能を追加したもので、INSERTが一意性制約に違反する場合に、INSERTをUPDATEまたは何もしないように動作させます。 UPSERTは標準SQLではありません。 SQLiteのUPSERTはPostgreSQLによって確立された構文に従います。

enter image description here

あるいは:

これを行うもう1つの完全に異なる方法は、次のとおりです。私のアプリケーションでは、メモリ内に行を作成するときに、メモリ内のrowIDをlong.MaxValueに設定します。 (MaxValueがIDとして使用されることは決してありません。十分に長く存続することはできません。次に、rowIDがその値でない場合は、データベースに存在している必要があります。アプリのrowIDを追跡できる場合に限り、これは役に立ちます。

68
AnthonyLambert

私はこれが古いスレッドであることを認識していますが、私は最近sqlite3で作業していて、動的にパラメータ化されたクエリを生成するという私のニーズにより適したこの方法を思いつきました。

insert or ignore into <table>(<primaryKey>, <column1>, <column2>, ...) values(<primaryKeyValue>, <value1>, <value2>, ...); 
update <table> set <column1>=<value1>, <column2>=<value2>, ... where changes()=0 and <primaryKey>=<primaryKeyValue>; 

それはまだ更新時にwhere句を持つ2つのクエリですが、うまくいくようです。私の頭の中には、sqliteがcha​​nges()の呼び出しがゼロより大きければupdateステートメントを完全に最適化できるというビジョンもあります。それが実際にそれが私の知識を超えているかどうか、しかし男は夢を見ることができますかどうか? ;)

ボーナスポイントの場合は、新しく挿入された行であろうと既存の行であろうと、行のIDを返すこの行を追加できます。

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;
59

これは実際にはINSERT OR REPLACEではなくUPSERT(UPDATEまたはINSERT)です(多くの状況で動作が異なります)。

それはこのように動作します:
1。同じIDを持つレコードが存在する場合は更新してみてください。
2。更新によって行が変更されなかった場合(NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0))、レコードを挿入します。

そのため、既存のレコードが更新されたか挿入が実行されます。

重要な詳細は、changes()SQL関数を使用してupdateステートメントが既存のレコードにヒットしたかどうかを確認し、insertステートメントがレコードにヒットしなかった場合にのみ実行することです。

言及する1つのことは、changes()関数が低レベルのトリガーによって実行された変更を返さないことです( http://sqlite.org/lang_corefunc.html#changes を参照)。それを考慮に入れてください。

これがSQLです...

テストアップデート

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 2;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 2, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;

テストインサート:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 3;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 3, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;
13
David Liebeherr

あなたは確かにSQLiteでupsertを行うことができます。それは次のようになります。

INSERT INTO table name (column1, column2) 
VALUES ("value12", "value2") WHERE id = 123 
ON CONFLICT DO UPDATE 
SET column1 = "value1", column2 = "value2" WHERE id = 123
6
Brill Pappin

アリストテレスの答え に展開すると、ダミーの「シングルトン」テーブル(単一行の自分用のテーブル)からSELECTできます。これにより、重複を避けることができます。

また、MySQLとSQLite間でこの例を移植可能にしておき、最初に列を設定する方法の例として 'date_added'列を使用しました。

 REPLACE INTO page (
   id,
   name,
   title,
   content,
   author,
   date_added)
 SELECT
   old.id,
   "about",
   "About this site",
   old.content,
   42,
   IFNULL(old.date_added,"21/05/2013")
 FROM singleton
 LEFT JOIN page AS old ON old.name = "about";
5
user2403761

バージョン3.24.0以降、UPSERTはSQLiteによってサポートされています。

のドキュメントから

UPSERTはINSERTに特殊な構文追加機能を追加したもので、INSERTが一意性制約に違反する場合はINSERTをUPDATEまたはノーオペレーションとして動作させます。 UPSERTは標準SQLではありません。 SQLiteのUPSERTはPostgreSQLによって確立された構文に従います。UPSERT構文はバージョン3.24.0(保留中)でSQLiteに追加されました。

UPSERTは、特別なON CONFLICT句が続く通常のINSERT文です

enter image description here

画像のソース: https://www.sqlite.org/images/syntax/upsert-clause.gif

5
Lukasz Szozda

私が知っている最善の方法は、更新を行い、その後に挿入を行うことです。 「選択のオーバーヘッド」は必要ですが、高速である主キーを検索しているのでそれほどひどい負担にはなりません。

あなたはあなたが望むことをするためにあなたのテーブルとフィールド名で下記のステートメントを修正することができるはずです。

--first, update any matches
UPDATE DESTINATION_TABLE DT
SET
  MY_FIELD1 = (
              SELECT MY_FIELD1
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
 ,MY_FIELD2 = (
              SELECT MY_FIELD2
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
WHERE EXISTS(
            SELECT ST2.PRIMARY_KEY
            FROM
              SOURCE_TABLE ST2
             ,DESTINATION_TABLE DT2
            WHERE ST2.PRIMARY_KEY = DT2.PRIMARY_KEY
            );

--second, insert any non-matches
INSERT INTO DESTINATION_TABLE(
  MY_FIELD1
 ,MY_FIELD2
)
SELECT
  ST.MY_FIELD1
 ,NULL AS MY_FIELD2  --insert NULL into this field
FROM
  SOURCE_TABLE ST
WHERE NOT EXISTS(
                SELECT DT2.PRIMARY_KEY
                FROM DESTINATION_TABLE DT2
                WHERE DT2.PRIMARY_KEY = ST.PRIMARY_KEY
                );
3
JosephStyons

誰かがCordovaでSQLiteのための私の解決策を読みたいと思うならば、私は上記の@david回答のおかげでこの一般的なjs方法を得ました。

function    addOrUpdateRecords(tableName, values, callback) {
get_columnNames(tableName, function (data) {
    var columnNames = data;
    myDb.transaction(function (transaction) {
        var query_update = "";
        var query_insert = "";
        var update_string = "UPDATE " + tableName + " SET ";
        var insert_string = "INSERT INTO " + tableName + " SELECT ";
        myDb.transaction(function (transaction) {
            // Data from the array [[data1, ... datan],[()],[()]...]:
            $.each(values, function (index1, value1) {
                var sel_str = "";
                var upd_str = "";
                var remoteid = "";
                $.each(value1, function (index2, value2) {
                    if (index2 == 0) remoteid = value2;
                    upd_str = upd_str + columnNames[index2] + "='" + value2 + "', ";
                    sel_str = sel_str + "'" + value2 + "', ";
                });
                sel_str = sel_str.substr(0, sel_str.length - 2);
                sel_str = sel_str + " WHERE NOT EXISTS(SELECT changes() AS change FROM "+tableName+" WHERE change <> 0);";
                upd_str = upd_str.substr(0, upd_str.length - 2);
                upd_str = upd_str + " WHERE remoteid = '" + remoteid + "';";                    
                query_update = update_string + upd_str;
                query_insert = insert_string + sel_str;  
                // Start transaction:
                transaction.executeSql(query_update);
                transaction.executeSql(query_insert);                    
            });
        }, function (error) {
            callback("Error: " + error);
        }, function () {
            callback("Success");
        });
    });
});
}

そのため、まずこの関数で列名を選びます。

function get_columnNames(tableName, callback) {
myDb.transaction(function (transaction) {
    var query_exec = "SELECT name, sql FROM sqlite_master WHERE type='table' AND name ='" + tableName + "'";
    transaction.executeSql(query_exec, [], function (tx, results) {
        var columnParts = results.rows.item(0).sql.replace(/^[^\(]+\(([^\)]+)\)/g, '$1').split(','); ///// RegEx
        var columnNames = [];
        for (i in columnParts) {
            if (typeof columnParts[i] === 'string')
                columnNames.Push(columnParts[i].split(" ")[0]);
        };
        callback(columnNames);
    });
});
}

それからプログラム的にトランザクションを構築します。

"Values"は前に構築しなければならない配列で、テーブルに挿入または更新したい行を表します。

"remoteid"は、リモートサーバーと同期しているので、参照として使用したIDです。

SQLite Cordovaプラグインの使用については、公式の リンクを参照してください

3
Zappescu

Aristotle PagaltzisEric Bの答え からのCOALESCEのアイデアに続いて、ここ少数の列のみを更新する、または全行が存在しない場合は全行を挿入するというupsertオプションです。

この場合、タイトルとコンテンツを更新し、既存の場合は他の古い値を保持し、名前が見つからない場合は提供されたものを挿入することを想像してください。

NOTEidはオートインクリメントであると想定されるため、INSERTはNULLに強制されます。それが単なる生成された主キーであるならば、COALESCEも使うことができます( Aristotle Pagaltzisコメント を参照)。

WITH new (id, name, title, content, author)
     AS ( VALUES(100, 'about', 'About this site', 'Whatever new content here', 42) )
INSERT OR REPLACE INTO page (id, name, title, content, author)
SELECT
     old.id, COALESCE(old.name, new.name),
     new.title, new.content,
     COALESCE(old.author, new.author)
FROM new LEFT JOIN page AS old ON new.name = old.name;

そのため、古い値を保持する場合はCOALESCEを使用し、値を更新する場合はnew.fieldnameを使用するのが一般的な規則です。

1
Miquel

私はこれがあなたが探しているものかもしれないと思います: ON CONFLICT句

テーブルをこのように定義したとします。

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    field1 TEXT 
); 

さて、あなたが既に存在するIDでINSERTをすると、SQLiteは自動的にINSERTの代わりにUPDATEを行います。

あ….

1
kmelvn

この方法では、この質問に対する他の方法のいくつかを回答からリミックスし、CTE(Common Table Expressions)を使用します。クエリを紹介してから、自分がしたことを実行した理由を説明します。

従業員300がいる場合は、従業員300の姓をDAVISに変更します。それ以外の場合は、新しい従業員を追加します。

テーブル名:employees列:id、first_name、last_name

クエリは次のとおりです。

INSERT OR REPLACE INTO employees (employee_id, first_name, last_name)
WITH registered_employees AS ( --CTE for checking if the row exists or not
    SELECT --this is needed to ensure that the null row comes second
        *
    FROM (
        SELECT --an existing row
            *
        FROM
            employees
        WHERE
            employee_id = '300'

        UNION

        SELECT --a dummy row if the original cannot be found
            NULL AS employee_id,
            NULL AS first_name,
            NULL AS last_name
    )
    ORDER BY
        employee_id IS NULL --we want nulls to be last
    LIMIT 1 --we only want one row from this statement
)
SELECT --this is where you provide defaults for what you would like to insert
    registered_employees.employee_id, --if this is null the SQLite default will be used
    COALESCE(registered_employees.first_name, 'SALLY'),
    'DAVIS'
FROM
    registered_employees
;

基本的に、CTEを使用して、デフォルト値を決定するためにSELECTステートメントを使用しなければならない回数を減らしました。これはCTEなので、テーブルから必要な列を選択するだけで、INSERTステートメントはこれを使用します。

これで、COALESCE関数内のNULLを値の指定に置き換えることによって、どのデフォルトを使用するのかを決定できます。

1
Dodzi Dzakuma