Mostrando postagens com marcador otimização. Mostrar todas as postagens
Mostrando postagens com marcador otimização. Mostrar todas as postagens

quinta-feira, 28 de outubro de 2010

Exemplo de desnormalização de banco de dados

Se você já trabalhou a mais de 2 meses em TI já ouviu os termos "normalização de banco de dados" e
"boas práticas". E provavelmente também ouviu a relação das duas coisas: é uma boa prática normalizar um banco de dados.

Não há o que se discutir sobre isso numa primeira instância. Sim, porque uma das técnicas avançadas de tuning é a desnormalização. Aqui vamos a um exemplo prático de como se fazer isso. Mas antes, as ressalvas:

- Aprenda a normalizar um banco de dados de forma decente, nada de fazer um serviço porco e querer dizer que é um padrão avançado inventado no Paquistão.

- Assim como para normalizar, estude o que pode ser desnormalizado. O processo é um método, racional e com suas próprias regras.

O exemplo.

Vamos supor que você cuide de um portal de artigos médicos, que são publicados sempre em 4 línguas (inglês, francês, alemão e espanhol).

Numa forma normalizada, você teria um banco de dados com estrutura semelhante a:

Tabela [autor]
- id
- nome

Tabela [idioma]
- id
- idioma

Tabela [artigo]
- id
- autor_id

Tabela [artigo_contents]
- id
- artigo_id
- idioma_id
- conteudo

Como vemos acima, temos uma tabela para idioma, uma para artigo com informações gerais, e uma tabela com o conteúdo dos artigos, indexada ao artigo e ao idioma. Essa estrutura facilita a organização dos conceitos uma vez que está normalizada.

Agora como saber se essa estrutura poderia ser otimizada e desnormalizada? Bem, poderíamos ter o conteúdo na própria tabela artigos. Antes de prosseguir com a idéia, precisamos responder a algumas perguntas:

- o número de idiomas vai/pode mudar? quais as chances disso acontecer?
- pode ser que um artigo seja publicado num número parcial de línguas, por exemplo só em inglês e alemão?


A resposta a estas perguntas é que determina se a atual estrutura do banco de dados pode ser desnormalizada ou não.

Se o número de línguas NÃO vai mudar ao longo do tempo, ou a chance disto é extremamente remota E os artigos serão todos sempre publicados em todas as línguas, então podemos desnormalizar o banco de dados, movendo o conteúdo para a tabela artigos.

A nova estrutura seria semelhante a:

Tabela [autor]
- id
- nome

Tabela [artigo]
- id
- autor_id
- conteudo_en
- conteudo_de
- conteudo_fr
- conteudo_es

Muito mais simples. A vantagem explícita desse tipo de operação é a simplicidade na hora de escrever e rodar as queries.

Antes deveria ser algo como:

SELECT c.conteudo, at.nome FROM artigo_conteudo c, autor a WHERE a.id = artigo.autor_id AND ac.artigo_id = artigo.id AND ac.idioma_id = '2' AND artigo.id = 27;

Agora se traduz em algo como:

SELECT a.conteudo_fr, at.nome FROM artigo a, autor at WHERE at.id = a.autor_id AND artigo.id = 27;


Reduzindo o número de joins e tudo o mais. Claro, vai ser necessário refatorar parte dos códigos, mas pode valer a pena. O ideal é desnormalizar ANTES de criar o código, na fase de design da solução. Assim o custo de refactor é 0.
----------- keepReading

sábado, 9 de outubro de 2010

Turbinando suas buscas internas

O problema

Quase sempre vemos um formulário de busca interna em sites que publicam conteúdos com frequencia. Este formulário, assim como a navegação por categorias ou por tags, ajuda muito os visitantes a encontrarem o que procuram. Mas desenvolvedores iniciantes - ou nem tão iniciantes assim - ainda implementam esse mecanismo de busca de uma forma "crua".

Geralmente é um simples formulário, com método GET, um campo de texto e um botão de OK. Até aí, sem problemas. No front end é isso mesmo. Mas no back end, geralmente a implementação que vemos é algo assim:


<?php
$terms = explode(' ', $_GET);
$query = "SELECT * FROM table WHERE conteudo LIKE ";

foreach ($terms as $term) {
$query .= "%" . $term . "% OR LIKE";
}
... // roda a query, pega resultados, mostra
?>


Pois bem, esta abordagem, apesar de funcional, não é elegante, nem tem a precisão desejável. Se você buscar por "um cone" pode encontrar "a coleção de ícones do Humberto". Isso sem falar da performance das queries, que é seriamente afetada quanto mais registros no banco e mais termos são procurados.

A solução? Um recurso que os principais RDBMS tem chamado Full-Text Search. Vou ensinar aqui como implementar e usar estes recursos na prática (no MySQL e no PostgreSQL).

MySQL

NOTA: o recurso de Full-Text Search só está disponível para tabelas MyISAM.
Vamos supor que você tenha uma tabela de posts ou artigos com a estrutura parecida com a seguinte:

id
titulo
intro
conteudo
autor
tags
publishdate

Em primeiro lugar vamos precisar adicionar um índice, que é composto pelos campos que desejamos que sejam buscados. Rode isso pelo seu programa de administração do MySQL (MySQL Admin, phpMyAdmin).

ALTER TABLE posts ADD FULLTEXT (titulo, intro, conteudo, tags);


Depois disso, basta reescrever a query no PHP:

SELECT * FROM posts
WHERE MATCH (titulo, intro, conteudo, tags)
AGAINST (". $_GET .");


A busca será mais rápida e os resultados com menos falsos positivos. Sem contar que o código fica mais limpo e elegante.

PostgreSQL

O PostgreSQL dá um pouco mais de trabalho para fazer a implementação.

Pelo seu programa de administração, rode as seguintes queries.

ALTER TABLE posts ADD COLUMN postsfts tsvector;

UPDATE posts SET postsfts =
setweight(to_tsvector('pg_catalog.portuguese', coalesce(titulo,'')), 'A') ||
setweight(to_tsvector('pg_catalog.portuguese', coalesce(intro,'')), 'B') ||
setweight(to_tsvector('pg_catalog.portuguese', coalesce(conteudo,'')), 'C');

CREATE INDEX postsfts_idx
ON posts
USING gin(postsfts);

CREATE TRIGGER postsfts_tg
BEFORE INSERT OR UPDATE ON posts
FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger('postsfts', 'pg_catalog.portuguese', 'titlulo', 'intro', 'conteudo');


Entendendo o que foi feito.
A primeira query adiciona um outro campo à tabela, onde o vetor de busca ficará armazenado (isso aumenta de forma significativa a performance).
A segunda query define quais campos farão parte do full-text search. Note que no PostgreSQL você pode definir o peso de cada campo - A, B, C ou D, sendo A o campo mais importante, D o menos importante. Ah, e claro definimos a linguagem para português.
Na terceira query, construímos um index sobre o campo que acabamos de criar.
Por último, criamos um trigger que vai atualizar o campo que criamos, sempre que o o conteúdo da tabela for atualizado.

Com isto feito, podemos transformar as queries no PHP para:

SELECT * FROM posts, to_tsquery(' . $_GET . ') query
WHERE query @@ postsfts;


No PostgreSQL o operador "@@" funciona como o MATCH AGAINST do MySQL. E precisamos converter o que vai ser buscado através da função to_tsquery().

Qual é o mais relevante?

Os recursos de full-text search permitem ir um pouco mais além e definir um ranking sobre quais registros são mais relevantes sobre os termos buscados. No PostgreSQL isso pode ser feito com uma query similar à seguinte:


SELECT *, ts_rank_cd(postsfts, query) AS rank
FROM posts, to_tsquery(' . $_GET . ') query
WHERE query @@ postsfts
ORDER BY rank DESC LIMIT 5;


Aqui temos a função ts_rank_cd que faz essa função do ranking (além de já termos setado os pesos dos campos antes). E vocês podem notar que estamos ordenando pelo ranking. Ou seja, essa query trás os 5 resultados mais relevantes.

Para obter o mesmo efeito no MySQL podemos fazer a query da seguinte forma:

SELECT *, MATCH (titulo, intro, conteudo, tags) AGAINST (". $_GET .") AS rank
FROM posts
WHERE MATCH (titulo, intro, conteudo, tags) AGAINST (". $_GET .")
ORDER BY rank DESC LIMIT 5;


Conclusão

Espero ter criado um guia rápido e prático para que vocês possam aplicar o full-text search em seus próximos trabalhos e assim obter melhores resultados. Vocês codificam mais rápido, o sistema ganha em performance, e os usuários agradecem os resultados mais precisos.

Abraços e keep readin'

----------- keepReading

sábado, 17 de julho de 2010

Exemplo prático de normalização de banco de dados

Este post pretende ser muito menos teórico e muito mais prático sobre um tema que todo desenvolvedor já leu e, provavelmente, procurou saber mais. Normalização de bancos de dados.

O por que?

Claro, antes de meter a mão na massa, a motivação do post. Já vi em muitos foruns perguntas cuja resposta pode ser dada por um design apropriado do banco de dados, feitas tanto por quem está começando, quanto por quem já tem algum tempo de estrada.

Você irá precisar de normalizar os dados em duas situações principais: quando um campo do banco será variável como um vetor, ou quando julgar necessário manter informações devidamente separadas.

Exemplo 1. Vendas e nota fiscal.
Considere a seguinte tabela que armazena as vendas de uma papelaria.

tabelaVendas - desnormalizada
idVendaidClienteitens
113 cadernos, 5 lápis, 1 mochila
2310 lápis, 2 borrachas, 300 folhas de sulfite
372 mochilas, 4 borrachas

Como podemos ver, o campo itens armazena uma quantidade variável de informação. Vamos aplicar a normalização aí na primeira forma normal, ou 1NF.

tabelaVendas - quase 1NF
idVendaidClienteitem 1Qtd 1item 2Qtd 2item 3Qtd 3
11caderno3lápis5mochila1
23lápis10borracha2sulfite300
37mochila2borracha4

OK, separamos as colunas para ter um e apenas um tipo de dado, mas ainda assim podemos ver que o design dessa tabela ainda é bem falho. E se um consumidor quiser comprar mais de 4 itens na mesma compra? mesmo que vc crie 200 campos, certamente haverá um ou outro caso onde esse limite será um problema, sem contar nas desvantagens óbvias de tamanho da tabela, espaço ocupado (com informações nulas), e no trabalho para criar queries e relatórios em cima de uma tabela assim. Mas esta ainda não é a versão terminada da 1NF. Podemos fazer um design um pouco melhor para obter isso.

tabelaVendas - 1NF
idVendaidClientelinhaDaVendaItemQtd
111caderno3
112lápis5
113mochila1
231lápis10
232borracha2
233sulfite300
371mochila2
372borracha4

Pronto, chegamos assim à primeira forma normal. Sem valores multiplos dentro de um campos, e sem multiplos campos para a mesma função.

Os mais atentos devem ter percebido um pequeno "problema" na tabela acima. Não há nenhum campo que funcione bem como chave-primária. A chave-primária para se obter uma linha é a combinação das colunas idVenda e linhaDaVenda.

Para resolver isso aplicamos a segunda forma normal, 2NF, que diz que cada linha tem uma chave-primária representada por um e apenas um campo.

Considere a tabela anterior com um pouco mais de detalhes:
tabelaVendas2 - 1NF
idVendaidClienteDatalinhaDaVendaQtdidProdutodescProduto
1112/7/2010131caderno
1112/7/2010252lápis
1112/7/2010313mochila
2313/7/20101102lápis
2313/7/2010224borracha
2313/7/201033005sulfite
3715/7/2010123mochila
3715/7/2010244borracha

Vamos separar o que pertence a cada venda, do que pertence aos items de cada venda em duas tabelas.

tabelaVendas2 - 2NF
idVendaidClienteData
1112/7/2010
1112/7/2010
1112/7/2010
2313/7/2010
2313/7/2010
2313/7/2010
3715/7/2010
3715/7/2010

tabelaDetalheVenda - 2NF
idVendalinhaDaVendaQtdidProdutodescProduto
1131caderno
1252lápis
1313mochila
21102lápis
2224borracha
233005sulfite
3123mochila
3244borracha

Note que as duas tabelas estão agora relacionadas pelo campo idVenda, que é chave-primária da primeira tabela e chave-estrangeira na segunda.

Estamos agora a um passo da terminar a normalização destas entidades (geralmente se aplica a normalização até sua terceira forma normal. Dificilmente formas de normalização superiores se fazem necessárias). Vamos separar os dados dos produtos da tabela de detalhes de venda (Note que a tabela de vendas já está completamente normalizada).

tabelaDetalheVenda - 3NF
idVendalinhaDaVendaQtdidProduto
1131
1252
1313
21102
2224
233005
3123
3244

tabelaProdutos - 3NF
idProdutodescricaoProduto
1caderno
2lápis
3mochila
4borracha
5sulfite

Pronto. Normalizado.
Se você tem interesse vale a pena também dar uma lida em como desnormalizar (de forma ordenada) um banco de dados, que também é conhecido como formas 4N ou superiores.
----------- keepReading

segunda-feira, 12 de julho de 2010

Testes de Software - Tipos de teste

Falar em testes de software pode ser um tanto subjetivo. Para evitar essa abertura de interpretação, até mesmo porque é um serviço que exige rigor, vamos ver os tipos de teste. Quais são, quais as finalidades e como avaliá-los no processo.

Existem vários tipos de teste, cada um para avaliar uma determinada característica da aplicação.

Testes funcionais - estes são os testes que servem para determinar se a aplicação faz o que se propõe a fazer. Comecei falando deste tipo, porque certamente é o primeiro teste que se passa na cabeça dos programadores.

Como exemplo simples para teste de um sistema de login:
UserPassResultado esperado
válidoválidoLogin bem sucedido
inválidoválidoMensagem de erro
válidoinválidoMensagem de erro

Este é o exemplo mais básico, só para ilustrar. Como veremos adiante, existem outros cenários e casos de uso que aumentam a complexidade da bateria de testes.

Testes de performance - nem sempre este tipo de teste se faz necessário. Eles avaliam o tempo de resposta, ou quantidade de memória usada, ou algum outro parâmetro crítico em função da variação da quantidade de inputs ou requisições. São os famosos benchmarkings e variantes. Veremos mais detalhes num post futuro.

Testes de segurança - Há muito o que se discutir aqui. O que é hacking, o que não é, quais as técnicas, quais as medidas de proteção, como considerar algo como seguro... Responder essas questões básicas que servem de base para os testes é que são o maior desafio, e motivo de discussão.

Teste unitário - quando se procura alguma ferramenta ou alguma informação sobre testes em alguma linguagem específica, isto é geralmente o que se encontra. Testes unitários são testes realizados para cobrir um módulo isolado, uma função, ou um conjunto de funções intimamente relacionadas. São similares aos testes funcionais, só que aqueles podem ser extensos, enquanto estes são direcionados e geralmente curtos. Por serem testes simples, rápidos, e com foco na parte e não no todo, eles são muito importantes, e boa parte dos erros são encontrados através deles.

Teste de requisito - pode soar estranho para quem nunca trabalhou com QA antes, mas aqui está o teste mais fundamental de todos, seja qual for o modelo de desenvolvimento ou aplicação desenvolvida. Testar os requisitos é EXTREMAMENTE IMPORTANTE porque:
1. Evita interpretações dúbias tanto da parte dos desenvolvedores, quanto de quem testa;
2. Decorrente da afirmação acima, facilita o trabalho (e diminui o retrabalho) dos arquitetos, designers e implementadores;
3. Apesar de aumentar o tempo necessário inicial dos trabalhos, o ciclo completo de desenvolvimento é reduzido, reduzindo custos do projeto;
4. Evita a criação de testes desnecessários, ou com expectativa falsa, não só reduzindo o trabalho de testes como também aumentando a produtividade (erros encontrados x numero de testes) em quality assurance.

Revisão de código - tipo de teste feito pelos desenvolvedores. É precedente ao Unit Test, e se faz com a inspeção criteriosa do código. Vale a ressalva que nas metodologias ágeis, onde se programa em dupla, esse tipo de teste é mais comumente feita. Um faz um trecho do código, e o outro revisa.

Teste de cobertura de código - Primo do Unit Test, este tipo de teste visa garantir que todos os possíveis caminhos dentro do código sejam acessados. A principal diferença entre este e o Unit Test, é que este é mais abrangente.

Teste de atomicidade - pouco conhecido, este tipo de teste visa garantir a tipagem de dados. Uma variante deste tipo de teste mais difundida é a Fault Injection Test, onde se introduzem inputs não esperados, geralmente usando caracteres de controle mesclados os inputs normais, e observando o comportamento do sistema.

Teste de integração - testa a integração entre módulos e componentes para checar sua compatibilidade e eventual quebra no fluxo do processo.

Teste de recuperação de falha - tipo de teste feito geralmente em aplicações que funcionam como servidores (servidores HTTP, servidores de bancos de dados), sistemas operacionais e sistemas de arquivos. Neste tipo de teste, o sistema é forçado a falhar de alguma forma, e depois se verifica como ele se recupera de um erro grave, se há perdas de ddos e afins.

Testes de regressão - em softwares que são desenvolvidos em ciclos, esta é uma bateria que é feita ao finaldo ciclo funcional, e onde as funcionalidades anteriores que não foram modificadas são postas à prova, afim de garantir que nenhuma funcionalidade antiga foi alterada.

Teste de instalação - para software que serão distribuídos, este é o teste onde se verifica se a instalação será bem sucedida nas plataformas-alvo.

Testes estáticos - são os testes onde se observam o estado do sistema. Inspeções de códigos, e estabilidade inicial pertencem à esta classe de testes.

Testes dinâmicos - são feitos colocando o sistema para executar, com entradas, e então parando a execução e observando o estado das variáveis. Quem usa o Firebug para depurar javascript, ou o gdb para depurar um código em C++ sabe do que se trata.

Testes manuais - são feitos um a um, com um usuário criando dados e fazendo sua inserção no sistema.

Testes automatizados - é quando se usa algum tipo de ferramenta para auxiliar na execução de testes, agilizando o processo. Testes de performance, segurança, carga, estabilidade e regressão são alguns tipos de teste onde vale a pena usar tecnologias que automatizem o processo de teste. Vale notar que o que garante a qualidade deste tipo de teste (onde milhares, às vezes milhões de testes são rodados de uma única vez) é a massa de dados utilizada no input. Se ela for inválida ou inconsistente, a bateria de nada vale.

Fora do cunho técnico e relacionado com a criação de códigos propriamente ditos, ainda temos outros tipos de teste que podem ser feitos.

Testes de acessibilidade - verifica se o programa é acessível sem o uso de mouse, de tecnologias opcionais (addons), avalia a disposição dos elementos, contraste de cores, tamanho e legibilidade de fontes...

Testes de usabilidade - Verifica se o software é de fácil utilização. É um tipo de teste que quando bem feito e bem trabalhado, pode significar a diferença entre o sucesso e o fracasso de uma aplicação. Card sorting, eye tracking, mouse tracking e simple task são alguns dos tipos de teste de usabilidade.

Testes de internacionalização - verifica se a aplicação se comporta de forma adequada em termos de linguagens e seus padrões (padrão para o formato da data, da hora, da moeda, separador decimal, direção do texto).

Teste de documentação - no caso de produtos voltados à indústria, geralmente se faz o teste da documentação afim de avaliar a qualidade em termos de utilidade da mesma (o usuário consegue as instruções necessárias para efetuar alguma tarefa na documentação?). Alguns sites fazem o teste de suas FAQs.

Se você nunca leu nada antes a respeito sobre Quality Assurance deve ter se assustado com a quantidade de tópicos que temos dentro da área. Nos posts seguintes, vou procurar explicar o que é e como se faz cada tipo de teste, mas lembre-se que assim como segurança, qualidade é algo subjetivo, e devem ser considerados quais são os aspectos mais importantes para dimensionar corretamente o tempo e a verba empregadas nisso.

Se você vai fazer um site com estimativa de visitação máxima de 15 usuários logados simultaneamente, provavelmente não precisa de um ciclo de performance, ao passo que se você está desenvolvendo um software de CAD/CAE/CAM ou modelagem 3D com ray tracer, análises de performance serão tão importantes quanto testes funcionais.

Bom pessoal, é isso. Até a próxima.
----------- keepReading

segunda-feira, 7 de junho de 2010

Tipo ENUM no MySQL e no PostgreSQL

Tempos atrás eu publiquei aqui, sobre os tipos ENUM e SET no MySQL, porém as respostas dos leitores deixaram claro que eu não havia explicado o assunto direito. E relendo o texto, realmente assumo que ficou péssimo. Não consegui passar o conceito, quanto menos a aplicabilidade desses recursos.

Então aqui cabe a redenção. Vamos ao tipo ENUM. Ele não é um tipo definido no padrão SQL, e sim implementações proprietárias, encontrado nos bancos de dados mais comuns. Assim seu comportamento, espaço em disco e queries que podem ser usadas diferem ligeiramente entre suas implementações, mas a idéia por trás do tipo ENUM é comum a todos.

As vantagens do uso do tipo ENUM são:
  • ganho de performance
  • segurança dos dados

As desvantagens são:
  • limites de uso (a alteração requer a reestruturação da tabela)
  • a não portabilidade devido as diferenças de implementação

A principal característica do tipo ENUM é permitir a criação de uma lista enumerada, sendo cada registro um par index-value. A idéia é bem simples mesmo, mas extremamente eficaz e poderosa. O uso do tipo ENUM deve ser feito em parâmetros onde há pouca ou nenhuma alteração durante a existência da aplicação - por exemplo, sexo, ou um sistema de rating com valores fixos de entrada.

CREATE TABLE 'myPics' (
'id' INT NOT NULL AUTO_INCREMENT ,
'url' VARCHAR( 255 ) NOT NULL ,
'rate' ENUM('bad', 'good', 'excellent') NULL DEFAULT 'good',
PRIMARY KEY ('id') ,
INDEX ('url')
) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_bin;

Na tabela de exemplo acima, criamos o campo rate, que deve armazenar a classificação da foto. Os possíveis valores são: NULL, 'bad', 'good' e 'excellent'. Ou seja você tem valores pré-definidos para o input de dados, e a cada um é atribuído um índice na ordem em que foram declarados. Os possíveis valores para o nosso campo são:
valor índice
NULL NULL
'' 0
bad 1
good 2
excellent 3

Na hora da query uma possível construção seria:

SELECT name, rate FROM myPics WHERE rate >= 'good';

Isso demonstra a utilidade do tipo ENUM. Ao invés de criar uma tabela auxiliar e atribuir valores a ela, podemos reduzir essa complexidade fazendo uso do tipo ENUM em nossa arquitetura. Ah sim, qualquer função que demande tipos numéricos (SUM(), AVG(), MIN(), MAX(), COUNT()....) podem ser usadas em cima dos campos ENUM. Nesse caso, os valores são primeiro transformados no seu índice numérico.

E vale mencionar mais uma vez que o índice atribuído depende da ordem da declaração dos possíveis valores. Se você declarar ENUM ('bom', 'ruim'), 'bom' terá um valor menor do que 'ruim', e se declarar ENUM ('ruim', 'bom'), 'ruim' terá um valor menor do que 'bom'.

No Postgresql o uso do tipo enum é ligeiramente diferente. O trecho a seguir ilustra o mesmo exemplo anterior, só no que postgre.

CREATE TYPE rating AS ENUM ('bad', 'good', 'excellent');
CREATE TABLE myPics (
id serial,
url varchar(255),
rate rating
);

É necessário criar um tipo de dado com nome próprio do tipo ENUM, e depois atribuí-lo à coluna. Na hora de utilizar, nos beneficiamos dos mesmos recursos de construção.

Agora sobre o tipo SET. O tipo SET só existe no MySQL. E uma possível aplicação para ilustrar sua utilidade seria um sistema de tags, implementado de forma nativa na tabela.

Considere a seguinte tabela:

CREATE TABLE 'myPics' (
'id' INT NOT NULL AUTO_INCREMENT ,
'url' VARCHAR( 255 ) NOT NULL ,
'rate' ENUM('bad', 'good', 'excellent') NULL,
'tags' SET('outdoor', 'studio', 'stock', 'fruits', 'nature', 'close-up', 'people') NULL
PRIMARY KEY ('id') ,
INDEX ('url')
) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_bin

Nela, podemos atribuir valores como:

INSERT INTO myPics (id, url, rate, tags) VALUES (NULL, 'xxx', 'good', 'people, close-up'), (NULL, 'yyy', 'excellent', 'fruits, nature, close-up');

Ou seja declarando as tags direto no insert. Na hora de fazer o SELECT, contamos com mais uma função nativa do MySQL bem útil, a FIND_IN_SET:

SELECT * from myPics WHERE FIND_IN_SET('nature', tags) > 0 AND FIND_IN_SET('close-up', tags) > 0;

Isso vai trazer todos os registros cujo campo tags contenha os valores 'nature' e 'close-up'.

Bom, acho que agora o conceito e a aplicação dos tipos ENUM e SET está agora mais clara e mais útil.

-----------
keepReading

sábado, 16 de agosto de 2008

IFS - Internal Field Separator

No post anterior, eu disse que o script reconhecia nomes de arquivos que incluiam espaços e outros caracteres especiais, mas não escrevi sobre como fiz isso. Aliás, este foi um dos passos que mais me deu dor de cabeça durante o desenvolvimento do script.

Voltando um pouco na causa... minha esposa e eu temos o costume de renomear em massa os arquivos e as cópias que ela faz, usando espaços nos nomes, tanto dos arquivos quanto dos diretórios. Porém quando fui criar o script me deparei com o problema. A saída do comando find. Sempre que havia um espaço em branco nos nomes de diretórios e arquivos, era como se fosse um valor diferente para o loop for.

Após muita pesquisa, vi que o problema era uma variável especial do shell, o IFS. Ela é uma variável criada pelo shell (builtin) e é extensivamente usada para reconhecimento e expansão de strings. Seu valor padrão é "", e ela afeta o comportamento de vários outros comandos, como grep, cut, ls... Assim, "o pulo do gato" foi definir o valor dessa variável para "".

Bem, além de mais elegante e eficiente, esse tipo de tranformação ajuda a diminuir o tempo de execução, o que pode tornar o uso racional desse recurso muito interessante quando temos de lidar com grandes quantidades de dados, por exemplo importação/exportação de valores em bancos de dados.

Agora cabe algumas observações sobre o uso do IFS.
- É recomendável que você guarde o valor antigo e retorne ele ao normal no final do script.
- Definir o novo valor de forma direta funciona apenas para a atual instância do shell onde se trabalha. Se você cria subshells, você deve usar o comando (também builtin) export.
- Essa é uma variável existente para vários sabores de shell (sh, dash, bash, ksh, zsh).

E para fechar, dois links para aprofundar a leitura:
- http://www.dicas-l.com.br/cantinhodoshell/cantinhodoshell_20070103.php
- http://nixshell.wordpress.com/2007/09/26/ifs-internal-field-separator/

See ya.
-----------
keepReading

quarta-feira, 6 de agosto de 2008

Um pouquinho de velocidade

Fala pessoal.

Como sempre venho trazer alguma coisa para vocês, baseada em erros comuns que vejo. E hoje trago algumas dicas de como melhorar o desempenho em bancos de dados.

Tempos atrás eu já tinha publicado algo sobre isso em relação ao Postgre aqui.

Bem, negócio seguinte, quando tratamos com uma quantidade grande de dados (1GB ou mais), algumas mudanças podem e devem ser feitas para acelerar o andamento das atividades (importar, exportar, selecionar, filtrar, pesquisar, etc...).

Primeiro em relaçãoà modelagem de dados. Veja se a estrutura não precisa de modificações, como por exemplo partições nas tabelas, índices, etc...

Depois, veja se a tabela está em dia. Um ANALYZE TABLE faz milagres. Agendar ele no CRON para rodar todo dia num horário de pouco uso, nem se fala.

Se tem uma opção mais eficiente que essa, é o OPTIMIZE TABLE do MySQL.

Para importar/exportar quantidades muito grandes de informação, evite usar alguma linguagem de programação. Faça pela linha de comando, usando recursos do próprio banco de dados. Prepare scripts para automatizar estas tarefas. Se for necessária alguma verificação, faça com stored procedures. Isso aumenta absurdamente a performance.

Outra coisa, em caso de queries complexas, use o EXPLAIN. Aprenda a otimizar suas queries com ele (a ordem dos joins importa, assim como a sua quantidade e o método usado).

Por último, nos casos mais críticos, verifique a condição física da máquina. Se a operação que está sendo executada começa a usar espaço em disco (swap/arquivo de paginação) é hora de rever alguns parâmetros do banco de dados, do sistema operacional ou mesmo instalar mais memória.

[]'s

-----------
keepReading

domingo, 29 de junho de 2008

Uma conversa sobre boas práticas de desenvolvimento

Quando começamos a desenvolver, a principal meta é obter uma aplicação que funcione. E com o tempo vamos melhorando nossas práticas, facilitando e polindo nosso código - claro que estou me referindo aos verdadeiros profissionais.

Mas mesmo assim, na enorme maioria das vezes os desenvolvedores não vão tão a fundo sobre o funcionamento da aplicação, sendo que isto muitas vezes pode ser muito útil. Seja por falta de informação, ou na dificuldade de encontrar e entender a mesma.

Por exemplo, quantos de vocês já pararam para pensar sobre o processo de requisição de uma simples página de internet? Vamos fazer uma pequena viagem neste processo (acredite, isso é a forma simplificada!).

Ao clicar num link para uma página, o seu navegador pede ao sistema operacional que envie um pacote de requisição pela interface de rede. Este pacote contém os request headers, ou seja informações sobre o destino solicitado (o SO é que acrescenta o endereço de origem), informações sobre o navegador, quais linguagens e encodings ele aceita, etc.. Essa solicitação é resolvida num servidor DNS, que converte o nome para um endereço IP, e a saga do pacote continua de roteador em roteador até que o IP da página requisitada seja encontrado. Chegando lá, o SO do servidor, vai direcioná-lo para o servidor HTTP (geralmente o Apache), onde será enfim processado.

O servidor é responsável por localizar qual arquivo está sendo solicitado, checar regras e políticas de segurança junto ao SO, formatá-lo, codíficá-lo se for o caso, e por fim, enviar de volta o arquivo em pacotes, fazendo o caminho inverso. Se o seu navegador, ao reconstruir os pacotes, identificar que existem outros arquivo sendo solicitados na própria página, como folhas de estilo externas (CSS), arquivos javascript, imagens, arquivos flash, ou mesmo frames, ele irá enviar novas solicitações ao servidor para que este envie os arquivos necessários.

Com tudo recebido, o seu navegador monta a página, rederiza o CSS sobre os elementos, e carrega os javascripts para execução.

Bastante coisa não? Vamos deixar um pouco de lado a parte do seu navegador e vamos focar em duas coisas: o processamento do lado do servidor, e o tráfego de rede.

O servidor tem que alocar memória para cada requisição feita. Este processo também demanda processamento, tanto do servidor HTTP, quanto do SO. Ele tem que gerenciar muitas coisas, ainda mais quando se tem várias requisições chegando ao mesmo tempo.

Agora quando uma página dinâmica é solicitada, o trabalho do lado do servidor acaba aumentando bastante. Ao identificar que se trata de uma página PHP ou Python, por exemplo, o servidor HTTP precisa ler o arquivo e passá-lo ao engine responsável pelo processamento. Isso faz com que o SO reserve mais memória e consuma mais processamento, pois há mais um aplicativo participando da bricadeira. Pelo amor de Deus, eu não estou falando para criar apenas páginas estáticas, mas estou dando uma visão geral para que vocês entendam a importância de uma polida no código no sentido de otimização quando forem fazer um sistema.

Bem, vamos lá... temos um SO gerenciando vários processos e alocando memória para o Apache e para o engine do PHP. Até aí nenhum problema, os softwares são bem escritos e as máquinas de hoje dão conta do recado sem precisar fazer muita força. Mas o mundo não é feito só de hello world's, e de scripts para mostrar que horas são em Estocolmo.

Então, não raro o seu script PHP vai precisar acessar um banco de dados ou arquivos. E aí é onde a coisa pode começar a complicar. Quando acessa um banco de dados, o PHP carrega um módulo, com uma API para o banco de dados, e então ele abre uma conexão para o banco de dados. O processo é muito parecido com a requisição de uma página (exceto, geralmente, o que diz respeito a ficar passando de roteador em roteador para encontrar o banco de dados). E o servidor de banco de dados trabalha de forma muito parecida (guardando-se as devidas proporções) com o servidor HTTP. Ele válida a sessão, reserva memória e começa a processar prioridades de acesso - em outras palavras, mais memória alocada, mais processamento consumido.

Aí voltamos ao seu script. Ele se conectou no banco de dados. Hora de enviar as queries. Para cada query enviada, uma nova requisição é enviada ao banco de dados, através da conexão que você abriu (sim, isto é tráfego de rede). Ao receber a requisição, o engine do banco de dados realiza primeiro o que é chamado de execution plan. Ele estima algumas formas de como pode executar sua requisição, e executa aquela que ele julga ser a mais apropriada (com menor custo ao sistema). Isso é assunto pra tese de doutorado, então não vou me ater a explicações profundas sobre isso. Mas só para constar, triggers, rules, reconstrução de índices, validação de constraints e outros recursos também entram nas contas das estimativas do execution plan, daí você imagina o trabalho do servidor de banco de dados ao decodificar cada query ANTES de executá-la.

Executar a query, envolve além de consumo de processamento e memória, acesso a discos, e possivelmente gravação neles (o que nos trás de volta ao SO, que é o responsável por essa parte de acesso e gravação em discos).

Terminada a maratona da execução, o banco de dados retorna o resultado disso para o PHP (através daquela conexão que ele tem, lembra?). Para daí em diante o PHP continuar o seu processamento. Lembre-se que tudo o que foi descrito acima vale para CADA QUERY enviada. Chegando ao fim do script, o PHP fornece o seu produto ao Apache, que o formata, codifica, etc... e envia em gentis pacotes ao cliente através da rede.

Agora, o que aconteceria se enviássemos menos queries? Conseguem ver a vantagem? Este é o motivo pelo qual é muito importante usar de forma eficiente e inteligente os recursos que os servidores de bancos de dados oferecem. Isso deixa os códigos mais elegantes e limpos, o sistema gerlamente fica mais seguro, mais fácil de fazer manutenção, e otimizado (sem contar que dá pra cobrar uma grana a mais pelo serviço mais profissional).

Dou o artigo como encerrado por aqui. Em breve escrevo mais sobre otimização e segurança no lado dos BDs, e no PHP em si.

See ya.
-----------
keepReading

segunda-feira, 7 de janeiro de 2008

O MySQL não checa chaves estrangeiras!

Dando uma lida na documentação recentemente fiquei pasmo ao descobrir que as chaves estrangeiras não servem para absolutamente nada funcional no MySQL.

Sim, você leu direito. As chaves estrangeiras não servem para absolutamente nada no MySQL.

Ao definí-las, elas podem ser usadas como um lembrete, um aviso, etc... Mas os engines, com exceção ao InnoDB, não fazem checagem se o mesmo existe (na verdade eles não checam nem se a tabela onde é feita a referência!).

No InnoDB elas funcionam normalmente, mas nos outros a criação de chaves estrangeiras apenas criam uma coluna. Nem ao menos um índice é criado.

Uma vergonha para um banco com tantas funções, recursos, tipos de dados, extensões, conectores, etc...

A manual em questão é da versão 5.1 do MySQL, mais especificamente na seção 3.6.6.

Isso moçada: fiquem de olho.

-----------
keepReading

domingo, 6 de janeiro de 2008

Dicas de otimização e segurança

Olá, amigos. Depois de muito tempo away deste blog volto a publicar algumas informações coletadas e experimentadas.

Estudei bastante e fiz várias anotações durante os últimos meses. Além deste post se preparem para mais novidades legais em breve.

Neste reinício trago a vocês dicas sobre otimização e segurança. Algumas boas práticas que espero ser de valia.

Uma das decisões mais importantes durante o desenvolvimento de um sistema é onde implementar as regras do negócio. É comum deixar as regras ao encargo do linguagem de programação, mas quero mostrar que o melhor lugar para se fazer isso, sempre que possível, é no banco de dados.

Quando se implementa a lógica do negócio na linguagem de programação algumas coisas aumentam, como o tempo de desenvolvimento, o volume de códigos e a dificuldade em se fazer manutenção. Além de diminuir a portabilidade da aplicação (no caso de uma migração por exemplo).

Isso sem falar no ponto mais crítico: o aumento do tráfego e do processamento.

Vou exemplificar com um cenário comum - Apache 2, PHP 5 e MySQL 5.

Quando se tem a lógica implementada nos scripts/classes PHP, o fluxo segue mais ou menos como na figura:


Explicando... o cliente faz uma requisição ou envia um form. Estas informações são recebidas pelo servidor web que chamará o engine. O engine irá processar as requisições, se conectar com o banco de dados, enviar a(s) query(ies), processar a(s) resposta(s), fechar a conexão com o banco de dados, preparar a saída, repassá-la para o servidor e este por fim enviará para o cliente.

Isso é o processo que acontece geralmente em qualquer aplicação. Mas note que quando se deixa a regra dos negócios para o engine resolver, podem ser necessárias várias conexões e consultas ao banco de dados, e é aí onde o sistema perde performance - e muita.

A meta é diminuir este gargalo e aliviar este ponto crítico (a conversa entre o engine e o banco de dados). Deixando a lógica de dados implementada no banco de dados, o engine que trabalha com ele fará menos conexões, diminuirá a carga de processamento e melhorará o tempo de resposta, além de deixar o sistema mais robusto.

Assim, é altamente recomendável lançar mão de recursos como Views, Triggers, Procedures, Functions, e fazer um bom uso de índices e chaves disponíveis no banco de dados.

Digo isso porque tive uma experiência realmente dura em um sistema onde TODA a lógica dos dados estava implementada com o design pattern MVC. E para retornar um simples relatório com 50 linhas o sistema demorava uns 6 minutos - simplesmente crítico.

O problema é que haviam várias tabelas com relação Muitos-para-Muitos e a cada linha que se queria obter era necessário realizar uma série de queries e filtragens. Posso citar um dos casos onde para cada usuário era feita uma consulta numa tabela de relacionamento que retornava cerca de 8 chaves, que depois seriam pesquisadas em outra tabela, comparados, validados segundo sua data, organizados (aí já tinha apenas uns 2 ou 3 registros que realmente seriam úteis para o relatório), para depois fazer mais queries em duas ou três tabelas.

Ou seja para produzir uma linha, era necessário um volume de tráfego e processamento absurdo, sendo que isso poderia ser facilmente resolvido (ou pelo menos fortemente otimizado) com a criação de uma view e de uma function.

Aqui cabe um adendo: a diferença entre o modelo entidade-relacional dos bancos de dados e do que se pode implementar na programação orientada a objeto se chama impedância, e as boas práticas aqui citadas visam diminuir este problema. Eis um bom artigo aqui: http://www.linhadecodigo.com.br/ArtigoImpressao.aspx?id=70

Então a dica de otimização é essa: implemente a lógica de dados sempre que possível na própria base de dados.

Mas e a linguagem de programação seria útil para quê então?
Ora, para validar os dados enviados, fazer a conexão, enviar e receber dados do servidor, enviar e-mails, montar páginas dinamicamente... as mesmas coisas que antes. O que quero dizer é que ao invés de rechear seus scripts e classes com ifs, elses, switch cases e queries, você deve diminuir isso e usar apenas onde for realmente necessário.

Em relação à segurança, ela aumenta quando se adota esse procedimento e estes cuidados. Além disso você terá mais tempo livre para criar validações nos seus scripts server-side, e contará com mais uma camada de validação - a do servidor de bancos de dados.

É mais econômico para o sistema você checar se, por exemplo, um id existe numa determinada tabela do que fazer uma query select e depois uma comparação no script.

Algo assim: ao invés de fazer
<?
...
$valid_user = 0;
$sql = "SELECT 1 FROM users WHERE id = $posted_id";
$result = mysql_query($sql);
$x = mysql_fetch_assoc($result);
if ($x) {
$valid_user = 1;
}

if ($valid_user == 1) {
$sql_2 = "INSERT INTO...";
...
}
...

?>


Faça assim:
<?
$sql = INSERT INTO table_x VALUES ($x,$y,$z) WHERE $z INTO (SELECT id FROM users);
?>


Ou que você crie uma function no banco de dados para fazer isso.

Até mais.
-----------
keepReading