Programing

SQLite-UPSERT * not * INSERT 또는 REPLACE

lottogame 2020. 2. 12. 07:59
반응형

SQLite-UPSERT * not * INSERT 또는 REPLACE


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

SQL Server에 업데이트 저장 프로 시저 삽입

내가 생각하지 않은 SQLite에서 이것을 수행하는 영리한 방법이 있습니까?

기본적으로 레코드가 존재하면 4 개 열 중 3 개를 업데이트하고 싶습니다.없는 경우 4 번째 열의 기본 (NUL) 값으로 레코드를 삽입하고 싶습니다.

ID는 기본 키이므로 UPSERT에 대한 레코드는 하나뿐입니다.

(명확하게 업데이트하거나 삽입 해야하는 경우 결정하기 위해 SELECT의 오버 헤드를 피하려고합니다.)

제안?


TABLEite에 대한 SQLite 사이트의 구문을 확인할 수 없습니다. 테스트 할 데모를 만들지 않았지만 지원되지 않는 것 같습니다.

그렇다면 세 개의 열이 있으므로 실제로 다음과 같이 나타납니다.

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

그러나 처음 두 얼룩은 충돌을 일으키지 않으며 ID만이 가능하므로 Blob1을 asusme하고 Blob2는 (원하는대로) 교체하지 않습니다


바인딩 데이터가 완전한 트랜잭션 일 때 SQLite의 UPDATEs 업데이트 될 각 전송 된 행에는 다음이 필요합니다. 리셋 기능을 사용할 수있는 INSERT와 달리 Prepare / Bind / Step / Finalize 문

명령문 객체의 수명은 다음과 같습니다.

  1. sqlite3_prepare_v2 ()를 사용하여 객체를 만듭니다.
  2. sqlite3_bind_ 인터페이스를 사용하여 호스트 매개 변수에 값을 바인드하십시오.
  3. sqlite3_step ()을 호출하여 SQL을 실행하십시오.
  4. sqlite3_reset ()을 사용하여 명령문을 재설정 한 후 2 단계로 돌아가서 반복하십시오.
  5. sqlite3_finalize ()를 사용하여 명령문 오브젝트를 삭제하십시오.

업데이트 INSERT에 비해 느린 것으로 생각되지만 기본 키를 사용하여 SELECT와 어떻게 비교합니까?

아마도 select를 사용하여 4 번째 열 (Blob3)을 읽은 다음 REPLACE를 사용하여 원래 4 번째 열을 처음 3 열의 새 데이터와 혼합하는 새 레코드를 작성해야합니까?


테이블에 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이 존재하면 ROLE은 영향을받지 않습니다. ID = 1이 없으면 역할이 기본값 대신 'Benchwarmer'로 설정됩니다.

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

INSERT OR REPLACE는 "UPSERT"와 동일 하지 않습니다 .

id, name 및 role 필드가있는 Employee 테이블이 있다고 가정하십시오.

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의 예상 결과는 역할을 변경하고 이름을 유지하는 것입니다.


기존 행에서 하나 또는 두 개의 열만 보존하려는 경우 Eric B의 대답 은 정상입니다. 많은 열을 유지하려면 너무 번거로워집니다.

다음은 양쪽의 열에 맞게 확장 할 수있는 방법입니다. 이를 설명하기 위해 다음 스키마를 가정합니다.

 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.idwill이 NULL있고 SQLite는 자동으로 ID를 할당하지만 이미 그러한 행 old.id이 있으면 실제 값을 가지므로 재사용됩니다. 정확히 내가 원하는 것입니다.

실제로 이것은 매우 유연합니다. 참고 어떻게 ts열은 완전히 모든면에서 누락 된 - 그것이 있기 때문에 DEFAULT값을 내가 나 자신을 돌볼 필요가 없습니다, SQLite는 그냥 어떤 경우에 옳은 일을 할 것입니다.

또한 양쪽에 열을 포함 할 수 있습니다 newold예를 들어, 사용 후 측면과 COALESCE(new.content, old.content)외부에서 SELECT당신이 고정 된 쿼리를 사용하고 새로운 바인딩하는 경우 예 - "그렇지 않으면 기존 내용을 유지, 어떤이 있다면 새로운 내용 삽입"말을 자리 표시 자와 함께 값.


일반적으로 업데이트를 수행하는 경우 ..

  1. 거래 시작
  2. 업데이트를
  3. 행 개수 확인
  4. 0이면 삽입하십시오
  5. 범하다

일반적으로 인서트를하고 있다면

  1. 거래 시작
  2. 삽입을 시도하십시오
  3. 기본 키 위반 오류 확인
  4. 오류가 발생하면 업데이트를 수행하십시오
  5. 범하다

이렇게하면 선택을 피하고 Sqlite에서 거래 적으로 건전합니다.


이 답변은 업데이트되었으므로 아래 주석은 더 이상 적용되지 않습니다.

2018-05-18 스톱 프레스.

SQLite에서 UPSERT 지원! UPSERT 구문이 버전 3.24.0 (보류 중) 인 SQLite에 추가되었습니다!

UPSERT는 INSERT가 고유성 제약 조건을 위반하는 경우 INSERT가 UPDATE 또는 no-op로 작동하게하는 INSERT에 추가 된 특수 구문입니다. UPSERT는 표준 SQL이 아닙니다. SQLite의 UPSERT는 PostgreSQL에서 설정 한 구문을 따릅니다.

여기에 이미지 설명을 입력하십시오

대안 적으로 :

이 작업을 수행하는 또 다른 완전히 다른 방법은 다음과 같습니다. 내 응용 프로그램에서 메모리에서 행을 만들 때 내 메모리 rowID를 long.MaxValue로 설정했습니다. (MaxValue는 ID로 사용되지 않으며 오래 살 수 없습니다 .... rowID가 해당 값이 아닌 경우 이미 데이터베이스에 있어야하므로 MaxValue 인 경우 UPDATE가 필요하며 삽입이 필요합니다. 앱에서 rowID를 추적 할 수있는 경우에만 유용합니다.


나는 이것이 오래된 스레드라는 것을 알고 있지만 늦게 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 개의 쿼리이지만 트릭을 수행하는 것 같습니다. 또한 변경 사항 ()에 대한 호출이 0보다 큰 경우 sqlite가 업데이트 문을 완전히 최적화 할 수 있다는 비전을 가지고 있습니다. 그것이 실제로인지 아닌지는 내 지식을 넘어서는 것이지만 사람은 꿈을 꿀 수 없습니다. ;)

보너스 포인트의 경우이 행을 추가하면 새로 삽입 된 행이든 기존 행이든 행의 ID를 반환합니다.

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

다음은 실제로 여러 상황에서 다르게 작동하는 INSERT 또는 REPLACE 대신 UPSERT (UPDATE 또는 INSERT) 솔루션입니다.

다음과 같이 작동합니다.
1. 동일한 ID를 가진 레코드가 존재하면 업데이트를 시도하십시오.
2. 업데이트가 행을 변경하지 않은 경우 ( NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0)) 레코드를 삽입하십시오.

따라서 기존 레코드가 업데이트되거나 삽입이 수행됩니다.

중요한 세부 사항은 changes () SQL 함수를 사용하여 업데이트 명령문이 기존 레코드에 도달했는지 확인하고 레코드에 도달하지 않은 경우에만 insert 문을 수행하는 것입니다.

한 가지 언급 할 사항은 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;

버전 3.24.0부터 UPSERT는 SQLite에서 지원됩니다.

로부터 문서 :

UPSERT는 INSERT가 고유성 제약 조건을 위반하는 경우 INSERT가 UPDATE 또는 no-op로 작동하게하는 INSERT에 추가 된 특수 구문입니다. UPSERT는 표준 SQL이 아닙니다. SQLite의 UPSERT는 PostgreSQL에서 설정 한 구문을 따릅니다. UPSERT 구문이 버전 3.24.0 (보류 중) 인 SQLite에 추가되었습니다.

UPSERT는 특수한 ON CONFLICT 절이 뒤 따르는 일반적인 INSERT 문입니다.

여기에 이미지 설명을 입력하십시오

이미지 출처 : https://www.sqlite.org/images/syntax/upsert-clause.gif


실제로 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

Aristotle의 답변확장 하면 더미 '싱글 톤'테이블 (단일 행이있는 자신의 창작 테이블)에서 선택할 수 있습니다. 이것은 일부 중복을 피합니다.

또한 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";

내가 아는 가장 좋은 방법은 업데이트를 수행 한 다음 삽입하는 것입니다. "선택 오버 헤드"가 필요하지만 기본 키를 검색하기 때문에 끔찍한 부담이 아닙니다.

테이블 및 필드 이름으로 아래 명령문을 수정하여 원하는 것을 수행 할 수 있어야합니다.

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

누군가가 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);
    });
});
}

그런 다음 프로그래밍 방식으로 트랜잭션을 빌드하십시오.

"값"은 이전에 빌드해야하는 배열이며 테이블에 삽입하거나 업데이트하려는 행을 나타냅니다.

"원격"은 원격 서버와 동기화하기 때문에 참조로 사용한 ID입니다.

SQLite Cordova 플러그인 사용에 대해서는 공식 링크 를 참조하십시오


나는 이것이 당신이 찾고있는 것이라고 생각합니다 : ON CONFLICT clause .

다음과 같이 테이블을 정의하면 :

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

이제 이미 존재하는 ID로 INSERT를 수행하면 SQLite는 자동으로 INSERT 대신 UPDATE를 수행합니다.

Hth ...


이 방법은이 질문에 대한 답변에서 몇 가지 다른 방법을 리믹스하고 CTE (공통 테이블 식)의 사용을 통합합니다. 쿼리를 소개하고 내가 한 일을 왜했는지 설명하겠습니다.

직원 300이있는 경우 직원 300의 성을 DAVIS로 변경하고 싶습니다. 그렇지 않으면 새 직원을 추가합니다.

테이블 이름 : 직원 열 : 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 함수에서 널을 대체하여 사용하려는 기본값을 값이 무엇인지 결정할 수 있습니다.


다음 아리스토텔레스 Pagaltzis 과의 아이디어 COALESCE에서 에릭 B의 답변을 여기에 단지 몇 열을 업데이트하거나 존재하지 않는 경우 전체 행을 삽입 할 수있는 upsert 옵션입니다.

이 경우 제목과 내용을 업데이트하여 기존의 다른 값을 유지하고 이름을 찾을 수없는 경우 제공된 값을 삽입해야합니다.

자동 증가로 가정 id할 때 NOTE 는 NULL이어야합니다 INSERT. 생성 된 기본 키인 경우 COALESCE에도 사용할 수 있습니다 ( Aristotle Pagaltzis comment 참조 ).

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


이 글을 읽고이 "UPSERT"를하기가 쉽지 않다는 것에 실망한 후, 나는 더 조사했다 ...

실제로 SQLITE에서 직접 쉽고 쉽게 수행 할 수 있습니다.

사용하는 대신: INSERT INTO

사용하다: INSERT OR REPLACE INTO

이것은 당신이 원하는 것을 정확하게합니다!


SELECT COUNT(*) FROM table1 WHERE id = 1;

만약 COUNT(*) = 0

INSERT INTO table1(col1, col2, cole) VALUES(var1,var2,var3);

그렇지 않으면 COUNT(*) > 0

UPDATE table1 SET col1 = var4, col2 = var5, col3 = var6 WHERE id = 1;

참고 URL : https://stackoverflow.com/questions/418898/sqlite-upsert-not-insert-or-replace

반응형