AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / dba / Perguntas / 179629
Accepted
Stranger6667
Stranger6667
Asked: 2017-07-13 00:05:45 +0800 CST2017-07-13 00:05:45 +0800 CST 2017-07-13 00:05:45 +0800 CST

Como evitar uma subconsulta na cláusula FILTER?

  • 772

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.createddeve ser o mesmo do primeiro application.createdpara esta pessoa, independentemente do is_preliminaryvalor.

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 ( xzsaí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_appcoluna 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
postgresql performance
  • 3 3 respostas
  • 3842 Views

3 respostas

  • Voted
  1. Best Answer
    Erwin Brandstetter
    2017-07-15T05:46:50+08:002017-07-15T05:46:50+08:00

    Várias pequenas melhorias:

    SELECT created::date AS the_date
         , COUNT(*) AS total
         , COUNT(*) FILTER( WHERE is_preliminary = false
                            AND   NOT EXISTS (
                               SELECT 1
                               FROM   applications
                               WHERE  physical_person_id = a.physical_person_id
                               AND    created < a.created
                            -- AND    created < a.created::date  -- alternative? see below
                            -- AND    is_preliminary = false     -- omission? see below
                            -- AND    country = 'CZ'             -- not sure. see below
                               LIMIT  1
                               )
                            ) AS is_first_app
    FROM   applications a
    WHERE  created >= '2017-01-01'::timestamptz
    AND    created <  '2017-07-01'::timestamptz
    AND    country = 'CZ'
    GROUP  BY created::date
    ORDER  BY created::date;
    
    • COALESCE( count(...), 0)é sempre ruído redundante, pois count()nunca retorna NULL para começar. Basta removê-lo. Relacionado:

      • Otimização de consultas ou índices ausentes?
    • Do jeito que você fez, você agrupa e classifica pela representação de texto da sua timestamptzcoluna created, 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ós GROUP BY created::date, você deve repetir a expressão agrupada.

    • Não use comoBETWEEN foi aconselhado. Seu filtro com >=e <é superior. BETWEENtraduziria para >=e <=, o que leva a casos de canto feios com valores fracionários em timestamp(ou timestamptz). Mas como o tipo de dados da coluna subjacente é timestamptz, converta timestamptzdiretamente para. Mesmo resultado, apenas uma operação de conversão a menos:

      WHERE  created >= '2017-01-01'::timestamptz
      AND    created <  '2017-07-01'::timestamptz
      
    • Você está ciente de que a data derivada de um timestamptzvalor (assim como a conversão para timestamptzsem 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:

      • Ignorando completamente os fusos horários no Rails e no PostgreSQL
      • Desvantagens de usar date_trunc
    • 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 linha applicationsda mesma pessoa é anterior à atual. Mas, embora você permita apenas is_preliminary = falsea 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ão is_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 arquivo SELECT.

    Índices

    Como você está preocupado em otimizar o desempenho de leitura ...

    Seu índice de várias colunas country_createdparece ideal para o SELECT. Mas continue lendo...

    Mas você pode melhorar facilmente a EXISTSsubconsulta com outro índice de várias colunas:

    CREATE INDEX app_person_created_idx ON applications (physical_person_id, created);
    

    Para permitir varreduras somente de índice (somente se seus padrões de gravação permitirem!):

    CREATE INDEX app_country_created_person_preliminary_idx
    ON applications (country, created, physical_person_id, is_preliminary);
    

    As colunas anexadas physical_person_ide is_preliminarysó 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:

    • Varreduras de índice lentas em uma tabela grande

    Solução alternativa

    Seu último comentário abre novas opções:

    quando o aplicativo é criado pela primeira vez, uma nova pessoa também é criada com o mesmo valor criado.

    (A afirmação anterior na pergunta era muito ambígua para trabalhar com ela.)

    Se isso for aplicado de forma confiável (e creatednunca for atualizado em nenhuma tabela), há uma consulta mais simples e rápida que também acontece para " evitar uma subconsulta na FILTERcláusula " - usando um LEFT [OUTER] JOINem vez disso:

    SELECT a.created::date AS date
         , COUNT(*)        AS total
         , COUNT(p.id)     AS is_first_app  -- count only counts non-null values
    FROM   applications a
    LEFT   JOIN persons p ON a.is_preliminary = false
                         AND p.id = a.physical_person_id  -- FK enforces max. 1 match
                         AND p.created = a.created
    WHERE  a.created >= '2017-01-01'::timestamptz
    AND    a.created <  '2017-07-01'::timestamptz
    AND    a.country = 'CZ'
    GROUP  BY a.created::date
    ORDER  BY a.created::date;
    

    Para um desempenho de leitura perfeito com duas varreduras somente de índice, você teria o índiceapp_country_created_person_preliminary_idxacima. Além disso, este empersons:

    CREATE INDEX pers_id_created ON persons (id, created);
    
    • 6
  2. Evan Carroll
    2017-07-14T07:05:47+08:002017-07-14T07:05:47+08:00

    Por alguns pontos.

    1. Se o seu tipo já é timestamp with timezone, então ::timestampnão faz nada.
    2. Seu intervalo é escrito de forma mais limpa com BETWEEN.
    3. GROUP BY 1no seu caso, está realmente lançando seus tempos para string e, em seguida, agrupando por isso. O que você quer fazer é simplesmente GROUP BY datee, 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).
    4. ORDER BY 1no seu caso está realmente ordenando uma lista de strings.
    5. Todos esses varchar acima devem ser texto. No PostgreSQL raramente usamos varchar. É texto, mas mais lento devido a uma restrição de comprimento muitas vezes inútil. A única coisa que não é texto que eu teria em seu esquema é o código do país de dois caracteres, eu o chamaria de country2char e o teria explicitamente, char(2)mas apenas por verbosidade.
    6. Aspas duplas são uma prática muito ruim e são sempre altamente desencorajadas.

    Então tente isso,

    SELECT to_char(created, 'YYYY-MM-DD') AS "Date", total AS "Total", is_first_app AS "Is First App"
    FROM (
      SELECT
        created::date AS created
        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 BETWEEN '2017-01-01' AND '2017-07-01'
        AND country = 'CZ'
      GROUP BY 1
    ) AS t
    ORDER BY created;
    

    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

    • O GROUP BY select-list-ref é menos otimizado que o GROUP BY colname?
    • 1
  3. RDFozz
    2017-07-13T09:28:17+08:002017-07-13T09:28:17+08:00

    Não tenho certeza se teria um desempenho melhor, mas você pode tentar separar as duas funções de contagem e mesclar os resultados:

    SELECT "Date"
          ,"Total"
          ,"Is First App"
    FROM
           (SELECT
              to_char(created, 'YYYY-MM-DD')     AS "Date",
              COUNT(*) AS "Total",
            FROM
              applications
            WHERE
              created >= '2017-01-01'::TIMESTAMP AND created < '2017-07-01'::TIMESTAMP
              AND country = 'CZ'
            GROUP BY 1
           ) ttl
           LEFT  JOIN 
           (SELECT
              to_char(created, 'YYYY-MM-DD')     AS "Date",
              COUNT(*) AS "Is First App"
            FROM
                 (SELECT
                    physical_person_id,
                    MIN(created) as created
                  FROM
                    applications
                  WHERE
                    country = 'CZ'
                    AND is_preliminary = false
                  GROUP BY 1
                 ) fdt -- first date
            WHERE
              created >= '2017-01-01'::TIMESTAMP AND created < '2017-07-01'::TIMESTAMP
            GROUP BY 1
           ) ifa ON (ttl."Date" = ifa."Date")
    ORDER BY 1
    ;
    

    A subconsulta rotulada fdtobtém a primeira data de aplicação (onde countryé 'CZ') para cada physical_person_id.

    A subconsulta rotulada ifapega 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 JOINesses resultados para os ifaresultados, e você deve ter o que você quer.

    Não precisamos combinar physical_person_identre ttle ifa; 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.

    • 0

relate perguntas

  • Sequências Biológicas do UniProt no PostgreSQL

  • Como determinar se um Índice é necessário ou necessário

  • Onde posso encontrar o log lento do mysql?

  • Como posso otimizar um mysqldump de um banco de dados grande?

  • Qual é a diferença entre a replicação do PostgreSQL 9.0 e o Slony-I?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host

    • 12 respostas
  • Marko Smith

    Como fazer a saída do sqlplus aparecer em uma linha?

    • 3 respostas
  • Marko Smith

    Selecione qual tem data máxima ou data mais recente

    • 3 respostas
  • Marko Smith

    Como faço para listar todos os esquemas no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    Como usar o sqlplus para se conectar a um banco de dados Oracle localizado em outro host sem modificar meu próprio tnsnames.ora

    • 4 respostas
  • Marko Smith

    Como você mysqldump tabela (s) específica (s)?

    • 4 respostas
  • Marko Smith

    Listar os privilégios do banco de dados usando o psql

    • 10 respostas
  • Marko Smith

    Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Como faço para listar todos os bancos de dados e tabelas usando o psql?

    • 7 respostas
  • Martin Hope
    Jin conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host 2014-12-02 02:54:58 +0800 CST
  • Martin Hope
    Stéphane Como faço para listar todos os esquemas no PostgreSQL? 2013-04-16 11:19:16 +0800 CST
  • Martin Hope
    Mike Walsh Por que o log de transações continua crescendo ou fica sem espaço? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland Listar todas as colunas de uma tabela especificada 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney O MySQL pode realizar consultas razoavelmente em bilhões de linhas? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx Como posso monitorar o andamento de uma importação de um arquivo .sql grande? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison Como você mysqldump tabela (s) específica (s)? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    Jonas Como posso cronometrar consultas SQL usando psql? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas Como faço para listar todos os bancos de dados e tabelas usando o psql? 2011-02-18 00:45:49 +0800 CST

Hot tag

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve