Eu tenho uma tabela grande (talvez talvez um bilhão de linhas, mas atualmente ~ 26 milhões) para a qual quero definir um sinalizador no PK mais alto para um determinado agrupamento, em um lote único.
Optei por criar uma tabela temporária que armazena os PKs que devem ser configurados current=true
, o restante deve ser configurado current=false
. Fiz uma tabela temporária em vez de uma visão materializada, mas acho que não faria diferença real.
O processo de descobrir o ID máximo para cada um não é muito doloroso:
CREATE TABLE assertion (
pk integer NOT NULL,
a bigint NOT NULL,
b bigint NOT NULL,
c bigint NOT NULL,
d integer NOT NULL,
current boolean DEFAULT false NOT NULL
);
CREATE INDEX assertion_current_idx ON assertion USING btree (current) WHERE (current = true);
CREATE INDEX assertion_current_idx1 ON assertion USING btree (current);
CREATE UNIQUE INDEX assertion_a_b_c_d_idx ON assertion USING btree (a, b, c, d) WHERE (current = true);
SELECT COUNT(pk) FROM assertion;
-- 26916858
-- Time: 2912.403 ms (00:02.912)
CREATE TEMPORARY TABLE assertion_current AS
(SELECT MAX(pk) as pk, a, b, c, d
FROM assertion
GROUP BY a, b, c, d);
-- Time: 72218.755 ms (01:12.219)
ANALYZE assertion_current;
CREATE INDEX ON assertion_current(pk);
-- Time: 22107.698 ms (00:22.108)
SELECT COUNT(pk) FROM assertion_current;
-- 26455092
-- Time: 15650.078 ms (00:15.650)
De acordo com a contagem de assertion_current
, precisamos definir o sinalizador 'atual' como verdadeiro para 98% das linhas.
O complicado é como atualizar a assertion
tabela em tempo razoável com base nos valores atuais. Existe uma restrição única na a, b, c, d, current
qual deve ser mantida, portanto, a atualização da current
coluna precisa ser atômica para evitar a quebra da restrição.
Tenho algumas opções:
Opção 1
Atualize apenas os current
valores que mudam. Isso tem a vantagem de atualizar o menor número de linhas necessário com base em um campo indexado:
BEGIN;
UPDATE assertion
SET current = false
WHERE assertion.current = true AND PK NOT IN (SELECT pk FROM assertion_current);
UPDATE assertion
SET current = true
WHERE assertion.current = false AND PK IN (SELECT pk FROM assertion_current);
COMMIT;
Mas ambas as consultas envolvem varreduras de sequência nas assertion_current
quais (eu acho) teriam que ser multiplicadas por um grande número de linhas.
Update on assertion (cost=0.12..431141.55 rows=0 width=0)
-> Index Scan using assertion_current_idx on assertion (cost=0.12..431141.55 rows=1 width=7)
Index Cond: (current = true)
Filter: (NOT (SubPlan 1))
SubPlan 1
-> Materialize (cost=0.00..787318.40 rows=29982560 width=4)
-> Seq Scan on assertion_current (cost=0.00..520285.60 rows=29982560 width=4)
e
Update on assertion (cost=595242.56..596693.92 rows=0 width=0)
-> Nested Loop (cost=595242.56..596693.92 rows=17974196 width=13)
-> HashAggregate (cost=595242.00..595244.00 rows=200 width=10)
Group Key: assertion_current.pk
-> Seq Scan on assertion_current (cost=0.00..520285.60 rows=29982560 width=10)
-> Index Scan using assertion_pkey on assertion (cost=0.56..8.58 rows=1 width=10)
Index Cond: (pk = assertion_current.pk)
Filter: (NOT current)
Isso significa que uma dessas consultas (muitas atuais verdadeiras ou muitas atuais falsas) sempre leva muito tempo.
opção 2
Passe único, mas tem que tocar em todas as linhas desnecessariamente.
UPDATE assertion
SET current =
(CASE WHEN assertion.pk IN (select PK from assertion_current)
THEN TRUE ELSE FALSE END)
mas isso resulta em uma varredura de sequência em assertion_current novamente
Update on assertion (cost=0.00..15498697380303.70 rows=0 width=0)
-> Seq Scan on assertion (cost=0.00..15498697380303.70 rows=35948392 width=7)
SubPlan 1
-> Materialize (cost=0.00..787318.40 rows=29982560 width=4)
-> Seq Scan on assertion_current (cost=0.00..520285.60 rows=29982560 width=4)
Opção 3
Como a opção 1, mas use WHERE
na atualização:
BEGIN;
UPDATE assertion SET current = false WHERE current = true;
UPDATE assertion SET current = true FROM assertion_current
WHERE assertion.pk = assertion_current.pk;
COMMIT;
mas a segunda consulta envolve duas varreduras seq:
Update on assertion (cost=1654256.82..2721576.65 rows=0 width=0)
-> Hash Join (cost=1654256.82..2721576.65 rows=29982560 width=13)
Hash Cond: (assertion_current.pk = assertion.pk)
-> Seq Scan on assertion_current (cost=0.00..520285.60 rows=29982560 width=10)
-> Hash (cost=1029371.92..1029371.92 rows=35948392 width=10)
-> Seq Scan on assertion (cost=0.00..1029371.92 rows=35948392 width=10)
Opção 4
Obrigado @jjanes, isso levou > 6 horas, então eu cancelei.
UPDATE assertion
SET current = not current
WHERE current <>
(CASE WHEN assertion.pk IN (select PK from assertion_current)
THEN TRUE ELSE FALSE END)
produz
Update on assertion (cost=0.00..11832617068493.14 rows=0 width=0)
-> Seq Scan on assertion (cost=0.00..11832617068493.14 rows=27307890 width=7)
Filter: (current <> CASE WHEN (SubPlan 1) THEN true ELSE false END)
SubPlan 1
-> Materialize (cost=0.00..787318.40 rows=29982560 width=4)
-> Seq Scan on assertion_current (cost=0.00..520285.60 rows=29982560 width=4)
Opção 5
Obrigado @a_horse_with_no_name. Isso leva 24 minutos na minha máquina.
UPDATE assertion tg SET current = EXISTS (SELECT pk FROM assertion_current cr WHERE cr.pk = tg.pk);
dá
Update on assertion tg (cost=0.00..233024784.94 rows=0 width=0)
-> Seq Scan on assertion tg (cost=0.00..233024784.94 rows=27445116 width=7)
SubPlan 1
-> Index Only Scan using assertion_current_pk_idx on assertion_current cr (cost=0.44..8.46 rows=1 width=0)
Index Cond: (pk = tg.pk)
Existe uma maneira melhor de conseguir isso em tempo hábil?
Assim
NOT current
será o caso raro. Parece que você tem tentado fazer as coisas de trás para frente até agora.Seus índices atuais são ativamente inúteis para a distribuição de dados fornecida:
Precisamos manter o
UNIQUE
índice para impor seus requisitos, e também é útil:Mas simplifique
(current = true)
para apenascurrent
. Não adianta executar uma expressão redundante, apenas use oboolean
valor.é absolutamente inútil e nunca será usado. Mas ainda precisa ser mantido atualizado. Largue.assertion_current_idx
é quase tão inútil. Pelo menos faria sentido para consultas procurando por arquivosassertion_current_idx1
current = false
. Mas é muito mais barato ter esse índice parcial - que também suporta minha segunda consulta sugerida abaixo:Crie este índice após a "consulta inicial" abaixo.
Observe que eu pulo a tabela temporária completamente em favor de um CTE. Menos sobrecarga.
Consulta inicial
Você divulgou em um comentário posterior que "todas as linhas estão definidas
current = false
" inicialmente. Podemos usar uma consulta muito mais simples e rápida para init. Basta atualizar uma linha por grupo onde não há outra com um PK maior. Nenhuma violação única possível, nenhuma atualização para "não atual":Consulta geral
Assumindo que não pode haver nenhuma linha atual por grupo.
Deve superar qualquer outra coisa que você tentou até agora.
A subconsulta
a1
obtém a linha com maior descontinuidade por grupo com uma agregaçãopk
simples .max()
Esse é o ideal enquanto houver poucas linhas candidatas (NOT current
) por grupo. Caso contrário, otimize ainda mais esta etapa com uma verificação de salto de índice emulado:A parte complicada é manter a
UNIQUE
restrição feliz . Ele não permite duas linhas atuais por grupo em um determinado momento. Não há ordem de execução entre CTEs de uma mesma consulta - desde que uma não faça referência à outra. Eu coloquei uma referência fictícia para forçar a ordem das atualizações .Quando ainda não havia nenhuma linha atual, o CTE
up1
não retorna nenhuma linha. Então usamosLEFT JOIN
. ELIMIT 1
nunca duplicar linhas.Eu não acho que você deve tentar evitar cada varredura seq. Mas você não quer uma varredura seq que será executada um grande número de vezes (como em um subplano sem hash ou no segundo filho de um loop aninhado).
Em seu primeiro plano, ele tem uma varredura seq em um subplano sem hash, mas afirma que isso será executado apenas uma vez, portanto, não seria tão ruim se isso fosse preciso. Mas isso parece contradizer sua descrição de "o sinalizador 'atual' é verdadeiro para 98% das linhas", então talvez as estatísticas estejam totalmente erradas.
Você pode aumentar nosso work_mem até que o subplano mude para um subplano com hash, ou você pode reescrever sua consulta de NOT IN para NOT EXISTS.
No caso da opção 2, basta adicionar um WHERE para eliminar as atualizações inoperantes:
Mas, novamente, usar EXISTS em vez de IN provavelmente será melhor.