Pergunta

Eu estou lidando com uma tabela Postgres (chamado de "vidas") que contém registros com colunas para time_stamp, usr_id, transaction_id e lives_remaining. Eu preciso de uma consulta que vai me dar a recente total de mais lives_remaining para cada usr_id

  1. Existem vários usuários (de usr_id distintas)
  2. time_stamp não é um identificador único:., Às vezes, eventos do usuário (um por linha na tabela) irá ocorrer com a mesma time_stamp
  3. trans_id é exclusivo apenas para muito pequenos intervalos de tempo: com o tempo ele repete
  4. remaining_lives (para um determinado usuário) podem tanto aumentar como diminuir ao longo do tempo

exemplo:

time_stamp|lives_remaining|usr_id|trans_id
-----------------------------------------
  07:00  |       1       |   1  |   1    
  09:00  |       4       |   2  |   2    
  10:00  |       2       |   3  |   3    
  10:00  |       1       |   2  |   4    
  11:00  |       4       |   1  |   5    
  11:00  |       3       |   1  |   6    
  13:00  |       3       |   3  |   1    

Como vou precisar para acessar outras colunas da linha com os dados mais recentes para cada dada usr_id, eu preciso de uma consulta que dá um resultado como este:

time_stamp|lives_remaining|usr_id|trans_id
-----------------------------------------
  11:00  |       3       |   1  |   6    
  10:00  |       1       |   2  |   4    
  13:00  |       3       |   3  |   1    

Como mencionado, cada usr_id pode ganhar ou perder vidas, e às vezes esses eventos com estampas de tempo ocorrer tão juntos que eles têm a mesma hora! Portanto, esta consulta não irá funcionar:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp) AS max_timestamp 
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp = b.time_stamp

Em vez disso, eu preciso usar tanto time_stamp (primeiro) e trans_id (segundo) para identificar a linha correta. Eu também precisa então de passar essa informação a partir da subconsulta para a consulta principal que irá fornecer os dados para as outras colunas das linhas apropriadas. Esta é a hackeou consulta que eu comecei a trabalhar:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp || '*' || trans_id) 
       AS max_timestamp_transid
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id 
ORDER BY b.usr_id

Ok, então isso funciona, mas eu não gosto dele. Ela exige uma consulta dentro de uma consulta, um auto juntar-se, e parece-me que poderia ser muito mais simples, agarrando a linha que MAX encontrado para ter o maior timestamp e trans_id. A tabela "vidas" tem dezenas de milhões de linhas para analisar, então eu gostaria que essa consulta para ser o mais rápido e eficiente possível. Eu sou novo para RDBM e Postgres em particular, então eu sei que eu preciso de fazer uso efetivo dos índices adequados. Eu estou um pouco perdido sobre como otimizar.

Eu encontrei um semelhante discussão aqui . Posso executar algum tipo de Postgres equivalente a uma função analítica do Oracle?

Algum conselho sobre como acessar informações da coluna relacionada usada por uma função agregada (como MAX), a criação de índices, e criando melhores consultas seria muito apreciada!

P.S. Você pode usar o seguinte para criar o meu exemplo de caso:

create TABLE lives (time_stamp timestamp, lives_remaining integer, 
                    usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);
Foi útil?

Solução

Sobre uma mesa com 158K linhas pseudo-aleatório (usr_id distribuído uniformemente entre 0 e 10k, trans_id distribuído uniformemente entre 0 e 30),

por consulta custo, abaixo, eu estou me referindo a Postgres' estimativa de custo do otimizador baseado em custo (com Postgres' valores xxx_cost padrão), que é uma estimativa da função ponderada de recursos de I / O e CPU necessários; você pode obter este disparando-se pgAdminIII e funcionando "Consulta / Explique (F7)" na consulta com "Consulta / Explique opções" set "analisar"

  • consulta de Quassnoy tem uma estimativa de custo de 745k (!), E completa em 1,3 segundos (dado um índice composto em (usr_id, trans_id, time_stamp))
  • consulta de Bill tem uma estimativa de custo de 93k, e completa em 2,9 segundos (dado um índice composto em (usr_id, trans_id))
  • consulta # 1 abaixo tem uma estimativa de custo de 16k, e é efectuado em 800ms (dado um índice composto em (usr_id, trans_id, time_stamp))
  • consulta # 2 abaixo tem uma estimativa de custo de 14k, e é efectuado em 800ms (dado um índice função composto em (usr_id, EXTRACT(EPOCH FROM time_stamp), trans_id))
    • este é específico do PostgreSQL
  • consulta # 3 abaixo (Postgres 8.4+) tem um cálculo de custos e tempo de conclusão comparável a (ou melhor do que) consulta # 2 (dado um índice composto em (usr_id, time_stamp, trans_id)) ; tem a vantagem de digitalizar a tabela de lives apenas uma vez e, se você aumentar temporariamente (se necessário) work_mem para acomodar o tipo na memória, será, de longe, o mais rápido de todas as consultas.

Todos os horários acima incluem a recuperação dos 10k linhas completa resultar-set.

Seu objetivo é estimativa de custo mínima e tempo de execução da consulta mínima, com ênfase no custo estimado. Consulta de execução pode depender significativamente em condições de tempo de execução (por exemplo, se as linhas relevantes já estão totalmente em cache na memória ou não), enquanto que a estimativa de custos não é. Por outro lado, lembre-se que a estimativa de custo é exatamente isso, uma estimativa.

O melhor tempo de execução de consulta é obtido quando rodando em um banco de dados dedicado sem carga (por exemplo, brincar com pgAdminIII em um PC de desenvolvimento.) Tempo de consulta irá variar em produção com base na actual acesso propagação carga da máquina / dados. Quando uma consulta aparece ligeiramente mais rápido (<20%) do que o outro, mas tem um muito custo mais elevado, geralmente será mais sensato para escolher aquele com maior tempo de execução, mas menor custo.

Quando você espera que não haverá competição por memória em sua máquina de produção no momento da consulta é executada (por exemplo, o cache RDBMS e cache do sistema de arquivos não será goleou por consultas simultâneas e / ou atividade de sistema de arquivos), então a consulta tempo obtido na autônomo (por exemplo pgAdminIII em um PC desenvolvimento) modo será representativa. Se houver contenção no sistema de produção, tempo de consulta irá degradar proporcionalmente à relação custo estimado, como a consulta com o custo mais baixo não confiar tanto em cache de enquanto a consulta com maior custo vai revisitar o mesmos dados mais e mais (desencadeamento adicionais de I / o na ausência de um cache estável), por exemplo:

              cost | time (dedicated machine) |     time (under load) |
-------------------+--------------------------+-----------------------+
some query A:   5k | (all data cached)  900ms | (less i/o)     1000ms |
some query B:  50k | (all data cached)  900ms | (lots of i/o) 10000ms |

Não se esqueça de executar ANALYZE lives uma vez depois de criar os índices necessários.


Consulta nº 1

-- incrementally narrow down the result set via inner joins
--  the CBO may elect to perform one full index scan combined
--  with cascading index lookups, or as hash aggregates terminated
--  by one nested index lookup into lives - on my machine
--  the latter query plan was selected given my memory settings and
--  histogram
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
    SELECT
      usr_id,
      MAX(time_stamp) AS time_stamp_max
     FROM
      lives
     GROUP BY
      usr_id
  ) AS l2
 ON
  l1.usr_id     = l2.usr_id AND
  l1.time_stamp = l2.time_stamp_max
 INNER JOIN (
    SELECT
      usr_id,
      time_stamp,
      MAX(trans_id) AS trans_max
     FROM
      lives
     GROUP BY
      usr_id, time_stamp
  ) AS l3
 ON
  l1.usr_id     = l3.usr_id AND
  l1.time_stamp = l3.time_stamp AND
  l1.trans_id   = l3.trans_max

Consulta nº 2

-- cheat to obtain a max of the (time_stamp, trans_id) tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
--  by far the least I/O intensive operation even in case of great scarcity
--  of memory (least reliant on cache for the best performance)
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
   SELECT
     usr_id,
     MAX(ARRAY[EXTRACT(EPOCH FROM time_stamp),trans_id])
       AS compound_time_stamp
    FROM
     lives
    GROUP BY
     usr_id
  ) AS l2
ON
  l1.usr_id = l2.usr_id AND
  EXTRACT(EPOCH FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
  l1.trans_id = l2.compound_time_stamp[2]

2013/01/29 update

Finalmente, a partir da versão 8.4, Postgres suportes Janela Função seja, você pode wralgo ite tão simples e eficiente como:

Consulta nº 3

-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
  last_value(time_stamp) OVER wnd,
  last_value(lives_remaining) OVER wnd,
  usr_id,
  last_value(trans_id) OVER wnd
 FROM lives
 WINDOW wnd AS (
   PARTITION BY usr_id ORDER BY time_stamp, trans_id
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
 );

Outras dicas

Eu proporia uma versão limpa com base em DISTINCT ON (ver docs ):

SELECT DISTINCT ON (usr_id)
    time_stamp,
    lives_remaining,
    usr_id,
    trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;

Aqui está um outro método, que passa a usar subconsultas não correlacionados ou GROUP BY. Eu não sou especialista em ajuste de desempenho PostgreSQL, então eu sugiro que você tente tanto esta como as soluções dadas por outras pessoas para ver qual funciona melhor para você.

SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
  ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp 
   OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;

Estou assumindo que trans_id é única, pelo menos, sobre qualquer valor dado de time_stamp.

Eu gosto do estilo de resposta de Mike Woodhouse na outra página que você mencionou. É especialmente concisa quando a coisa que está sendo maximizada mais é apenas uma única coluna, caso em que a subconsulta pode apenas usar MAX(some_col) e GROUP BY as outras colunas, mas no seu caso você tem uma quantidade de 2-parte a ser maximizada, você ainda pode fazê-lo usando ORDER BY mais LIMIT 1 vez (como feito por Quassnoi):

SELECT * 
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
    SELECT usr_id, time_stamp, trans_id
    FROM lives sq
    WHERE sq.usr_id = outer.usr_id
    ORDER BY trans_id, time_stamp
    LIMIT 1
)

I encontrar usando a linha-construtor sintaxe WHERE (a, b, c) IN (subquery) bom porque reduz a quantidade de verborragia necessário.

Actaully há uma solução hacky para este problema. Vamos dizer que você quer selecionar a maior árvore de cada floresta em uma região.

SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id

Ao agrupar árvores de florestas, haverá uma lista não ordenada de árvores e você precisa encontrar o maior deles. Primeira coisa que você deve fazer é ordenar as linhas por seus tamanhos e selecione o primeiro de sua lista. Pode parecer ineficiente, mas se você tem milhões de linhas será muito mais rápido do que as soluções que inclui condições JOIN de e WHERE.

BTW, nota que ORDER_BY para array_agg é introduzido no PostgreSQL 9.0

Há uma nova opção no PostgresSQL 9.5 chamado ON DISTINCT

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

Ele elimina linhas duplicadas um deixa apenas a primeira linha como definidas minha cláusula ORDER BY.

SELECT  l.*
FROM    (
        SELECT DISTINCT usr_id
        FROM   lives
        ) lo, lives l
WHERE   l.ctid = (
        SELECT ctid
        FROM   lives li
        WHERE  li.usr_id = lo.usr_id
        ORDER BY
          time_stamp DESC, trans_id DESC
        LIMIT 1
        )

Criação de um índice em (usr_id, time_stamp, trans_id) irá melhorar significativamente esta consulta.

Você deve sempre, sempre tem algum tipo de PRIMARY KEY em suas tabelas.

Eu acho que você tem um grande problema aqui: não há nenhuma monótona crescente "contra" a garantia de que uma determinada linha que aconteceu mais tarde no tempo do que o outro. Veja este exemplo:

timestamp   lives_remaining   user_id   trans_id
10:00       4                 3         5
10:00       5                 3         6
10:00       3                 3         1
10:00       2                 3         2

Você não pode determinar a partir destes dados que é a entrada mais recente. É a segunda ou a última? Não há sorte ou max () função que você pode aplicar a qualquer desses dados para dar-lhe a resposta correta.

Aumentar a resolução do timestamp seria uma grande ajuda. Uma vez que o mecanismo de banco serializa solicitações, com resolução suficiente você pode garantir que não há duas marcas de tempo será o mesmo.

Como alternativa, use um trans_id que não vai rolar por muito, muito tempo. Ter um trans_id que rola sobre os meios você não pode dizer (para a mesma hora) se trans_id 6 é mais recente do que trans_id 1 a menos que você fazer alguma matemática complicada.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top