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 / 143044
Accepted
maxTrialfire
maxTrialfire
Asked: 2016-07-06 13:19:11 +0800 CST2016-07-06 13:19:11 +0800 CST 2016-07-06 13:19:11 +0800 CST

Otimize uma consulta LATERAL JOIN em uma tabela grande

  • 772

Estou usando Postgre 9.5. Eu tenho uma tabela que registra acessos de página de vários sites. Esta tabela contém cerca de 32 milhões de linhas de 1º de janeiro de 2016 a 30 de junho de 2016.

CREATE TABLE event_pg (
   timestamp_        timestamp without time zone NOT NULL,
   person_id         character(24),
   location_host     varchar(256),
   location_path     varchar(256),
   location_query    varchar(256),
   location_fragment varchar(256)
);

Estou tentando ajustar uma consulta que conta o número de pessoas que realizaram uma determinada sequência de acessos à página. A consulta destina-se a responder a perguntas como "quantas pessoas visualizaram a página inicial, acessaram o site de ajuda e visualizaram a página de agradecimento"? O resultado se parece com isso

╔════════════╦════════════╦═════════════╗
║  home-page ║ help site  ║ thankyou    ║
╠════════════╬════════════╬═════════════╣
║ 10000      ║ 9800       ║1500         ║
╚════════════╩════════════╩═════════════╝

Observe que os números estão diminuindo, o que faz sentido, porque dos 10.000 que visualizaram a página inicial, 9.800 foram para o site de ajuda e desses 1.500 foram para a página de agradecimento.

O SQL para uma sequência de 3 etapas usa junções laterais da seguinte maneira:

SELECT 
  sum(view_homepage) AS view_homepage,
  sum(use_help) AS use_help,
  sum(thank_you) AS thank_you
FROM (
  -- Get the first time each user viewed the homepage.
  SELECT X.person_id,
    1 AS view_homepage,
    min(timestamp_) AS view_homepage_time
  FROM event_pg X 
  WHERE X.timestamp_ between '2016-04-23 00:00:00.0' and timestamp '2016-04-30 23:59:59.999'
  AND X.location_host like '2015.testonline.ca'
  GROUP BY X.person_id
) e1 
LEFT JOIN LATERAL (
  SELECT
    Y.person_id,
    1 AS use_help,
    timestamp_ AS use_help_time
  FROM event_pg Y 
  WHERE 
    Y.person_id = e1.person_id AND
    location_host = 'helpcentre.testonline.ca' AND
    timestamp_ BETWEEN view_homepage_time AND timestamp '2016-04-30 23:59:59.999'
  ORDER BY timestamp_
  LIMIT 1
) e2 ON true 
LEFT JOIN LATERAL (
  SELECT
    1 AS thank_you,
    timestamp_ AS thank_you_time
  FROM event_pg Z 
  WHERE Z.person_id = e2.person_id AND
    location_fragment =  '/file/thank-you' AND
    timestamp_ BETWEEN use_help_time AND timestamp '2016-04-30 23:59:59.999'
  ORDER BY timestamp_
  LIMIT 1
) e3 ON true;

Eu tenho um índice em timestamp_, person_ide as locationcolunas. As consultas em intervalos de alguns dias ou semanas são muito rápidas (1s a 10s). Onde fica lento é quando tento executar a consulta para tudo entre 1º de janeiro e 30 de julho. Demora mais de um minuto. Se você comparar as duas explicações abaixo, poderá ver que ele não usa mais o índice timestamp_ e, em vez disso, faz um Seq Scan porque o índice não compraria nada, pois estamos consultando "o tempo todo", portanto, praticamente todos os registros na tabela .

  • Explique por um período de alguns dias (usa o índice): https://explain.depesz.com/s/2tOi
  • Explique por 6 meses (seq scan): https://explain.depesz.com/s/c0yq

Agora percebo que a natureza do loop aninhado da junção lateral vai desacelerar quanto mais registros ele tiver que percorrer, mas existe alguma maneira de acelerar essa consulta para grandes intervalos de datas para que ela seja dimensionada melhor?

postgresql optimization
  • 1 1 respostas
  • 11075 Views

1 respostas

  • Voted
  1. Best Answer
    Erwin Brandstetter
    2016-07-06T20:58:33+08:002016-07-06T20:58:33+08:00

    Notas preliminares

    • Você está usando tipos de dados ímpares. character(24)? char(n)é um tipo desatualizado e quase sempre a escolha errada. Você tem índices ativados person_ide se junta a eles repetidamente. integerseria muito mais eficiente por vários motivos. (Ou bigint, se você planeja gravar mais de 2 bilhões de linhas durante o tempo de vida da tabela.) Relacionado:

      • A pesquisa de índice seria visivelmente mais rápida com char vs varchar quando todos os valores forem 36 caracteres
    • LIKEé inútil sem curingas. Use =em vez disso. Mais rápido.
      x.location_host LIKE '2015.testonline.ca'
      x.location_host = '2015.testonline.ca'

    • Use count(e1.*)ou count(*)em vez de adicionar uma coluna fictícia com o valor 1para cada subconsulta. (Exceto para o último ( e3), onde você não precisa de nenhum dado real.)

    • Você é inconsistente em às vezes converter a string literal para timestampe às vezes não ( timestamp '2016-04-30 23:59:59.999'). Ou faz sentido, então faça o tempo todo , ou não faz, então não faça.
      Não. Quando comparado a uma timestampcoluna, uma string literal é coagida de timestampqualquer maneira. Portanto, você não precisa de um elenco explícito.

    • O tipo de dados Postgres timestamptem até 6 dígitos fracionários. Suas BETWEENexpressões deixam casos de canto. Eu os substituí por expressões menos propensas a erros.

    Índices

    Importante: para otimizar o desempenho, crie índices multicolunas .
    Para a primeira subconsulta hp:

    CREATE INDEX event_pg_location_host_timestamp__idx
    ON event_pg (location_host, timestamp_);
    

    Ou, se você puder obter varreduras somente de índice, anexe person_idao índice:

    CREATE INDEX event_pg_location_host_timestamp__person_id_idx
    ON event_pg (location_host, timestamp_, person_id);
    

    Para intervalos de tempo muito grandes abrangendo a maior parte ou toda a tabela, esse índice deve ser preferível - ele também oferece suporte à hlpsubconsulta, portanto, crie-o de qualquer maneira:

    CREATE INDEX event_pg_location_host_person_id_timestamp__idx
    ON event_pg (location_host, person_id, timestamp_);
    

    Para tnk:

    CREATE INDEX event_pg_location_fragment_timestamp__idx
    ON event_pg (location_fragment, person_id, timestamp_);
    

    Otimizado com índices parciais

    Se seus predicados forem constantes location_host, location_fragmentpodemos usar índices parciais muito mais baratos , especialmente porque suas location_*colunas parecem grandes:

    CREATE INDEX event_pg_hp_person_id_ts_idx ON event_pg (person_id, timestamp_)
    WHERE  location_host = '2015.testonline.ca';
    
    CREATE INDEX event_pg_hlp_person_id_ts_idx ON event_pg (person_id, timestamp_)
    WHERE  location_host = 'helpcentre.testonline.ca';
    
    CREATE INDEX event_pg_tnk_person_id_ts_idx ON event_pg (person_id, timestamp_)
    WHERE  location_fragment = '/file/thank-you';
    

    Considerar:

    • Índice e desempenho de várias colunas
    • Um índice composto também é bom para consultas no primeiro campo?

    Novamente, todos esses índices são substancialmente menores e mais rápidos com integerou bigintpara person_id.

    Geralmente, você precisa acessar ANALYZEa tabela depois de criar um novo índice - ou esperar até que o autovacuum entre em ação para fazer isso por você.

    • Índice que não é usado, mas influencia a consulta
    • Índice parcial do PostgreSQL não utilizado quando criado em uma tabela com dados existentes

    Para obter varreduras somente de índice , sua tabela deve ser VACUUM'editada o suficiente. Teste imediatamente depois VACUUMcomo prova de conceito. Leia a página vinculada do Postgres Wiki para obter detalhes se você não estiver familiarizado com varreduras somente de índice .

    Consulta básica

    Implementando o que eu discuti. Consulta para intervalos pequenos ( poucas linhas por person_id):

    SELECT count(*)::int           AS view_homepage
         , count(hlp.hlp_ts)::int AS use_help
         , count(tnk.yes)::int     AS thank_you
    FROM  (
       SELECT DISTINCT ON (person_id)
              person_id, timestamp_ AS hp_ts
       FROM   event_pg
       WHERE  timestamp_ >= '2016-04-23'
       AND    timestamp_ <  '2016-05-01'
       AND    location_host = '2015.testonline.ca'
       ORDER  BY person_id, timestamp_
       ) hp
    LEFT JOIN LATERAL (
       SELECT timestamp_ AS hlp_ts
       FROM   event_pg y 
       WHERE  y.person_id = hp.person_id
       AND    timestamp_ >= hp.hp_ts
       AND    timestamp_ <  '2016-05-01'
       AND    location_host = 'helpcentre.testonline.ca'
       ORDER  BY timestamp_
       LIMIT  1
       ) hlp ON true 
    LEFT JOIN LATERAL (
       SELECT true AS yes                   -- we only need existence
       FROM   event_pg z
       WHERE  z.person_id = hp.person_id    -- we can use hp here
       AND    location_fragment = '/file/thank-you'
       AND    timestamp_ >= hlp.hlp_ts      -- this introduces dependency on hlp anyways.
       AND    timestamp_ <  '2016-05-01'
       ORDER  BY timestamp_
       LIMIT  1
       ) tnk ON true;
    

    DISTINCT ONgeralmente é mais barato para algumas linhas por person_id. Explicação detalhada:

    • Selecione a primeira linha em cada grupo GROUP BY?

    Se você tiver muitas linhas porperson_id(mais provável para intervalos de tempo maiores), o CTE recursivo discutido nesta resposta no capítulo 1a pode ser (muito) mais rápido:

    • Otimize a consulta GROUP BY para recuperar o registro mais recente por usuário

    Veja integrado abaixo.

    Otimize e automatize a melhor consulta

    É o velho enigma: uma técnica de consulta é melhor para um conjunto menor, outra para um conjunto maior. No seu caso particular, temos um indicador muito bom desde o início - a duração de um determinado período de tempo - que podemos usar para decidir.

    Envolvemos tudo em uma função PL/pgSQL. Minha implementação muda DISTINCT ONpara rCTE quando o período de tempo determinado é maior que um limite definido:

    CREATE OR REPLACE FUNCTION f_my_counts(_ts_low_inc timestamp, _ts_hi_excl timestamp)
      RETURNS TABLE (view_homepage int, use_help int, thank_you int) AS
    $func$
    BEGIN
    
    CASE
    WHEN _ts_hi_excl <= _ts_low_inc THEN
       RAISE EXCEPTION 'Timestamp _ts_hi_excl (1st param) must be later than _ts_low_inc!';
    
    WHEN _ts_hi_excl - _ts_low_inc < interval '10 days' THEN  -- example value !!!
    -- DISTINCT ON for few rows per person_id
       RETURN QUERY
       WITH hp AS (
          SELECT DISTINCT ON (person_id)
                 person_id, timestamp_ AS hp_ts
          FROM   event_pg
          WHERE  timestamp_ >= _ts_low_inc
          AND    timestamp_ <  _ts_hi_excl
          AND    location_host = '2015.testonline.ca'
          ORDER  BY person_id, timestamp_
          )
        , hlp AS (
          SELECT hp.person_id, hlp.hlp_ts
          FROM   hp
          CROSS  JOIN LATERAL (
             SELECT timestamp_ AS hlp_ts
             FROM   event_pg
             WHERE  person_id = hp.person_id
             AND    timestamp_ >= hp.hp_ts
             AND    timestamp_ < _ts_hi_excl
             AND    location_host = 'helpcentre.testonline.ca'  -- match partial idx
             ORDER  BY timestamp_
             LIMIT  1
             ) hlp
          )
       SELECT (SELECT count(*)::int FROM hp)   -- AS view_homepage
            , (SELECT count(*)::int FROM hlp)  -- AS use_help
            , (SELECT count(*)::int            -- AS thank_you
               FROM   hlp
               CROSS  JOIN LATERAL (
                  SELECT 1                     -- we only care for existence
                  FROM   event_pg
                  WHERE  person_id = hlp.person_id
                  AND    location_fragment = '/file/thank-you'
                  AND    timestamp_ >= hlp.hlp_ts
                  AND    timestamp_ < _ts_hi_excl
                  ORDER  BY timestamp_
                  LIMIT  1
                  ) tnk
               );
    
    ELSE
    -- rCTE for many rows per person_id
       RETURN QUERY
       WITH RECURSIVE hp AS (
          (  -- parentheses required
          SELECT person_id, timestamp_ AS hp_ts
          FROM   event_pg
          WHERE  timestamp_ >= _ts_low_inc
          AND    timestamp_ <  _ts_hi_excl
          AND    location_host = '2015.testonline.ca'  -- match partial idx
          ORDER  BY person_id, timestamp_
          LIMIT  1
          )
          UNION ALL
          SELECT x.*
          FROM   hp, LATERAL (
             SELECT person_id, timestamp_ AS hp_ts
             FROM   event_pg
             WHERE  person_id  > hp.person_id  -- lateral reference
             AND    timestamp_ >= _ts_low_inc  -- repeat conditions
             AND    timestamp_ <  _ts_hi_excl
             AND    location_host = '2015.testonline.ca'  -- match partial idx
             ORDER  BY person_id, timestamp_
             LIMIT  1
             ) x
          )
        , hlp AS (
          SELECT hp.person_id, hlp.hlp_ts
          FROM   hp
          CROSS  JOIN LATERAL (
             SELECT timestamp_ AS hlp_ts
             FROM   event_pg y 
             WHERE  y.person_id = hp.person_id
             AND    location_host = 'helpcentre.testonline.ca'  -- match partial idx
             AND    timestamp_ >= hp.hp_ts
             AND    timestamp_ < _ts_hi_excl
             ORDER  BY timestamp_
             LIMIT  1
             ) hlp
          )
       SELECT (SELECT count(*)::int FROM hp)   -- AS view_homepage
            , (SELECT count(*)::int FROM hlp)  -- AS use_help
            , (SELECT count(*)::int            -- AS thank_you
               FROM   hlp
               CROSS  JOIN LATERAL (
                  SELECT 1                     -- we only care for existence
                  FROM   event_pg
                  WHERE  person_id = hlp.person_id
                  AND    location_fragment = '/file/thank-you'
                  AND    timestamp_ >= hlp.hlp_ts
                  AND    timestamp_ < _ts_hi_excl
                  ORDER  BY timestamp_
                  LIMIT  1
                  ) tnk
               );
    END CASE;
    
    END
    $func$  LANGUAGE plpgsql STABLE STRICT;
    

    Ligar:

    SELECT * FROM f_my_counts('2016-01-23', '2016-05-01');
    

    O rCTE trabalha com um CTE por definição. Também coloquei CTEs para a DISTINCT ONconsulta (como discuti com @Lennart nos comentários ), o que nos permite usar CROSS JOINem vez de LEFT JOINreduzir o conjunto a cada etapa, pois podemos contar cada CTE separadamente. Isso tem efeitos trabalhando em direções opostas:

    • Por um lado, reduzimos o número de linhas, o que deve tornar a terceira junção mais barata.
    • Por outro lado, introduzimos sobrecarga para os CTEs e precisamos de muito mais RAM, o que pode ser particularmente importante para grandes consultas como a sua.

    Você terá que testar qual supera o outro.

    • 12

relate perguntas

  • Posso ativar o PITR depois que o banco de dados foi usado

  • Práticas recomendadas para executar a replicação atrasada do deslocamento de tempo

  • Os procedimentos armazenados impedem a injeção de SQL?

  • Sequências Biológicas do UniProt no PostgreSQL

  • 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