Por que as funções agregadas SQL são muito mais lentas que Python e Java (ou OLAP do Poor Man)

StackOverflow https://stackoverflow.com/questions/51553

Pergunta

Preciso da opinião de um DBA real.O Postgres 8.3 leva 200 ms para executar esta consulta no meu Macbook Pro, enquanto Java e Python realizam o mesmo cálculo em menos de 20 ms (350.000 linhas):

SELECT count(id), avg(a), avg(b), avg(c), avg(d) FROM tuples;

Esse comportamento é normal ao usar um banco de dados SQL?

O esquema (a tabela contém as respostas a uma pesquisa):

CREATE TABLE tuples (id integer primary key, a integer, b integer, c integer, d integer);

\copy tuples from '350,000 responses.csv' delimiter as ','

Escrevi alguns testes em Java e Python para contexto e eles esmagam o SQL (exceto para python puro):

java   1.5 threads ~ 7 ms    
java   1.5         ~ 10 ms    
python 2.5 numpy   ~ 18 ms  
python 2.5         ~ 370 ms

Até o sqlite3 é competitivo com o Postgres, apesar de assumir que todas as colunas são strings (para contraste:mesmo usando apenas a mudança para colunas numéricas em vez de números inteiros no Postgres resulta em lentidão de 10x)

As configurações que tentei sem sucesso incluem (seguindo cegamente alguns conselhos da web):

increased the shared memory available to Postgres to 256MB    
increased the working memory to 2MB
disabled connection and statement logging
used a stored procedure via CREATE FUNCTION ... LANGUAGE SQL

Então, minha pergunta é: minha experiência aqui é normal e é isso que posso esperar ao usar um banco de dados SQL?Entendo que o ACID tenha custos, mas isso é meio maluco na minha opinião.Não estou pedindo velocidade do jogo em tempo real, mas como o Java pode processar milhões de duplicatas em menos de 20 ms, sinto um pouco de inveja.

Existe uma maneira melhor de fazer OLAP simples e barato (tanto em termos de dinheiro quanto de complexidade do servidor)?Eu pesquisei Mondrian e Pig + Hadoop, mas não estou muito animado em manter outro aplicativo de servidor e não tenho certeza se eles ajudariam.


Não, o código Python e o código Java fazem todo o trabalho internamente, por assim dizer.Acabei de gerar 4 matrizes com 350.000 valores aleatórios cada e, em seguida, calculo a média.Não incluo a geração nos tempos, apenas o passo médio.O tempo dos threads java usa 4 threads (um por média de array), um exagero, mas é definitivamente o mais rápido.

O tempo do sqlite3 é controlado pelo programa Python e é executado a partir do disco (não :memory:)

Sei que o Postgres está fazendo muito mais nos bastidores, mas a maior parte desse trabalho não importa para mim, pois são dados somente leitura.

A consulta do Postgres não altera o tempo nas execuções subsequentes.

Executei novamente os testes do Python para incluir o spool do disco.O tempo diminui consideravelmente para quase 4 segundos.Mas estou supondo que o código de manipulação de arquivos do Python está praticamente em C (embora talvez não seja a lib csv?), então isso me indica que o Postgres também não está transmitindo do disco (ou que você está correto e eu deveria me curvar antes de quem escreveu sua camada de armazenamento!)

Foi útil?

Solução

O Postgres está fazendo muito mais do que parece (mantendo a consistência dos dados para começar!)

Se os valores não precisam ser 100% corretos ou se a tabela é atualizada raramente, mas você executa esse cálculo com frequência, convém consultar as Visualizações materializadas para acelerá-lo.

(Observe que eu não usei visualizações materializadas no Postgres, elas parecem um pouco hackeadas, mas podem ser adequadas à sua situação).

Visualizações materializadas

Considere também a sobrecarga de realmente conectar-se ao servidor e o percurso de ida e volta necessário para enviar a solicitação ao servidor e vice-versa.

Eu consideraria 200 ms para algo assim muito bom. Um teste rápido no meu servidor oracle, a mesma estrutura de tabela com cerca de 500 mil linhas e sem índices, leva cerca de 1 a 1,5 segundos, o que é quase tudo apenas oracle sugando os dados fora do disco.

A verdadeira questão é: 200ms são rápidos o suficiente?

-------------- Mais --------------------

Eu estava interessado em resolver isso usando visualizações materializadas, já que nunca brinquei com elas.Isso está no oráculo.

Primeiro criei um MV que é atualizado a cada minuto.

create materialized view mv_so_x 
build immediate 
refresh complete 
START WITH SYSDATE NEXT SYSDATE + 1/24/60
 as select count(*),avg(a),avg(b),avg(c),avg(d) from so_x;

Embora seja atualizado, nenhuma linha foi retornada

SQL> select * from mv_so_x;

no rows selected

Elapsed: 00:00:00.00

Depois de atualizado, é MUITO mais rápido do que fazer a consulta bruta

SQL> select count(*),avg(a),avg(b),avg(c),avg(d) from so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:05.74
SQL> select * from mv_so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:00.00
SQL> 

Se inserirmos na tabela base, o resultado não fica imediatamente visível na visualização do MV.

SQL> insert into so_x values (1,2,3,4,5);

1 row created.

Elapsed: 00:00:00.00
SQL> commit;

Commit complete.

Elapsed: 00:00:00.00
SQL> select * from mv_so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:00.00
SQL> 

Mas espere um minuto ou mais, e o MV será atualizado nos bastidores e o resultado será retornado com a rapidez que você deseja.

SQL> /

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899460 7495.35823 22.2905352 5.00276078 2.17647059

Elapsed: 00:00:00.00
SQL> 

Isso não é o ideal.para começar, não é em tempo real, inserções/atualizações não serão imediatamente visíveis.Além disso, você tem uma consulta em execução para atualizar o MV, quer precise ou não (isso pode ser ajustado para qualquer período de tempo ou sob demanda).Mas isso mostra o quanto um MV pode fazer com que pareça mais rápido para o usuário final, se você puder conviver com valores que não são precisos até o segundo.

Outras dicas

Eu diria que seu esquema de teste não é realmente útil.Para atender à consulta do banco de dados, o servidor do banco de dados passa por várias etapas:

  1. analisar o SQL
  2. elaborar um plano de consulta, i.e.decidir quais índices usar (se houver), otimizar etc.
  3. se um índice for usado, pesquise os ponteiros para os dados reais e, em seguida, vá para o local apropriado nos dados ou
  4. se nenhum índice for usado, verifique a mesa inteira para determinar quais linhas são necessárias
  5. carregue os dados do disco em um local temporário (espero, mas não necessariamente, memória)
  6. execute os cálculos count() e avg()

Portanto, criar um array em Python e obter a média basicamente pula todas essas etapas, exceto a última.Como a E/S de disco está entre as operações mais caras que um programa deve realizar, esta é uma grande falha no teste (veja também as respostas para essa questão Eu perguntei aqui antes).Mesmo se você ler os dados do disco em seu outro teste, o processo é completamente diferente e é difícil dizer quão relevantes são os resultados.

Para obter mais informações sobre onde o Postgres passa seu tempo, sugiro os seguintes testes:

  • Compare o tempo de execução da sua consulta com um SELECT sem as funções de agregação (ou seja,e.corte passo 5)
  • Se você achar que a agregação leva a uma desaceleração significativa, tente se o Python faz isso mais rápido, obtendo os dados brutos por meio do SELECT simples da comparação.

Para acelerar sua consulta, reduza primeiro o acesso ao disco.Duvido muito que seja a agregação que demore.

Existem várias maneiras de fazer isso:

  • Armazene dados em cache (na memória!) para acesso subsequente, seja por meio dos próprios recursos do mecanismo de banco de dados ou com ferramentas como memcached
  • Reduza o tamanho dos seus dados armazenados
  • Otimize o uso de índices.Às vezes, isso pode significar ignorar completamente o uso do índice (afinal, também é acesso ao disco).Para o MySQL, lembro que é recomendado pular os índices se você assumir que a consulta busca mais de 10% de todos os dados da tabela.
  • Se sua consulta faz bom uso de índices, sei que para bancos de dados MySQL ajuda colocar índices e dados em discos físicos separados.No entanto, não sei se isso é aplicável ao Postgres.
  • Também pode haver problemas mais sofisticados, como a troca de linhas para o disco se, por algum motivo, o conjunto de resultados não puder ser completamente processado na memória.Mas eu deixaria esse tipo de pesquisa até encontrar sérios problemas de desempenho que não consigo encontrar outra maneira de corrigir, pois requer conhecimento sobre muitos pequenos detalhes ocultos em seu processo.

Atualizar:

Acabei de perceber que você parece não usar índices para a consulta acima e provavelmente também não está usando nenhum, então meu conselho sobre índices provavelmente não foi útil.Desculpe.Ainda assim, eu diria que a agregação não é o problema, mas o acesso ao disco é.Vou deixar o material do índice, de qualquer maneira, ainda pode ter alguma utilidade.

Testei novamente com MySQL especificando ENGINE = MEMORY e isso não muda nada (ainda 200 ms).Sqlite3 usando um banco de dados na memória também fornece tempos semelhantes (250 ms).

A matemática aqui parece correto (pelo menos o tamanho, pois é o tamanho do banco de dados sqlite :-)

Só não estou acreditando no argumento da lentidão que causa o disco, pois há todas as indicações de que as tabelas estão na memória (todos os caras do postgres alertam contra tentar muito fixar tabelas na memória, pois juram que o sistema operacional fará isso melhor do que o programador )

Para esclarecer os tempos, o código Java não está lendo do disco, tornando uma comparação totalmente injusta se o Postgres estiver lendo do disco e calculando uma consulta complicada, mas isso não vem ao caso, o banco de dados deve ser inteligente o suficiente para trazer um pequeno tabela na memória e pré-compilar um procedimento armazenado IMHO.

ATUALIZAÇÃO (em resposta ao primeiro comentário abaixo):

Não tenho certeza de como testaria a consulta sem usar uma função de agregação de uma forma que fosse justa, pois se eu selecionar todas as linhas, gastarei muito tempo serializando e formatando tudo.Não estou dizendo que a lentidão se deva à função de agregação; ainda pode ser apenas uma sobrecarga de simultaneidade, integridade e amigos.Só não sei como isolar a agregação como a única variável independente.

Essas são respostas muito detalhadas, mas a maioria levanta a questão: como obtenho esses benefícios sem sair do Postgres, visto que os dados cabem facilmente na memória, requerem leituras simultâneas, mas não gravam, e são consultados com a mesma consulta repetidamente.

É possível pré-compilar o plano de consulta e otimização?Eu teria pensado que o procedimento armazenado faria isso, mas realmente não ajuda.

Para evitar o acesso ao disco é necessário armazenar em cache toda a tabela na memória, posso forçar o Postgres a fazer isso?Acho que já está fazendo isso, já que a consulta é executada em apenas 200 ms após execuções repetidas.

Posso dizer ao Postgres que a tabela é somente leitura, para otimizar qualquer código de bloqueio?

Acho que é possível estimar os custos de construção da consulta com uma tabela vazia (os tempos variam de 20 a 60 ms)

Ainda não consigo entender por que os testes Java/Python são inválidos.O Postgres simplesmente não está fazendo muito mais trabalho (embora eu ainda não tenha abordado o aspecto da simultaneidade, apenas o cache e a construção da consulta)

ATUALIZAR:Não acho justo comparar os SELECTS conforme sugerido, puxando 350.000 através das etapas de driver e serialização para Python para executar a agregação, nem mesmo omitir a agregação, pois é difícil separar a sobrecarga na formatação e exibição do tempo.Se ambos os mecanismos estiverem operando em dados de memória, deve ser uma comparação idêntica, mas não tenho certeza de como garantir que isso já esteja acontecendo.

Não consigo descobrir como adicionar comentários, talvez não tenha reputação suficiente?

Eu também sou um cara de MS-SQL e usaríamos DBCC PINTABLE para manter uma tabela em cache, e DEFINIR ESTATÍSTICAS IO para ver se ele está lendo do cache e não do disco.

Não consigo encontrar nada no Postgres para imitar o PINTABLE, mas pg_buffercache parece fornecer detalhes sobre o que está no cache - você pode querer verificar isso e ver se sua tabela está realmente sendo armazenada em cache.

Um cálculo rápido no verso do envelope me faz suspeitar que você está paginando a partir do disco.Supondo que o Postgres use números inteiros de 4 bytes, você tem (6 * 4) bytes por linha, portanto sua tabela tem no mínimo (24 * 350.000) bytes ~ 8,4 MB.Supondo uma taxa de transferência sustentada de 40 MB/s em seu HDD, você terá cerca de 200 ms para ler os dados (o que, como apontado, deve ser onde quase todo o tempo está sendo gasto).

A menos que eu tenha estragado minha matemática em algum lugar, não vejo como é possível ler 8 MB em seu aplicativo Java e processá-lo nos horários que você está mostrando - a menos que esse arquivo já esteja armazenado em cache pela unidade ou pelo seu SO.

Não acho que seus resultados sejam tão surpreendentes - na verdade, é que o Postgres é tão rápido.

A consulta do Postgres é executada mais rapidamente pela segunda vez depois de ter a chance de armazenar os dados em cache?Para ser um pouco mais justo, seu teste para Java e Python deve cobrir o custo de aquisição dos dados (de preferência, carregá-los do disco).

Se esse nível de desempenho for um problema para sua aplicação na prática, mas você precisar de um RDBMS por outros motivos, poderá consultar memcached.Você teria então acesso mais rápido ao cache aos dados brutos e poderia fazer os cálculos no código.

Você está usando TCP para acessar o Postgres?Nesse caso, Nagle está atrapalhando o seu tempo.

Outra coisa que um RDBMS geralmente faz por você é fornecer simultaneidade, protegendo você do acesso simultâneo por outro processo.Isso é feito colocando bloqueios, e há alguma sobrecarga nisso.

Se você estiver lidando com dados totalmente estáticos que nunca mudam e, especialmente, se estiver basicamente em um cenário de "usuário único", o uso de um banco de dados relacional não traz necessariamente muitos benefícios.

Você precisa aumentar os caches do postgres até o ponto em que todo o conjunto de trabalho caiba na memória antes de poder esperar um desempenho comparável ao de fazê-lo na memória com um programa.

Obrigado pelos horários do Oracle, esse é o tipo de coisa que estou procurando (embora decepcionante :-)

Provavelmente vale a pena considerar as visualizações materializadas, pois acho que posso pré-calcular as formas mais interessantes dessa consulta para a maioria dos usuários.

Não acho que o tempo de ida e volta da consulta deva ser muito alto, pois estou executando as consultas na mesma máquina que executa o Postgres, portanto, não é possível adicionar muita latência.

Eu também verifiquei os tamanhos do cache, e parece que o Postgres depende do sistema operacional para lidar com o cache, eles mencionam especificamente o BSD como o sistema operacional ideal para isso, então acho que o Mac OS deveria ser muito inteligente ao trazer a tabela para memória.A menos que alguém tenha parâmetros mais específicos em mente, acho que o cache mais específico está fora do meu controle.

No final, provavelmente consigo suportar tempos de resposta de 200 ms, mas saber que 7 ms é uma meta possível me deixa insatisfeito, pois mesmo tempos de 20 a 50 ms permitiriam que mais usuários tivessem consultas mais atualizadas e se livrassem de muitos caches e hacks pré-computados.

Acabei de verificar os tempos usando o MySQL 5 e eles são um pouco piores que o Postgres.Portanto, exceto alguns avanços importantes no cache, acho que é isso que posso esperar na rota do banco de dados relacional.

Gostaria de poder votar em algumas de suas respostas, mas ainda não tenho pontos suficientes.

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