SQLite - UPSERT * não * INSERIR ou SUBSTITUIR

535

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

Inserir processo armazenado de atualização no SQL Server

Existe alguma maneira inteligente de fazer isso no SQLite que eu não tenha pensado?

Basicamente, quero atualizar três das quatro colunas se o registro existir. Se ele não existir, insira o registro com o valor padrão (NUL) da quarta coluna.

O ID é uma chave primária, portanto, sempre haverá um registro para a UPSERT.

(Estou tentando evitar a sobrecarga de SELECT para determinar se preciso atualizar ou inserir obviamente)

Sugestões?


Não posso confirmar essa sintaxe no site SQLite para TABLE CREATE. Eu não construí uma demonstração para testá-la, mas ela não parece ser suportada.

Se fosse, eu tenho três colunas, então seria assim:

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

mas os dois primeiros blobs não causam conflito, apenas o ID causaria. Então, eu usava o Blob1 e o Blob2 não seriam substituídos (conforme desejado)


ATUALIZAÇÕES no SQLite quando os dados de ligação são uma transação completa, o que significa que cada linha enviada a ser atualizada requer: Instruções de Preparação / Ligação / Etapa / Finalização ao contrário do INSERT, que permite o uso da função de redefinição

A vida de um objeto de instrução é mais ou menos assim:

  1. Crie o objeto usando sqlite3_prepare_v2 ()
  2. Vincule valores aos parâmetros do host usando interfaces sqlite3_bind_.
  3. Execute o SQL chamando sqlite3_step ()
  4. Redefina a instrução usando sqlite3_reset (), volte para a etapa 2 e repita.
  5. Destrua o objeto de instrução usando sqlite3_finalize ().

ATUALIZAÇÃO Acho que é lento comparado ao INSERT, mas como ele se compara ao SELECT usando a chave Primária?

Talvez eu deva usar o select para ler a 4ª coluna (Blob3) e, em seguida, usar REPLACE para escrever um novo registro misturando a 4ª coluna original com os novos dados para as 3 primeiras colunas?

Mike Trader
fonte
5
SQLite - UPSERT disponível em pré-lançamento referem: sqlite.1065341.n5.nabble.com/...
gaurav
3
UPSERT disponível na versão 3.24.0 do SQLite
pablo_worker

Respostas:

854

Supondo três colunas na tabela: ID, NAME, ROLE


RUIM: Isso irá inserir ou substituir todas as colunas por novos valores para ID = 1:

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

RUIM: Isso irá inserir ou substituir 2 das colunas ... a coluna NAME será definida como NULL ou o valor padrão:

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

BOM : Use a cláusula de conflito SQLite On no suporte ao UPSERT no SQLite! A sintaxe UPSERT foi adicionada ao SQLite com a versão 3.24.0!

UPSERT é uma adição de sintaxe especial ao INSERT que faz com que o INSERT se comporte como UPDATE ou no-op se o INSERT violar uma restrição de exclusividade. UPSERT não é SQL padrão. O UPSERT no SQLite segue a sintaxe estabelecida pelo PostgreSQL.

insira a descrição da imagem aqui

BOM, mas tendencioso: isso atualizará duas das colunas. Quando o ID = 1 existir, o NAME não será afetado. Quando o ID = 1 não existir, o nome será o padrão (NULL).

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

Isso atualizará duas das colunas. Quando ID = 1 existir, o ROLE não será afetado. Quando o ID = 1 não existe, a função será definida como 'Benchwarmer' em vez do valor padrão.

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (  1, 
            'Susan Bar',
            COALESCE((SELECT role FROM Employee WHERE id = 1), 'Benchwarmer')
          );
Eric B
fonte
33
+1 brilhante! A cláusula de seleção incorporada oferece a flexibilidade de substituir a funcionalidade ON CONFLICT REPLACE padrão, se você precisar combinar / comparar o valor antigo e o novo valor para qualquer campo.
G__
24
Se o Funcionário for referenciado por outras linhas com exclusão em cascata, as outras linhas ainda serão excluídas por substituição.
Don Reba
246
Isso dói. O SQlite precisa de UPSERT.
andig
21
Você poderia explicar por que isso inserirá ou substituirá todas as colunas por novos valores para ID = 1: é considerado ruim no seu primeiro exemplo? O comando que você apresenta visa criar um novo registro com o ID 1, nome John Foo e CEO da função , ou substitua o registro cujo ID é 1, se já estiver lá, com esses dados (assumindo que o id é a chave primária) . Então, por que é ruim se exatamente isso acontece?
OR Mapper
10
@Cornelius: Isso está claro, mas não é o que acontece no primeiro exemplo. O primeiro exemplo tem como objetivo definir à força todas as colunas, o que é exatamente o que acontece, independentemente de o registro ser inserido ou substituído. Então, por que isso é considerado ruim? A resposta vinculada também mostra apenas por que algo ruim pode acontecer ao especificar um subconjunto das colunas, como no seu segundo exemplo; ele não parece elaborar nenhum efeito ruim do que acontece no seu primeiro exemplo, INSERT OR REPLACEenquanto especifica valores para todos colunas.
OR Mapper
134

INSERIR OU SUBSTITUIR NÃO é equivalente a "UPSERT".

Digamos que eu tenha a tabela Employee com os campos id, name and 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")

Crescimento, você perdeu o nome do funcionário número 1. O SQLite o substituiu por um valor padrão.

A saída esperada de um UPSERT seria alterar a função e manter o nome.

gregschlom
fonte
21
-1 de mim, eu tenho medo. Sim, a resposta aceita está errada, mas enquanto a sua resposta aponta o problema, também não é uma resposta. Para uma resposta real, consulte a solução inteligente de Eric B usando uma coalesce((select ..), 'new value')cláusula incorporada . A resposta de Eric precisa de mais votos aqui, eu acho.
Dia
22
De fato. Definitivamente, Eric é a melhor resposta e merece mais votos. Dito isto, acho que, ao apontar o problema, contribuí um pouco para encontrar a boa resposta (a resposta de Eric veio mais tarde e baseia-se nas tabelas sql de amostra na minha resposta). Então, não tenho certeza se eu mereço a -1, mas deixa pra lá :)
gregschlom
6
+1 para compensar o -1 acima. ri muito. Linha do tempo interessante sobre este tópico. Claramente, sua resposta foi feita mais de uma semana antes da de Eric, mas vocês dois responderam à pergunta quase dois anos depois que ela foi feita. +1 para Eric também, embora para elaboração.
Rich
Aqui está uma biblioteca upsert de rubi e uma biblioteca upsert para python
Seamus Abshere
2
@QED não, porque delete + insert (que é uma substituição) é de 2 instruções dml, com seus próprios gatilhos, por exemplo. Não é o mesmo que apenas uma instrução de atualização.
Seb
109

Resposta de Eric B é boa se você deseja preservar apenas uma ou talvez duas colunas da linha existente. Se você deseja preservar muitas colunas, fica muito rápido e complicado.

Aqui está uma abordagem que será bem dimensionada para qualquer quantidade de colunas nos dois lados. Para ilustrá-lo, assumirei o seguinte esquema:

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

Observe, em particular, que nameé a chave natural da linha - idé usada apenas para chaves estrangeiras; portanto, o ponto é que o SQLite selecione o valor do ID ao inserir uma nova linha. Mas ao atualizar uma linha existente com base em suaname , quero que ela continue com o antigo valor de ID (obviamente!).

Consigo um verdadeiro UPSERTcom a seguinte construção:

 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;

A forma exata desta consulta pode variar um pouco. A chave é o uso deINSERT SELECT uma junção externa esquerda, para unir uma linha existente aos novos valores.

Aqui, se uma linha não existir anteriormente, old.idhaverá NULLe o SQLite atribuirá um ID automaticamente, mas se já houver uma linha, old.idterá um valor real e isso será reutilizado. Qual é exatamente o que eu queria.

De fato, isso é muito flexível. Observe como a tscoluna está completamente ausente por todos os lados - porque tem um DEFAULTvalor, o SQLite fará a coisa certa em qualquer caso, então eu não preciso cuidar disso sozinha.

Você também pode incluir uma coluna em ambos os newe oldos lados e, em seguida, usar por exemplo COALESCE(new.content, old.content)no exterior SELECTpara dizer “insert o novo conteúdo se havia alguma, caso contrário, manter o conteúdo antigo” - por exemplo, se você estiver usando uma consulta fixo e são obrigatórias a nova valores com espaços reservados.

Aristóteles Pagaltzis
fonte
13
+1, funciona muito bem, mas adiciona uma WHERE name = "about"restrição SELECT ... AS oldpara acelerar as coisas. Se você tiver mais de 1m de linhas, isso é muito lento.
Bom ponto, +1 no seu comentário. Deixarei isso de fora da resposta, porque adicionar essa WHEREcláusula requer exatamente o tipo de redundância na consulta que eu estava tentando evitar em primeiro lugar quando me deparei com essa abordagem. Como sempre: quando você precisar de desempenho, desnormalize - a estrutura da consulta, neste caso.
Aristóteles Pagaltzis
4
Você pode simplificar de Aristóteles exemplo resume a isto, se você quiser: INSERT OR REPLACE INTO page (id, name, title, content, author) SELECT id, 'about', 'About this site', content, 42 FROM ( SELECT NULL ) LEFT JOIN ( SELECT * FROM page WHERE name = 'about' )
jcox
4
Isso não seria necessário para disparar desnecessariamente ON DELETEquando ele executa uma substituição (ou seja, uma atualização)?
Karakuri
3
Certamente disparará ON DELETEgatilhos. Não sei desnecessariamente. Para a maioria dos usuários, provavelmente seria desnecessário, até indesejado, mas talvez não para todos os usuários. Da mesma forma, também exclui em cascata todas as linhas com chaves estrangeiras na linha em questão - provavelmente um problema para muitos usuários. Infelizmente, o SQLite não tem nada mais perto de um UPSERT real. (Save para fingir com um INSTEAD OF UPDATEgatilho, eu acho.)
Aristóteles Pagaltzis
86

Se você geralmente está fazendo atualizações, eu faria ..

  1. Iniciar uma transação
  2. Faça a atualização
  3. Verifique o número de linhas
  4. Se for 0, insira
  5. Confirmar

Se você geralmente faz inserções, eu

  1. Iniciar uma transação
  2. Experimente uma inserção
  3. Verifique se há erro de violação da chave primária
  4. se tivermos um erro, faça a atualização
  5. Confirmar

Dessa forma, você evita a seleção e está transacionalmente sólido no Sqlite.

Sam Saffron
fonte
Obrigado, perguntei a este @ stackoverflow.com/questions/2717590/… . +1 de mim! =)
Alix Axel
4
Se você estiver indo para verificar o número de linhas usando sqlite3_changes () na terceira etapa, certifique-se de não usar o identificador de banco de dados de vários threads para modificações.
Linulin
3
O seguinte não seria menos prolixo ainda com o mesmo efeito: 1) selecione a tabela de formulários de identificação em que id = 'x' 2) if (ResultSet.rows.length == 0) atualize a tabela em que id = 'x';
Florin
20
Eu realmente gostaria inserção ou atualização foi parte da linguagem
Florin
Isso acabou funcionando melhor para mim, tentar fazer SUBSTITUIR INTO ou sempre e inserir / atualizar estava prejudicando meu desempenho quando eu fiz atualizações principalmente em vez de inserções.
PherricOxide
83

Esta resposta foi atualizada e, portanto, os comentários abaixo não se aplicam mais.

2018-05-18 STOP PRESS.

Suporte UPSERT no SQLite!A sintaxe UPSERT foi adicionada ao SQLite com a versão 3.24.0 (pendente)!

UPSERT é uma adição de sintaxe especial ao INSERT que faz com que o INSERT se comporte como UPDATE ou no-op se o INSERT violar uma restrição de exclusividade. UPSERT não é SQL padrão. O UPSERT no SQLite segue a sintaxe estabelecida pelo PostgreSQL.

insira a descrição da imagem aqui

alternativamente:

Outra maneira completamente diferente de fazer isso é: No meu aplicativo, defino meu ID da linha na memória como long.MaxValue quando crio a linha na memória. (MaxValue nunca será usado como um ID, você não viverá o suficiente ... Então, se rowID não for esse valor, ele já deverá estar no banco de dados, portanto, precisará de um UPDATE se for MaxValue e precisará de uma inserção. Isso só é útil se você puder rastrear os IDs de linha no seu aplicativo.

AnthonyLambert
fonte
5
Amém. Simples é melhor que complexo. Isso parece um pouco mais simples que a resposta aceita.
kkurian
4
Eu pensei que você não poderia fazer INSERIR EM ... ONDE em sqlite? Este é um erro de sintaxe para mim em sqlite3
Charlie Martin
5
@CharlieMartin está correto. Essa sintaxe é inválida para o SQLite - que é o que o OP solicitou. Uma WHEREcláusula não pode ser anexada a uma INSERTinstrução: sqlite-insert ...
colm.anseo 24/10
4
Esta resposta me fez perder muito tempo, a pergunta é sobre o SQLITE e eu não sabia que INSERT WHERE não era suportado no sqite, por favor
Miquel
3
@colminator e outros: Eu consertei sua instrução SQL usando sua sugestão.
F Lekschas
62

Sei que esse é um encadeamento antigo, mas tenho trabalhado no sqlite3 ultimamente e criei esse método que melhor se adequava às minhas necessidades de gerar consultas parametrizadas dinamicamente:

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

Ainda são 2 consultas com uma cláusula where na atualização, mas parece fazer o truque. Eu também tenho essa visão de que o sqlite pode otimizar completamente a instrução update se a chamada para changes () for maior que zero. Se realmente faz ou não, isso está além do meu conhecimento, mas um homem pode sonhar, não pode? ;)

Para pontos de bônus, você pode acrescentar esta linha que retorna o ID da linha, seja uma linha recém-inserida ou uma linha existente.

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;
Chris Stavropoulos
fonte
+ 1. Exatamente o que eu estava tentando substituir fazendo uma conversão de algum código TSQL do SQL Server. Obrigado. SQL que eu estava tentando substituir era comoUpdate <statement> If @@ROWCOUNT=0 INSERT INTO <statement>
DarrenMB 30/11
14

Aqui está uma solução que realmente é um UPSERT (UPDATE ou INSERT) em vez de um INSERT OR REPLACE (que funciona de maneira diferente em muitas situações).

Funciona assim:
1. Tente atualizar se existir um registro com o mesmo ID.
2. Se a atualização não alterou nenhuma linha ( NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0)), insira o registro.

Portanto, um registro existente foi atualizado ou uma inserção será executada.

O detalhe importante é usar a função SQL changes () para verificar se a instrução update atingiu algum registro existente e somente executar a instrução insert se não atingiu nenhum registro.

Uma coisa a mencionar é que a função changes () não retorna as alterações realizadas pelos gatilhos de nível inferior (consulte http://sqlite.org/lang_corefunc.html#changes ), portanto, leve isso em consideração.

Aqui está o SQL ...

Atualização de teste:

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

Inserto de teste:

--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;
David Liebeherr
fonte
2
Esta parece ser a melhor solução para mim do que as do Eric. No entanto INSERT INTO Contact (Id, Name) SELECT 3, 'Bob' WHERE changes() = 0;, também deve funcionar.
bkausbk
Obrigado homem, ele também funciona em WebSQL (usando Cordova e SQLite plugin)
Zappescu
11

A partir da versão 3.24.0, o UPSERT é suportado pelo SQLite.

A partir da documentação :

UPSERT é uma adição de sintaxe especial ao INSERT que faz com que o INSERT se comporte como UPDATE ou no-op se o INSERT violar uma restrição de exclusividade. UPSERT não é SQL padrão. O UPSERT no SQLite segue a sintaxe estabelecida pelo PostgreSQL. A sintaxe UPSERT foi adicionada ao SQLite com a versão 3.24.0 (pendente).

Um UPSERT é uma instrução INSERT comum que é seguida pela cláusula especial ON CONFLICT

insira a descrição da imagem aqui

Fonte da imagem: https://www.sqlite.org/images/syntax/upsert-clause.gif

Lukasz Szozda
fonte
8

Você pode realmente fazer um upsert no SQLite, apenas parece um pouco diferente do que você está acostumado. Seria algo como:

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

Expandindo a resposta de Aristóteles, você pode SELECIONAR a partir de uma tabela fictícia 'singleton' (uma tabela de sua própria criação com uma única linha). Isso evita alguma duplicação.

Eu também mantive o exemplo portátil em MySQL e SQLite e usei uma coluna 'date_added' como um exemplo de como você pode definir uma coluna somente na primeira vez.

 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";
Comunidade
fonte
4

A melhor abordagem que conheço é fazer uma atualização, seguida por uma inserção. A "sobrecarga de uma seleção" é necessária, mas não é um fardo terrível, pois você está pesquisando na chave primária, que é rápida.

Você deve poder modificar as instruções abaixo com os nomes de tabela e campo para fazer o que quiser.

--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
                );
JosephStyons
fonte
7
Caminho para complicado.
Sexta-
Eu acho que não é uma boa ideia, porque você precisa fazer o pedido duas vezes para o mecanismo de banco de dados.
Ricardo da Rocha Vitor
3

Se alguém quiser ler minha solução para SQLite em Cordova, obtive este método js genérico graças à resposta @david acima.

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

Portanto, escolha primeiro os nomes das colunas com esta função:

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

Em seguida, crie as transações programaticamente.

"Valores" é uma matriz que você deve criar antes e representa as linhas que deseja inserir ou atualizar na tabela.

"remoteid" é o ID que eu usei como referência, pois estou sincronizando com meu servidor remoto.

Para o uso do plug-in SQLite Cordova, consulte o link oficial

Zappescu
fonte
2

Esse método corrige alguns dos outros métodos da resposta para esta pergunta e incorpora o uso de CTE (Common Table Expressions). Vou apresentar a consulta e explicar por que fiz o que fiz.

Gostaria de alterar o sobrenome do funcionário 300 para DAVIS se houver um funcionário 300. Caso contrário, adicionarei um novo funcionário.

Nome da tabela: funcionários Colunas: id, first_name, last_name

A consulta é:

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
;

Basicamente, usei o CTE para reduzir o número de vezes que a instrução select deve ser usada para determinar os valores padrão. Como se trata de um CTE, basta selecionar as colunas que queremos da tabela e a instrução INSERT usa isso.

Agora você pode decidir quais padrões deseja usar substituindo os valores nulos, na função COALESCE pelos valores que devem ser.

Dodzi Dzakuma
fonte
2

Seguindo Aristóteles Pagaltzis e a ideia da respostaCOALESCE de Eric B , aqui está uma opção melhor: atualizar apenas algumas colunas ou inserir linha completa, se ela não existir.

Nesse caso, imagine que o título e o conteúdo devem ser atualizados, mantendo os outros valores antigos ao existir e inserindo os valores fornecidos quando o nome não for encontrado:

OBSERVAÇÃO id é forçado a ser NULL quando, INSERTcomo deveria ser o incremento automático. Se for apenas uma chave primária gerada, COALESCEtambém poderá ser usada (consulte o comentário de Aristóteles 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;

Portanto, a regra geral seria: se você deseja manter valores antigos, use COALESCE, quando desejar atualizar valores, usenew.fieldname

Miquel
fonte
COALESCE(old.id, new.id)está definitivamente errado com uma chave de incremento automático. E embora “mantenha a maior parte da linha inalterada, exceto onde os valores estão ausentes” parece um caso de uso que alguém possa ter, não acho que é isso que as pessoas procuram quando estão procurando um UPSERT.
Aristóteles Pagaltzis
@AristotlePagaltzis pede desculpas se eu estiver errado, não estou usando acréscimos automáticos. O que eu estou procurando com esta consulta é atualizar apenas algumas colunas ou inserir linha completa com os valores fornecidos, caso isso não exista . Joguei com sua consulta e não consegui alcançá-la ao inserir: as colunas selecionadas na oldtabela foram atribuídas a NULL, e não aos valores fornecidos new. Esta é a razão para usar COALESCE. Eu não sou um especialista em SQLite, eu tenho vindo a testar esta consulta e parece funcionar para o caso, eu seria muito obrigado se você poderia me aponte para a solução com autoincrements
Miquel
2
No caso de uma chave de incremento automático, você deseja inserir NULLcomo a chave, porque isso instrui o SQLite a inserir o próximo valor disponível.
Aristóteles Pagaltzis
Não estou dizendo que você não deveria estar fazendo o que está fazendo (se precisar, precisa), apenas que não é realmente uma resposta para a pergunta aqui. UPSERT geralmente significa que você tem uma linha que deseja armazenar na tabela e simplesmente não sabe se você já possui uma linha correspondente para inserir os valores ou se precisa inseri-los como uma nova linha. Seu caso de uso é que, se você já possui uma linha correspondente, deseja ignorar a maioria dos valores da nova linha . Tudo bem, não é apenas a pergunta que foi feita.
Aristóteles Pagaltzis
1

Eu acho que isso pode ser o que você está procurando: cláusula ON CONFLICT .

Se você definir sua tabela assim:

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

Agora, se você fizer um INSERT com um ID que já exista, o SQLite fará automaticamente UPDATE em vez de INSERT.

Hth ...

kmelvn
fonte
6
Eu não acho que isso funciona, ele vai acabar com as colunas ausentes da instrução de inserção
Sam Saffron
3
@ Mosor: -1 de mim, desculpe. É o mesmo que emitir uma REPLACEdeclaração.
Alix Axel
7
-1 porque isso exclui e depois insere se a chave primária já existe.
Sexta-
1

Se você não se importa em fazer isso em duas operações.

Passos:

1) Adicione novos itens com "INSERIR OU IGNORAR"

2) Atualize itens existentes com "UPDATE"

A entrada para as duas etapas é a mesma coleção de itens novos ou com capacidade de atualização. Funciona bem com itens existentes que não precisam de alterações. Eles serão atualizados, mas com os mesmos dados e, portanto, o resultado líquido não será alterado.

Claro, mais lento, etc. Ineficiente. Sim.

Fácil de escrever o sql, manter e entender? Definitivamente.

É uma troca a considerar. Funciona muito bem para pequenas upserts. Funciona muito bem para aqueles que não se importam em sacrificar a eficiência pela manutenção do código.

sapbucket
fonte
Nota para todos: INSERIR OU SUBSTITUIR NÃO é sobre o que é este post. INSERIR OU SUBSTITUIR criará uma NOVA linha na sua tabela, com um novo ID. Isso não é uma atualização.
sapbucket
-2

Tendo acabado de ler este tópico e desapontado por não ter sido fácil chegar a este "UPSERT", investiguei mais ...

Você pode fazer isso diretamente e facilmente no SQLITE.

Ao invés de usar: INSERT INTO

Usar: INSERT OR REPLACE INTO

Isso faz exatamente o que você quer que ele faça!

SBB
fonte
21
-1 INSERT OR REPLACEnão é um UPSERT. Veja a "resposta" de gregschlom pelo motivo. A solução de Eric B realmente funciona e precisa de alguns votos positivos.
Dia
-4
SELECT COUNT(*) FROM table1 WHERE id = 1;

E se COUNT(*) = 0

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

senão se COUNT(*) > 0

UPDATE table1 SET col1 = var4, col2 = var5, col3 = var6 WHERE id = 1;
mjb
fonte
Isso é muito complicado, SQL pode lidar com isso muito bem em uma consulta
Robin Kanters