Inserir, na atualização duplicado em PostgreSQL?
-
12-09-2019 - |
Pergunta
Alguns meses atrás eu aprendi com uma resposta sobre estouro de pilha como executar várias atualizações ao mesmo tempo no MySQL usando a seguinte sintaxe:
INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);
Eu já mudei para PostgreSQL e, aparentemente, isso não é correto. Ele está se referindo a todas as tabelas corretas assim que eu supor que é uma questão de diferentes palavras-chave que está sendo usado, mas eu não tenho certeza de onde na documentação do PostgreSQL este é coberto.
Para esclarecer, eu quero inserir várias coisas e se eles já existem para atualizá-los.
Solução
PostgreSQL desde a versão 9.5 tem UPSERT sintaxe, com < strong> ON CONFLICT cláusula. com a seguinte sintaxe (semelhante ao MySQL)
INSERT INTO the_table (id, column_1, column_2)
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE
SET column_1 = excluded.column_1,
column_2 = excluded.column_2;
Como pesquisar arquivos de grupo de e-mail do PostgreSQL para obter "Upsert" leva à descoberta de um exemplo de fazer o que você talvez queira fazer, no manual:
Exemplo 38-2. Exceções com UPDATE / INSERIR Este exemplo utiliza excepção de manuseamento para executar qualquer UPDATE ou INSERT, conforme apropriado:
CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
LOOP
-- first try to update the key
-- note that "a" must be unique
UPDATE db SET b = data WHERE a = key;
IF found THEN
RETURN;
END IF;
-- not there, so try to insert the key
-- if someone else inserts the same key concurrently,
-- we could get a unique-key failure
BEGIN
INSERT INTO db(a,b) VALUES (key, data);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing, and loop to try the UPDATE again
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');
Há possivelmente um exemplo de como fazer isso em massa, usando CTEs em 9.1 e acima, na hackers mailing List :
WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;
de a_horse_with_no_name resposta para um exemplo mais claro.
Outras dicas
Aviso: este não é seguro se executado a partir de várias sessões ao mesmo tempo (veja advertências abaixo)
.Outra forma inteligente de fazer uma "UPSERT" no PostgreSQL é fazer duas instruções UPDATE / INSERT seqüenciais que são cada um projetado para ter sucesso ou não têm nenhum efeito.
UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
SELECT 3, 'C', 'Z'
WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);
A atualização será bem sucedida se uma linha com "id = 3" já existe, caso contrário ele não tem nenhum efeito.
A INSERÇÃO só terá sucesso se fila com "id = 3" ainda não existir.
Você pode combinar estes dois em uma única string e executá-los ambos com uma única instrução SQL executar a partir da aplicação. Executá-los juntos em uma única transação é altamente recomendado.
Isso funciona muito bem quando executado isoladamente ou em uma tabela bloqueada, mas está sujeito a condições de corrida que significa que ainda pode falhar com erro de chave duplicada, se uma linha é inserida simultaneamente, ou pode terminar sem linha inserida quando uma linha é eliminado em simultâneo. Uma transação SERIALIZABLE
no PostgreSQL 9.1 ou superior vai lidar com isso de forma confiável com o custo de uma elevada taxa de falha de serialização, o que significa que você terá que repetir muito. Consulte porque é upsert tão complicado , que discute neste caso com mais detalhes.
Esta abordagem é também sujeita a atualizações perdidas em isolamento read committed
a menos que a aplicação verifica a contagem de linhas afetadas e verifica que tanto o insert
ou o update
afetou uma linha .
Com PostgreSQL 9.1 isto pode ser conseguido usando um gravável CTE ( mesa comum expressão ) :
WITH new_values (id, field1, field2) as (
values
(1, 'A', 'X'),
(2, 'B', 'Y'),
(3, 'C', 'Z')
),
upsert as
(
update mytable m
set field1 = nv.field1,
field2 = nv.field2
FROM new_values nv
WHERE m.id = nv.id
RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1
FROM upsert up
WHERE up.id = new_values.id)
Ver essas entradas no blog:
Note que esta solução faz não evitar uma violação de chave única, mas não é vulnerável a atualizações perdidas.
Veja a acompanhamento por Craig Ringer on dba.stackexchange.com
No PostgreSQL 9.5 e mais recente você pode usar INSERT ... ON CONFLICT UPDATE
.
Consulte a documentação .
A MySQL INSERT ... ON DUPLICATE KEY UPDATE
pode ser diretamente reformulada a um ON CONFLICT UPDATE
. Nem é a sintaxe SQL padrão, ambos são extensões específicas do banco de dados. Há boas razões MERGE
não foi utilizado para este , uma nova sintaxe não foi criado apenas por diversão. (Sintaxe do MySQL também tem questões que significa que não foi adotada diretamente).
por exemplo. dada configuração:
CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);
a consulta MySQL:
INSERT INTO tablename (a,b,c) VALUES (1,2,3)
ON DUPLICATE KEY UPDATE c=c+1;
torna-se:
INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;
As diferenças:
-
Você deve especificar o nome da coluna (ou nome de restrição exclusiva) a utilizar para a verificação de exclusividade. Essa é a
ON CONFLICT (columnname) DO
-
O
SET
palavra-chave deve ser utilizado, como se isso fosse uma declaraçãoUPDATE
normais
Ele tem algumas características interessantes também:
-
Você pode ter uma cláusula
WHERE
em seuUPDATE
(permitindo que você efetivamente transformarON CONFLICT UPDATE
emON CONFLICT IGNORE
para determinados valores) -
Os valores propostos-para-inserção estão disponíveis como a linha-
EXCLUDED
variável, que tem a mesma estrutura que a tabela de destino. Você pode obter os valores originais na tabela usando o nome da tabela. Portanto, neste casoEXCLUDED.c
será10
(porque isso é o que nós tentamos inserir) e"table".c
será3
porque esse é o valor atual na tabela. Você pode usar um ou ambos nas expressõesSET
e cláusulaWHERE
.
Para o fundo em upsert ver Como UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) no PostgreSQL?
Eu estava procurando a mesma coisa quando eu vim aqui, mas a falta de um genérico "upsert" função me botherd um pouco então eu pensei que você poderia simplesmente passar a atualização e inserção de SQL como argumentos de que forma a função do manual
que ficaria assim:
CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
RETURNS VOID
LANGUAGE plpgsql
AS $$
BEGIN
LOOP
-- first try to update
EXECUTE sql_update;
-- check if the row is found
IF FOUND THEN
RETURN;
END IF;
-- not found so insert the row
BEGIN
EXECUTE sql_insert;
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing and loop
END;
END LOOP;
END;
$$;
e, talvez, fazer o que você inicialmente queria fazer, lote "upsert", você poderia usar Tcl para dividir os SQL_UPDATE e loop as atualizações individuais, o hit preformance será muito pequeno ver http://archives.postgresql.org/pgsql-performance/2006-04/msg00557.php
o maior custo é executar a consulta do seu código, no banco de dados do lado do custo de execução é muito menor
Não existe um comando simples de fazê-lo.
A abordagem mais correta é a função de uso, como o de docs .
Outra solução (embora não tão seguro) é fazer atualização com o retorno, verificação de quais linhas foram atualizações, e inserir o resto deles
Algo ao longo das linhas de:
update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;
assumindo id: 2 foi devolvido:
insert into table (id, column) values (1, 'aa'), (3, 'cc');
Claro que vai socorrer mais cedo ou mais tarde (em ambiente concorrente), como não há condição de corrida clara aqui, mas normalmente ele vai trabalhar.
Aqui está um artigo mais longo e mais abrangente sobre o tópico .
Pessoalmente, eu configurar uma "regra" ligado à instrução de inserção. Digamos que você tinha um "dns" mesa que o DNS sucessos gravados por cliente em uma base per-time:
CREATE TABLE dns (
"time" timestamp without time zone NOT NULL,
customer_id integer NOT NULL,
hits integer
);
Você queria ser capaz de linhas re-inserção com valores atualizados, ou criá-los se eles já não existem. Digitado na customer_id eo tempo. Algo parecido com isto:
CREATE RULE replace_dns AS
ON INSERT TO dns
WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time")
AND (dns.customer_id = new.customer_id))))
DO INSTEAD UPDATE dns
SET hits = new.hits
WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));
Update: Este tem o potencial de falhar se inserções simultâneas estão acontecendo, como ele irá gerar exceções unique_violation. No entanto, a transação não-terminado vai continuar e ter sucesso, e você só precisa repetir a operação interrompida.
No entanto, se há toneladas de inserções acontecendo o tempo todo, você vai querer colocar um bloqueio de tabela em torno das instruções de inserção: bloqueio AÇÃO ROW EXCLUSIVE irá evitar quaisquer operações que possam inserir, excluir ou atualizar linhas em sua tabela de destino. No entanto, as atualizações que não atualizam a chave única são seguros, por isso, se você nenhuma operação vai fazer isso, use os bloqueios de aconselhamento em seu lugar.
Além disso, o comando COPY não usa regras, por isso, se você está inserindo com cópia, você vai precisar usar gatilhos vez.
I personalizado "upsert" função acima, se você deseja inserir e REPLACE:
`
CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)
RETURNS void AS
$BODY$
BEGIN
-- first try to insert and after to update. Note : insert has pk and update not...
EXECUTE sql_insert;
RETURN;
EXCEPTION WHEN unique_violation THEN
EXECUTE sql_update;
IF FOUND THEN
RETURN;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION upsert(text, text)
OWNER TO postgres;`
E depois de executar, fazer algo como isto:
SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)
É importante colocar double dollar-vírgula para evitar erros do compilador
- verificar a velocidade ...
Semelhante a resposta mais popular, mas funciona um pouco mais rápido:
WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)
Eu tenho o mesmo problema para gerenciar as configurações da conta, como pares de valores de nome. Os critérios do projeto é que diferentes clientes podem ter configurações diferentes conjuntos.
A minha solução, semelhante a JWP é apagar a granel e substituir, gerando o registro merge dentro de sua aplicação.
Isso é muito à prova de balas, independente de plataforma e uma vez que existem nunca mais do que cerca de 20 configurações por cliente, este é apenas 3 chamadas carga db bastante baixos -. Provavelmente o método mais rápido
A alternativa de atualizar linhas individuais - a verificação de exceções, em seguida, inserindo - ou alguma combinação é o código horroroso, lento e muitas vezes quebra porque (como mencionado acima) não padrão SQL manipulação de exceção mudando de db para db - ou até mesmo uma versão para outra .
#This is pseudo-code - within the application:
BEGIN TRANSACTION - get transaction lock
SELECT all current name value pairs where id = $id into a hash record
create a merge record from the current and update record
(set intersection where shared keys in new win, and empty values in new are deleted).
DELETE all name value pairs where id = $id
COPY/INSERT merged records
END TRANSACTION
De acordo com a documentação do PostgreSQL da declaração INSERT
, manipulação o caso ON DUPLICATE KEY
não é suportado. Essa parte da sintaxe é uma extensão do MySQL proprietário.
CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
RETURNS boolean AS
$BODY$
BEGIN
UPDATE users SET name = _name WHERE id = _id;
IF FOUND THEN
RETURN true;
END IF;
BEGIN
INSERT INTO users (id, name) VALUES (_id, _name);
EXCEPTION WHEN OTHERS THEN
UPDATE users SET name = _name WHERE id = _id;
END;
RETURN TRUE;
END;
$BODY$
LANGUAGE plpgsql VOLATILE STRICT
Eu uso essa função merge
CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
RETURNS void AS
$BODY$
BEGIN
IF EXISTS(SELECT a FROM tabla WHERE a = key)
THEN
UPDATE tabla SET b = data WHERE a = key;
RETURN;
ELSE
INSERT INTO tabla(a,b) VALUES (key, data);
RETURN;
END IF;
END;
$BODY$
LANGUAGE plpgsql
Para integrar pequenos conjuntos, usando a função acima é bom. No entanto, se você estiver mesclando grandes quantidades de dados, eu sugiro olhar em http: //mbk.projects.postgresql .org
O actual melhor prática que eu estou ciente de é:
- copiar dados novos / atualizados em tabela temporária (certeza, ou você pode fazer inserir se o custo é ok)
- adquirir a trava [opcional] (Advisory é preferível bloqueios de tabela, IMO)
- Mesclar. (A parte divertida)
Atualização irá retornar o número de linhas modificadas. Se você usar JDBC (Java), você pode então verificar este valor contra 0 e, se há linhas foram afetadas, INSERT fogo em seu lugar. Se você usar alguma outra linguagem de programação, talvez o número de linhas modificadas ainda pode ser obtido, a documentação de verificação.
Esta pode não ser tão elegante, mas você tem SQL muito mais simples que é mais simples de se usar a partir do código de chamada. Diferentemente, se você escrever o script de linha de dez em PL / PSQL, você provavelmente deve ter um teste de unidade de um ou outro tipo apenas para isso sozinho.
Editar: isso não funcionar como esperado. Ao contrário da resposta aceita, isso produz violações de chave única quando dois processos chamar repetidamente upsert_foo
simultaneamente.
Eureka! Eu descobri uma maneira de fazê-lo em uma consulta: uso UPDATE ... RETURNING
para testar se quaisquer linhas foram afetadas:
CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);
CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;
CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
INSERT INTO foo
SELECT $1, $2
WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;
O UPDATE
tem que ser feito em um procedimento separado, porque, infelizmente, este é um erro de sintaxe:
... WHERE NOT EXISTS (UPDATE ...)
Agora, ele funciona como desejado:
SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');