Esquema :
CREATE TABLE "applications" (
"id" SERIAL NOT NULL PRIMARY KEY,
"country" VARCHAR(2) NOT NULL,
"created" TIMESTAMP WITH TIME ZONE NOT NULL,
"is_preliminary" BOOLEAN NOT NULL,
"first_name" VARCHAR(128) NOT NULL,
"last_name" VARCHAR(128) NOT NULL,
"birth_number" VARCHAR(11) NULL
);
CREATE TABLE "persons" (
"id" UUID NOT NULL PRIMARY KEY,
"created" TIMESTAMP WITH TIME ZONE NOT NULL,
"modified" TIMESTAMP WITH TIME ZONE NOT NULL
);
ALTER TABLE "applications" ADD COLUMN "physical_person_id" UUID NULL;
CREATE INDEX "physical_person_id_idx" ON "applications" ("physical_person_id");
ALTER TABLE "applications" ADD CONSTRAINT "physical_person_id_fk" FOREIGN KEY ("physical_person_id") REFERENCES "persons" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "country_created" ON "applications" (country, created);
Notas : O valor de persons.created
deve ser o mesmo do primeiro application.created
para esta pessoa, independentemente do is_preliminary
valor.
Consulta :
SELECT
to_char(created, 'YYYY-MM-DD') AS "Date",
COUNT(*) AS "Total",
COALESCE(
COUNT(*) FILTER(
WHERE applications.is_preliminary = false
AND NOT EXISTS(
SELECT 1
FROM applications A
WHERE A.physical_person_id = applications.physical_person_id
AND A.created < applications.created
LIMIT 1
)
)
, 0
) AS "Is first app"
FROM applications
WHERE
created >= '2017-01-01'::TIMESTAMP AND created < '2017-07-01'::TIMESTAMP
AND country = 'CZ'
GROUP BY 1
ORDER BY 1
Objetivo : Meu objetivo é ver o número total de solicitações versus o número de primeiras solicitações por dia em determinado país. Por primeira aplicação quero dizer um número de aplicações em um determinado dia, que foram registradas pela primeira vez e não tiveram nenhuma aplicação antes.
Problema : Desempenho da consulta. O número de linhas está crescendo e o desempenho agora não está em um bom nível.
Amostra de dados : aqui ( xz
saída compactada de pg_dump
)
Os seguintes planos de consulta são retirados do meu laptop (na produção não houve "fusão externa")
Plano de consulta :
GroupAggregate (cost=54186.11..2391221.59 rows=186832 width=48) (actual time=2137.029..3224.937 rows=181 loops=1)
Group Key: (to_char(applications.created, 'YYYY-MM-DD'::text))
-> Sort (cost=54186.11..54653.19 rows=186832 width=57) (actual time=2128.554..2370.798 rows=186589 loops=1)
Sort Key: (to_char(applications.created, 'YYYY-MM-DD'::text))
Sort Method: external merge Disk: 8176kB
-> Bitmap Heap Scan on applications (cost=5262.54..30803.18 rows=186832 width=57) (actual time=93.993..411.096 rows=186589 loops=1)
Recheck Cond: (((country)::text = 'CZ'::text) AND (created >= '2017-01-01 00:00:00'::timestamp without time zone) AND (created < '2017-07-01 00:00:00'::timestamp without time zone))
Heap Blocks: exact=19640
-> Bitmap Index Scan on country_created (cost=0.00..5215.83 rows=186832 width=0) (actual time=90.945..90.945 rows=186589 loops=1)
Index Cond: (((country)::text = 'CZ'::text) AND (created >= '2017-01-01 00:00:00'::timestamp without time zone) AND (created < '2017-07-01 00:00:00'::timestamp without time zone))
SubPlan 1
-> Index Scan using physical_person_id_idx on applications a (cost=0.43..72.77 rows=6 width=0) (actual time=0.006..0.006 rows=1 loops=127558)
Index Cond: (physical_person_id = applications.physical_person_id)
Filter: (created < applications.created)
Rows Removed by Filter: 0
Planning time: 0.235 ms
Execution time: 3261.530 ms
Pergunta : Como posso melhorar o desempenho da consulta? Suponho que seja possível se livrar da subconsulta em "É o primeiro aplicativo", mas não sei como.
Versão do PostgreSQL : 9.6.3
Plano de consulta após atualização de Evan Carroll:
Subquery Scan on t (cost=51624.73..2390836.50 rows=186782 width=52) (actual time=291.726..1129.435 rows=181 loops=1)
-> GroupAggregate (cost=51624.73..2388034.77 rows=186782 width=20) (actual time=291.707..1128.057 rows=181 loops=1)
Group Key: ((applications.created)::date)
-> Sort (cost=51624.73..52091.69 rows=186782 width=29) (actual time=280.283..334.391 rows=186589 loops=1)
Sort Key: ((applications.created)::date)
Sort Method: external merge Disk: 6720kB
-> Bitmap Heap Scan on applications (cost=5261.90..30801.54 rows=186782 width=29) (actual time=42.944..181.325 rows=186589 loops=1)
Recheck Cond: (((country)::text = 'CZ'::text) AND (created >= '2017-01-01 00:00:00+01'::timestamp with time zone) AND (created <= '2017-07-01 00:00:00+02'::timestamp with time zone))
Heap Blocks: exact=19640
-> Bitmap Index Scan on country_created (cost=0.00..5215.20 rows=186782 width=0) (actual time=40.003..40.003 rows=186589 loops=1)
Index Cond: (((country)::text = 'CZ'::text) AND (created >= '2017-01-01 00:00:00+01'::timestamp with time zone) AND (created <= '2017-07-01 00:00:00+02'::timestamp with time zone))
SubPlan 1
-> Index Scan using physical_person_id_idx on applications a (cost=0.43..72.77 rows=6 width=0) (actual time=0.006..0.006 rows=1 loops=127558)
Index Cond: (physical_person_id = applications.physical_person_id)
Filter: (created < applications.created)
Rows Removed by Filter: 0
Planning time: 0.232 ms
Execution time: 1145.761 ms
A consulta inicial sem is_first_app
coluna leva ~300 ms.
Plano de consulta para uma solução alternativa de Erwin Brandstetter:
GroupAggregate (cost=51356.14..55562.83 rows=186964 width=20) (actual time=562.470..620.993 rows=181 loops=1)
Group Key: ((a.created)::date)
Buffers: shared hit=2137 read=4491, temp read=2491 written=2485
-> Sort (cost=51356.14..51823.55 rows=186964 width=20) (actual time=562.216..592.226 rows=186589 loops=1)
Sort Key: ((a.created)::date)
Sort Method: external merge Disk: 2640kB
Buffers: shared hit=2137 read=4491, temp read=2491 written=2485
-> Hash Right Join (cost=13394.71..31149.19 rows=186964 width=20) (actual time=119.488..464.407 rows=186589 loops=1)
Hash Cond: ((p.id = a.physical_person_id) AND (p.created = a.created))
Join Filter: (NOT a.is_preliminary)
Buffers: shared hit=2137 read=4491, temp read=2159 written=2153
-> Seq Scan on persons p (cost=0.00..9003.04 rows=364404 width=24) (actual time=3.800..73.486 rows=364404 loops=1)
Buffers: shared hit=868 read=4491
-> Hash (cost=9311.25..9311.25 rows=186964 width=25) (actual time=115.213..115.213 rows=186589 loops=1)
Buckets: 65536 Batches: 4 Memory Usage: 2875kB
Buffers: shared hit=1269, temp written=681
-> Index Only Scan using app_country_created_person_preliminary_idx on applications a (cost=0.56..9311.25 rows=186964 width=25) (actual time=0.054..64.392 rows=186589 loops=1)
reated < '2017-07-01 00:00:00+02'::timestamp with time zone))
Heap Fetches: 0
Buffers: shared hit=1269
Planning time: 0.401 ms
Execution time: 628.100 ms
Várias pequenas melhorias:
COALESCE( count(...), 0)
é sempre ruído redundante, poiscount()
nunca retorna NULL para começar. Basta removê-lo. Relacionado:Do jeito que você fez, você agrupa e classifica pela representação de texto da sua
timestamptz
colunacreated
, o que funciona muito bem. Mas é mais caro do que agrupar e classificar por uma data real (valor inteiro de 4 bytes internamente). Classificar por uma data ou carimbo de data/hora real também costuma ser mais confiável, embora não faça diferença nessa consulta específica. A maneira mais simples de conseguir isso é um elenco simples até o momento:created::date
. Você ainda pode formatar a saída se desejar:to_char(created::date, 'YYYY-MM-DD') AS date
. Mesmo resultado, mas como nósGROUP BY created::date
, você deve repetir a expressão agrupada.Não use como
BETWEEN
foi aconselhado. Seu filtro com>=
e<
é superior.BETWEEN
traduziria para>=
e<=
, o que leva a casos de canto feios com valores fracionários emtimestamp
(outimestamptz
). Mas como o tipo de dados da coluna subjacente étimestamptz
, convertatimestamptz
diretamente para. Mesmo resultado, apenas uma operação de conversão a menos:Você está ciente de que a data derivada de um
timestamptz
valor (assim como a conversão paratimestamptz
sem especificar um fuso horário) sempre depende da configuração do fuso horário atual , certo? Você pode colocar a consulta em um fuso horário selecionado explicitamente se quiser eliminar essa fonte de erro sorrateira. Fundamentos:Pode haver erro(s) de lógica no cálculo de
is_first_app
. No entanto, isso é apenas especulação do meu lado: você está verificando se alguma linhaapplications
da mesma pessoa é anterior à atual. Mas, embora você permita apenasis_preliminary = false
a linha atual, não impõe o mesmo predicado para as linhas com as quais comparar. Normalmente, você gostaria de comparar com linhas que também sãois_preliminary = false
. Eu adicionei uma linha comentada na consulta acima.Além disso, como você forma grupos por dia , você realmente deseja contar as linhas que têm uma entrada anterior no mesmo dia também? Talvez sim, mas talvez você realmente queira verificar as linhas anteriores ao dia com
created < a.created::date
.Finalmente, ainda menos seguro sobre isso, você pode querer repetir o predicado
AND country = 'CZ'
para restringir a comparação ao mesmo país. Não tenho informações suficientes para dizer mais.Eu também encurtei a sintaxe cortando aspas duplas de ruído (todos os identificadores são legais de qualquer maneira) e usando um alias de tabela estratégica (
applications a
) no arquivoSELECT
.Índices
Como você está preocupado em otimizar o desempenho de leitura ...
Seu índice de várias colunas
country_created
parece ideal para oSELECT
. Mas continue lendo...Mas você pode melhorar facilmente a
EXISTS
subconsulta com outro índice de várias colunas:Para permitir varreduras somente de índice (somente se seus padrões de gravação permitirem!):
As colunas anexadas
physical_person_id
eis_preliminary
só fazem sentido se você obtiver varreduras somente de índice.Recebo duas varreduras somente de índice depois de adicionar o último índice, que é muito mais rápido para tabelas grandes.
Mais sobre verificações somente de índice:
Solução alternativa
Seu último comentário abre novas opções:
(A afirmação anterior na pergunta era muito ambígua para trabalhar com ela.)
Se isso for aplicado de forma confiável (e
created
nunca for atualizado em nenhuma tabela), há uma consulta mais simples e rápida que também acontece para " evitar uma subconsulta naFILTER
cláusula " - usando umLEFT [OUTER] JOIN
em vez disso:Para um desempenho de leitura perfeito com duas varreduras somente de índice, você teria o índice
app_country_created_person_preliminary_idx
acima. Além disso, este empersons
:Por alguns pontos.
timestamp with timezone
, então::timestamp
não faz nada.BETWEEN
.GROUP BY 1
no seu caso, está realmente lançando seus tempos para string e, em seguida, agrupando por isso. O que você quer fazer é simplesmenteGROUP BY date
e, em seguida, definir a data para stringify em outro select se você insistir em fazer isso no servidor (o que eu não faria de qualquer maneira).ORDER BY 1
no seu caso está realmente ordenando uma lista de strings.char(2)
mas apenas por verbosidade.Então tente isso,
Agora, para a subconsulta, acho que exigiria acesso aos dados para grok e reescrever esse. Eu não posso fazer isso na minha cabeça.
Veja este post para mais informações
Não tenho certeza se teria um desempenho melhor, mas você pode tentar separar as duas funções de contagem e mesclar os resultados:
A subconsulta rotulada
fdt
obtém a primeira data de aplicação (ondecountry
é'CZ'
) para cadaphysical_person_id
.A subconsulta rotulada
ifa
pega esses resultados, elimina todas as linhas fora do intervalo desejado e fornece a contagem dos primeiros aplicativos para cada data.A subconsulta rotulada
ttl
é sua consulta original, com a parte "é o primeiro aplicativo" removida.LEFT JOIN
esses resultados para osifa
resultados, e você deve ter o que você quer.Não precisamos combinar
physical_person_id
entrettl
eifa
; cada contagem é independente e completa em si mesma, então só precisamos combinar as datas.Obviamente, se demorar muito para obter o primeiro aplicativo para todos os usuários, isso pode ter um desempenho pior do que a consulta original. No entanto, ao eliminar a subconsulta correlacionada, estamos procurando esses primeiros aplicativos apenas uma vez, em vez de cada pessoa.
NOTA: o código não foi testado.