Escalabilidade e Performance – Stored Procedures

Introdução

Em um tópico anterior sobre “Escalabilidade e performance – Técnicas”, um dos tópicos falava sobre Stored Procedures, inclusive sugerindo que seu uso deveria ser minimizado. Vamos entrar neste tema com um pouco mais de profundidade neste tópico. Vamos começar com o clone do tópico abordado, e esmiuçar ele dentro do contexto do AdvPL e Protheus.

Minimize o uso de Stored Procedures

Este é um ponto aberto a discussão, depende muito de cada caso. Não é uma regra geral, existem pontos em um sistema onde uma stored procedure realmente faz diferença, mas seu uso excessivo ou como regra geral para tudo impõe outros custos e consequências. O Princípio 1 diria: “use apenas stored procedures”. No entanto, esta decisão pode causar grandes problemas para o Princípio 2 devido à escalabilidade. As Stored procedures têm a vantagem de ser pré-compiladas, e não há nada mais perto de um dado no Banco de Dados.

Porém Bancos de Dados transacionais são especializados em quatro funções: Sort, Merge, gerência de Locks e Log. A gerência de lock é uma das tarefas mais críticas para a implementação de algoritmos distribuídos, e este é o real motivo de existirem poucos Bancos de Dados que possam implementar a escalabilidade horizontal. Se as máquinas de Banco de Dados têm dificuldade de escalar horizontalmente, ela é um recurso escasso e precioso. Temos então que otimizar seu uso para não consumir excessivamente seus recursos a ponto de onerar os demais processos do ambiente. Isto acaba adiando a necessidade de escalar horizontalmente o SGBD.

Abordando a questão de desempenho

Se o algoritmo para processamento de um grande grupo de informações pode ser escrito dentro de uma Stored Procedure no próprio Banco de Dados, esta alternativa tende fortemente a ser a mais performática. Num cenário onde o algoritmo é escrito usando um programa sendo executado dentro do servidor de aplicação da linguagem, cada processamento que dependa da leitura de grupos de dados e tenha como resultado a geração de novos dados vai ser onerado pelo tempo de rede de tráfego destes dados, na ida e na volta. Logo, com uma base de dados modelada adequadamente, e uma stored procedure bem construída, ela naturalmente será mais rápida do que um processamento que precisa trafegar os dados pra fora do SGDB e depois receba novos dados de fora.

Porém, este recurso não deve ser usado como solução mágica para tudo. Afinal, o SGDB vai processar uma Stored Procedure mais rápido, pois ele não vai esperar um processamento ser realizado “fora dele”, porém o SGDB vai arcar com o custo de ler, processar e gravar a nova informação gerada. Se isto for feito sem critério, você pode mais facilmente esgotar os recursos computacionais do Banco de Dados, ao ponto da execução concorrente de Stored Procedures afetar o desempenho das demais requisições da aplicação.

Outras técnicas pra não esgotar o SGDB

Existem alternativas de adiar um upgrade no SGDB, inclusive em alguns casos as alternativas são a solução para você não precisar comprar um computador da “Nasa” …risos… Normalmente estas alternativas envolvem algum tipo de alteração na aplicação que consome o SGDB.

Réplicas de leitura

Alguns SGDBs permitem criar nativamente réplicas da base de dados acessadas apenas para consulta, onde as cópias de leitura são sincronizadas em requisições assíncronas. Existem muitas partes da aplicação que podem fazer uma leitura “suja”. Neste caso, a aplicação pode ler os dados de uma base sincronizada para leitura, e os processos que precisam de leitura limpa são executados apenas na instância principal. Para isso a aplicação precisaria saber qual e o banco “quente” e qual é o espelho, para fazer as coisas nos lugares certos.

Caches

Outra alternativa é a utilização de caches especialistas, implementados na própria aplicação. Utilizando por exemplo uma instância de um “MemCacheDB” em cada servidor, cada aplicação que pode reaproveitar a leitura de um dado com baixo índice de volatilidade (dados pouco atualizados ou atualizados em momentos específicos), poderiam primeiro consultar o cache, e somente se o cache não têm a informação desejada, a aplicação acessa o banco e popula o cache, definindo uma data de validade. Neste caso, o mais legal a fazer é definir um tempo de validade do cache (Expiration Time). E, paralelo a isso, para informações de baixa volatilidade, a rotina que fizer update desta informação pode eliminar ela do cache no momento que um update for realizado, ou melhor ainda, pode ver se ela se encontra no cache, e em caso afirmativo, ela mesma poderia atualizar o cache 😉

Sequenciamento de operações

Operações de inserção ou atualização de dados que não precisam ser refletidas em real-time no SGDB podem ser enfileiradas em pilhas de requisições, e processadas por um processo dedicado. O enfileiramento de requisições não essenciais em tempo real limita o consumo de recursos para uma determinada atividade. Caso a pilha se torne muito grande, ou um determinado processo dependa do esvaziamento total da pilha, podem ser colocados mais processos para ajudar a desempilhar, consumindo mais recursos apenas quando estritamente necessário.

Escalabilidade Vertical

Devido a esta questão de dificuldade de escalabilidade de bancos relacionais horizontalmente, normalmente recorremos a escalabilidade vertical. Escalamos horizontalmente as máquinas de processamento, colocando mais máquinas menores no cluster e balanceando carga e conexões, e quando a máquina de banco começa a “sentar”, coloca-se uma máquina maior só para o SGDB, com vários processadores, discos, memória e placas de rede. Mas tudo tem um limite, e quando ele for atingido, a sua máquina de Banco de Dados pode ficar mais cara que o seu parque de servidores de processamento.

Dificuldade de Implementação

Usar caches e réplicas e pilhas não é uma tarefa simples, fatores como a própria modelagem da base de dados podem interferir negativamente em algumas destas abordagens. Não se pode colocar tudo em cache, senão não vai ter memória que aguente. O cache é aconselhável para blocos de informações repetidas constantemente requisitadas, e de baixa volatilidade. Também não é necessário criar pilhas para tudo que é requisição, apenas aquelas que não são essenciais em tempo real, e que podem ter um delay em sua efetivação.

Stored Procedures no AdvPL

O ERP Microsiga disponibiliza um pacote de Stored Proecures, aplicadas no SGDB em uso por um programa do módulo “Configurador” (SIGACFG). As procedures foram desenvolvidas para funcionalidades específicas dentro de cada módulo, normalmente aquelas que lidam com grandes volumes de dados, e foi possível criar um algoritmo que realize o processamento dentro do SGDB, trafegando menos dados “pra fora” do Banco de Dados. Normalmente um pacote de procedures é “casado” com a versão dos fontes do Repositório, pois uma alteração na aplicação pode envolver uma alteração na procedure. Os ganhos de performance são nítidos em determinados processamentos, justamente por eliminar uma boa parte do tráfego de informações para fora do SGDB durante os processos.

Conclusão

Dado o SGDB como um recurso “caro e precioso”, como mencionado anteriormente, a utilização de recursos adicionais como réplicas e caches, ajuda a dar mais “fôlego” pro SGDB, você consegue aumentar o seu parque de máquinas e volume de dados processados sem ter que investir proporcionalmente na escalabilidade do SGDB. E em tempos de “cloudificação” , SaaS e IaaS, quando mais conseguimos aproveitar o poder computacional que temos em mãos, melhor !

Desejo novamente a todos TERABYTES de Sucesso 😀

Até o próximo post, pessoal 😉

Referências

“Escalabilidade e performance – Técnicas”

Balanceamento de Carga no Protheus

Introdução

No ano passado, ajudei um colega que estava concluindo um mestrado, cuja tese envolvia diretamente a eficiência de mecanismos de balanceamento de carga e suas abordagens. E, uma vez absorvido algum conhecimento a mais a respeito, acho que podemos dar um mergulho no assunto, e aproveitar para conhecer mais de perto o balanceamento de conexões nativo Application Server para conexões do SmartClient.

Balanceamento de Carga

Também conhecido por “Load balancing”, é um mecanismo que distribui requisições de processamento para múltiplos recursos computacionais, como computadores, “Cluster” de computadores, links de rede, unidades de processamento ou unidades de disco. O objetivo é otimizar o uso de recursos, maximizando disponibilidade e resultados, minimizando tempos de resposta, e evitar sobrecarga de uso de recursos. A utilização de múltiplos componentes com balanceamento de carga ao invés de um único componente deve aumentar a escalabilidade e disponibilidade através da redundância. Normalmente este recurso envolve software e/ou hardware específicos.

Entre as técnicas mais comuns, podemos citar o Round-Robin de DNS (onde uma URL de um WebSite retorna um IP diferente para cada consulta ao DNS, direcionando as requisições para outros servidores), algoritmos de distribuição (normalmente round-robin ou escolha randômica) são usados para decidir qual servidor irá processar uma chamada qualquer. Algoritmos mais sofisticados de balanceamento podem usar como fatores determinantes informações como tempo médio de resposta, estado do serviço, tráfego atual, etc.). Existe também um outro recurso, chamado “Proxy reverso” (Reverse Proxy), que além de outras funcionalidades, também pode ser utilizado para distribuição de requisições e balanceamento.

Pontos comuns

Cada mecanismo atua sob princípios diferentes, e visando suprir as necessidades dos ambientes onde estão inseridos, mas de uma forma geral, todos aderem aos mesmos princípios de desempenho, escalabilidade e resiliência. Como eu já disse, dentre os vários mecanismos disponíveis, não existe um “melhor de todos”, existem aqueles que melhor atendem as suas necessidades. Um sistema complexo pode usar vários níveis de balanceamento usando abordagens diferentes, para partes diferentes do sistema.

Balanceamento de carga no Protheus

O Protheus Server (ou Application Server) do AdvPL possui um mecanismo de balanceamento de carga nativo para as conexões recebidas das aplicações Smartclient. Basicamente, você provisiona dois ou mais serviços do Application Server, com as mesmas configurações de ambiente, distribuídos em quaisquer máquinas, e configura um novo serviço, chamado de “Master” ou “Balance”, com as configurações de balanceamento de carga das conexões especificados na seção [servernetwork] do arquivo de configuração do servidor (appserver.ini).

Quando iniciamos uma aplicação AdvPL a partir de um SmartClient, normalmente o programa executado é responsável pela montagem das telas de interface e pela execução do processamento. Por exemplo, a inclusão de um pedido de venda pelo SmartClient, quando você finaliza a operação e confirma a inclusão, a função de inclusão de pedido é executada pelo mesmo processo de interface, mantendo a interface em stand-by até a conclusão do processo e o programa devolver uma mensagem para a interface e permitir você prosseguir com a operação.

Neste modelo, como o processamento é realizado pelo mesmo processo que atende a conexão, toda a “carga” do processo está atrelada a esta conexão, e a aplicação foi projetada sob o conceito de persistência de conexão. Logo, em se tratando das conexões de SmartClient, ao balancear uma conexão, automaticamente estamos balanceando a “carga”.

Requisitos e comportamentos

Ao configurar no “Master” todos os serviços “Slave” de balanceamento, devemos enumerar host/ip e porta de conexão TCP de cada serviço. Este IP e porta deve ser visível na rede usada pelos SmartClients para estabelecer a conexão. Se houver um firewall entre os smartclients e os application servers, os IPs e portas dos “Slave’s” também precisam ser “abertos”, não basta abrir o IP e porta do “Master”.

O serviço de balanceamento não atua como um “Proxy Reverso”, as conexões estabelecidas com ele são indiretamente redirecionadas ao “Slave” escolhido no momento que a conexão foi estabelecida. Se ele atuasse como um proxy reverso, ele se tornaria um gargalo de comunicação, e caso ele fosse derrubado, todas as conexões atualmente em uso de todo o ambiente cairíam junto.

Quando o Balance recebe a conexão de um SmartClient, ele decide na hora qual dos serviços cadastrados para balanceamento é o mais adequado a receber esta conexão, e retorna ao SmartClient uma instrução de reconexão, indicando ao SmartClient qual é o Serviço que ele deve se conectar. Logo, o Smartclient conecta com o “Master”, recebe a informação de qual o serviço adequado para conextar-se, desconecta do “Master”, e conecta-se diretamente com este serviço.

Toda a regra e controle de distribuição é realizado pelo “Master”. Como ele já possui pré-configurados os serviços que podem receber conexões, ele próprio mantém com cada serviço “Slave” uma conexão dedicada de monitoramento, através da qual ele “sabe” quantos processos estão em execução em cada um dos serviços, e se cada serviço está apto a receber novas conexões. Como cada conexão pode exercer uma carga variável durante seu uso, a uma métrica utilizada é direcionar a nova conexão ao serviço que têm o menor número de processos em execução no momento.

Cada serviço do Protheus, após a Build 7.00.090818, possui um processo interno dedicado ao monitoramento de memória de sua instância de serviço. Caso um limite de ocupação de memória da instância seja atingido, este serviço torna-se automaticamente bloqueado para aceitar novas conexões de SmartClient e subir novos Jobs. O Serviço “Master” se mantém atualizado sobre este status, e não direciona novas conexões para este “Slave” enquanto seu status for “bloqueado”. Veja as referências no final do artigo para as documentações da TDN que abordam em maior profundidade estes assuntos.

Não seria melhor usar outra regra ?

Muitas pessoas já me perguntaram isso, e a resposta é “neste modelo de processamento, não têm opção melhor”. Como a carga e consumo de memória e CPU depende da rotina que será chamada, e a rotina está atrelada a conexão, qualquer cenário de distribuição está sujeito a desequilíbrios, afinal as rotinas possuem consumo de memória, CPU e rede variáveis durante a sua execução, e mesmo se fosse possível transferir um processo para outro serviço “a quente”, se este processo também apresenta a mesma variabilidade de consumo de recursos, a transferência da conexão seria freqüente, tornando-se ineficiente e pesada.

Assumindo que cada conexão persistente possui características variáveis de consumo, o mais elegante é dividir pelo número de processos por serviço. Mesmo havendo um eventual desequilíbrio no consumo de recursos, uma vez atingida uma quantidade de memória acima do normal em um serviço, ele automaticamente “bloqueia-se” para não aceitar mais conexões. Quando ao consumo de CPU, quando sabe-se que determinadas rotinas vão demandar muito tempo e consumo de recursos, pode-se montar um ambiente de balanceamento secundário, com outros “Slave’s” dedicados, onde os usuários de rotinas mais pesadas pode alocar outros serviços, e alguns deles podem ser colocados em scheduler para serem executados após o expediente.

Configuração de Balanceamento no AdvPL

Basicamente, você deve criar uma nova seção no appserver.ini para cada “Slave” que você quer relacionar no balanceamento. Recomenda-se criar um identificador cujo nome esteja relacionado de alguma forma com a máquina servidora e o número do serviço utilizado. Vamos dar um exemplo de balanceamento de serviços em apenas uma máquina, subindo o servidor “Master” na porta 6000, e quatro outros serviços “Slave” nas portas seguintes ( 6001 a 6004 )

[servernetwork]
servers=SL_1,SL_2,SL_3,SL_4
[SL_1]
Server=172.16.10.201
Port=6001
Connections=20
[SL_2]
Server=172.16.10.201
Port=6002
Connections=20
[SL_3]
Server=172.16.10.201
Port=6003
Connections=20
[SL_4]
Server=172.16.10.201
Port=6004
Connections=20

A configuração acima deve ser inserida no appserver.ini do serviço eleito como “Master”, que neste exemplo está na mesma máquina que os “Slave’s””, mas na porta 6000. Quando o serviço é iniciado, ele verifica se existe a seção [servernetwork], e verifica quais são os serviços enumerados para balanceamento. O “Master” sobe um processo interno dedicado para monitorar cada um dos “Slave’s”, e saber em tempo real quantos processos estão sendo executados em cada um, e se eles não estão bloqueados para aceitar novas conexões. Caso um processo não consiga encontrar um “Slave”, ele fica tentando estabelecer uma conexão, e enquanto ele não sabe se o “Slave” está no ar ou não, ele não redireciona nenhuma conexão para aquele “Slave”.

Cada serviço que nós chamamos de “Slave”, não precisa de nenhuma chave especial, ele precisa apenas refletir as mesmas configurações de ambiente entre os servidores, e o mesmo ambiente tem que apontar para o mesmo RootPath, para o mesmo License Server, e todos os ambientes devem estar usando a mesma cópia do repositório.

Como os IPs e Portas de cada “Slave” precisam estar visíveis para o SmartClient conseguir conectar, todos os arquivos de configuração dos SmartClients do parque de máquinas deve estar configurado para apontar diretamente para o IP e porta do “Master”. Caso algum Smartclient não esteja configurado desta forma, e estiver apontando para um determinado “Slave”, isto não atrapalha o balanceamento, pois mesmo que a conexão não tenha sido redirecionada pelo “Master”, ele vai decidir o balanceamento baseado no número de processos em execução em cada “Slave”. Porém, ao apontar diretamente para um “Slave”, se ele estiver bloqueado ou indisponivel, a conexão não será direcionada para nenhum outro “Slave”.

Limitação de Balanceamento

Para cada “Slave”, devemos especificar um número na chave “connections”. Este número por padrão não é um número “absoluto” de conexões, mas sim um número de “peso” de distribuição. Por exemplo, ao configurarmos o número 20 na chave “connections” de todos os “Slave’s” para balanceamento, o “Master” vai entender que a carga deve ser distribuída igualmente entre todos os “Slave’s”. Quando configuramos um dos “Slave’s” com connections=40 e os outros três com connections=20, haverá um percentual de diferença nesta distribuição.

A fórmula é calculada primeiro somando todos os números de conexão:

40+20+20+20 = 100

Agora, calculamos o percentual de conexões a serem desviadas para cada “Slave” dividindo a quantidade de conexões pelo numero total, e multiplicando por 100:

40/100 * 100 = 40 %
20/100 * 100 = 20 %
20/100 * 100 = 20 %
20/100 * 100 = 20 %

Logo, se neste ambiente forem feitas 10 conexões, 4 vão para o “Slave”1, e 2 para cada um dos outros “Slave’s”.

Porém, como a distribuição é feita igualmente, a primeira vai pro “Slave” 1 , a segunda também, a terceira vai pra um dos demais “Slave’s”, a quarta também, a quinta também. Quando o balanceamento atinge uma posição de equilíbrio, a sexta conexão deve ir para o balance 1, a sétima também, e as três últimas serão distribuídas para os demais “Slave’s”.

Normalmente em um ambiente equilibrado, o número é o mesmo. Você pode determinar um comportamento de excessão e priorizar mais os serviços de uma determinada máquina, caso ela tenha mais memória e CPU por exemplo. Porém, o mais saudável neste caso é criar mais de um serviço “Slave” na mesma máquina, para haver uma melhor utilização de recursos. Mesmo que este número originalmente não seja um fator limitante, você deve preenchê-lo com um valor aceitável do número máximo de conexões que realmente é saudável fazer em apenas um serviço.

Existe uma configuração na seção ServerNetwork, chamada BALANCELIMIT. Uma vez habilitada, ela considera que o somatório de conexões ( no nosso exemplo, 100 ) é um fator limitante. Por default, se o ambiente passar de 100 conexões, o balanceamento vai continuar distribuindo as conexões usando a mesma regra para os “Slave’s”, até que os “Slave’s” que atingirem um pico de memória não aceitarem mais conexões. Quando habilitamos esta configuração, o Balance vai parar de distribuir conexões para todos os “Slave’s”, caso a soma do numero de processos rodando em todos os “Slave’s” mapeados atingir ou superar a soma de conexões estipuladas nas chaves “connections” de cada “Slave” mapeado no balanceamento. Quando isso acontece, o Balance não redireciona mais conexões, retornando erros de indisponibilidade de serviços para o Smartclient que tentou se conectar.

Desequilíbrio de balanceamento

Enquanto todas as pessoas estão entrando no sistema, a distribuição de conexões é sempre uniforme. Porém, quando os usuários começam a sair do sistema, os serviços podem ficar desbalanceados, onde coincidentemente os usuários que saíram estavam conectados em alguns serviços específicos. Em condições normais de uso, este desbalanceamento não interfere no poder de processamento dos usuários que ainda estão conectados — exceto se coincidirem vários processos consumindo em excesso um determinado recurso de uma máquina, pois neste caso os demais usuários conectados em serviços naquela máquina podem ser penalizados pelo consumo destes processos. De qualquer modo, mesmo temporariamente desbalanceada a quantidade de conexões, as novas conexões feitas no ambiente vão ser priorizadas pelo “Master”, para entrar justamente nos “Slave’s” com o menor número de processos.

Configurações adicionais

Por default, um servidor “Master” pode aceitar conexões vindas do SmartClient, caso não haja nenhum outro “Slave” disponivel para redirecionar a conexão. Este comportamento nem sempre é desejável em um ambiente, e pode ser desligado usando a configuração “Master”Connection=0 na seção [servernetwork].

E, existe uma configuração de diagnóstico, para colocar o mecanismo de balanceamento do “Master” em modo “Verbose”, onde cada conexão recebida por um SmartClient é registrada no log de console do Application Server (console.log), informando sua origem, e para qual “Slave” ela foi encaminhada. Basta colocar na seção ServerNetwork a chave Verbose=1

Balanceamento com SSL

Quando utilizamos SSL entre os SmartClients e o Application Server, cada serviço do Protheus, inclusive o “Master”, deve ser configurados com uma conexão SSL, em uma porta distinta. No momento de configurar cada seção de “Slave” no appserver.ini do serviço “Master”, devemos especificar a porta TCP original de cada serviço, e a porta SSL usando a configuração “SecurePort=nnnn”, onde nnnn é o número da porta reservada para conexão segura (SSL) com aquele “Slave”.

Internet e Intranet

Quando disponibilizamos um balanceamento de carga para SmartClients em um ambiente de Intranet, e queremos expandí-lo para a internet ou outra rede externa, a utilização do balanceamento de carga nativo do Application Server exige que os IPs e portas usadas pelos serviços estejam públicos, e também seja visíveis pelo mecanismo de balanceamento (Serviço “Master”), e criar dois serviços de balanceamento. Um IP e porta do “Master” de “Intranet” para serem usados pelos SmartClients dentro da sua rede interna, e um IP e Porta do “Master” para configurar os Smartclients vindos “de fora”.

Internamente, você pode fazer cada balance apontar para um grupo distinto de servidores dedicados ao acesso interno ou externo, ou colocar 2 IPs nas máquinas onde estão todos os seus serviços “Slave”, e pelo balanceamento apontar para todos os serviços disponíves na sua rede, lembrando que o “Master” de acesso interno precisa enxergar os IPs locais, e o “Master” para acesso externo tem que ser cofigurado com os IPs externos, e precisam acessar cada um dos serviços por estes IPs.

Posso usar “localhost” ?

Não, não pode. Se o “Master” retorna o IP e porta configurados para um “Slave”, na seção correspondente ao “Slave” no appserver.ini do “Master”, no ini do smartclient você apontou um IP e porta do “Master”, mas ao estabelecer a conexão, o “Master” verificou que o “localhost:6001” é a melhor opção, e vai devolver isso pro SmartClient … Logo, o smartclient vai tentar conectar com um serviço Protheus na própria máquina onde ele está instalado. Você pode usar uma máquina grande e colocar muitos serviços de Protheus nela, e fazer um balanceamento “local”, MAS os IPs e Portas usados na configuração devem ser visíveis aos SmartClients, ok ? Se você usar localhost, seu balanceamento somente vai funcionar para os SmartClients que você iniciar de dentro da própria máquina onde estão os serviços do Protheus.

Posso usar outro mecanismo de balanceamento ?

Sim, pode. Você pode usar um NLB, um Proxy reverso, qualquer outro mecanismo que não interfira no conteúdo dos pacotes trafegados e que mantenha a persistência da conexão com o “Slave” escolhido, sendo assim totalmente transparente para as aplicações envolvidas. Normalmente a única desvantagem deste mecanismo é saber se o “Slave” escolhido está realmente disponível para executar o programa. Quando um “Slave” está bloqueado, a porta TCP de conexão permanece no ar e aceita novas conexões, mas responde a elas com uma mensagem de erro e desconectam. Com este balanceamento, caso seja usada uma métrica qualquer, onde naquele momento este serviço esteja atendendo aquela métrica, uma nova conexão vai ser direcionada para ele, mas ele vai atender e retornar erro. Sem persistir a conexão, e a métrica sendo favorável para este “Slave”, você corre o risco de ninguém mais conseguir entrar no sistema, pois todas as conexões serão direcionadas para um “Slave” que está no ar, mas não está preparado para atender as conexões.

O mundo ideal

Um modelo muito interessante de distribuição de processos seria um MVC com o processamento realizado por um pool de processos dedicados, separados do processo de interface. Deste modo, o processo de interface teria um consumo de CPU e memória relativamente mais baixos, em razão apenas da quantidade de componentes de interface, e as requisições efetivas de processamento e iteração com os dados fossem distribuídos em um pool de serviços distribuídos (SOA – Service Oriented Architecture). Se as implementações de processamento mantivessem uma constante média de consumo de recursos por requisição, o mecanismo de balanceamento das requisições de processamento poderia utilizar métricas mais eficientes, direcionando um processo que consome mais CPU para um serviço onde a CPU não esteja “no gargalo”, e sabendo que um processo vai consumir mais memória que os demais, o mecanismo poderia verificar antes de distribuir quais os serviços disponíveis estão com a melhor ocupação, ou que aguentariam aceitar a requisição sem comprometer os processos em execução.

Isto atenderia tanto o cenário de desempenho quando o de resiliência. Com mais de uma máquina fisica configurada para atender mais de um tipo de requisição, caso um serviço ou uma máquina inteira apresentasse um problema ou indisponibilidade, até mesmo uma manutenção programada, cada máquina teria o seu próprio “orquestrador” de eventos, que ao perceber que uma máquina saiu fora do circuito, poderia direcionar para outro serviço em outra máquina. A sua alta disponibilidade teria um grau de tolerância de acordo com quantos porcento de máquina você tem de “sobra”.

Ao invés de configurar um cluster ativo-passivo, você determina qual é o mínimo de máquinas que precisam estar no ar para dar conta do recado, e coloca uma ou mais máquinas adicionais. Se o seu dia a dia ocuparia duas máquinas inteiras, com serviços espalhados nelas, você pode colocar uma terceira máquina. Logo, você vai ter uma máquina “de sobra”, mas utilizaria as três juntas para não bater 100% do seu uso de recursos. No pior cenário, se uma máquina inteira sair do ar por uma pane física ou uma janela de manutenção de hardware, o software redirecionaria toda a carga para os dois hardwares disponíveis, que em tese deveriam dar conta do recado enquanto essa terceira máquina não “volta”.

Se você coloca o dobro de poder computacional do que você realmente precisa, você pode “perder” momentaneamente até duas máquinas inteiras, que as duas restantes vão bater “na tampa” mas vão segurar o sistema no ar. Se neste cenário a terceira máquina ir pro vinagre, você tem ainda têm duas saídas de emergência: Priorizar os serviços importantes e tirar tudo que é supérfluo do ar naquele momento, limitando a quantidade de conexões e processos, pra a primeira máquina não sobrecarregar — é melhor alguns trabalhando do que tudo parado — e dependendo da granularização dos serviços, você pode pegar algum equipamento de “emergência” e colocar alguns “Slave’s” neles para operação de “contingência”, distribuindo os serviços essenciais que a maquina 1 não iria dar conta.

Uma outra vertente no balanceamento de carga seria a utilização dos “Slave’s” encadeados, onde os processos seriam distribuídos somente para um slave, até que um “threshold” (limite) seja atingido, então os novos processos passam a alocar o segundo slave, e assim por diante. Este cenário é muito atrativo quando pensamos em escalabilidade para ambientes “Cloud”, onde seria possível por exemplo determinar um número inicial ou mínimo de máquinas virtuais, e criar uma regra para, quando o último slave começar a receber conexões, uma nova VM é disponibilizada no ambiente, e automaticamente inserida no balanceamento. Estas máquinas virtuais adicionais, assim que todas as conexões forem encerradas, poderia ‘fechar-se’ e ser descartada.

Em um ambiente de processamento de múltiplos hardwares, isto também seria útil para fazer uma janela de manutenção de um Hardware. Por exemplo, alterando a priorização dos slaves, deixamos por último na fila todos os slaves de um determinado equipamento, e bloqueamos as conexões para estes slaves. Conforme os usuários fossem desconectando da interface, no final do dia a máquina não estaria com nenhuma conexão, podendo ser tirada inteira do ar para manutenção, se que nenhum usuário desse falta disso.

Conclusão

A busca por eficiência de processos deve ser sempre uma constante em qualquer sistema com previsão de crescimento. E já existem muitos recursos voltados para isso. Entender como a aplicação funciona é um passo a mais para abrir caminhos para mudanças, com mais certeza de sucesso nas mudanças planejadas. Espero que este post tenha dado alguma luz “a mais” neste mecanismo, e que vocês tenham TERABYTES de sucesso na utilização deles 😀

Até o próximo post, pessoal 😉

Documentação TDN

Balanceamento de carga entre serviços

Seção [ServerNetwork]

Processo de monitoramento e controle de memória

Configuração BALANCELIMIT

Referências

Load balancing (computing). (2015, October 16). In Wikipedia, The Free Encyclopedia. Retrieved 00:49, October 31, 2015, from https://en.wikipedia.org/w/index.php?title=Load_balancing_(computing)&oldid=686092130

Reverse proxy. (2015, October 23). In Wikipedia, The Free Encyclopedia. Retrieved 13:47, October 31, 2015, from https://en.wikipedia.org/w/index.php?title=Reverse_proxy&oldid=687157637

Acesso a Dados AdvPL com c-Tree – Parte 01

Introdução

Depois de tantas introduções sobre acesso a dados em AdvPL, vamos descer um degrau na implementação dos drivers c-Tree no Application Server AdvPL. Vamos entender o que é, como funciona, o que exatamente ele faz, do que se alimenta, como cuidar, etc…

O que é e como funciona

A empresa de software norte americana Faircom(R) disponibiliza uma engine multi plataforma de acesso a dados ISAM e relacional, conhecida por c-Tree. A Microsiga fez uma parceria comercial com a Faircom, para adotar o driver em suas aplicações, como uma alternativa ao suporte de arquivos locais, temporários e meta-dados (dicionários do ERP).

A Faircom(r) fornece as três versões do c-Tree Server ( Stand-alone ou “ctree local”), c-Tree Server, e c-Tree Embedded ( c-Tree BoundServer ou CtreeServer DLL), em uma build exclusiva, compatível apenas com o Application Server.

As funcionalidades são fornecidas por um conjunto de APIs, de alto e baixo nível, que permitem a criação de arquivos ISAM, com transações ACID, registros de tamanho fixo e variável, e um forte controle de integridade. Uma tabela em formato DBF parece um “txt” perto do nível de controle de uma tabela c-Tree.

APIS fornecidas junto do Application Server

Junto do pacote de executáveis do Application Server, para todas as plataformas homologadas, são fornecidas as dlls do cTree “local” (ctreestd.dll), API client do c-Tree Server (mtclient.dll), e um c-Tree BoundServer (ctreecsv.dll + arquivos auxiliares).

c-Tree Local

Trata-se de uma versão stand-alone da API de acesso a dados, sem transacionamento ou cache, que faz leitura e gravação diretamente nos arquivos em disco. Usada pelo ERP Microsiga a partir da versão AP6 (Advanced Protheus 6), para acessar o arquivo de senhas do ERP (sigapss.spf) e os arquivos de help de campo por idioma (sigahl?.hl?). Estes arquivos são conhecidos por “SuperFiles”, eles possuem uma estrutura interna fixa, onde o mesmo container de arquivo armazena dados de tamanho variável e índices. A API de acesso a estes arquivos não é publicada por razões de segurança, e não é possível chamar diretamente a API de dentro de User Functions.

Em instalações de instância única de Application Server, como por exemplo o módulo FrontLoja do ERP (onde é instalado um Application Server e um SmartClient nas estações de trabalho), ele pode ser usado para gerenciar uma base local da estação, sincronizada pela aplicação com a base principal na retaguarda do sistema.

Não deve ser usada para ambientes com mais de um Application Server acessando o mesmo diretório raiz de ambiente (rootpath), como por exemplo em balanceamento de carga. Caso mais de um Application Server acesse o mesmo RootPath, é necessário utilizar um c-Tree Server.

c-Tree Server

Serviço dedicado de acesso a arquivos c-Tree, multi-thread e multi-plataforma. Deve ser instalado na máquina física onde está armazenado o RootPath do ambiente. Sua utilização envolve a instalação do serviço do c-Tree Server, e alterar todos os arquivos de configuração do Application Server (appserver.ini) para indicar que o Protheus está configurado em modo client-server com o c-Tree(chave ctreemode=server na seção [general]), e devemos informar a URN da instância do c-Tree Server em seção especifica (chave CTServerName na seção [ctreeserver]). Caso não especificada, o default é FAIRCOMS@localhost.

c-Tree BoundServer (ou c-tree Server DLL, c-Tree Embedded)

Trata-se de uma compilação do c-Tree Server em DLL (creeecsv.dll), que possui todas as funcionalidades de um c-Tree Server, mas pode ser carregado diretamente pelo Application Server, eliminando a camada de rede no acesso a dados. Passou a ser distribuído junto do pacote de executáveis do Application Server a partir da build 7.00.121227P, para ser utilizada para a geração de arquivos temporários “locais” na máquina onde está sendo executada a instância do Application Server. Pode ser usada para gerenciar as tabelas locais e meta-dados (dicionários do ERP), desde que a instalação não possua outro Application Server usando a mesma pasta raiz do ambiente (rootpath).

E como o Application Server usa isso ?

Através da configuração ctreemode, na seção [general] do arquivo de configuração do Application Server (appserver.ini), definimos qual API será usada para acesso aos arquivos c-Tree daquela instância do servidor. Caso não especificada, o default é “local”.

Quando o ctreemode é “local”, cada acesso a um arquivo no formato c-Tree, seja o “superfile”, ou os meta-dados dos ambientes configurados com localfiles=ctree, ou o driver “CTREECDX”, vão ser requisitados para a DLL do c-Tree Local (ctreestd.dll).

Quando informamos que o ctreemode é “server”, o Protheus carrega a dll client do c-Tree Server (mtclient.dll), e todas as requisições para acesso aos arquivos c-tree são realizadas para o c-tree server configurado na seção [ctreeserver].

Quando informamos que o ctreemode é “boundserver”, o protheus carrega a c-Tree Server DLL (ctreecsv.dll), e faz todo o acesso através dela. A vantagem da c-tree server DLL em relação ao c-Tree local, usadas em ambiente de instância única de Application Server, é que o c-Tree Server DLL possui os caches de leitura e escrita, que dão maior desempenho, escalabilidade e segurança para a aplicação.

E o que ele faz de diferente de um DBF ?

Em uma palavra … TUDO. A estrutura interna de um arquivo DBF armazena apenas dados de tamanho fixo em blocos sequenciais dentro do arquivo, e a área de header apenas guarda a estrutura da tabela e um contador de registros. Um arquivo DBF é praticamente um “TXT melhorado” por dentro. Não há amarração explícita de um DBF com um arquivo indexador criado no disco. Se você cria um índice no disco, e esquece de abrir o índice ao fazer uma inserção ou atualização na tabela, o índice fica desatualizado, e quando você for utilizá-lo, ene não apresenta erro, apenas deixa de encontrar as informações e se comporta de forma errática.

Um arquivo c-Tree possui um header com os detalhes completos do arquivo, inclusive o path completo em disco onde o mesmo foi criado. Não é uma “tabelinha”, é um container de persistência de dados transacionados. Cada índice permanente criado para a tabela também é registrado no header do arquivo de dados do c-Tree. Quando você abre uma tabela c-Tree, se ela foi trocada de diretório, ou se algum índice permanente não estiver no disco ou não estiver sincronizado, a API retorna erro de abertura por inconsistência.

Como era possível pintar e bordar com um DBF, o Application Server verifica se existe procedimento automático para recuperar a tabela para ela ser aberta novamente. Se houve mudança de diretório, os índices permanentes do header são excluídos e os paths internos são ajustados. Se apenas algum índice permanente não foi encontrado, mas não houve mudança no path, os índices que faltam são recriados automaticamente. Caso a tabela apresente alguma inconsistência de abertura, como um fechamento inesperado, o Protheus solicita um rebuild da tabela ao c-Tree.

Questões de Desempenho

Junto da segurança adicional do c-Tree, a camada de transacionamento de arquivos provê um overhead em operações de inserção e update. Mesmo as transações atômicas são primeiro gravadas em LOG, para depois serem efetivadas nas tabelas. A dll do c-tree “local” não tem esta camada de transacionamento, tudo é escrito no disco direto. Isto passa a impressão que o c-tree local é muito mais rápido.

E, realmente, se você coloca até 8 ou 10 conexões concorrentes em um Application Server usando c-Tree local, o desempenho seria melhor. Porém, ao passar de 12, 13 conexões, o desempenho dos processos começa a cair, a partir do momento que o sistema operacional começa a enfileirar as requisições de disco, o desempenho cai proporcionalmente conforme o volume de requisições cresce. Quando usamos um c-Tree Server, os caches de leitura e gravação permitem a aplicação aguentar mais requisições sem que a queda no desempenho seja proporcional.

No ponto onde a curva de desempenho de ambos se encontra (por exemplo, com 12 processos simultâneos c-tree local e c-tree server “empatam” no desempenho), se dobramos a quantidade de processos, o ctree local vai ficar duas vezes mais lento (50 % de perda) , enquanto o c-tree server perdeu apenas 15% do desempenho.

Este cenário fica muito pior quando utilizamos o c-Tree Server pela rede, pois quanto o Application Server e o c-Tree Server estão na mesma máquina, eles estabelecem uma conexão TCP por “loopback”, sem efetivamente usar a interface de rede. Quando colocamos um serviço slave do Protheus em outra máquina, cada I/O realizado para, por exemplo, ler um próximo registro da tabela, é enviado pela rede física em um pequeno pacote TCP, e por mais rápido que seja o seu retorno, a camada de comunicação física pode interferir no desempenho de uma sequência de milhares de requisições em até 8 vezes. A natureza das operações é altamente sensível a latência de rede.

Exemplo prático: Uma leitura sequencial de 20 mil registros de um determinado arquivo, vai gerar 20 mil requisições para o c-Tree Server. Se a minha latência de rede for 0,001 segundo (um milissegundo), mesmo que o tempo de leitura dos dados no cache do c-Tree server seja 0 ( zero ), foram consumidos 20 segundos na camada de comunicação. Qualquer aumento da latência para 0,002 ou 0,003 já são suficientes para dobrar ou triplicar este tempo.

Quando uma aplicação lê um bloco de registros retornados por uma Query do SGDB, e vai criar uma tabela temporária no ERP (usando a CriaTrab() por exemplo), esta tabela é criada dentro da pasta “\system\” do diretório raiz do ambiente, onde cada inserção de registro faz um I/O pela rede ao c-Tree Server, que cria no disco uma tabela com transacionamento completo (chamado pela Faircom de acesso “TRNLOG”). Então, além do overhead da camada de rede, você têm um transacionamento completo com o dobro de escrita em disco.

Outro ponto é a escalabilidade de leitura. Mesmo com caches de leitura, a rede representa um overhead significativo. Se conseguimos jogar tudo na mesma máquina, tudo é lindo. Porém, se o seu ambiente cresce e você precisa suportar mais conexões e mais instância de processamento do Application Server, escalar esta máquina verticalmente não é viável, e mais cedo ou mais tarde você vai esbarrar em um limite físico. A sua super-máquina teria que ter várias placas de rede para evitar contenção, muitos cores, e uma placa mãe com barramentos “sinistros” pra aproveitar o que for possível de paralelismo…

E agora ? O que fazer ?

Primeiramente, pensando em escalabilidade horizontal, ao colocarmos mais máquinas físicas em um cluster, quanto mais servidores forem colocados, mais processos vão realizar requisições simultâneas de acesso de leitura dos meta-dados do ERP pela rede. Em um ambiente com 100, 120 conexões simultâneas, isto pode não apresentar contenção. Porém, quando passamos de 150 conexões fazendo I/O simultaneamente, a rede pode se tornar um gargalo.

Muita atenção neste ponto: Não é a banda de rede que torna-se o gargalo, mas sim a quantidade de IOs, o enfileiramento dos pacotes para transmissão e recepção atinge um limite físico. Mesmo que a sua banda de rede suporte 1 GB por segundo, e o consumo de banda chega a menos de 20% deste valor, a quantidade de pacotes é tão grande que a rede não dá conta dos eventos e começa a “enfileirar”.

Pensando neste cenário, a TOTVS homologou junto da Faircom um mecanismo de cache dos meta-dados (dicionários) do ERP sincronizado em memória. A idéia é simples, mas a implementação por baixo do capô exigiu um mecanismo de sincronismo complexo, mas de uma eficiência impressionante.

Dicionários em Memória usando c-Tree

Vamos partir de um ambiente padrão, onde a máquina que contém a unidade de disco onde estão os arquivos da pasta raiz do sistema (RootPath) possui o c-tree Server instalado, e uma segunda máquina possua um serviço do Application Server, configurado com ctreemode=server. Todos os IOs de leitura passam pela rede.

Agora, imagine que este Application Server possa ser iniciado usando a DLL do c-Tree Server (BoundServer), e você configure no INI deste Application Server quais serão os arquivos do seu ambiente que serão cacheados na memória, e este cache possui um mecanismo de notificação de alterações de dados, para manter todos os nós sincronizados caso algum processo de algum slave faça alguma alteração em um registro de uma tabela no c-Tree SErver que esteja em cache em um ou mais nós ? Isto seria lindo, pois todas as leituras seriam feitas diretamente da cópia da tabela feita em memória no Ctree BoundServer que o Application Server carregou … Escalabilidade horizontal “plena”, pode enfiar nó nesse cluster, que uma vez que o cache esteja carregado, tudo é lido da memória.

É exatamente isso que o mecanismo de cache e de notificação fazem. Cada Application Server configurado com o modo BoundServer, mais a configuração de cache em memória — definida no appserver.ini na seção [ctreeservermaster] — vai conectar com o c-Tree Server no momento da subida do serviço do Application Server, vai verificar quais as tabelas que devem ser trazidas para a memória, vai abrir uma a uma e copiar os dados para a memória local do c-Tree BoundServer, e vai registrar-se no c-Tree Server como um nó cliente de cache. A partir daí, todas as leituras das tabelas que entraram no cache são feitas da memória local, e qualquer alteração que qualquer um dos nós faça nesta tabela vai refletir instantaneamente na cópia da tabela na memória do Protheus Server que alterou u registro, e na tabela principal no disco. Quando o c-Tree Server gravar a alteração da tabela no disco, ele envia uma notificação para todos os demais serviços de Protheus configurados como bound-Servers, avisando que eles devem pegar uma cópia atualizada do registro alterado para manterem os seus caches sincronizados. Para isso, um Application Server configurado desta forma vai abrir uma conexão pela rede com o c-Tree Server, para receber as notificações de atualizações de cache, e cada tabela que está na memória deste BoundServer vai ser aberta por este Application Server pela API do boundServer, e pela conexão de rede com o c-Tree Server, onde esta conexão somente será utilizada se a aplicação fizer um insert ou lock e update em algum registro.

Mas, quanto isso consome de recurso ?

Bem, cada tabela configurada no mecanismo de cache em memória precisa ser carregada inteira para a memória, bem como os seus índices. A carga não é sob demanda, é feita na subida do Application Server. O espaço médio em memória do Application Server utilizado é aproximadamente duas vezes o tamanho do arquivo de dados mais os índices. Por exemplo, um arquivo SX3990.DTC com 169 MB e seu índice de 10 MB, vão ocupar 350 MB da memória deste Application Server. Por esta razão, a configuração de réplica realmente deve priorizar apenas os dicionários do ERP mais acessados, e no caso do ambiente ter múltiplas empresas (e consequentemente múltiplos arquivos de dicionário), devem ser priorizados apenas os arquivos das empresas mais acessadas.

Devido ao alto consumo de memória por instância de Application Server, e devido a cada serviço de Protheus criar um cache apenas para si mesmo, quando você possui máquinas maiores, que comportam por exemplo 8 instâncias de Application Server na mesma máquina, isso gerada dois inconvenientes: O primeiro é que o startup do ambiente iniciava uma cópia maciça de dados para cada um dos serviços da mesma máquina, onde o serviço poderia demorar muito para estar disponível para a execução dos programas AdvPL. E, como cada serviço criava uma cópia para si mesmo, você teria oito vezes o consumo de memória para um cache no mesmo equipamento.

Pensando nisso, foi criado um modo de configuração chamado “boundclient”. Você elege um dos outro serviços para ser retirado do balanceamento de carga, para ele não receber requisições de conexão de SmartClient ou fazer qualquer processamento Advpl. Este serviço você configura como um BoundServer com o cache de arquivos na memória, e habilita a conexão TCP no c-Tree BoundServer desta instância (depois te explico como). Para todos os outros demais serviços, eles serão configurados exatamente como este primeiro boundServer, apenas trocando a chave ctreemode=boundserver para ctreemode=boundclient

Com este cenário, o primeiro serviço que deve ser colocado no ar é o Application Server configurado para ser o BoundServer. Somente ele vai fazer a cópia dos arquivos para a memória, uma vez. E, todos os demais serviços de Protheus desta máquina vão conectar-se neste BoundServer para ler a cópia dos dados em memória. Isso economiza horrores no tempo de iniciar o ambiente, e economiza a memoria da máquina, pois nela haverá apenas uma instância de serviço dedicada a prover o cache e mantê-lo sincronizado.

E isso resolve tudo ?

Não. Como o c-Tree Server também vai ser usado como o driver para a criação de arquivos locais de trabalho, que por default são criados dentro da pasta ‘\system\’ do diretório raiz do ambiente (rootpath), todos os IOs de leitura e gravação ainda passam pela rede … Por isso, também na Build 7.00.121227, foi criado um novo driver, chamado “CTREETMP”, encapsulado pelas funções FWOpenTemp() e FwCloseTemp(), para permitir criar um arquivo temporário “puro”, aberto e acessado exclusivamente pelo processo que o criou em modo exclusivo, e que é criado na pasta temporária do servidor onde o Application Server está sendo executado, carregando para isso em memória uma instância do c-Tree BoundServer, e criando uma tabela sem nenhum controle de transacionamento. Com isso, todos os IOs feitos para esta tabela temporária serão tão ou mais rápidos que um c-Tree “local”.

O driver “CTREETMP” têm uma diferença em relação aos demais recursos de acesso do c-Tree. Mesmo que o seu application server esteja configurado com ctreemode=local ou ctreemode=server, quando um programa AdvPL usar o Driver temporário CTREETMP pela primeira vez, o protheus vai carregar uma instância do c-Tree BoundServer para gerenciar os arquivos temporários acessados por este driver. Todo o resto fica inalterado. Caso o seu modo de trabalho já seja ctreemode=boundserver, o protheus já aproveita a instância do BoundServer carregada para gerenciar os arquivos do driver temporário “CTREETMP”.

Aí sim, com os programas AdvPL criando as tabelas temporárias de trabalho usando o encapsulamento do Framework para o Driver temporário, e a leitura dos meta-dados do ERP em um cache de memória distribuído e sincronizado, a coisa muda de figura !

E, o que eu preciso ?

Não basta configurar o circo todo … Existe a necessidade de adquirir uma licença “Enterprise” do c-Tree Server para conseguir habilitar este cache. O c-Tree Server e o c-Tree BoundServer distribuídos pela TOTVS possuem uma licença de uso apenas para arquivos locais, não é possível usá-lo como base principal do ERP (RPODB=CTREE) ou com o sistema de cache de dicionários em memória. Uma vez adquirida e aplicada uma licença “Enterprise” no c-Tree Server, é possível usufruir deste mecanismo de cache.

Conclusão

Como o assunto é extenso e cheio de detalhes, este post foi só para “abrir” o apetite. A documentação oficial do recurso de cache de dicionários em memória foi recentemente disponibilizada na TDN, veja os links de referência do post no final do artigo !

Espero que estas informações sejam de alguma forma valiosas e aproveitadas para que possamos cada vez mais utilizar todo o potencial computacional disponível graças ao avanço da micro-informática.

Desejo a todos TERABYTES de sucesso 😀

Referências

C-treeACE. (2014, November 29). In Wikipedia, The Free Encyclopedia. Retrieved 16:14, October 25, 2015, from https://en.wikipedia.org/w/index.php?title=C-treeACE&oldid=635850139

Documentação da TDN

c-Tree Server com SXS em memória
Seção cTreeServerMaster
Seção ctreeServer
ctServerName
cTreeMode
c-tree BoundServer

Acesso a dados – ALIAS e WORKAREA no AdvPL

Introdução

Já que o assunto está rendendo, e eu estou na maior inspiração, uma coisa que muita gente me pergunta, principalmente quem é novo em AdvPL… “O que é um ALIAS” ? E, “o que é uma WORKAREA” ? Estes termos estão presentes em todas as documentações de funções de acesso a dados do AdvPL. Neste post eu vou responder esta pergunta com a maior riqueza de detalhes que eu puder.

Definição de ALIAS

“Alias (termo técnico de informática), apelido designado a caminho de rede ou a uma tabela ou visão em um script ou “query” de banco de dados.” (Wikipedia)

Traduzindo pro AdvPL

A abordagem de acesso a dados usada em aplicações xBase (desde o DBASE II, DBASE III Plus, Clipper) foi criada sob uma abstração de acesso a dados ISAM. Logo, foram criadas na linguagem funções de acesso a dados com um comportamento pré-definido, onde você poderia escolher o “motor” ou engine de acesso a dados, sem precisar reescrever a aplicação inteira, apenas fazendo o link com um novo driver, e trocando o nome da RDD informada nos comandos de criação e abertura de tabela.

No TDN existe uma árvore de funções genéricas, onde eu acredito que quase todas as funções de acesso a dados estão documentadas, veja no link http://tdn.totvs.com/pages/viewpage.action?pageId=6063423

Conceito de RDD

Basicamente usamos a função DbCreate() para criar uma nova tabela de dados, partindo de uma estrutura informada em um array, e para abrir e utilizar efetivamente a tabela, usamos a função DbUseArea(). Em ambas as funções devemos informar a RDD (Driver) a ser utilizado.

Parte-se da premissa que todo o resto das operações, inclusive a criação de índices, pode e deve ser feita com a tabela “aberta” ( inclusive algumas operações exigem acesso “exclusivo”). As operações de inclusão de dados podem ser feitas por múltiplos usuários ao mesmo tempo, cada registro de informação tem um identificador físico único sequencial (RECord Number Order ou RECNO), e cada operação de alteração de dados de um registro exige um pedido de bloqueio explícito para alteração, que pode ser negado caso a tabela esteja aberta em modo compartilhado por outro processo, que já tenha obtido o lock daquele registro.

Com a criação de índices, a navegação pelos dados da tabela poderia ser feito movendo o ponteiro de registro atual para frente ou para trás, na ordem natural do arquivo (ordem de RECNO ou ordem de inserção), ou caso um índice esteja aberto e selecionado, a navegação é determinada pela chave do índice em uso.

No Clipper, originalmente você tinha os drivers nativos (distribuídos com o compilador) chamados DBFNTX e DBFCDX, e haviam também drivers de terceiros, como o SIXCDX. As diferenças significativas entre elas basicamente eram os formatos de armazenamento de campos “M” Memo ( em arquivos com o mesmo nome da tabela, com extensão .DBT ou .FPT), e a forma de armazenamento de índices. O Driver DBFNTX criava índices usando BTREE, sem nenhum tipo de compressão, onde um arquivo continha apenas uma chave de índice, o driver DBFCDX criava índices usando compressão de dados, e permitia mais de uma chave de indexação no mesmo arquivo de índice (extensão .CDX).

E o “ALIAS” ?

Certo … depois de explicar o início do universo, vamos ao ALIAS. Quando abrimos uma tabela no disco, precisamos informar para a função de abertura de tabelas (DbUseArea) qual é o nome do arquivo físico no disco que será aberto, e precisamos informar um ALIAS, ou APELIDO da tabela, pela qual ela será referenciada nas operações de leitura, gravação e etc.

Como podemos abrir e manter abertas mais de uma tabela de dados, precisamos dar para cada uma um ALIAS diferente. E, dentro do programa, fazemos referência ao alias na operação com a tabela e seus dados. Por exemplo, no ERP Microsiga, usando por exemplo o DBAccess com um SGDB homologado, para a empresa “99” (Empresa teste do ERP), o cadastro de clientes será criado no SGDB com o nome de “SA1990”, montado com o prefixo “SA1”, mais a empresa a qual o arquivo de clientes se refere, seguido do sufixo “0” zero).

Porém, quando o cadastro de clientes é aberto dentro do ERP, explicitamente pela função ChkFile() ou implicitamente sob demanda, a tabela de clientes da empresa atual que o usuário está logado no sistema sempre será aberta com o ALIAS SA1, e todos os programas que vão ler ou gravar dados no cadastro de clientes referenciam apenas o alias “SA1”.

E “WorkArea”, o que é ?

Definindo de modo bem simplista, uma WorkArea é o nome dado ao espaço utilizado para referenciar cada ALIAS aberto no processo atual. O Clipper originalmente permitia até 250 WORKAREAS, isto significa que um processo poderia abrir e manter abertas até 250 tabelas com ALIAS distintos. Usando o Protheus, poderiamos ter até 512 alias abertos ao mesmo tempo, e a partir do Protheus 10, podemos ter até 1024 alias abertos simultaneamente por processo.

E como eu uso isso ?

Se eu posso abrir mais de uma tabela no meu ambiente, cada tabela precisa ter um ALIAS, para ser possível referenciá-la. Porém, para inserir um novo registro de uma tabela, ou pedir um bloqueio de registro, como cada tabela aberta não é um “objeto” armazenado em uma variável, eu precisaria informar para a função da RDD qual é o alias que eu quero utilizar.

A especificação XBASE encontrou duas formas elegante de fazer isso, sem passar o ALIAS como parâmetro:

– WORKAREA ATUAL
– Expressão ALIASADA

Quando abrimos uma tabela sob um alias qualquer, a WORKAREA criada para este alias passa a ser a minha WORKAREA ATUAL. Isto é, todas as funções da RDD chamadas a partir deste momento irão agir sob a WORKAREA ATUAL. A documentação do AdvPL refere-se a WORKAREA ATUAL usando a terminologia “área de trabalho atual”, que significa o alias atualmente em uso. Podemos consultar a WorkArea atual através da função ALIAS(), que retorna o nome do ALIAS atualmente em uso. Caso a função Alias() retorne uma string em branco, não há um alias selecionado na WorkArea atual — o que não quer dizer que não existam outros alias abertos, apenas que nenhum alias está “selecionado”. Podemos trocar a WorkArea atual usando a função DbSelectArea(), informando o nome do ALIAS aberto que desejamos selecionar.

Já a EXPRESSÃO ALIASADA é uma forma elegante de fazer referência a um ALIAS, inclusive sem mexer na WORKAREA ATUAL. Por exemplo, para pegar o conteúdo de um dos campos de uma tabela, no registro atualmente posicionado, podemos usar por exemplo a Função FIELDGET(), onde passamos como parâmetro o número do campo na estrutura da tabela, e a função retornará o conteúdo deste campo. Caso não saibamos o número do campo, podemos descobrir o número do campo usando a função FIELDPOS(), onde informamos uma string contendo o nome do campo desejado, e a função retorna um número maior que zero caso o campo exista na WORKAREA ATUAL, ou zero caso contrário.

Agora, imagine escrever um código inteiro assim:

DbSelectArea("SA1")
DbGoTop()
conout("Codigo ... "+fieldget(fieldpos("A1_COD")))
conout("Nome ..... "+fieldget(fieldpos("A1_NOME")))

Fica legível, vai funcionar, mas para recuperar cada um dos dados dos campos, você precisa pegar a posição de cada um, para então recuperar o conteúdo daquela posição … nada prático. Fica muito mais fácil usar a EXPRESSÃO ALIASADA. A expressão aliasada possui duas variantes, a primeira nada mais é do que o alias da tabela, seguido do operador aliasado “->”, seguido pelo nome do campo do alias a ser acessado. Veja o fonte acima como fica:

DbSelectArea("SA1")
DbGoTop()
conout("Codigo ... "+SA1->A1_COD)
conout("Nome ..... "+SA1->A1_NOME)

A segunda forma de expressão aliasada é composta pelo alias, mais o operador aliasado “->”, seguido de uma expressão Advpl entre parênteses. Na prática, a expressão colocada entre parênteses vai ser executada considerando que a WORKAREA atual é o ALIAS que prefixa a expressão.

Por exemplo, em um determinado ponto do código, você não sabe (e nem lhe interessa saber) qual é a WorkArea atual, mas você precisa fazer uma busca em um alias aberto. Você poderia fazer de duas formas: Guardando o alias corrente, trocando o alias corrente para o alias desejado, realizando a operação e restaurando o alias corrente, ou usando expressões aliasadas entre parênteses.

cAliasAntr := Alias()
DbSelectArea("SA2")
DbSetOrder(2)
DbGoTop()
conout("Primeiro fornecedor = "+SA2->A2_NOME)
DbSelectArea(cAliasAnt)

Agora, imagine que você queira fazer estas operações no alias SA2, sem mexer na Workarea atual.

SA2->(DbSetOrder(2))
SA2->(DbGoTop())
conout("Primeiro fornecedor = "+SA2->A2_NOME)

Na prática, as funções DbSetOrder(2) e DbGoTop(), executadas de dentro das expressões aliasadas prefixadas com o alias SA2, serão executadas atuando sobre o alias da expressão, mesmo que a área de trabalho atual em uso seja outra. Isto permite fazer coisas mais rebuscadas na aplicação, como por exemplo ler o conteúdo de um campo de um alias e atribuir em outro, na mesma linha.

ALIAS1->CAMPO1 := ALIAS2->CAMPO2

Ou ainda, fazer uma busca por chave de índice composta em um determinado alias, partindo do conteúdo dos campos de outro alias:

ALIAS2->(DbSeek( ALIAS1->CAMPO1+ALIAS1->CAMPO2 ))

A operação de busca será feita no ALIAS2, mas a montagem da chave informada como parâmetro para a busca será montada baseado nos valores do registro atualmente posicionado no ALIAS1.

Vamos a mais um exemplo: Você precisa armazenar em um array todos os dados de um determinado registro do alias corrente. Logo, você cria algo como:

Static Function GetRecord()
Local nI, nT := len(DbSTruct())
Local aDados := {}
For nI := 1 to nT
 aadd(aDados,fieldget(nI))
Next
Return aDados

Esta função sempre vai atual na workarea atual. Agora, para pegar os dados de dois alias distintos, você pode chamar a função assim:

DbSelectArea("SB1")
aDadosSA1 := SA1->(GetRecord())
aDadosSA2 := SA2->(GetRecord())

Mesmo que, neste ponto do fonte, o alias corrente seja o SB1, a função GetRecord() foi chamada de dentro da expressão aliasada SA1->() … Logo, ela vai considerar o alias corrente como o SA1, e na próxima chamada, o alias corrente para a função será o SA2. Isto é uma “mão na roda”.

Expressão aliasada com alias variável

Quase que eu ia esquecendo … Quando você fizer uma função que recebe por exemplo uma string com um alias como parâmetro, você não precisa saber qual o alias informado, você pode usar macro-substituição apenas do alias, ou uma forma mais elegante, a expressão aliasada com alias variável. Basta colocar a variável do alias entre parênteses, seguido do operador aliasado “->”, seguido do campo ou expressão entre parênteses desejada. Por exemplo:

Static function InsertRec(cAlias)
(cAlias)->(DbAppend())
Return

Funcionalidades adicionais

Cada WorkArea possui um ALIAS em uso, mas também possui um número sequencial de controle, atribuído internamente quando um novo ALIAS é aberto. Para saber qual é o número da WorkArea, pode ser utilizado a função Select(). Se nada for informado como parâmetro, será retornado o número da workarea atual. Pode ser informado nesta função uma string com um ALIAS. Caso este ALIAS esteja aberto, será retornado um número maior que zero. E, a função DbSelectArea() pode receber o número da WorkArea como parâmetro.

Logo, a operação de salvar o alias corrente e restaurá-lo após uma operação onde o mesmo foi ou pode ter sido trocado, ficaria assim:

nArea := select()
DbSelectArea("SA1")
// Faz alguma coisa...
DbSelectArea(nArea)

Esta forma de salvar e restaurar a área corrente de trabalho é mais interessante do que guardar a string do alias, por duas razões:

  1. Caso não exista alias aberto na workarea atual, a função alias() retorna uma string em branco. Se esta string em branco for passada como parâmetro para a função DbSelectArea(), a mesma vai gerar um erro de “Alias does not exist”. Quando usamos o número da WorkArea, caso não haja workarea em uso, a função Select() retorna 0, e o DbSelectArea() para a área 0 deixa novamente a workarea sem alias relacionado.
  2. A busca pelo número da área de trabalho ao invés do nome da área é alguns microsegundos mais rápida do que a busca por nome.

Usos interessantes

Como um ALIAS é um identificador de uma tabela aberta, eu não posso abrir uma tabela usando um alias já aberto no meu processo, porém nada impede de eu abrir a mesma tabela física duas vezes usando alias diferentes. É um cenário não muito comum de ser necessário, mas já foi usado anteriormente em situações específicas, em determinadas funcionalidades escritas por código totalmente ISAM, sem uso de Queries.

Por exemplo, em um cenário hipotético onde você precisa ler uma faixa de dados específica de uma tabela para serem processados, e o resultado deste processamento vai gerar novos dados para a mesma tabela. Você teria que, por exemplo, fazer um filtro na tabela principal, iniciar um loop de processamento enquanto houvessem registros a serem processados, e antes de cada nova inserção, você teria que salvar o registro atual da tabela, fazer a inserção, depois reposicionar no último registro processado, para então pular para o próximo. Porém, a rotina de gravação possui um ponto de entrada, onde é assumido que a tabela não está filtrada. Isto faria você ter que desligar o filtro antes de chamar o ponto de entrada, guardar a posição atual, chamar a sub-rotina, e na volta setar novamente o filtro e reposicionar o registro.

Esta operação em ambientes como o ADS Server, ou o TOPConnect2 para AS400, eram lentas, pois cada vez que um filtro era definido, ele era enviado ao ADS Server e/ou ao AS400, onde a faixa de dados deste filtro já era separada na montagem do filtro, e não sob demanda. Para resolver isso, nada mais elegante do que abrir a tabela para filtro com um outro alias, fazer o filtro nesta tabela, e manter a tabela para manutenção de dados aberta com o alias original, sem o filtro. Assim, um alias apontava para a tabela filtrada, e outro alias apontava para a tabela sem os filtros, sendo possível realizar este processamento sem ficar ligando e desligando filtros e reposicionando registros. No final do processo, o alias filtrado era fechado.

Nunca mais vi alguém usar isso, mas foi uma saída bem interessante para a época. Hoje normalmente abrimos um alias com uma Query no SGDB, onde os dados já vem filtrados e ordenados de acordo com a query, e fechamos a query no final do processo. Codificar e trabalhar 100 % sem query exigia alguns movimentos criativos naquela época 😉

Conclusão

Neste final de semana já foram 4 artigos publicados acerca do acesso a dados em AdvPL, e eu sinto em informar que este post encerra a penas a “Introdução” ao assunto. Porém, esta abordagem é necessária para sabermos um pouco mais sobre como o circo funciona por baixo da lona. Isto é a base da abordagem. A partir daqui é possível entrar em mais detalhes das demais funcionalidades, e nos comportamentos dos operadores de dados do AdvPL.

Valeu moçada, até o próximo post 😀

Referências

ALIAS. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2014. Disponível em: <https://pt.wikipedia.org/w/index.php?title=Alias&oldid=40855180>. Acesso em: 25 out. 2015.

Acesso a dados – Drivers e RDDs no AdvPL

Introdução

Qualquer aplicação que trabalhe com persistência ou armazenamento de dados precisa ter uma forma eficiente de leitura e gravação destes dados. A maneira, forma ou aplicação usada para armazená-los deve ser a que melhor atende as suas necessidades, sejam elas de volume de dados, organização, ou desempenho de busca ou processamento.

Na linguagem AdvPL, temos acesso aos drivers chamados “locais”, para arquivos no disco em uma instância única de Application Server, e acesso a dados em arquitetura client-server, como o c-Tree Server, e bancos relacionais (SGBD’s) acessados através do gateway DBAccess. Neste post vamos nos aprofundar um pouco no armazenamento e acesso a dados do Protheus, com foco maior no acesso a dados em bancos relacionais homologados através do DBAccess.

Acesso a Dados no AdvPL

O AdvPL possui uma série de funções de acesso a dados, baseados na arquitetura xBase, de raiz ISAM, desenhada para obterm um excelente desempenho em acessos a dados sequenciais e indexados. Sua implementação é baseada no conceito de Drivers de Acesso, conhecido desde o Clipper como “RDD” (Replaceable Database Driver). Uma RDD possui uma série de funções pré-definidas, com comportamentos pré-definidos. Logo, ela funciona como uma abstração de acesso às informações, independente do meio físico onde a informação esteja armazenada.

Por exemplo, ao criar um programa que crie uma tabela, abra e popule alguns registros, originalmente para trabalhar com uma tabela no formato DBF, informamos o nome do Driver (ou RDD) que deve ser utilizada como parâmetro nas funções de criação da tabela e de abertura da tabela. Normalmente precisamos apenas alterar o nome do Driver neste programa para ele usar outro meio físico de armazenamento, sendo que o programa mantém as suas características funcionais.

DBF  e c-Tree

Com o surgimento do Protheus em 1999, sendo lançada a primeira versão do ERP Microsiga usando o AP5 (Advanced Protheus 5), o Protheus possuía uma versão do AP5Server para Windows 9x e uma para Windows NT, um Client de Interface, um IDE e um Monitor de processos, todos para Windows. O ERP Microsiga poderia usar arquivos no formato DBF para armazenar os meta-dados (dicionários) do ERP, e os dados efetivamente do ERP poderiam ser armazenados em DBF, usando ADS Local ou ADS Server, ou armazenados em um banco relacional homologado, acessado através do TOPConnect 2.x.

Pensando em oferecer outras opções de armazenamento de dados, E pensando também no porte do servidor de aplicação para o sistema operacional Linux, a Microsiga fez uma parceria com a empresa de software norte-americana Faircom(r), que fornece uma API de acesso a dados client-server e multi-plataforma. Como até aquele momento todo o software do ERP estava escrito para realizar manutenção e consulta dos dados usando RDDs ISAM, o desafio de tecnologia era implementar uma RDD nova, chamada CTREECDX, para ser usada como armazenamento dos meta-dados e/ou dos dados principais da aplicação. Como os primeiros testes realizados com os portes iniciais de Driver DBF para Linux apresentavam instabilidades operacionais em cenários de concorrência, optou-se por fazer uma parceria com a Faircom, e criar em conjunto uma versão especifica do c-Tree ACE para atender as necessidades e características de acesso a dados necessárias pela aplicação já existente, visando reaproveitar o máximo de código AdvPL possível, trocando nos códigos apenas o nome da RDD, e mantendo os comportamentos esperados.

Também com pequenas alterações, a RDD foi criada, e para minimizar mais ainda as alterações de código nos fontes já existentes, como as tabelas de dados da aplicação já eram abertas pelo ERP usando funções de FrameWork, e a abertura de arquivos de meta-dados e arquivos de dados temporários usava a RDD chamada “DBFCDX”, foi criado um parâmetro de configuração no Application Server, chamado localfiles, para permitir informar qual o driver que seria endereçado “por baixo” da aplicação, quando ela usasse o driver “DBFCDX”.

Com isso, bastava você criar um ambiente vazio, usando na configuração do ambiente (environment) a configuração localfiles=ctree, para que qualquer código fonte escrito usando o driver “DBFCDX” passasse a usar o motor de acesso a dados do c-Tree. Em ambientes Linux, não se têm a opção de usar DBF, apenas c-Tree. Houve também uma iniciativa de implementar uma RDD Btrieve(r) para acesso a meta-dados e dados da aplicação, mas devido a descontinuidade do fabricante da aplicação, esta RDD foi abandonada.

Como ainda havia a necessidade de alguns sistemas legados de fazer integrações com arquivos em formato DBF, foi criado um driver chamado “DBFCDXADS”, que ao ser utilizado, utiliza o driver do ADS Local para acesso aos dados. Logo, mesmo que o seu localfiles seja ctree, em um ambiente Windows, é possível abrir uma tabela em formato DBF. Com o novo porte do Application Server para 64 bits, o suporte a este driver foi descontinuado, e não está presente na Build 64 bits do Application Server.

Acesso a dados relacionais – SQL

Na época existia o TOPConnect 2, que também foi portado para Linux, mas existia uma versão específica para cada SGDB. MSSQL 6,5, MSSQL 7, Sybase, DB2, Oracle, PostgreSQL, Informix… A TOTVS optou por reescrever este gateway, dando-lhe o nome de TOPCOnnect 4.x, também multi-plataforma, mas com uma única versão (multi-database), capaz de se conectar com todos os bancos homologados. No lançamento do ERP Protheus 10, o TOPCOnnect 4.x passou por uma re-estruturação, ganhou mais funcionalidades, e passou a ser chamado TOTVSDBAccess, e posteriormente apenas DBAccess.

Com o uso de bancos relacionais, desde o TOPConnect 2, foi implementada uma forma de leitura de dados usando queries, ao invés da engine ISAM de acesso. Uma query é aberta na aplicação AdvPL como se fosse um ALIAS normal de sistema, aberta em modo exclusivo e somente leitura, onde a navegação nos registros é apenas para frente (Read-only e forward-only). Isto permite obter dados de mais de uma tabela, de forma mais simples e eficaz do que a abordagem ISAM.

Porém, o DBAccess mantém uma camada de acesso ISAM emulado, para que todos os códigos que usavam a RDD “TOPCONN” continuassem funcionando. E, as inserções e alterações de dados também são feitas usando este paradigma, com diversas otimizações para manter o comportamento da forma mais escalavel possível.

Acessando os dados pelo DBAccess

Através do DBAccess, podemos acessar os dados gravados nas tabelas usando Queries, havendo apenas a necessidade de algumas instruções adicionais para a abertura do “Alias” ou área de trabalho da query, devido a razões de compatibilidade de armazenamento. As queries dos módulos do software padrão do ERP Microsiga são escritas usando um conjunto de regras definidos pela Microsiga, chamado “SIGASQL”, que é muito parecido com o SQL ANSI, com algumas restrições e sintaxe pré-definida. Antes de submeter a query ao banco de dados, é usada uma função do FrameWork AdvPL chamada “ChangeQuery”, que verifica qual é o SGDB em uso, e realiza os ajustes necessários na Query para ela ser adequada / transformada para ser executada no banco em questão. Caso seja necessário em algum ponto específico do código, que seja usada uma abordagem ou instrução não suportada pela ChangeQuery, a aplicação AdvPL deve verificar qual o banco de dados da conexão atual (tcGetDB), e montar a query com a sintaxe específica para aquele SGDB, sem passar ela pela ChangeQuery(). A vantagem de uso da ChanegQuery() está na portabilidade. A utilização dela visa tornar transparente uma eventual migração do software entre SGDBs diferentes, ou mesmo a homologação de um novo SGDB, onde a ChangeQuery fará “por baixo” eventuais adequações para um novo SGDB homologado.

Convenções do SIGASQL

– Caso seja desejado obter uma coluna calculada a partir da concatenação de duas colunas, deve ser utilizado o operador “||” (dois pipes).
– Caso seja necessário extrair uma parte de uma string na Query, deve-se utilizar a função SUBSTRING().
– São permitidos apenas ANSI JOINS. Os JOINS com *= e afins não são suportados.
– Podem ser usados UNION, UNION ALL, ORDER BY, GROUP BY e até selects encadeados.

Existem outras características relacionadas diretamente com estes aspectos, acredito que elas são abordados em maiores detalhes na documentação da função ChangeQuery() na TDN, no link “http://tdn.totvs.com/display/public/mp/ChangeQuery“.E, existe outra documentação bem interessante na TDN, onde outros aspectos e alternativas de desenvolvimento com Queries em Advpl são abordadas, como por exemplo o “Embedded SQL”. Veja o documento na íntegra no link “http://tdn.totvs.com/display/framework/Desenvolvendo+queries+no+Protheus“.

Acesso a dados ISAM emulado pelo DBAccess

Uma tabela de dados criada pelo DBAccess pode ser acessada simplesmente abrindo a tabela diretamente pela função DbUseArea(), informando o nome físico da tabela e a RDD TOPCONN. Com algumas restrições, as funcionalidades de filtro (DbSetFilter), criação de índices (OrdCreate), busca ordenada (DbSetOrder/DbSeek), navegação (DbSkip/DbGoTop/DbGoBottom), deleção lógica (DbDelete) e demais operações são suportadas.

Os tipos de dados armazenados no SGDB não necessariamente são os mesmos dos dados originais do AdvPL. Por exemplo, um campo do tipo “D” data no AdvPL, será gravado como um campo Char(8) ou Varchar(8) no SGDB, o tipo lógico “L” no AdvPL será gravado como Char(1) ou Varchar(1), contendo as letras ‘T’ ou ‘F’. Um campo “M” Memo pode ser gravado usando vários tipos de container, porém estes dados são sempre acessados pelo ALIAS da tabela, não são recuperados por Query.

RDDs no AdvPL

O TOTVS application server oferece várias alternativas de acesso a dados. Nem todas são homologadas em todas as versões do executável do Application Server em todas as plataformas, algumas também não são homologadas pelos produtos do ERP Microsiga.

“DBFCDX” : Driver de acesso aos arquivos locais, ou meta-dados (dicionário do ERP). Sensível à configuração de ambiente “Localfiles” do arquivo de configuração do Application Server, seu default é “ADS” para plataforma Windows (usando DLLs do ADS Local), e CTREE para plataformas Linux (sensível a configuração CtreeMode do Application Server, pode tanto fazer acesso usando o driver local do c-Tree como através da mtCient.dll a um c-Tree Server).

“CTREECDX” : Driver de acesso a arquivos via c-Tree. Caso o Application Server não esteja configurado para conectar a um c-Tree Server, é carregada uma versão do driver local do c-Tree (ctreestd.dll) para acesso aos dados. Caso um ambiente vá utilizar mais de um Application Server, deve ser usado o c-Tree Server.

“DBFCDXADS” : Driver de acesso a arquivos via ADS Local. Pode ser utilizado em customizaçoes e integrações, apenas nas versões 32 bits do TOTVS Application Server para Windows.

“DBFCDXAX” : Driver para uso com ADS Server, não mais homologado pelas recentes versões de produtos do ERP Microsiga, acessava um ADS Server usando uma das versões de DLL client do ADS (ACE) distribuídas junto do executável do Application Server.

“TOPCONN” : Driver de acesso a tabelas em bancos relacionais, através do TOPCOnnect / DBAccess. Permite a execução de Queries. Existem vários bancos homologados para os produtos do ERP Microsiga, e o Driver TOPCONN também permite uma conexão direta via ODBC, chamada ODBC Genérica — desde que configurada no DBAccess Server — para acessar qualquer outra fonte de dados. Porém, neste caso, não há emulação de ISAM (Não é possível usar a ODBC genérica como base principal de dados do ERP), mas é possível executar statements diretamente pela conexão e abrir queries para consulta de dados. A ODBC genérica normalmente utilizada em integrações customizadas.

“CTREETMP” : Driver de acesso a arquivos temporários exclusivos por processo, implementado a partir da build 7.00.131227A do Application Server, encapsulado pelas funções FWOPENTEMP e FWCLOSETEMP. Utilizam um c-Tree Server DLL distribuído junto do pacote de eecutáveis do Application Server, para criar arquivos temporários na máquina onde a instância do Application Server está sendo executada, ao invés de criar na pasta raiz do ambiente (RootPath). Em ambientes com balanceamento de carga, é extremanente performática, pois não usa a conexão TCP (rede) para acessar os dados.

Qual driver devo utilizar

A aplicação padrão usa os drivers TOPCONN para informações do banco de dados da aplicação ERP, e a rdd local “DBFCDX” para os meta-dados e arquivos temporários — quando usada a função CriaTrab() do Framework AdvPL. Quando podemos usar um arquivo temporário que não precisa ser compartilhado com outros processos, podemos usar as funções FWOPENTEMP() e FWCLOSETEMP() para criar um arquivo temporário ‘local’ na máquina onde o Protheus Server está sendo executado, eliminando o overhead de acesso via rede.

A questão da escolha do local de armazenamento deve levar em conta de onde vêm a informação que será alimentada o arquivo, qual é o tamanho estimado destas informações, e qual é o tempo de vida desta informação. Um dos “mandamentos” do desempenho diz que, quanto mais perto do algoritmo estão os dados, melhor.

Uma tabela temporária criada para um procedimento exclusivo, como uma consulta ou um browse de informações em tela, onde o algoritmo está no AdvPL, e a informação será percorrida várias vezes, pode muito bem utilizar uma Query para recuperar as informações do SGBD, e armazená-la em uma tabela temporária exclusiva, usando a FwOpenTemp(). Uma vez criada, todas as iterações com estes dados estão isentos do overhead de rede, porém apenas o processo atual têm acesso a estes dados. E, caso a aplicação termine de forma inesperada (erro, queda de conexão, etc), estes dados estão “já eram”.

Se eles faziam parde de um processo demorado, onde vários resultados intermediários já haviam sido armazenados nela, tudo terá que ser refeito, a não ser que os resultados parciais já tenham sido gravados na base principal durante o processamento, e uma nova solicitação de processamento leve em conta que os dados já processados podem ser ignorados, uma segunda requisição do processo somente vai processar o que “falta”. Se os dados eram para consulta, não tem problema eles terem “evaporado”.

Quando conseguimos aplicar a regra de negócio direto no SGDB, podemos criar uma tabela para fins temporários no SGDB, alimentá-la com uma Query, por exemplo, sem precisar trafegar a informação pela rede, e depois fazer updates em blocos de informações usando instruções SQL ou Stored Procedures, eliminando a necessidade de tráfego do “grosso” dos dados ao Application Server, ou pelo menos conseguimos fazer uma parte do processo direto no SGDB, e trafegamos para o Application Server apenas os dados relevantes ao algoritmo, quando as regras de processamento estão no código AdvPL.

Existem outros aspectos que também precisam ser levados em conta, como por exemplo a necessidade de se ter todos os dados prontos de uma vez. Quando isto não é necessário, os dados podem ser trafegados sob demanda, o que evita do início de um processo paralelizável “sugar” o banco ou a banda de rede … Mas isto já seria tema para um post mais avançado sobre escalabilidade e performance.

Conclusão

Cada driver oferece vantagens e desvantagens em um determinado cenário. Nem sempre as respostas para tudo vão estar escritas e algum lugar. Na dúvida entre duas abordagens, teste e meça a eficiência do resultado (consumo de recursos e tempo de processo).

Espero que os posts de hoje tenham sido de alguma forma úteis, agradeço de novo a audiência, e desejo a todos mais TERABYTES de sucesso 😀 Até o próximo post, pessoal 😉

Acesso a dados – ISAM versus Relacional

Introdução

Em um post anterior ( https://siga0984.wordpress.com/2014/12/22/persistencia-de-dados-isam/) foi feita uma introdução à abordagem ISAM para persistência de dados. Acho interessante a leitura desse artigo antes de continuar a ler este post. Atendendo a pedidos, vamos analisar com uma lupa as características de uma engine ISAM versus uma engine relacional disponibilizada através de um SGBD (Sistema Gerenciador de Banco de Dados).

Exemplo de acesso a dados

Vamos usar o exemplo clássico da dupla “cabeçalho de pedido” e “itens de pedido”. Criamos duas tabelas distintas, uma para os dados do pedido (cabeçalho) e outra para os itens dos pedidos. Fazemos isso praticamente da mesma forma em um banco ISAM como em um banco relacional.

Normalmente criamos um ou mais índices na tabela que armazena os ítens de pedido usando o código do pedido — que é gravado em cada item do pedido — e um campo adicional, como o código do produto ou um campo de sequência ou item, para ordenar os itens que pertencem ao mesmo pedido, de modo que eles fiquem agrupados pelo índice e possam ser rapidamente lidos em sequência. E, naturalmente criamos um ou mais índices também na tabela que armazena o cabeçalho do pedido, para que os pedidos possam ser rapidamente localizados. Isto também é feito praticamente da mesma forma em um banco ISAM bem como em um SGBD.

Agora, o que vai fazer a diferença é a forma como os dados destas tabelas são recuperados pela abordagem ISAM e pela abordagem relacional através de uma ou mais queries.

Cenário proposto

Uma rotina simples de recuperação dos dados de um pedido, onde é fornecido um código do pedido, e a aplicação deve recuperar todos os dados relacionados aquele pedido, considerando que devem também ser trazidos para o pedido algumas informações do cadastro do cliente que fez o pedido, e ao trazer os itens de pedido, devem ser trazidas informações mais detalhadas do cadastro do produto envolvido na operação. No cenário proposto, vamos lidar com quatro tabelas: Clientes, Pedidos, Itens de Pedidos e Produtos, e a aplicação para recuperar estes dados acessa os arquivos de dados através de uma conexão de rede.

Pseudocódigo com abordagem ISAM

Selecione a tabela de Pedidos
Selecione a ordem de codigo de pedido
Busque pelo codigo do pedido informado 
recupere os dados do pedido 
selecione a tabela de clientes 
selecione a ordem de codigo de cliente
busque pelo codigo do cliente
recupere os dados do cliente 
selecione a tabela de itensdepedido 
selecione a ordem de codigo do pedido 
busque pelo codigo do pedido 
enquanto o codigo do pedido deste item for do mesmo pedido e nao for final de arquivo faça:
 recupere os dados do item do pedido
 selecione a tabela de produtos
 busque pelo codigo do produto
 recupere as informaçoes desejadas do produto
 selecione a tabela de itens de pedido
 posicione no proximo item de pedido
mostre os dados na interface
fim da rotina

Na engine ISAM, cada tabela é endereçada por um apelido (Alias) dado no momento de sua abertura, então para cada informação a ser buscada, precisamos selecionar a tabela para a operação, definir a ordem de busca, fazer a busca indexada e recuperar a informação. Para o caso dos itens de pedido, cada item recuperado precisa trazer algumas informações do cadastro de produto, fazendo exatamente este processo, e a cada item recuperado, devemos ir para o próximo item naquela ordem ( DbSkip() ), e verificar se o novo item posicionado é do mesmo pedido, para repetir o processo para todos os itens.

Levando-se em conta que cada operação ISAM faz pelo menos um I/O pela rede para acessar os dados, em um pedido com 20 itens, temos 1 IO para buscar o pedido, 1 para o cliente, e 2 para cada item de pedido ( posicionar no item, buscar o produto, posicionar no proximo item, buscar o produto), totalizando em uma abordagem bem enxuta 2 + ( 20 * 2 ) = 42 IOs…

Pseudocódigo com abordagem relacional ( Query )

"selecione os dados do pedido e dados do cliente das tabelas de pedidos e de clientes onde o codigo do pedido é igual ao código a pesquisar e o código do cliente no cadastro de clientes é o mesmo código de cliente da tabela de pedidos"
"selecione itens dos pedidos e dados do produto das tabelas itensdepedido e produtos onde o código do pedido é igual ao código informado e o código do produto no cadastro de produtos é o mesmo código de produto da tabela de itens de pedido, ordenado por codigodopedido e itemdopedido"

Na engine relacional, um IO recupera os dados do pedido e do cliente, e 20 IOs recuperam os dados de cada item de pedido com os detalhes do produto informado naquele item. É praticamente a metade dos IOs que uma engine ISAM. Na prática, escrito sem toda a riqueza de detalhes, considerando que %CODPED% é o código do pedido informado para buscar, o pseudocódigo mais próximo do Advpl seria:

SELECT PED.* , CLI.* FROM CLIENTES CLI, PEDIDOS PED WHERE PED.CODPED = %CODPED% AND CLI.CODCLI = PED.CODCLI
SELECT ITEM.* , PROD.* FROM ITENSDEPEDIDO ITEM, PRODUTOS PROD WHERE ITEM.CODPED = %CODPED% AND 
ITEM.CODPRO = PROD.CODPRO ORDER BY ITEM.CODPED, ITEM.SEQ

Considerações do ISAM

  • Dada a natureza da operação ISAM, cada vez que pulamos para o próximo registro, todas as colunas do registro são lidas para a memória, sejam elas usadas pelo programa ou não.
  • Uma alteração estrutural na tabela fatalmente envolve fazer uma cópia da tabela atual, criar uma nova estrutura com um novo alinhamento para os campos, e copiar todos os dados de volta da cópia para a nova estrutura.
  • Qualquer acesso otimizado aos dados deve envolver um índice que ordene os dados de modo a agrupá-los em sequência.
  • Você pode reaproveitar um índice para varias situações comuns de acesso em um programa, porém uma vez que um índice permanente seja criado e utilizado no sistema, alterar ou excluir esse índice pode causar transtornos e bastante retrabalho para refatorar os códigos dele se utilizam.
  • Caso seja necessária uma extração de dados em uma ordem não indexada, necessária eventualmente, precisamos explicitamente criar um índice temporário, e esta operação normalmente envolve a leitura e avaliação de todos os registros da tabela.
  • Manter muitos índices permanentes criados para a tabela pode prejudicar os tempos de inserção e atualização de registro, pois todos os campos da tabela que fazem parte de um índice precisam ser atualizados quando um novo registro é inserido, ou quando alteramos um campo que faça parte da chave de um ou mais índices.
  • A questão de filtros de acesso a dados é mais delicada, pois quando a aplicação acessa direto a tabela, o filtro é aplicado na camada de leitura, onde ao avançar para o próximo registro, podem ser feitas muitas leituras de registro que serão desconsideradas até que um novo registro dentro da condição de filtro seja retornado (filtro muito restritivo em tabelas muito grandes). Quando usamos a abordagem ISAM através de uma engine client-server, como o c-Tree Server por exemplo, o filtro pode ser processado em server-side, sem trafegar desnecessariamente os registros que não pertencem ao filtro.
  • Quando temos mais de uma tabela, com dados relacionados entre elas, o relacionamento é explicitamente escrito do programa, a busca de cada informação em uma tabela relacionada implica em abrir e selecionar a área de trabalho da tabela relacionada, verificar se o índice em uso é adequado para a busca, e posicionar o registro desta tabela fazendo um DBSeek (busca pela chave indexada) usando os dados dos campos da chave de índice da tabela principal e verificar se o dado relacionado foi encontrado, para então trazê-lo e restaurar o alias de trabalho novamente. Quanto mais amarrados forem os dados a serem manipulados, maior é o custo de criar e dar manutenção no fonte, para que ele seja performático. Quando consideramos um ambiente onde o servidor de aplicação está separado da engine server ISAM por uma camada de rede, cada operação atômica sofre uma penalização do overhead de rede.

Acesso a banco relacional por Queries

Quando utilizamos um SGBD relacional para acesso aos dados, temos algumas vantagens em relação ao ISAM:

  • – Suporte a alteração estrutural, para adicionar, alterar ou remover colunas. O SGBD relacional normalmente tem mecanismos de armazenamento segmentados, que permitem fazer pequenas alterações sem precisar reescrever a tabela inteira.
    – Em uma query, podemos pedir ao banco de dados apenas as colunas que nos interessam.
    – Quando precisamos trazer na mesma query dados de tabelas relacionadas, podemos usar os recursos de OUTER JOIN para localizar os dados nas tabelas relacionadas, e trazer delas as colunas desejadas, onde trazemos colunas de diferentes tabelas em um mesmo evento de rede. ( menos IO para buscar os mesmos dados em relação ao ISAM).
    – O SGBD possui um mecanismo de análise e geração de plano de execução, que leva em conta a verificação dos índices já existentes para optimizar a busca, e a geração de sub-sets de dados internamente para optimizar as operações.
  • Uma query é escrita praticamente de forma literal, respeitando algumas definições sintáticas. Praticamente, a estrutura de uma query simples é “select coluna(s) from tabela(s) where condição(ões) order by campo(s)”, algo intuitivamente compreensível.
  • Existem palavras-chave que podem ser usadas nas queries para realizar somas e agrupamentos de dados (SUM, GROUP BY).

Isto traz ao SGBD a parte complexa de separar os dados exatamente da forma que foram solicitados. Para auxiliar neste processo, existem boas práticas que devem ser adotadas para a codificação de queries, e técnicas como a normalização de dados, para evitar redundâncias desnecessárias e agrupar os dados de forma eficiente, além de configurações do banco de dados específicas para melhor atender o tipo e volume de requisições realizados (tuning), inclusive algumas destas ferramentas podem sugerir a criação de índices para melhorar o desempenho da execução de queries.

Comparando desempenho

Avaliando ambas as abordagens, naturalmente o acesso por queries tende a ser mais rápido, principalmente nos cenários onde buscamos informações relacionadas e aplicamos filtros. Tudo é feito no SGBD, e apenas os dados selecionados são trafegados. Porém, o desempenho de uma query mais complexa requer alguns cuidados no momento de escrevê-la, para que o SGBD seja capaz de elaborar um plano de execução para a recuperação das informações da melhor forma possível, onde a principal consideração é haver a existência de um índice que contemple todos os campos utilizados para filtro de informações.

Outros cenários

Não necessariamente uma abordagem com Queries será sempre mais rápida que uma abordagem usando ISAM, existem casos onde não compensa trocar tudo por Query. O exemplo clássico é a busca de dados exatos na base usando DbSeek(), informando uma chave completa do índice. A busca por um dado usando DbSeek() poir default procura a existência no SGDB do primeiro registro na ordem em uso cujo conteúdo dos campos atenda a chave informada na busca. Caso a chave não seja encontrada, a função retorna .F. (falso) e o alias é posicionado no final de arquivo (EOF).

Embora alguns de vocês já possam tenham ouvido falar que “Seek” é mais lento do que “Query”, o cenário proposto para isso é: Uma parte de um código AdvPL que busque informações relacionadas em uma ou mais tabelas fazendo o posicionamento das tabelas relacionadas com DbSeek() é mais lento do que criar uma Query com JOIN que traga também os campos das tabelas relacionadas.

Existe um cenário de uso do DBSeek(), onde podemos parametrizá-lo para, caso a chave não seja encontrada, a tabela seja posicionada no próximo registro cuja chave seja imediatamente superior à chave informada para busca. Isto é chamado de “SoftSeek”. Neste cenário, quando a chave não é encontrada, o DBAccess vai reduzindo a query, chave por chave, para localizar e posicionar no primeiro registro imediatamente maior do que a chave informada. Esta operação pode fazer o DBAccess submeter uma query para cada campo da chave de índice, até que algum dado seja encontrado ou não sobrem mais chaves no índice para fazer a comparação.

Por exemplo, para saber qual é o último item sequencial de um pedido, onde a chave é o codigodopedido+sequencia, é muito fácil fazer isso no ISAM, bastando incrementar em uma unidade o código do pedido, fazendo um SoftSeek(), e apos a busca, fazemos um DbSkip(-1) para posicionar no registro imediatamente anterior. Isto no ISAM é muito rápido, mas quando realizado através do DBAccess, o DbSeek() com softseek pode ser mais lento, e ao posicionar no registro adequado, o posicionamento do registro anterior vai fazer o DBAcces montar um cache “backwards” dos registros anteriores. Neste cenário, um select max(itemdepedido) from itensdepedido where pedido=%CODPED% definitivamente será mais rápido — desde que exista um índice que contemple a chave “pedido+itemdepedido”.

Mesmo que em um teste “de mesa” os tempos possam ser muito próximos, o peso da operação é muito mais leve. Isto significa que, mesmo que o tempo da operação apresente um ganho de tempo pouco significante, quanto mais operações simultâneas você fizer usando esta abordagem, você não vai consumir tanto recurso de máquina. Logo, o seu ambiente vai suportar muito mais requisições do que usando a outra abordagem, pois com uma abordagem mais pesada, você consegue atingir o limite de desempenho do equipamento com bem menos requisições simultâneas. Este é um ponto chave de escalabilidade e desempenho. Mesmo que o seu projeto sistemico possa escalar horizontalmente, quanto mais leve for o processo, menos máquinas você precisa acrescentar no seu grid computacional para obter os mesmos resultados.

Conclusão

Dos pontos de vista de usabilidade e desempenho, no geral é mais fácil e performático realizar consultas com queries, desde que as mesmas sejam bem escritas, utilizando-se das boas práticas recomendadas para a construção das requisições, que normalmente são as mesmas para praticamente todos os bancos relacionais. Uma query mal escrita, junto de um algoritmo mal escrito, podem até gerar p resultado esperado, mas a um custo de máquina e desempenho não satisfatórios ou não escaláveis.

Vários SGDBs possuem ferramentas interativas de averiguação de desempenho de Queries. Por exemplo, o Microsoft SQL Management Studio possui um recurso de “mostrar plano de execução estimado”, onde a ferramenta analisa as etapas que o SGDB terá que fazer para acessar as informações desejadas na Query, informando quais das etapas serão as com maior custo.

Procure sempre conhecer e aprender a usar as ferramentas você têm a sua disposição, este conhecimento definitivamente faz a diferença no desenvolvimento de uma aplicação.

Desejo a todos TERABYTES de sucesso 😀 Até o próximo post, pessoal 😀

Acesso a Dados – IndRegua()

Introdução

Desde os primórdios do AdvPL, quando o ERP Microsiga ainda era um executável stand-alone, que usava arquivos DBF para armazenar os meta-dados (dicionários do ERP) e tabelas de dados da aplicação, foi criada uma função no FrameWork AdvPL, para encapsular e tratar a criação de um índice temporário para uma tabela DBF qualquer.

Embora esta função seja do FrameWork do AdvPL, ainda restam hoje algumas dúvidas sobre qual é a melhor forma de utilizá-la, inclusive quais são as diferenças de comportamento quando usamos a função em um alias de uma tabela usando ADS ou c-Tree, e o que ela faz de “diferente” quando é utilizada em uma tabela acessada pelo DBAccess em um SGBD relacional.

Comportamento original da IndRegua

A função foi desenhada para, a partir de um ALIAS ou WorkArea de uma tabela DBF aberta, permitir criar um índice temporário para a tabela, onde você deve informar uma condição de ordenação, usando a mesma sintaxe de composição de chaves de índice do AdvPL, pode ser especificada uma condição de filtro, para que o índice seja criado considerando apenas os registros do alias atual que atendam a condição de filtro especificada.

Com um alias de uma tabela DBF ou c-Tree, a função cria fisicamente no disco um índice de uso temporário (extensão .idx) atendendo as condições parametrizadas. Mesmo quando especificada uma condição de filtro, toda a tabela é percorrida internamente para a criação deste índice. Logo, o tempo de criação do índice filtrado é proporcional ao tamanho da tabela. Porém, uma vez criado e selecionado para uso, este índice permitia uma navegação praticamente instantânea sobre os dados filtrados e ordenados. Após o uso, este índice deve ser fechado, e removido do disco.

Este processo somente é indicado quando você precisa fazer a leitura completa dos dados filtrados mais de uma vez, como por exemplo para a exibição em tela de um Browse para marcar itens de processamento (MarkBrowse). Caso contrário, você poderia simplesmente aplicar um filtro na tabela, e percorrer os registros válidos apenas uma vez. Embora pudesse ser criada uma tabela temporária baseada em uma condição de filtro, e depois fazer o browse diretamente nesta tabela, caso o filtro fosse pouco restritivo, demoraria mais para criar a tabela temporária do que para fazer um índice filtrado, além de consumir muito espaço em disco.

Usando com uma tabela do DBAccess

A utilização da IndRegua no DBAccess, em uma tabela acessada pela RDD TOPCONN, não haverá a criação física explícita pela aplicação de um índice temporário. Na verdade, as condições de filtro e ordenação serão traduzidas pelo motor de navegação do DBAccess, e repassados ao SGBD durante a leitura dos dados. Logo, elimina-se logo de cara o tempo de espera pela criação explícita de um índice físico.

Porém, como estamos na prática submetendo por baixo queries no SGBD, o motor de execução SQL do Banco de Dados em questão vai “se virar” para selecionar os dados nas condições solicitadas, levando em conta as condições de filtro e a ordenação informada. Esta é uma prerrogativa do SDBD, onde ele avalia se é possível aproveitar um índice já existente no SGBD para selecionar os registros que atendem a condição de filtro informada. Caso não seja possível fazer isso, o SGBD vai dar um jeito de resolver a query, mas seu desempenho pode ser muito prejudicado de acordo com o tamanho da tabela. Naturalmente esta operação será muito bem executada pelo SGBD, caso exista algum índice no banco de dados que contemple todos os campos utilizados nas condições de filtro ou seleção de registros desejados.

Este comportamento do banco justifica por que determinadas condições de filtro, quando acrescentadas pelo usuário em um Browse do ERP padrão ficam muito lentas. Existem contornos operacionais para estes tipos de casos, onde normalmente um DBA utiliza um mecanismo de monitoramento de desempenho do próprio SGBD, para elencar as queries menos performáticas, onde normalmente a inclusão de um índice específico no SGDB para favorecer o processamento da query resolve a questão de desempenho.

Filtro ou Query

Nada impede que você faça uma Query para recuperar os dados que você deseja do banco, já colocando nela apenas as colunas desejadas, JOINS quando necessários, colocando as condições e ordenação desejadas. Vale a mesma regra do filtro: Caso exista um índice no SGBD que contemple os campos usados nas condições de seleção de registros (Where da query), mais fácil será para o SGBD processar e retornar os dados.

Uma Query vai ser naturalmente mais rápida que o filtro, pois ela será submetida apenas uma vez ao SGBD, enquanto um filtro vai inferir condições no motor de navegação de registros do DBAccess, que sob demanda vai submeter várias queries para identificar os blocos de dados a serem recuperados. Porém, esta diferença não é tão gritante. A engine de ISAM emulada é bem eficiente, e em muitos casos um filtro atende melhor às condições do Programa. Não é viável colocar um alias de uma Query, que somente permite movimentação para os próxios registros dentro de um Browse de visualização de interface. Dependendo do caso, alguns programas lêem o resultado da query e criam um arquivo temporário no disco, e isto têm o custo da leitura dos dados pela rede, e posterior envio de cada registro para gravação em disco. Mas isto é assunto para outro capítulo … eheheh ….

Boas práticas

Nas condições de filtro, você consegue “ajudar” o motor de queries do SGBD, quando você previamente identifica um índice no banco que contenha todos ou a maioria dos campos que você vai selecionar os dados, e especifica todas as condições de filtro dos dados na ordem de campos do índice. Por exemplo, se você vai recuperar apenas os dados sob condições que usam os campos CPO1, CPO2 e CPO3, e você têm um índice no seu SGBD cuja chave seja “CPO1,CPO3,CPO2”, você ajuda o banco na hora de escrever a sua query informando as condições nesta ordem: “CPO1 >= ’01’ AND CPO3 = ‘001’ AND CPO2 between ’01’ and ’05′”

Outra coisa importante, tanto em filtros como em Queries, é evitar ao máximo usar condições que concatenam valores de campos. Por exemplo, você tem uma variável na memória do Advpl, que contem a concatenação de dois campos, CPO2 e CPO3. Logo, ao escrever a query, você faz algo assim:

cQuery := "SELECT CPO1,CPO2,CPO3,CPO4,CPO5 FROM TABELA WHERE "
cQuery += "CPO1 = '01'"
cQuery += " AND (CPO2 || CPO3) = '"+cChave+"'"

Isto obriga o SGBD a criar internamente uma coluna calculada para então validar o critério de comparação. Para o SGBD, é MUITO mais leve você quebrar as condições:

cQuery := "SELECT CPO1,CPO2,CPO3,CPO4,CPO5 FROM TABELA WHERE "
cQuery += "CPO1 = '01'"
cQuery += " AND CPO2 = '"+left(cChave,2)+"'"
 cQuery += " AND CPO3 = '"+substr(cChave,3)+"'"

Na prática, uma IndRegua() em uma tabela do DBAccess possui basicamente um comportamento de filtro, permitindo especificar uma ordenação diferenciada. Logo, todas as regras de performance aplicáveis a um filtro de IndRegua() também se aplicam a um filtro definido pelo comando SET FILTER TO — ou pela função DbSetFilter().

Lembrando de um ponto importante do filtro: Por questões de compatibilidade, um filtro AdvPL definido via SET FILTER ou DbSetFilter() pode conter expressões AdvPL, incluindo operadores e funções não suportadas pelo SGDB. Porém não existe mágica, o Application Server manda o filtro pro DBAccess, e ele remonta o filtro considerando apenas as condições que o SGDB é capaz de resolver, e cada registro retornado pelo DBAccess ao Appllication Server é revalidado, e caso não seja válido pela expressão de filtro re-ececutada no AdvPL, o registro é ignorado e o próximo registro é solicitado, até que seja retornado um registro válido.

O pior cenário de desempenho e consumo de recursos é um filtro muito restritivo em uma tabela muito grande, pois muitos registros podem ser trafegados desnecessariamente ao Application Server até que os registros que atendem realmente a condição de filtro sejam percorridos pelo programa. Logo, é extremamente recomendável que o seu filtro tenha o menor número possivel de instruções que não possam ser processadas no SGDB. Quanto mais simples, melhor. Na documentação de referências no final deste artigo, a documentação da função DbSetFilter() aborda em detalhes vários destes aspectos.

Conclusão

Este artigo terá continuações, para abordar outros aspectos de acesso a dados, mas já é um bom começo. Ainda existem muitos espaços em branco para serem preenchidos com mais informações, espero ter ajudado de alguma forma. Acompanhem os próximos posts, têm muita coisa boa pra ser publicada 😀

Abraços e até o próximo post, pessoal 😉

Referências

http://tdn.totvs.com/pages/releaseview.action?pageId=6814909
http://tdn.totvs.com/display/tec/SET+FILTER+TO
http://tdn.totvs.com/display/tec/DBSetFilter