[database] SQLite UPSERT / 업데이트 또는 삽입

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    |



답변

이것은 늦은 답변입니다. 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).

그러나 업그레이드 옵션이있는 경우 내 솔루션과 달리 업그레이드 하는 것이 좋습니다. 여기에 게시 된 솔루션은 단일 명령문에서 원하는 동작을 달성합니다. 또한 일반적으로 최신 릴리스와 함께 제공되는 다른 모든 기능, 개선 사항 및 버그 수정을 얻을 수 있습니다.


답변

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


답변

여기에 키 위반이있는 경우에만 작동하는 무차별 대입 ‘무시’가 필요하지 않은 접근 방식이 있습니다. 이 방법에 따라 작품 어떤 업데이트에서 지정한 조건.

이 시도…

-- 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()표현식이 허용되는 모든 곳에서 원하는 / 필요한 모든 것과 조합하여 사용할 수 있습니다 .


답변

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

INSERT OR IGNORE ...
UPDATE ...

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

적절한 해결책은

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

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


답변

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


답변

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

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


답변

옵션 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 추가.