Melhorar o desempenho de COUNT / GROUP-BY na grande tabela do PostgresSQL?

24

Estou executando o PostgresSQL 9.2 e tenho uma relação de 12 colunas com cerca de 6.700.000 linhas. Ele contém nós em um espaço 3D, cada um referenciando um usuário (quem o criou). Para consultar qual usuário criou quantos nós eu faço o seguinte (adicionado explain analyzepara obter mais informações):

EXPLAIN ANALYZE SELECT user_id, count(user_id) FROM treenode WHERE project_id=1 GROUP BY user_id;
                                                    QUERY PLAN                                                         
---------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=253668.70..253669.07 rows=37 width=8) (actual time=1747.620..1747.623 rows=38 loops=1)
   ->  Seq Scan on treenode  (cost=0.00..220278.79 rows=6677983 width=8) (actual time=0.019..886.803 rows=6677983 loops=1)
         Filter: (project_id = 1)
 Total runtime: 1747.653 ms

Como você pode ver, isso leva cerca de 1,7 segundos. Isso não é tão ruim, considerando a quantidade de dados, mas me pergunto se isso pode ser melhorado. Tentei adicionar um índice BTree na coluna do usuário, mas isso não ajudou em nada.

Você tem sugestões alternativas?


Por uma questão de completude, esta é a definição de tabela completa com todos os seus índices (sem restrições de chave estrangeira, referências e gatilhos):

    Column     |           Type           |                      Modifiers                    
---------------+--------------------------+------------------------------------------------------
 id            | bigint                   | not null default nextval('concept_id_seq'::regclass)
 user_id       | bigint                   | not null
 creation_time | timestamp with time zone | not null default now()
 edition_time  | timestamp with time zone | not null default now()
 project_id    | bigint                   | not null
 location      | double3d                 | not null
 reviewer_id   | integer                  | not null default (-1)
 review_time   | timestamp with time zone |
 editor_id     | integer                  |
 parent_id     | bigint                   |
 radius        | double precision         | not null default 0
 confidence    | integer                  | not null default 5
 skeleton_id   | bigint                   |
Indexes:
    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE CONSTRAINT, btree (id)
    "skeleton_id_treenode_index" btree (skeleton_id)
    "treenode_editor_index" btree (editor_id)
    "treenode_location_x_index" btree (((location).x))
    "treenode_location_y_index" btree (((location).y))
    "treenode_location_z_index" btree (((location).z))
    "treenode_parent_id" btree (parent_id)
    "treenode_user_index" btree (user_id)

Edit: Este é o resultado, quando eu uso a consulta (e índice) proposta por @ypercube (a consulta leva cerca de 5,3 segundos sem EXPLAIN ANALYZE):

EXPLAIN ANALYZE SELECT u.id, ( SELECT COUNT(*) FROM treenode AS t WHERE t.project_id=1 AND t.user_id = u.id ) AS number_of_nodes FROM auth_user As u;
                                                                        QUERY PLAN                                                                     
----------------------------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on auth_user u  (cost=0.00..6987937.85 rows=46 width=4) (actual time=29.934..5556.147 rows=46 loops=1)
   SubPlan 1
     ->  Aggregate  (cost=151911.65..151911.66 rows=1 width=0) (actual time=120.780..120.780 rows=1 loops=46)
           ->  Bitmap Heap Scan on treenode t  (cost=4634.41..151460.44 rows=180486 width=0) (actual time=13.785..114.021 rows=145174 loops=46)
                 Recheck Cond: ((project_id = 1) AND (user_id = u.id))
                 Rows Removed by Index Recheck: 461076
                 ->  Bitmap Index Scan on treenode_user_index  (cost=0.00..4589.29 rows=180486 width=0) (actual time=13.082..13.082 rows=145174 loops=46)
                       Index Cond: ((project_id = 1) AND (user_id = u.id))
 Total runtime: 5556.190 ms
(9 rows)

Time: 5556.804 ms

Edit 2: Este é o resultado, quando eu uso um indexon project_id, user_id(mas ainda não otimizamos o esquema) como @ erwin-brandstetter sugerido (a consulta é executada com 1,5 segundos na mesma velocidade da minha consulta original):

EXPLAIN ANALYZE SELECT user_id, count(user_id) as ct FROM treenode WHERE project_id=1 GROUP BY user_id;
                                                        QUERY PLAN                                                      
---------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=253670.88..253671.24 rows=37 width=8) (actual time=1807.334..1807.339 rows=38 loops=1)
   ->  Seq Scan on treenode  (cost=0.00..220280.62 rows=6678050 width=8) (actual time=0.183..893.491 rows=6678050 loops=1)
         Filter: (project_id = 1)
 Total runtime: 1807.368 ms
(4 rows)
tomka
fonte
Você também tem uma tabela Userscom user_ida chave primária?
precisa saber é o seguinte
Acabei de ver que há um addon columnstore de terceiros para o Postgres. Além disso, eu só queria postar a partir do novo iOS App
swasheck
2
Agradecimentos para o bom, claro, pergunta completa - versões, definições de tabela, etc.
Craig Ringer
@ ypercube Sim, eu tenho uma tabela de usuários.
Tomka
Quantas diferentes project_ide user_id? A tabela é atualizada continuamente ou você pode trabalhar com uma visão materializada (por algum tempo)?
precisa

Respostas:

25

O principal problema é o índice ausente. Mas tem mais.

SELECT user_id, count(*) AS ct
FROM   treenode
WHERE  project_id = 1
GROUP  BY user_id;
  • Você tem muitas bigintcolunas. Provavelmente um exagero. Normalmente, integeré mais que suficiente para colunas como project_ide user_id. Isso também ajudaria o próximo item.
    Ao otimizar a definição da tabela, considere esta resposta relacionada, com ênfase no alinhamento e preenchimento dos dados . Mas a maior parte do resto também se aplica:

  • O elefante na sala : não há índiceproject_id . Crie um. Isso é mais importante que o restante desta resposta.
    Enquanto estiver nisso, crie um índice com várias colunas:

    CREATE INDEX treenode_project_id_user_id_index ON treenode (project_id, user_id);

    Se você seguisse meu conselho, integerseria perfeito aqui:

  • user_idé definido NOT NULL, count(user_id)é equivalente a count(*), mas o último é um pouco mais rápido e mais rápido. (Nesta consulta específica, isso se aplica mesmo sem user_idser definido NOT NULL.)

  • idjá é a chave primária, a UNIQUErestrição adicional é um lastro inútil . Largue:

    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE CONSTRAINT, btree (id)

    Além: eu não usaria idcomo nome da coluna. Use algo descritivo como treenode_id.

Informação adicionada

Q: How many different project_id and user_id?
A: not more than five different project_id.

Isso significa que o Postgres precisa ler cerca de 20% da tabela inteira para satisfazer sua consulta. A menos que ele possa usar uma varredura apenas de índice , uma varredura seqüencial na tabela será mais rápida do que envolve qualquer índice. Não há mais desempenho a ganhar aqui - exceto otimizando as configurações da tabela e do servidor.

Quanto à varredura apenas de índice : Para ver quão eficaz isso pode ser, execute VACUUM ANALYZEse você puder pagar por isso (bloqueia a tabela exclusivamente). Em seguida, tente sua consulta novamente. Agora deve ser moderadamente mais rápido usando apenas o índice. Leia esta resposta relacionada primeiro:

Assim como a página de manual adicionada ao Postgres 9.6 e ao Wiki do Postgres em verificações apenas de índice .

Erwin Brandstetter
fonte
11
Erwin, obrigado por suas sugestões. Você está certo, user_ide project_id integerdeve ser mais do que suficiente. Usar em count(*)vez de count(user_id)economizar cerca de 70ms aqui, é bom saber. Adicionei EXPLAIN ANALYZEa consulta depois de adicionar sua sugestão indexà primeira postagem. Porém, não melhora o desempenho (mas também não dói). Parece que indexnão é usado. Testarei as otimizações do esquema em breve.
Tomka
11
Se eu desabilitar seqscan, o índice será usado ( Index Only Scan using treenode_project_id_user_id_index on treenode), mas a consulta levará cerca de 2,5 segundos (o que é cerca de 1 segundo a mais que com seqscan).
Tomka
11
Obrigado pela sua atualização. Esses bits ausentes deveriam ter sido parte da minha pergunta, isso mesmo. Eu simplesmente não estava ciente do impacto deles. Otimizarei meu esquema como você sugeriu - vamos ver o que posso ganhar com isso. Obrigado pela sua explicação, isso faz sentido para mim e, portanto, marcarei sua resposta como a aceita.
Tomka
7

Primeiro eu adicionaria um índice (project_id, user_id)e, na versão 9.3, tente esta consulta:

SELECT u.user_id, c.number_of_nodes 
FROM users AS u
   , LATERAL
     ( SELECT COUNT(*) AS number_of_nodes 
       FROM treenode AS t
       WHERE t.project_id = 1 
         AND t.user_id = u.user_id
     ) c 
-- WHERE c.number_of_nodes > 0 ;   -- you probably want this as well
                                   -- to show only relevant users

Na 9.2, tente este:

SELECT u.user_id, 
       ( SELECT COUNT(*) 
         FROM treenode AS t
         WHERE t.project_id = 1 
           AND t.user_id = u.user_id
       ) AS number_of_nodes  
FROM users AS u ;

Presumo que você tenha uma usersmesa. Caso contrário, substitua userspor:
(SELECT DISTINCT user_id FROM treenode)

ypercubeᵀᴹ
fonte
Muito obrigado pela sua resposta. Você está correto, eu tenho uma tabela de usuários. No entanto, usando sua consulta na 9.2, leva cerca de 5 segundos para obter o resultado - independentemente de o índice ser criado ou não. Criei o índice assim:, CREATE INDEX treenode_user_index ON treenode USING btree (project_id, user_id);mas tentei também sem a USINGcláusula. Perco alguma coisa?
Tomka
Quantas linhas existem na userstabela e quantas linhas a consulta retorna (quantos usuários existem project_id=1)? Você pode mostrar a explicação dessa consulta depois de adicionar o índice?
usar o seguinte código
11
Primeiro, eu estava errado no meu primeiro comentário. Sem o índice sugerido, leva cerca de 40s (!) Para recuperar o resultado. Demora cerca de 5s com o indexlocal. Desculpe pela confusão. Na minha usersmesa, tenho 46 entradas. A consulta retorna apenas 9 linhas. Surpreendentemente, SELECT DISTINCT user_id FROM treenode WHERE project_id=1;retorna 38 linhas. Adicionei o explainao meu primeiro post. E para evitar confusão: minha usersmesa é chamada de verdade auth_user.
Tomka
Gostaria de saber como pode SELECT DISTINCT user_id FROM treenode WHERE project_id=1;retornar 38 linhas, enquanto as consultas retornam apenas 9. Buffled.
precisa saber é o seguinte
Você pode tentar isso?:SET enable_seqscan = OFF; (Query); SET enable_seqscan = ON;
ypercubeᵀᴹ