Queda da coluna do PostgreSQL 9.6 e efeitos colaterais nas funções SQL com CTEs

15

Se eu tivesse uma tabela com 3 colunas - digamos A, B e D - e tivesse que introduzir uma nova - digamos C para substituir a posição atual de D. Eu usaria o seguinte método:

  1. Introduzir 2 novas colunas como C e D2.
  2. Copie o conteúdo de D para D2.
  3. Excluir D.
  4. Renomeie D2 para D.

A nova ordem seria A, B, C e D.

Eu pensei que essa era uma prática legítima, pois (até agora) ela não produziu problemas.

No entanto, hoje me deparei com um problema quando uma função que executava uma instrução na mesma tabela retornava o seguinte erro:

table row type and query-specified row type do not match

E o seguinte detalhe:

Query provides a value for a dropped column at ordinal position 13

Tentei reiniciar o PostgreSQL, executando VACUUM FULLe, finalmente, excluindo e recriando a função, conforme sugerido aqui e aqui, mas essas soluções não funcionaram (exceto pelo fato de tentarem resolver uma situação em que uma tabela do sistema foi alterada).

Tendo o luxo de trabalhar com um banco de dados muito pequeno, eu o exportei, excluí-o e reimportei-o, e isso corrigiu o problema com a minha função.


Eu estava ciente do fato de que não se deve mexer com a ordem natural das colunas modificando as tabelas do sistema (sujar as mãos compg_attribute etc.), como visto aqui:

É possível alterar a ordem natural das colunas no Postgres?

A julgar pelo erro gerado pela minha função, agora percebo que mudar a ordem das colunas com o meu método também é um não-não. Alguém pode esclarecer por que o que estou fazendo também está errado?


A versão do Postgres é 9.6.0.

Aqui está a função:

CREATE OR REPLACE FUNCTION "public"."__post_users" ("facebookid" text, "useremail" text, "username" text) RETURNS TABLE (authentication_code text, id integer, key text, stripe_id text) AS '

-- First, select the user:
WITH select_user AS
(SELECT
users.id
FROM
users
WHERE
useremail = users.email),

-- Second, update the user (if user exists):
update_user AS
(UPDATE
users
SET
authentication_code = GEN_RANDOM_UUID(),
authentication_date = current_timestamp,
facebook_id = facebookid
WHERE EXISTS (SELECT * FROM select_user)
AND
useremail = users.email
RETURNING
users.authentication_code,
users.id,
users.key,
users.stripe_id),

-- Third, insert the user (if user does not exist):
insert_user AS
(INSERT INTO
users (authentication_code, authentication_date, email, key, name, facebook_id)
SELECT
GEN_RANDOM_UUID(),
current_timestamp,
useremail,
GEN_RANDOM_UUID(),
COALESCE(username, SUBSTRING(useremail FROM ''([^@]+)'')),
facebookid
WHERE NOT EXISTS (SELECT * FROM select_user)
RETURNING
users.authentication_code,
users.id,
users.key,
users.stripe_id)

-- Finally, select the authentication code, ID, key and Stripe ID:
SELECT
*
FROM
update_user
UNION ALL
SELECT
*
FROM
insert_user' LANGUAGE "sql" COST 100 ROWS 1
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER

Eu executei a renomeação / reordenação nas duas colunas facebook_ide stripe_id(uma nova coluna foi adicionada antes delas, que é o motivo da renomeação, mas não é tocada por esta consulta).

Ter as colunas em uma determinada ordem é totalmente desinteressante para a ordem. No entanto, a razão para fazer esta pergunta é preocupante, pois uma simples renomeação e exclusão de uma coluna pode desencadear problemas reais para alguém que usa funções no modo de produção (como aconteceu comigo).

Andy
fonte
Veja como altero a posição de uma coluna em uma tabela de banco de dados PostgreSQL? no estouro de pilha que pode ser útil, embora na maioria das vezes sugira NÃO reorganizar suas colunas.
Joanolo

Respostas:

16

Provável bug no 9.6 e 9.6.1

Isso me parece completamente um bug ...

Não sei por que isso acontece, mas posso confirmar que isso acontece. Essa é a instalação mais simples encontrada que reproduz o problema (na versão 9.6.0 e 9.6.1).

CREATE TABLE users
(
    id SERIAL PRIMARY KEY,
    email TEXT NOT NULL,
    column_that_we_will_drop TEXT
) ;

-- Function that uses the previous table, and that has a CTE
CREATE OR REPLACE FUNCTION __post_users
    (_useremail text) 
RETURNS integer AS
$$
-- Need a CTE to produce the error. A 'constant' one suffices.
WITH something_even_if_useless(a) AS
(
    VALUES (1)
)
UPDATE
    users
SET
    id = id
WHERE 
    -- The CTE needs to be referenced, if the next
    -- condition were not in place, the problem is not reproduced
    EXISTS (SELECT * FROM something_even_if_useless)
    AND email = _useremail
RETURNING
    id
$$
LANGUAGE "sql" ;

Após essa configuração, a próxima instrução funciona

SELECT * FROM __post_users('[email protected]');

Neste ponto, deixamos cair uma coluna:

ALTER TABLE users 
    DROP COLUMN column_that_we_will_drop ;

Essa alteração faz com que a próxima instrução gere um erro

SELECT * FROM __post_users('[email protected]');

que é o mesmo mencionado por @Andy:

ERROR: table row type and query-specified row type do not match
SQL state: 42804
Detail: Query provides a value for a dropped column at ordinal position 3.
Context: SQL function "__post_users" statement 1
    SELECT * FROM __post_users('[email protected]');

Soltar e recriar a função NÃO resolve o problema.
VACUUM FULL (a tabela ou o banco de dados inteiro) não resolve o problema.


O relatório de erro foi passado para a lista de discussão apropriada do PostgreSQL e tivemos uma resposta muito rápida :

Não consigo reproduzir isso na ponta da cabeça HEAD ou 9.6. Acredito que já foi corrigido por esse patch, que ocorreu um pouco depois da 9.6.1:

https://git.postgresql.org/gitweb/?p=postgresql.git&a=commitdiff&h=f4d865f22

Mas obrigado pelo relatório!

Atenciosamente, Tom Lane


Versão 9.6.2

Em 06/03/2017, posso confirmar que não consigo reproduzir esse comportamento na versão 9.6.2. Ou seja, o bug parece ter sido corrigido nesta versão.

ATUALIZAR

Por comentário de @Jana: "Posso confirmar que o bug está presente na 9.6.1 e foi corrigido na 9.6.2. A correção também está listada no site de lançamento do postgres : A correção" espúria "a consulta" fornece um valor para uma coluna eliminada "erros durante INSERT ou UPDATE em uma tabela com uma coluna descartada "


joanolo
fonte
4
Estou usando 9.6.0 e @joanolo está correto, posso reproduzir o bug com seu método. Se Tom não pode reproduzi-lo, provavelmente foi um bug isolado com esta versão específica (e 9.6.1 eu assumo?). Conforme mencionado na resposta, o problema aparece ao soltar uma coluna em uma tabela e usar uma função com o CTE que afeta essa tabela. Portanto, o problema não é apenas a reordenação, e é isso que eu estava tentando entender com a minha pergunta. Vou editar o título para refletir isso.
Andy
Posso confirmar que o bug está presente na 9.6.1 e foi corrigido na 9.6.2. A correção também está listado na postgres liberar website : Fix espúria "consulta fornece um valor para uma queda coluna" erros durante INSERT ou UPDATE em uma tabela com uma coluna caiu
Jana
0

Eu sei que você provavelmente já ouviu isso antes, mas essa é uma ideia horrível.

  • A ordem lógica afeta apenas coisas como SELECT *
  • O efeito do pedido lógico é limitado à aparência.

Portanto, se não importa nada não o dissuade e reconhecemos que estamos apenas jogando Photoshop com estrutura de linhas e obcecado com a exibição, vamos explicar mais algumas coisas.

  • A ordenação lógica não existe no PostgreSQL
  • O pedido físico tem benefícios reais (embalagem da mesa)
  • O PostgreSQL também não oferece controle sobre o pedido físico CREATE TABLE(embora isso seja uma prioridade muito maior)

Portanto, o PostgreSQL é uma camada de exibição ruim. Além de tudo isso, enquanto ALTERfunciona bem, você não deve temperar o dragão. Ambos ALTER TABLE, e da secção CAVEAT fazer menção a isso,

Alguns comandos DDL, atualmente apenas TRUNCATEe os formulários de reescrita de tabela ALTER TABLE, não são seguros para MVCC. Isso significa que, após o truncamento ou reescrita confirmado, a tabela aparecerá vazia para transações simultâneas, se eles estiverem usando uma captura instantânea tirada antes do commit do comando DDL. Isso será apenas um problema para uma transação que não acessou a tabela em questão antes do início do comando DDL - qualquer transação que tenha feito isso conteria pelo menos um bloqueio de tabela ACCESS SHARE, que bloquearia o comando DDL até que a transação fosse concluída. Portanto, esses comandos não causarão nenhuma inconsistência aparente no conteúdo da tabela para consultas sucessivas na tabela de destino, mas podem causar inconsistência visível entre o conteúdo da tabela de destino e outras tabelas no banco de dados.

E, se tudo isso não for suficiente, e você ainda quiser fingir que essa é uma boa ideia, é horrível. Então tente isso,

  1. Despejar a mesa com pg_dump -t
  2. Reordene as colunas manualmente no despejo.
  3. Carregue o material em uma tabela TEMP.
  4. BEGIN uma transação
  5. DROP a velha mesa inteiramente,
  6. RENAME a tabela temporária para a tabela prod.
  7. COMMIT

Se tudo isso parecer excessivo, lembre-se de que a atualização de linhas no banco de dados exige a reescrita das linhas (supondo que elas não sejam TOAST . Você está tendo que analisar os dados e reconstruir o esquema da tabela, mas de qualquer forma você deve reescrever Se eu tivesse que fazer essa tarefa, é assim que eu faria.

Mas, tudo isso está falando em geral. Ninguém reproduziu seus resultados.

Você deu um caso de teste que não podemos executar

ERROR:  column users.authentication_code does not exist
LINE 24: users.authentication_code,

E você não nos disse a versão exata em que está.

Evan Carroll
fonte
Obrigado pela explicação completa, editei a pergunta para incluir a versão específica.
Andy
4
O bug é reproduzível na versão 9.6.0
ypercubeᵀᴹ
0

Eu resolvi esse erro fazendo backup e restaurando meu banco de dados.

Passos para Heroku

  • Ative o modo de manutenção: heroku maintenance:on
  • Banco de dados de backup: heroku pg:backups:capture
  • Restaurar banco de dados: heroku pg:backups:restore
  • Reinicie o aplicativo: heroku restart
  • Desative o modo de manutenção: heroku maintenance:off
ma11hew28
fonte
0

Também encontrei esse bug. Para aqueles que não desejam fazer backup / restauração completos de seus bancos de dados. Saiba que simplesmente copiar a tabela funciona. Não existe uma maneira "mágica" de copiar uma tabela. Eu fiz isso usando:

SELECT * INTO mytable_copy FROM mytable;
ALTER TABLE mytable RENAME TO mytable_backup; -- just in case. you never know
ALTER TABLE mytable_copy RENAME TO mytable;

Depois disso, você ainda precisará recriar manualmente seus índices, chaves estrangeiras e padrões. Recriar a tabela assim fez o bug desaparecer.

Thibauld
fonte