SQLite UPSERT / 업데이트 또는 삽입


102

SQLite 데이터베이스에 대해 UPSERT / INSERT 또는 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=?"). "에"나에게 단어의 구문 오류를 제공
바스티안 보이트

1
@BastianVoigt Android의 다양한 버전에 설치된 SQLite3 라이브러리가 3.24.0보다 오래 되었기 때문입니다. 참조 : developer.android.com/reference/android/database/sqlite/… 안타깝게도 Android 또는 iOS에서 SQLite3 (또는 기타 시스템 라이브러리)의 새로운 기능이 필요합니다. 특정 버전의 SQLite를 번들로 제공해야합니다. 시스템에 의존하는 대신 응용 프로그램이 설치되었습니다.
prapin

UPSERT가 아니라 삽입을 먼저 시도하기 때문에 INDATE에 가깝지 않습니까? ;)
Mark A. Donohoe

@BastianVoigt, 3.24.0 이전 버전에 대한 아래의 답변 (위의 질문에 링크 됨)을 참조하십시오.
Mark A. Donohoe

105

Q & A 스타일

글쎄, 몇 시간 동안 문제를 조사하고 해결 한 후, 테이블의 구조와 무결성을 유지하기 위해 활성화 된 외래 키 제한이 있는지 여부에 따라이를 수행하는 두 가지 방법이 있음을 발견했습니다. 제 상황에있는 사람들에게 시간을 절약하기 위해 이것을 깨끗한 형식으로 공유하고 싶습니다.


옵션 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 (user_name = 'steven'인 플레이어에서 id 선택) 인 경우 하나의 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이 아닌지 확인한 다음 삽입을 수행합니다.
Tom Spencer

그것도 꽤 좋은 접근 방식입니다. 유일한 작은 단점은 "upsert"에 대한 SQL이 하나뿐이라는 것입니다.
bgusach 2013

2
마지막 코드 샘플의 update 문에서 user_name을 다시 설정할 필요가 없습니다. 나이를 설정하는 것으로 충분합니다.
Serg Stetsuk

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이므로 명령문 의 Where절이 Inserttrue로 평가되고 지정된 데이터와 함께 새 행이 삽입됩니다.

(가) 경우 Update 업데이 트를 기존 행을, 다음 Changes()1 (또는 더 정확하게, 더 이상의 행이 업데이트 된 제로가 아닌 경우), 그래서에서 '어디서'절 = Insert지금은 false로 평가하고, 따라서 더 삽입은 발생하지 않습니다.

이것의 장점은 무차별 대입이 필요하지 않으며 불필요하게 삭제 한 다음 데이터를 다시 삽입하여 외래 키 관계에서 다운 스트림 키를 엉망으로 만들 수 있다는 것입니다.

또한 표준 Where조항 일 뿐이 므로 키 위반뿐 아니라 사용자가 정의한 모든 항목을 기반으로 할 수 있습니다. 마찬가지로 Changes()표현식이 허용되는 모든 곳에서 원하는 / 필요한 모든 것과 조합하여 사용할 수 있습니다 .


1
이것은 나를 위해 잘 작동했습니다. 이 솔루션은 모든 INSERT OR REPLACE 예제와 함께 다른 곳에서는 본 적이 없으며 내 사용 사례에 훨씬 더 유연합니다.
csab

@MarqueIV 및 업데이트하거나 삽입해야하는 두 개의 항목이있는 경우 어떻게합니까? 예를 들어, 전나무가 업데이트되었고 두 번째는 존재하지 않습니다. 이 경우에 Changes() = 0반환됩니다 거짓 과 두 행 할 것 INSERT 또는 REPLACE
안드리 안토 노프

일반적으로 UPSERT는 하나의 레코드에서 작동해야합니다. 둘 이상의 레코드에서 작동하고 있음을 알고 있다고 말하는 경우 그에 따라 카운트 확인을 변경하십시오.
Mark A. Donohoe

나쁜 점은 행이 존재하면 행이 변경되었는지 여부에 관계없이 update 메서드를 실행해야한다는 것입니다.
지미

1
그게 왜 나쁜가요? 그리고 데이터가 변경되지 않았다면 왜 UPSERT처음에 전화 를하십니까? 그러나 그렇다고하더라도, 그것은의 좋은 설정, 업데이트가 발생하는 일이 Changes=1그렇지 않으면 INSERT문 것 당신이 그것을 원하지 않아요 잘못 화재.
Mark A. Donohoe 2019

25

제시된 모든 답변의 문제는 트리거 (및 아마도 다른 부작용)를 고려하지 않은 것입니다. 같은 솔루션

INSERT OR IGNORE ...
UPDATE ...

행이 존재하지 않으면 두 트리거가 모두 실행됩니다 (삽입 및 업데이트).

적절한 해결책은

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

이 경우 하나의 명령문 만 실행됩니다 (행이 존재하는지 여부).


1
너의 의도를 알 겠어. 내 질문을 업데이트하겠습니다. 그건 그렇고, UPDATE OR IGNORE행이 발견되지 않으면 업데이트가 중단 되지 않기 때문에 왜 필요한지 모르겠습니다 .
bgusach

1
가독성? Andy의 코드가 무엇을하는지 한눈에 볼 수 있습니다. 당신의 bgusach 나는 알아 내기 위해 잠시 공부해야했다.
Brandan 2016 년

6

고유 키 및 기타 키를 중계하지 않는 구멍이없는 순수 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); 

이것은 @fiznool이 그의 의견에서 제안한 것과 동일합니다 (하지만 그의 해결책을 찾을 것입니다). 괜찮고 실제로 잘 작동하지만 고유 한 SQL 문이 없습니다. PK 또는 기타 고유 키를 기반으로하지 않는 UPSERT는 나에게 거의 또는 전혀 의미가 없습니다.
bgusach

4

user_name 고유 제약 조건에 ON CONFLICT REPLACE 절을 추가 한 다음 INSERT 만하면 충돌 발생시 수행 할 작업을 파악하기 위해 SQLite에 남겨 둘 수도 있습니다. 참조 : https://sqlite.org/lang_conflict.html .

또한 삭제 트리거에 관한 문장에 유의하십시오. REPLACE 충돌 해결 전략이 제약 조건을 충족하기 위해 행을 삭제하면 반복 트리거가 활성화 된 경우에만 삭제 트리거가 실행됩니다.


1

옵션 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';

트리거 관련

주의 : 어떤 트리거가 호출되는지 확인하기 위해 테스트하지는 않았지만 다음을 가정 합니다.

행이 존재하지 않는 경우

  • 삽입하기 전에
  • INSTEAD OF를 사용하여 INSERT
  • 삽입 후
  • 업데이트 전
  • 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.