Olá, pessoal!
Depois que publiquei os artigos sobre dicas do componente DBGrid e sobre a crueldade de um software lento, recebi algumas perguntas de leitores sobre como melhorar o desempenho de consultas e exibição de dados. O artigo de hoje é uma sugestão dor Artur, um destes leitores, que deixou um comentário recente relacionado a esse assunto. Confira!
A lentidão ao carregar dados em um componente TDBGrid pode ser causada por vários fatores que, muitas vezes, nos passam despercebidos. As seis orientações abaixo podem ser úteis para agilizar o carregamento de dados ou, talvez, para evitar futuras lentidões.
1) Revise a instrução Select que consulta os dados
Assim como mencionei no artigo sobre práticas de otimização em banco de dados, as consultas SQL devem trazer apenas os dados que são necessários na visão da DBGrid. Por exemplo, se existem 10 colunas em uma tabela e somente 4 são exibidas em uma DBGrid, as outras 6 podem (e devem) ser retiradas da consulta. Para isso, substitua o asterisco pelas colunas requisitadas, conforme a comparação abaixo:
1 2 3 4 5 |
-- Seleciona TODAS as colunas da tabela, desnecessário na maioria das consultas Select * from PRODUTOS -- Seleciona somente as colunas necessárias para exibir os dados Select Codigo, Descricao, Valor, Qtde from PRODUTOS |
Além disso, em alguns casos, há tabelas com colunas do tipo BLOB, que armazenam dados binários de arquivos ou textos longos. Se essas colunas forem adicionadas na consulta, o tempo de retorno será ainda mais demorado. A minha recomendação é trazer estes dados sob demanda em uma instrução separada, como comentei no artigo sobre eventos de tela.
2) Evite abrir e fechar o DataSet repetidas vezes
Quando for necessário aplicar um filtro nos dados, como um intervalo de datas, procure utilizar as propriedades Filter
e Filtered
, ao invés de comandos que fazem acesso ao banco, como CommandText
, Close
/Open
e ExecSQL
. A explicação é que a propriedade Filter
trabalha com dados em memória, isto é, que já foram carregados.
Entrando mais em detalhes técnicos, o ideal é substituir esses tipos de consulta:
1 2 3 |
DataSet.Close; DataSet.CommandText := 'Select * from CLIENTES where Nome like ' + QuotedStr(Edit1.Text + '%'); DataSet.Open; |
Por filtros como esse:
1 2 |
DataSet.Filter := 'Nome like ' + QuotedStr(Edit1.Text + '%'); DataSet.Filtered := True; |
3) Atente-se aos eventos do DataSet, dos Fields e de pintura da DBGrid
Por falar em eventos de tela, o desenvolvedor também deve ser prudente ao utilizá-los, já que podem impactar no tempo de carregamento dos dados. Por exemplo, você sabia que o evento OnDrawColumnCell
do componente DBGrid é chamado para cada registro que é carregado? Insira um ShowMessage
neste evento, abra o DataSet e observe a quantidade de vezes que a mensagem é exibida. Logo, se houver um processamento neste evento, é evidente que os dados levarão um tempo maior para serem carregados.
A mesma orientação é válida para eventos do DataSet conectado à DBGrid, como BeforeOpen
, AfterOpen
, BeforeGetRecords
e AfterGetRecords
. Embora sejam executados apenas uma vez ao consultar os dados, podem apresentar lentidões se os processamentos forem extensos.
Seguindo a mesma lógica, o evento OnGetText
do TField
(campo da tabela), como trata a exibição dos dados de uma coluna, também é executado para cada registro e deve ser observado.
4) Adicione filtros na tela para evitar a consulta de vários registros
Muitos desenvolvedores costumam executar a consulta de dados logo quando a tela é aberta. Isso significa que, se houverem 10 mil registros, todos eles serão consultados de uma vez só. Bom, nem preciso comentar que isso é uma falha de desempenho, não é?
Além da demora, esse procedimento também não deixa de ser uma questão de usabilidade. Muitas vezes, o usuário entra na tela para visualizar os dados de apenas 1 registro, e precisa esperar, desnecessariamente, a consulta de todos os registros da tabela.
A recomendação é disponibilizar filtros na tela (como código, descrição, tipo, perÃodo, etc…), e realizar a consulta somente quando estes filtros forem informados. Com essa alteração, já consigo apontar 3 vantagens: 1) não haverá “gargalos” ao abrir a tela; 2) o usuário visualiza somente o(s) registro(s) desejado(s); 3) reduz o tráfego de dados em um ambiente cliente/servidor.
5) Ative a paginação de registros com a propriedade PacketRecords
Quem disse que não dá para fazer paginação com Delphi? A propriedade PacketRecords
do DataSet permite definir a quantidade de registros que serão “paginados”, ou carregados por vez.
Faça um teste: preencha a propriedade PacketRecords
com o valor “100” e abra um DataSet que tenha aproximadamente 1000 registros. Observe que apenas 100 registros serão exibidos inicialmente na DBGrid, porém, ao navegar até a última linha, os próximos 100 registros da tabela serão carregados e exibidos automaticamente.
Este recurso pode ser bastante útil quando a tela exige a consulta de milhares de registros, por uma questão técnica ou por solicitação do cliente, ou mesmo quando a terceira dica, sobre filtros na tela, é inviável.
6) Modere na utilização de componentes de terceiros
Às vezes precisamos de algum comportamento adicional, não existente no componente TDBGrid nativo do Delphi. Como solução, muitos desenvolvedores instalam componentes de terceiros para atender a necessidade. No entanto, vale ressaltar que estes componentes trazem vários outros comportamentos que, em sua maior parte, podem não interessar no momento. Por exemplo, suponha que um componente de terceiro monte toda uma estrutura de agrupamento dinâmico, ative campos Lookup ou faça uma indexação do conteúdo para buscas, quando, o que realmente precisamos, é só um totalizador de valores no rodapé. Por estarem acoplados ao componente, estes outros comportamentos “paralelos” também podem impactar indiretamente na exibição dos dados.
A minha sugestão é criar um novo componente herdado da classe TDBGrid e adicionar os comportamentos desejados, mesmo que exija um pouco mais de tempo. Além de dispensar a instalação de componentes de terceiros, o desenvolvedor ganha a liberdade de customizar o componente de acordo com suas próprias necessidades.
Espero que o artigo seja útil, pessoal!
Grande abraço!
Saudoso amigo André, gostaria de deixar minha contribuição para o artigo. Prefira utilizar parâmetros nas consultas para aproveitar o cache do banco, principalmente em colunas do tipo cadeia de caracteres. Seja MySQL, FireBird, Oracle com dbExpress, ADO, IBX, ZeosDB, AnyDac ou FireDac. Como:
‘SELECT campos FROM tabela WHERE descricao = :descricao’
oQuery.ParamByName(‘descricao’).AsString := ‘Foo’;
Ótima contribuição, Marcão!
Já elaborei um artigo sobre parâmetros em Queries mas, por falha minha, não comentei sobre a vantagem do cache do banco de dados.
Essa técnica evidentemente contribui muito para o desempenho da aplicação.
Obrigado, meu caro! Abraço!
Olá André,
Primeiramente parabéns pelo blog, realmente muito objetivo e “limpo”, que é o que precisamos.
Gostaria de saber sobre o uso do PacketRecords… Supondo que estou trazendo uma lista “paginada” com 100 registros, caso o usuário faça uma busca pela descrição num Edit, e caso o produto exista mas não esteja nessa faixa dos 100 registros, como fazer para localiza-lo sem ser por um filtro, pois acontece de ser mais prático para o usuário ver vários registros ao mesmo tempo, no caso que comecem com aquela string!
De já muito obrigado, e parabéns novamente!
Olá, Gleickson, tudo bem?
Primeiramente, obrigado pelo feedback sobre o artigo!
Bom, para localizar um registro em um conjunto de dados, mesmo com a propriedade PacketRecords configurada com um valor maior que zero, basta utilizar o comando Locate informando a opção loPartialKey no último parâmetro:
Vale ressaltar que o comando acima irá localizar somente os registros nos quais o inÃcio da descrição seja igual ao valor digitado no campo “Edit1”. Por exemplo, ao digitar “José” na “Edit1”, este será o resultado:
– José da Silva: será localizado
– Maria José: não será localizado
Se o objetivo é localizar comparações dentro de um string, o ideal é utilizar o Filter.
Abraço!
André, Muito obrigado pela rapidez e gentileza em responder!
Mas acho que não me fiz entender muito bem. No caso preciso localizar um registro que está fora da faixa que está carregada no cach do PacketRecords, isto é, estou exibindo os 100 primeiros registros, mas quando eu faço uma busca, o primeiro registro iniciando com essa String no banco de dados estaria no registro número 2500(por exemplo). Esse registro será localizado, mesmo sem essa faixa de registros ter sido “paginada” ainda?
Mais uma vez obg!!
Isso mesmo, Gleickson!
Por isso que no comentário anterior fiz questão de frisar:
“para localizar um registro em um conjunto de dados, mesmo com a propriedade PacketRecords configurada com um valor maior que zero […]”
Abraço!
Boa tarde André,
Fiz os teste e como você falou, realmente o registro é localizado sem problemas, mesmo se estiver fora da faixa já carregada pelo PacketRecords naquele momento, Show! Só tem um inconveniente, que ao utilizar o Locate todos os registros são carregados e o PacketRecords perde sua função, mesmo que esse registro seja o primeiro da lista ou mesmo que não seja localizado o valor que estou buscando. Pena que o uso que eu estava pensando em fazer do PacketRecords não vai ficar legal assim, mas com certeza apareceram outras situações que sim.
Mais uma vez muito obrigado pela paciência e gentileza, prometo não perturbar mais (pelo menos sobre PacketRecords kkkk).
Grande abraço e sucesso.
Boa tarde, Gleickson!
Exato. A função “Locate” irá acessar os registros fora da faixa estipulada pela propriedade PacketRecords, caso seja necessário. Mas, ao meu ver, este é o comportamento correto. Se a intenção é mostrar apenas o registro encontrado, a propriedade Filter deve ser utilizada.
O PacketRecords é apenas um mecanismo de paginação que possibilita a exibição de poucos registros por vez, evitando várias chamadas de métodos de pintura, validações de valores em Grid, etc. Por isso que mencionei este recurso no artigo! 🙂
Abraço!
Fala amigo!
Espero contribuir mesmo que anos após a publicação.
Não basta que o PacketRecords esteja acionado. É preciso que você insira no evento afterscroll os comandos abaixo:
Assim, quando estiver no rolando para baixo no dbgrid, automaticamente o sistema vai carregar os próximos 100 até que chegue no final da tabela.
Se me permite uma colocação, não concordo com o coleto em utilizar filter, locate, findkey ou findnearestkey. Estas pesquisas são feitas nos dados que já foram carregados na memória e limitando-se a eles.
Se estiver utilizando um sistema em rede, onde houve o cadastro de registros que seriam pertinentes serem exibidos no filtro já exibido, os mesmos não serão exibidos. Ou mesmo um registro que tenha sofrido alteração em outro computador, mas que não tenha sido carregado novamente. Estará com dados desatualizados e estará alterando algo que já foi alterado.
Enfim, espero ter contribuÃdo para os próximos que verem este artigo, pois acredito que os colegas já tenha resolvido suas dúvidas.
Boa colocação, Felipe. Concordo com você sobre o risco da utilização dos filtros e buscas em memória.
Eu, particularmente, prefiro implementar a paginação a nÃvel de SQL, principalmente pelo fato que o usuário dificilmente vai trabalhar com vários registros de uma vez só. Ele(a) sempre vai usar um filtro de busca para encontrar os registros que deseja visualizar. Essa é uma questão importante de performance.
Bom dia amigo, tudo bem? muito legal seu artigo, não é o primeiro que eu leio e já salvei sua pagina em meus favoritos para consultas.
Me ajuda com uma dúvida. Fiz uma pequena aplicação utilizando o Delphi Xe8 com base Firebird, porem quando uso pesquisas do tipo “select nome_campo from base where…….” ele da erro de execução. Quando substituo o “nome_campo” po ” * ” o programa funciona normalmente e me retorna o resultado da consulta.
O que pode ser? Será alguma função do Firebird que precisa ser habilitada, já que faço o mesmo tipo de pesquisa utilizando o MySql e funciona normalmente.
Apenas como informativo, para conexão com o Firebird uso o Firedac e para conexão com o MySql uso os componentes da palheta ADO.
Obrigado pela ajuda.
Abraços!!!
Boa noite, Rogério, tudo bem?
Fico grato em saber que salvou o blog nos favoritos! Obrigado!
Este erro de execução não deveria ocorrer. Vou entrar em contato com você para solicitar mais detalhes, ok?
Abraço!
Bom dia André, como vai?
Não consegui trabalhar com a propriedade Filter no componente TIBQuery.
Talvez essa opção só funcione em ClientDataSet?
Abraços
Olá, Renan!
A propriedade Filter tem a mesma função tanto para TClientDataSet quanto para TIBQuery. Peço que você verifique estes pontos:
1) A sintaxe do texto inserido no Filter está correta?
2) A propriedade Filtered está configurada como True?
3) O filtro está sendo aplicado na TIBQuery correta? É possÃvel que outra TIBQuery esteja conectada ao TDBGrid neste momento.
Abraço!
Caro André Celestino,
Estou tendo uma performance no Delphi2010 com Firebird (tanto com dbExpress como FireDAC) muito pior do que no Delphi 7 quando incluo campo Memo no select.
A diferença de tempo é absurda, ex.: de 5 segundos para 54 segundos. Quando retiro o Memo volta ao normal; quando converto Memo para String (usando CAST) o tempo cai poucos segundos, mas ainda abaixo do que temos na versão do Delphi 7.
Poderia partir para a sua recomendação 1) ref ao BLOB, mas antes você teria alguma ideia do que pode causar essa perda de desempenho, tratando do mesmo banco e mesma versão do Firebird?
Um abraço e parabéns pelo artigo.
João Batista
Olá, João Batista, tudo bem?
Essa lentidão não deveria ocorrer, embora seja necessário bastante cautela ao trabalhar com campos BLOB em instruções Select. O mais recomendado é trazê-lo separadamente, em uma consulta especÃfica, já que campos BLOB tendem a exigir muito tráfego na rede.
Experimente testar essa mesma consulta em um projeto novo. Talvez é alguma configuração que foi feita no Delphi 7 na qual a versão 2010 comporta-se de uma forma diferente.
Como não conheço o projeto e os componentes utilizados, sugiro também explorar as propriedades dos componentes para realizar possÃveis ajustes. Da versão 7 para 2010 houveram uma série de mudanças.
Abraço!
Prezado André Celestino, boa noite
Obrigado pela rápida orientação.
Estamos monitorando separadamente a SQL e a carga no Grid e o problema inicial é na execução da SQL no Delphi 2010, o que não acontecia no D7. É o mesmo cenário e Banco de dados no dbExpress que uso no Delphi 7 com boa performance e péssima no Delphi 2010 (isso quando trazemos os campos Memo). Conferi as propriedades da conexão e estão compatÃveis.
O mesmo problema acontece com o FireDAC, embora com uma discreta melhora de desempenho.
Precisamos dos campos Memos no Grid pois o usuário exporta para Excel, imprime etc e seria um retrocesso não usar visto que é performático no D7.
Será que há alguma implicação relativa ao Unicode do D2010? alterei de SUB-TYPE TEXT SEGMENT SIZE 80 para SUB-TYPE 1 SEGMENT SIZE 160 (já que o “TEXT” estava sendo interpretado no Firebird como “0” (binário), mas não afetou a performance.
Em último caso vou mudar os campos de BLOB para VARCHAR mas está longe de ser ideal
Um abraço!
Olá, João.
Não tenho conhecimento da solução deste problema, mas, mesmo assim, entrarei em contato.
Abraço!
Boa tarde Andre,
Estou tentando usar o PACKETRECORDS com um ClientDataSet em cache para 20 registros para cada pacote de um total de 1000 registros, porém, sempre vem todos mesmo colocando a propriedade FetchDemand como False e a PACKETRECORDS = 20. Pode me dar uma luz?
Olá, Judeir.
Aparentemente a configuração está correta. Com o FetchOnDemand como False, os próximos registros só deveriam ser recuperados com o comando GetNextPacket.
Você consegue fazer este teste em uma nova aplicação? Desconfio que há algum evento ou outra configuração que esteja impactando neste cenário.
Como apoio, visite essa página do Anderson Silva.
Abraço!
Boa dia Andre,
Fiz algumas pesquisas depois de ter testado no delphi 10.2.3 CE e funcionou de boa como falam a gurizada aqui no RS. O Delphi que uso no trabalho é o Delphi 7 e este tem um bug referente ao PACKETRECORDS com um ClientDataSet. Ai resolvi usando o Filter assim:
Pode me dar uma ajuda na forma de buscar os próximos 31 e voltar aos anteriores?
Olá, Judeir. Vou entrar em contato com você para entender melhor o problema.
Abraço!
Insira este comando no evento afterscroll do clientdataset:
Obrigado pela ajuda, Felipe!
Ok fico no aguardo. Vou passar o meu contado de whatshapp pelo e-mail.
Bom dia André,
Sobre a rotina de clonar os registros do clientDatSet importação deu tudo certo. Fiz uma melhoria para deixar em tela somente o intervalo quando vai para o próximo e volta para o anterior. Muito obrigado pela ajuda.
Que bom, Judeir!
Boa sorte no projeto. Abraço!
Olá não achei um tópico para isto e estou precisando muito de ajuda. é um problema antigo e não sei se ocorre no firedac, pois utilzo o ADO.
Delphi 10.3, Windows 10 x32
Preciso de ajuda porque isto acontece. Deveria ocorrer o disparo do evento AfterScroll na ordem em que rola os registros de mestre para os detalhes. Isto não está ocorrendo
O experimento de como foi feito para reproduzir e só assim poderão me ajudar:
1) Coloque um componente ADOConnection e ligue este a um banco de dados sql server.
2) Coloque 2 componentes ADODataSet (ADODataSet1 e ADODataSet2)
3) Coloque 1 componente DataSource (DataSource1)
4) No componente ADODataSet1 (ligue alguma tabela mestre)
5) No componente DataSource1 ligue ao ADODataSet1
5) No componente ADODataSet2 (ligue alguma tabela relacionada com a mestre)
6) No componente ADODataSet2 (coloque no campo Datasource o componente DataSource1)
7) Crie para cada um um evento Aftescroll para ADODAtaSet1 e ADODataSet2
8) coloque pontos de parada em cada evento
Agora a segunda etapa:
9) No formulário coloque dois botões. Um deles ADODataSet1.Prev e no outro ADODataSet1.Next para fazer a rolagem
10) coloque no form no evento OnShow a abertura de conexão com o banco de dados, a abertura do ADODataSet1 e ADODataSet2, nesta ordem.
11) Rode a aplicação
Resultados:
1) Ao abrir cada tabela, ira disparar por vez o evento AfterScroll do ADODataSet1 e depois o ADODataSet2. Até ai tudo bem. e se esperaria isto mesmo. Para cada tabela que abre, dispara este evento.
Agora a parte que parece um bug:
1) Role o registro para o próximo e ira perceber que dispara primeiro o evento do ADODataSet2 ao invés do ADODataSet1. como o ADODataSet2 está ligado ao ADODataSet1 deveria disparar o afterscroll do ADODAtaSer1 e depois o ADODataSet2, mas ocorre o contrário e isto não está certo, pois quem inicia a primeira rolagem deveria ser o master e depois o detail.
Isto acontece em outras versões no Delphi.
Alguém sabe o que está ocorrendo e como se pode fazer para contornar isto ou se isto é normal de ocorrer?
Olá, Guilherme!
Fiquei muito curioso com esse caso. Vou entrar em contato com você.
Bom dia,
Reli os comentários, mas so queria confirmar se com os procedimentos citados irão resolver meu problema.
Tenho uma procedure que executa uma pesquisa no BD toda vez que marco um flag. No entanto, quando escolho determinada opção que tem muitos registros, o grid demora a exibir os dados. São mais de 1000 registros.
Podem me ajudar? Talvez utilizado algum componente pra exibir ao usuário que está sendo carregado os dados pra não achar que a tela “travou”.
Olá, Diego, tudo bem?
Analisei o seu código e a minha primeira sugestão é tentar melhorar a instrução SQL. Por exemplo, é mesmo necessário trazer todos os campos da tabela MANIFESTODESTINATARIO (com asterisco)? Verifique também se é possÃvel substituir o Left Join por Inner Join.
Além disso, você pode acrescentar alguma verificação para obrigar o usuário a refinar a pesquisa, ou seja, o usuário dificilmente vai “precisar” desses 1000 registros de uma vez só. Se ele refinar a pesquisa, adicionando mais condições, a consulta ficará mais rápida e ele receberá somente os registros que o interessa.
Mas, se mesmo assim for necessário trazer os 1000 registros, você pode criar uma tela de espera enquanto a requisição é feita ao banco de dados. Por exemplo, antes de chamar o método
ExecutarPesquisa
, abra um formulário simples com alguma mensagem ou imagem indicando que há um processamento em andamento, mas é importante que esse formulário sejaStayOnTop
(FormStyle) e que não tenha botões na barra de tÃtulo (maximizar, minimizar e fechar). Na prática, o código ficaria basicamente dessa forma:Uma alternativa mais sofisticada é utilizar uma thread para executar essa consulta no banco de dados. Threads consistem na execução de processamentos paralelos (geralmente demorados) para não “congelar” a aplicação. No seu caso, por exemplo, é possÃvel executar a consulta em uma thread paralela, liberando a thread principal para exibir uma mensagem ou permitir que o usuário continue trabalhando na aplicação.
Espero ter ajudado, Diego.
Abraço!
Bom dia meu caro.
Primeiro, muito obrigado por ter respondido. Vou testar suas sugestões.. Sobre o uso de thread, nesse exemplo meu conseguiria mais ou menos esboçar como ficaria o código.
Andei pesquisando sobre esse recurso sofisticado, mas não consegui entender como encaixar no meu código.
Olá, Diego!
O contexto de threads no Delphi é realmente amplo mesmo. Atualmente, além de threads convencionais, o Delphi também traz Anonymous Threads, TTask, IFuture e TParallel, todos para essa finalidade de processamento paralelo.
O Renato Matos gravou dois vÃdeos interessantes no YouTube sobre como criar threads. Talvez eles possam ajudá-lo:
Parte 1: https://www.youtube.com/watch?v=I8pKk35VRvo
Parte 2: https://www.youtube.com/watch?v=TzN7qPnnRmU
Na prática, você terá que mover o método
ExecutarPesquisa
para dentro do métodoExecute
da thread. Em seguida, no seu formulário, basta chamar a thread!Abraço!