sábado, novembro 11, 2006

Abstração de SGBDs - Estudo comparativo - Parte 2 Desempenho

Esse post é uma continuação do anterior http://www.especializa.com.br/berardo/2006/11/abstrao-de-sgbds-estudo-comparativo.html. Para saber mais sobre as bibliotecas, visite o link acima.

Estudo comparativo

A partir de agora, apresentaremos um estudo comparativo entre as sete bibliotecas citadas. Não é nosso intuito procurar uma resposta definitiva sobre qual é a melhor, você vai perceber que nos três quesitos que avaliaremos (desempenho, facilidade e portabilidade) nenhuma conseguiu se sobressair completamente. Vale ressaltar aqui que este é um estudo bem inicial, servindo apenas para fomentar a pesquisa antes de tecer qualquer julgamento sobre alguma delas. Sem contar que muitos outros fatores importantes não estão sendo avaliados.

Desempenho
Este certamente é um fator da mais alta importância. Inspirado no blog de Joseph Scott (http://joseph.randomnetworks.com/), que trouxe um comparativo preliminar apenas com três destas bibliotecas, nosso estudo também é inicial.
Avaliaremos o tempo de processamento do PHP para consultar uma base de dados PostgreSQL de cadastro de atores de cinema. Nela, há 542 atores cadastrados e faremos uma consulta para listar seus nomes em ordem alfabética. Primeiramente, não exibiremos seus nomes e colhemos o resultado, depois exibiremos e também coletaremos o resultado. Uma consulta com essa quantidade de registros não é grande o suficiente, portanto, a repetiremos num laço 250 vezes.
Tomaremos como base um simples script procedural com acesso diretamente através das funções nativas do PostgreSQL. Confira no código abaixo:

Código 1 – Benchmark – PostgreSQL direto
1. 2. $time_start = microtime(true);
3. $db = pg_connect("host=localhost dbname=locadora " .
4. "user=postgres password=postgres");
5.
6. $runs = 250;
7. for($i = 0; $i < $runs; $i++) {
8. $sql = "select nome from atores order by nome";
9. $rs = pg_query($db, $sql);
10. while($row = pg_fetch_row($rs)) {
11. echo $row[0];
12. }
13. }
14.
15. $time_end = microtime(true);
16. $time = $time_end - $time_start;
17. print("\n\nTEMPO: {$time}\n");
18. ?>


O primeiro passo foi recuperar o timestamp atual (em $time_start). Depois conectamos ao banco PostgreSQL. A linha que nos interessa é a 11. Nela, exibiremos o resultado carregado em $row. Ao final das 250 consultas idênticas, recuperaremos o timestamp final e exibiremos a diferença.
Os testes foram realizados primeiramente numa máquina modesta. Foram feitas chamadas ao script diretamente da linha de comando (# php teste1.php), ou seja, sem requerer processamento do Apache e envio da resposta ao browser. Invocamos a execução do script omitindo a linha 11, que imprime os nomes. Apesar de não percebermos variações significativas de resultado, colhemos o valor do tempo mais baixo. Depois executamos mais cinco vezes com a linha omitida anteriormente e também colhemos o resultado mais baixo. Para tornar o teste mais real, efetuamos o mesmo procedimento em outra máquina um pouco mais potente. Confira suas configurações:
Máquina 1: Intel Pentium III, 256Mb RAM, SO Linux Slackware 10.2. PHP 5.1.6
Máquina 2: AMD Sempron 64 2800, 512Mb RAM, SO Linux Slackware 10.2. PHP 5.1.6. Confira os resultados na tabela abaixo (valores arredondados em segundos):

Tabela 1 – Benchmark – Resultados PostgreSQL direto


Máquina 1 - Sem impressãoMáquina 1 - Com impressãoMáquina 2 - Sem impressãoMáquina 2 - Com impressão
2,012687924,23952190,91986592,3729560




Agora veja como faríamos na primeira biblioteca citada, Metabase:

Código 2 – Benchmark – Metabase
1. 2. require("metabase/metabase_interface.php");
3. require("metabase/metabase_database.php");
4. ini_set('include_path',
5. ini_get('include_path').':'.
6. dirname(__FILE__).'/metabase');
7. $time_start = microtime(true);
8.
9. MetabaseSetupDatabaseObject(array(
10. "Type" => "pgsql",
11. "User" => "postgres",
12. "Password" => "postgres"),
13. $conn);
14. MetabaseSetDatabase($conn->database, "locadora");
15.
16. $runs = 250;
17. for($i = 0; $i < $runs; $i++) {
18. $sql = "select nome from atores order by nome";
19. $rs = $conn->Query($sql);
20. $rows = $conn->NumberOfRows($rs);
21. for ($x=0; $x<$rows; $x++) {
22. echo $conn->FetchResult($rs, $x, 0);
23. }
24. }
25.
26. $time_end = microtime(true);
27. $time = $time_end - $time_start;
28. print("\n\nTEMPO: {$time}\n");
29. ?>


Aproveitando para explicar o código, inicialmente, incluímos os arquivos necessários e adicionamos o diretório da biblioteca Metabase extraída (sob o próprio diretório dos testes) na include_path. Só a partir daí o script começou a contar o tempo. A função MetabaseSetupDatabaseObject estabeleceu a conexão com o servidor passado como parâmetro e MetabaseSetDatabase definiu a base de dados . Objeto $conn efetuou a consulta ($conn->Query()) e um recuperou o total de linhas ($conn->NumberOfRows()). Dentro do laço, cada linha foi recuperada através de $conn->FetchResult(). No primeiro teste, a linha 22 não foi totalmente omitida. Se fosse, nenhum dado seria carregado na memória, visto que o laço é um simples for de zero até o total de linhas consultadas. O que fizemos foi testar a linha 22 da seguinte forma:

$resultado = $conn->FetchResult($rs, $x, 0);

Trouxemos o resultado para uma variável em vez de imprimir direto. No segundo teste, a linha 22 ficou como está lá. Veja o resultado. Mantivemos os valores do PostgreSQL direto para efeito comparativo. A última linha mostra quanto em valores percentuais os novos tempos excederam os primeiros (valores arredondados):


Tabela 2 – Benchmark – Resultados Metabase

Máquina 1 - Sem impressãoMáquina 1 - Com impressãoMáquina 2 - Sem impressãoMáquina 2 - Com impressão
2,012687924,23952190,91986592,3729560
5,03319599,83236982,30786905,6365590
150,07%131,92%150,89%137,53%




Percebe-se que o PHP passou a sofrer um pouco para recuperar os mesmos dados. Se antes ele precisava de pouco mais de quatro segundos para efetuar uma operação, agora serão necessários mais do dobro. Dando seqüência aos testes, apresentaremos a ADOdb. Confira:


Código 3 – Benchmark ADOdb
1. 2. require_once("adodb/adodb.inc.php");
3. $time_start = microtime(true);
4.
5. $conn = NewADOConnection(
6. "pgsql://postgres:postgres@localhost/locadora");
7. $ADODB_FETCH_MODE = ADODB_FETCH_NUM;
8. $runs = 250;
9. for($i = 0; $i < $runs; $i++) {
10. $sql = "select nome from atores order by nome";
11. $rs = $conn->Execute($sql);
12. while(!$rs->EOF) {
13. echo $rs->fields[0];
14. $rs->MoveNext();
15. }
16. }
17.
18. $time_end = microtime(true);
19. $time = $time_end - $time_start;
20. print("\n\nTEMPO: {$time}\n");
21. ?>

Assim como na Metabase, fizemos o download da ADOdb e extraímos num diretório abaixo do nosso teste. Agora, apenas o arquivo adodb.inc.php foi necessário. Não precisamos adicionar este subdiretório da biblioteca na include_path.
A função NewADOConnection() entregou a $conn o objeto que utilizaremos para efetuar a consulta. Neste momento, também foi mais fácil passar os parâmetros necessário para ela saber qual banco conectar. Parâmetros na forma de uma única URL, no lugar de uma array de parâmetros e um método adicional para escolher a base, simplifica bem as coisas.
A variável definida na linha 8 foi necessária para informar à biblioteca que traga apenas arrays indexados, já que o padrão é trazer arrays com valores indexados e associativos (algo como MYSQL_BOTH que você já conhece).
Na linha 12, a instrução $conn->Execute() efetuou a consulta e devolveu o identificador do resultado para $rs. O atributo $rs->EOF (end of file) será verdadeiro quando o ponteiro de leitura do resultado alcançar o final do resultado. Já o atributo $rs->fields é um array (apenas indexado devido à variável $ADODB_FETCH_MODE ser ADODB_FETCH_NUM) com os campos da consulta.
A linha 14 foi comentada no primeiro teste e recolocada no segundo. O método $rs->MoveNext() foi o responsável para avançar ao próximo registro. Sem ele, entraríamos em loop infinito.
Da mesma maneira que fizemos com a Metabase, confira os resultados:

Tabela 3 – Benchmark – Resultados ADOdb

Máquina 1 - Sem impressãoMáquina 1 - Com impressãoMáquina 2 - Sem impressãoMáquina 2 - Com impressão
2,012687924,23952190,91986592,3729560
5,189945910,74717402,23551396,1193878
157,86%153,50%143,03%157,88%



Note que o resultado final foi ligeiramente mais lento do que a Metabase, chegando ser mais rápida apenas no terceiro teste. Podemos concluir que elas tiveram tecnicamente o mesmo desempenho, com vitória da Metabase no critério de desempate =D.
A próxima a ser testada será a extensão DBX. Confira seu código:

Código 4 – Benchmark DBX
1. 2. $time_start = microtime(true);
3.
4. $conn = dbx_connect("pgsql", "localhost",
5. "locadora", "postgres", "postgres");
6. $runs = 250;
7. for($i = 0; $i < $runs; $i++) {
8. $sql = "select nome from atores order by nome";
9. $rs = dbx_query($conn, $sql, DBX_RESULT_INDEX);
10. for ($x = 0; $x < $rs->rows; $x++) {
11. echo $rs->data[$x][0];
12. }
13. }
14. $time_end = microtime(true);
15. $time = $time_end - $time_start;
16. print("\n\nTEMPO: {$time}\n");
17. ?>


Por se tratar de uma extensão, não foi necessário incluir arquivo algum ao script. A função dbx_connect(), conectou ao nosso banco recebendo as informações cada uma como um parâmetro (banco, host, base, usuário e senha). A consulta ficou a cargo da função dbx_query(), que assim como a pg_query(), recebeu no primeiro argumento, a variável que representa a conexão e no segundo a instrução SQL. O terceiro, DBX_RESULT_INDEX, foi para informar que o resultado será recuperado num array apenas indexado. O mesmo que fizemos com a ADOdb.
Após a consulta, a variável $rs recebeu um objeto de stdClass com alguns atributos populados. $rs->rows corresponde ao total de linhas da consulta. Em $rs->data todo o conteúdo foi lançado como uma matriz. O que fizemos foi varrer essa matriz da maneira mais eficiente (através de um laço for - utilizar um foreach nesse caso degradaria o desempenho desnecessariamente). A linha 11 foi simplesmente comentada no primeiro teste e refeita no segundo. Confira o resultado:

Tabela 4 – Benchmark – Resultados DBX


Máquina 1 - Sem impressãoMáquina 1 - Com impressãoMáquina 2 - Sem impressãoMáquina 2 - Com impressão
2,012687924,23952190,91986592,3729560
4,79552419,35048991,95202514,8769801
138,26%120,26%112,21%105,52%



Houve um pequeno ganho de desempenho. Uma coisa interessante de se notar é a queda considerável de percentual nos testes com impressão do resultado. A explicação é bem simples. A DBX carregou os dados numa matriz, num processo semelhante à pg_fetch_row() que recuperou o resultado num array a cada iteração do laço. O ato de leitura em ambas foi apenas imprimir a posição de um array indexado. Assim a ação de exibição dos dados na tela levou o mesmo tempo em ambas. Em suma, a diferença está exclusivamente no momento de envio da consulta ao SGBD e recuperação do resultado em variável.

A próxima da lista é a PEAR::DB. Acompanhe:

Código 5 – Benchmark PEAR::DB
1. 2. require_once("PEAR.php");
3. require_once("DB.php");
4. $time_start = microtime(true);
5.
6. $conn = DB::Connect(
7. "pgsql://postgres:postgres@localhost/locadora");
8. $runs = 250;
9. for($i = 0; $i < $runs; $i++) {
10. $sql = "select nome from atores order by nome";
11. $rs = $conn->query($sql);
12. while($row = $rs->fetchRow()) {
13. echo $row[0];
14. }
15. }
16.
17. $time_end = microtime(true);
18. $time = $time_end - $time_start;
19. print("\n\nTEMPO: {$time}\n");
20. ?>


Incluímos, no início, os arquivos necessários. PEAR.php para todo e qualquer biblioteca do PEAR (ela seria incluída indiretamente se não fizéssemos, como nosso foco é o desempenho apenas da consulta, a incluímos logo de cara, antes de zerar os cronômetros) e DB.php específico para a PEAR::DB.
A conexão foi efetuada pela instrução DB::Connect(). DB é uma classe utilitária, seu método estático Connect() retorna um objeto do tipo DB_common. Esta última é uma classe extendida pelas classes particulares de cada SGBD suportado. Assim, no nosso caso, a variável $conn recebeu uma instância de DB_ pgsql (que herda de DB_common) devido à informação do protocolo (pgsql://) na URL de conexão.
A consulta se deu através do método $conn->query(), declarado em DB_common e redefinido em cada uma de suas subclasses. Num processo semelhante ao do PostgreSQL direto, fizemos um while onde uma variável $row recebeu o resultado do método $rs->fetchRow(), que por padrão já retorna um array indexado.
Comentando e descomentando a linha 13, obtivemos os seguintes resultados:

Tabela 5 – Benchmark – Resultados PEAR::DB


Máquina 1 - Sem impressãoMáquina 1 - Com impressãoMáquina 2 - Sem impressãoMáquina 2 - Com impressão
2,012687924,23952190,91986592,3729560
7,681608016,50980813,48861919,4957421
281,66%289,43%279,25%300,17%



Repetimos esses testes exaustivamente por simplesmente não acreditarmos que o resultado pudesse ter sido tão ruim. O mais estranho foi o aumento percentual nos testes com impressão do resultado. É como se houvesse uma alocação excessiva de memória, suficiente até para que uma simples exibição de dados nas variáveis também se tornasse uma operação mais custosa do que no diretamente nas funções de acesso ao PostgreSQL.
Com a sua sucessora PEAR::MDB2 o resultado curiosamente só piorou. Veja:

Código 6 – Benchmark PEAR::MDB2
1. 2. require_once("PEAR.php");
3. require_once("MDB2.php");
4. $time_start = microtime(true);
5.
6. $conn = MDB2::Connect(
7. "pgsql://postgres:postgres@localhost/locadora");
8. $runs = 250;
9. for($i = 0; $i < $runs; $i++) {
10. $sql = "select nome from atores order by nome";
11. $rs = $conn->query($sql);
12. while($row = $rs->fetchRow()) {
13. echo $row[0];
14. }
15. }
16.
17. $time_end = microtime(true);
18. $time = $time_end - $time_start;
19. print("\n\nTEMPO: {$time}\n");
20. ?>


Note que o script foi praticamente o mesmo. Os métodos de $conn e $rs não mudaram. Confira o resultado:

Tabela 6 – Benchmark – Resultados PEAR::MDB2

Máquina 1 - Sem impressãoMáquina 1 - Com impressãoMáquina 2 - Sem impressãoMáquina 2 - Com impressão
2,012687924,23952190,91986592,3729560
9,353699920,75852394,259263011,3491249
364,74%389,64%363,03%378,27%


Tornamos a falar que nosso teste não é conclusivo. Haveria uma série de outras coisas a se avaliar. No entanto, numa operação bastante corriqueira em qualquer sistema (simples listagem de dados de uma base), a diferença entre as demais soluções e as do PEAR foram expressivas.
A próxima biblioteca analisada foi a Creole, veja como foi:

Código 7 – Benchmark Creole
1. 2. require_once("creole/Creole.php");
3. $time_start = microtime(true);
4.
5. $conn = Creole::getConnection(
6. "pgsql://postgres:postgres@localhost/locadora");
7. $stmt = $conn->createStatement();
8. $runs = 250;
9. for($i = 0; $i < $runs; $i++) {
10. $sql = "select nome from atores order by nome";
11. $rs = $stmt->executeQuery($sql, ResultSet::FETCHMODE_NUM);
12. while ($rs->next()) {
13. echo $rs->getString(1);
14. }
15. }
16.
17. $time_end = microtime(true);
18. $time = $time_end - $time_start;
19. print("\n\nTEMPO: {$time}\n");
20. ?>

Assim como em Java, $conn é uma instância de uma classe que implementa a interface Connection (no JDBC seria java.sql.Connection), no caso, PgSQLConnection. A partir de $conn, obtivemos um objeto Statement (PgSQLStatement) que armazenamos em $stmt. Objetos são os responsável por efetuar as consultas.
O método $stmt->executeQuery() recuperou um objeto ResultSet, de onde iremos colher as informações. Seu método $rs->next() avança o ponteiro e retorna verdadeiro se houver próximo registro, falso caso contrário.
Seguir à risca a sintaxe do JDBC não rendeu tão bem, se você trocar o laço while das linhas 12 a 14 pelo laço abaixo, terá um resultado mais eficiente:

foreach ($rs as $row) {
echo $row[0];
}

A variável $rs é do tipo PgSQLResultSet, que por sua vez, implementa a interface ResultSet, como seria em Java. O que a Creole fez de interessante foi declarar que ResultSet herda da interface SPL IteratorAggregate. Assim, PgSQLResultSet implementou os métodos homônimos ao ResultSet do JDBC, mais o método getIterator(). No caso da PgSQLResult, o método retorna uma instância de PgSQLResultSetIterator. Assim, foi possível passar $rs no foreach, o PHP já vai saber que deve chamar os métodos de PgSQLResultSetIterator para recuperar cada valor colocado em $row. Finalmente, $row será apenas um array indexado, devido ao parâmetro ResultSet::FETCHMODE_NUM passado na consulta.
A tabela abaixo mostra os resultados das duas metodologias:

Tabela 7 – Benchmark – Resultados Creole

Máquina 1 - Sem impressãoMáquina 1 - Com impressãoMáquina 2 - Sem impressãoMáquina 2 - Com impressão
2,012687924,23952190,91986592,3729560
Método JDBC
3,995573011,82725591,72740607,6208939
98,52%178,98%87,79%221,16%
Método SPL
3,62128717,45382501,57320124,2920520
79,92%75,85%71,03%80,87%


O formato SPL garantiu um melhor desempenho em todos os testes, com destaque para o momento em que exibimos o resultado (Com impressão) que, no método JDBC foi bem mais lento em termos percentuais e no SPL chegou a ser mais rápido na máquina 1 e pouco mais lento na máquina 2. Afinal exibir o valor de um array é mais rápido do que chamar um método que pesquisará por esse valor no resultado da consulta.

A última biblioteca testada foi a PDO, confira:

Código 8 – Benchmark PDO
1. 2. $conn = new PDO("pgsql:host=localhost dbname=locadora " .
3. "user=postgres password=postgres");
4. $runs = 250;
5. for($i = 0; $i < $runs; $i++) {
6. $sql = "select nome from atores order by nome";
7. foreach($conn->query($sql, PDO::FETCH_NUM) as $row) {
8. echo $row[0];
9. }
10. }
11.
12. $time_end = microtime(true);
13. $time = $time_end - $time_start;
14. print("\n\nTEMPO: {$time}\n");
15. ?>


A conexão com o SGBD se dá através da instância de um objeto da classe PDO. Passamos como parâmetro, a mesma string que havíamos passado no pg_connect(), adicionando apenas o rótulo pgsql:. Num exemplo de desatenção à portabilidade, a conexão PDO com o MySQL, não seria desta forma, mas de acordo com a sintaxe da função mysql_connect(), ou seja, receberia três parâmetros, como abaixo:

$conn = new PDO("mysql:host=localhost;dbname=locadora", "root", "");

Nesse caso, as diferenças para a mysql_connect() são apenas o rótulo mysql: e a informação da base de dados, no lugar da chamada à infame mysql_select_db().
Como você deve saber que conexões com o banco devem ser declaradas em um único local para serem incluídas quando necessárias (afinal, trocar de banco só em casos especiais, mas a troca de servidor, usuário ou senha é um procedimento bem comum), a diferença nessa declaração de um SGBD para outro não lhe traria maiores problemas.
A consulta foi feita através da instrução $conn->query() que recebeu a intrução SQL como parâmetro e retornou um objeto do tipo PDOStatement, uma classe da API da PDO que implementa a interface SPL IteratorAggregate, por isso, é transpassável por um foreach como fosse um array. Devido ao segundo parâmetro PDO::FETCH_NUM, o resultado de cada iteração do laço será um array indexado.
O resultado final foi surpreendente. Apesar de já esperarmos que um bom desempenho do PDO por ser uma extensão escrita em C, a boa implementação da SPL proporcionou o melhor resultado, longe dos demais. Observe:

Tabela 8 – Benchmark – Resultados PDO

Máquina 1 - Sem impressãoMáquina 1 - Com impressãoMáquina 2 - Sem impressãoMáquina 2 - Com impressão
2,012687924,23952190,91986592,3729560
2,14941794,44300500,91163801,9495799
6,79%4,80%-0,89%-17,84%



Em todos os testes, ela andou bastante próxima das chamadas nativas. E por mais estranho que possa parecer, chegou a superá-las na segunda máquina. Não pense que foi um resultado isolado, durante os testes nesta máquina, a PDO andou regularmente abaixo do PostgreSQL direto.

As grades abaixo mostram o acumulado de resultados catalogados aqui, com as bibliotecas na ordem da média do percentual de tempo:

Tabela 9 – Benchmark – Grade Máquina 1


AcessoSem impressão (s)Com impressão (s)Sem impressão %Com impressão %
PostgreSQL2,012687921524,239521980290,00%0,00%
PDO2,14941787724,443005084996,79%4,80%
Creole SPL3,621287107477,4538249969579,92%75,82%
DBX4,795524120339,35048985481138,26%120,56%
Creole JDBC3,9955730438211,827255964398,52%178,98%
Metabase5,033195972449,83236980438150,07%131,92%
ADOdb5,189945936210,7471740246157,86%153,50%
DB7,6816079616516,5098080635281,66%289,43%
MDB29,3536999225620,758523941364,74%389,64%



Tabela 10 – Benchmark – Grade Máquina 2

AcessoSem impressão (s)Com impressão (s)Sem impressão %Com impressão %
PDO0,9116380214691,94957995415-0,89%-17,84%
PostgreSQL0,9198658466342,372956037520,00%0,00%
Creole SPL1,57320117954,2920520305671,03%80,07%
DBX1,952025175094,8769800663112,21%105,52%
Metabase2,307868957525,63655900955150,89%137,53%
ADOdb2,235513925556,11938786507143,03%157,88%
Creole JDBC1,727406024937,6208939552387,79%221,16%
DB3,488619089139,4957420826279,25%300,17%
MDB24,2592630386411,3491249084363,03%378,27%



Na média geral dos percentuais de tempo, o resultado foi:

Tabela 11 – Grade final – Média percentual dos quatro tempos

AcessoMédia percentual
PDO-1,79%
PostgreSQL0,00%
Creole SPL76,91%
DBX119,14%
Metabase142,61%
Creole JDBC146,61%
ADOdb153,07%
PEAR::DB287,63%
PEAR::MDB2373,92%




No próximo post, entraremos nos próximos quesitos do estudo: facilidade e portabilidade.



Abstração de SGBDs - Estudo comparativo - Parte 1 Bibliotecas

Abstração de bancos de dados

Talvez você ache fácil comunicar-se com SGBDs no PHP através de funções específicas (mysql_connect(), mysql_query(), etc.). No entanto, talvez tenha percebido que um sistema recheado de “mysql_queries” perde muito em flexibilidade. Imagine o trabalho que o programador teria no momento em que precisasse passar a trabalhar com outro SGBD. E mais, imagine o grau de “retrabalho” no desenvolvimento de algum software com suporte a cinco ou dez SGBDs diferentes.
Mesmo que haja uma tendência a certa degradação de desempenho (degradação inclusive questionável, mas, a rigor, há esta tendência), seria interessante construirmos alguma camada de código capaz de intermediar as requisições particulares do sistema ao banco de dados que de fato foi utilizado. Apesar de nosso objetivo ser aprender o “como fazer”, nesse momento, não reinventaremos a roda. Existe uma série de bibliotecas a esse respeito.
Não seria bicho de sete cabeças desenvolver algo nesse sentido, entretanto, construir uma biblioteca de abstração de SGBDs realmente portável, isto é, capaz de trabalhar com pelo menos uns dez bancos diferentes, de maneira homogênea, suportando questões avançadas como prepared statements e transações e sabendo aplicar as particularidades de cada banco quando estas acarretarem ganhos de desempenho e segurança, pode sim ser uma tarefa complicada e trabalhosa.

Abstração de SGBD é uma necessidade absoluta?

Na verdade, a resposta é não. Como já dissemos, a tendência natural do uso dessa camada é a degradação de desempenho.
Diversas soluções nessa direção já foram desenvolvidas. Se você já trabalhou ou pelo menos ouviu falar na solução da Microsoft ODBC (Open Database Connectivity, baseada na Call Level Interface, CLI, da SQL Access Group), sabe que se trata de uma camada intermediária entre os processos da aplicação e do SGBD a fim de proporcionar essa portabilidade, uma vez que o programa só precisará acessar o driver ODBC que este último se encarrega da comunicação com o banco adjacente.
Acessar determinado banco através de um driver ODBC, no PHP (e porque não estender essa observação às demais linguagens), é normalmente mais lento e custoso para o servidor do que nativamente.
A própria Microsoft desenvolveu uma API para acesso a servidores de bancos de dados chamada ADO (ActiveX Data Objects), popular desde versões mais antigas do seu Visual Basic, que pode trabalhar via ODBC ou nativamente com alguns SGBDs. A sintaxe desta API inspirou diversas bibliotecas com o mesmo propósito no PHP, mesmo sob o olhar crítico de alguns que não gostam da ADO ou simplesmente levantam questões de incompatibilidade entre os dois ambientes.
Nossa opinião é a seguinte, se o desempenho for o requisito mais importante do seu sistema, acesse diretamente e se preocupe com os mínimos detalhes da API nativa do SGBD. No entanto, se a portabilidade e o conhecimento único aplicável a um grande leque de SGBDs forem questões relevantes no desenvolvimento de software, definitivamente, vale a pena investir na abstração.

Bibliotecas de abstração

Nessa seção, apresentaremos algumas das mais populares bibliotecas de abstração de SGBDs do mercado. Praticamente 100% dos programadores experientes em PHP utilizam ou já utilizaram alguma(s) delas em seus projetos, e isso certamente, lhes rendeu algumas horas de sono a mais.

Metabase
Uma das mais antigas ainda atualizadas e ativas no mercado. Criada por Manuel Lemos, desde quando o PHP ainda estava sua terceira versão. Mas muito bem adequada ao PHP4, ainda podendo ser utilizada em projetos PHP5 sem maiores problemas. No endereço http://www.phpclasses.org/browse/package/20.html, você encontrará sua mais nova versão, além de uma boa documentação em português.
Há três formas de interagir com suas funcionalidades. A primeira, mais antiga, é através de um punhado de funções que iniciam com a palavra Metabase, a segunda, através dos métodos de um objeto gerado pela função MetabaseSetupDatabaseObject, a terceira seria uma mistura das outras duas.
A tabela abaixo mostra a lista de SGBDs suportados pela Metabase e suas respectivas siglas utilizadas na biblioteca:

Tabela 1 – SGBDs suportados pela Metabase

Sigla

SGBD

ibase

Interbase

ifx

Informix

msql

Msql

mysql

MySQL

oci

Oracle

odbc

ODBC

odbc_msaccess

ODBC (particularmente ao Microsoft Access)

pgsql

PostgreSQL

sqlite

SQLite



É uma das bibliotecas mais preocupadas com a portabilidade. Com ela é possível modelar uma base de dados num arquivo XML e criar um script PHP para processá-lo e gerar o esquema no banco. Para isso, é necessário baixar um outro pacote, o Generic XML Parser, disponível no endereço http://www.phpclasses.org/browse/package/4.html.
Possui tratamento homogêneo a campos auto incrementados (que cada SGBD faz à sua maneira), funções de conversão de dados que possam sofrer mudanças de representação de um banco para outro como strings, datas, horas, pontos flutuantes e boleanos. Suporta consultas preparadas (prepared statements) e interage com tipos textuais grandes (CLOBs) e grandes binários (BLOBs) de maneira fácil e eficiente.

ADOdb
Disponível também para a linguagem Phyton, além de ser uma das mais antigas, é, sem dúvida, uma das mais populares do mercado. Nasceu da iniciativa de portar a sintaxe da Microsoft ADO ao mundo PHP. Suporta uma gama ainda maior de SGBDs como: MySQL, Interbase, Firebird, Oracle, MS SQL Server, Foxpro, MS Access, ADO, IBM DB2, SAP DB, SQLite, ODBC e, graças à sua imensa comunidade, PostgreSQL, Informix, Sybase, FrontBase, Netezza, ODBTP e ainda é capaz de acessar dados de um servidor de domínios LDAP.
Seu site oficial http://adodb.sourceforge.net/ cita diversos softwares populares que a utilizam, como os gerenciadores de conteúdo PostNuke, Xaraya e Mambo e a ótima solução de groupware eGroupWare.
Preocupa-se bastante com a portabilidade, como por exemplo, quando disponibiliza em seu objeto fruto da conexão métodos como SelectLimit(), para que você não escreva uma instrução SQL SELECT que contenha a maneira como o seu SGBD escolhido trabalha com consultas limitadas (LIMIT x OFFSET y, PostgreSQL, LIMIT y, x no MySQL ou SELECT TOP x *, no MS SQL Server, por exemplo), leftOuter(), rightOuter() e ansiOuter() para variações de JOINs entre tabelas e mais uma série de outros que você pode consultar no endereço http://phplens.com/lens/adodb/tips_portable_sql.htm.
Suporta ainda transações, prepared statements e, hoje, ainda é possível trabalhar semelhantemente à sintaxa da biblioteca PEAR::DB, que se popularizou bastante nos últimos anos. É possível utilizar recursos específicos do PHP5, como levantamento de exceções quando houver problemas na conexão com o banco, por exemplo. Apesar da pouca documentação nesse ponto, ainda há uma versão que você pode instalar como extensão do PHP, escrita em C, o que deve aumentar bastante o desempenho final.

DBX
Esta é a primeira iniciativa do PHP em criar uma extensão nativa e não um conjunto de arquivos PHP que devem ser incluídos no seu script. Totalmente procedural, apresenta algumas simples funções que fazem todo o trabalho. Suporta os SGBDs FrontBase, Microsoft SQL Server, MySQL, ODBC, PostgreSQL, Sybase-CT, Oracle (oci8), e, mais recentemente o SQLite.
É fornecida nas próprias fontes do PHP. Para habilitá-la, basta informar a diretiva --enable-dbx no momento de compilação. Com a recente extensão PDO, a DBX perdeu espaço, já que seu maior argumento, o fato de ser uma extensão do PHP, gerando uma API nativa (sem precisa dar include em arquivo algum), também se enquadra nesta última.

PEAR::DB
Até meados de 2006, foi um dos pacotes mais importantes do PEAR, visto que compunha a lista de pacotes básica inserida em qualquer distribuição do PHP. Rapidamente angariou inúmeros adeptos. Apesar de não ser a mais portável, nem tão pouco a mais eficiente (veremos um benchmark adiante), foi considerada a “oficial” do PHP por muito tempo, visto que mesmo sem ser uma extensão nativa, estava lá disponível em qualquer servidor Web que suportasse PHP.
Hoje, em seu endereço, http://pear.php.net/package/DB, há uma mensagem sugerindo que o programador opte por baixar o pacote PEAR::MDB2, já que sua última versão 1.7.6 será atualizada apenas para correção de bugs e ajustes de segurança. Provavelmente não passará de 1.7.X.
Trouxe para o PHP algumas técnicas bastante interessantes, como os métodos simplificados de recuperação de dados getAll(), getAssoc(), getRow() e getOne(). Suporta campos auto incrementados (sequences), prepared statements e transações. No entanto, alguns recursos como manipulação de BLOBs ficaram de fora. O endereço http://phplens.com/phpeverywhere/node/view/39 faz um comparativo entre a DB e a ADOdb. Apesar de tendencioso, dá para você fazer uma análise de que falta a essa biblioteca.

PEAR::MDB2
Sucessora de fato da PEAR::DB, a MDB2 é a segunda versão do pacote PEAR::MDB, uma iniciativa de portar a Metabase ao formato padrão do PEAR e aproximar da sintaxe da PEAR::DB. Apesar de (ainda) não ser distribuída na lista básica de pacotes PEAR, esta disponível através do comando pear install ou no endereço http://pear.php.net/manual/en/package.database.mdb2.php.
Por se tratar de uma adaptação da Metabase, a MDB2 vai bem mais além, em questões de portabilidade. Está certamente entre as que mais garante a pretendida abstração de SGBDs. Suporta, por exemplo, a manipulação de BLOBs e alguns outros recursos ausentes em sua antecessora.

Creole
Biblioteca interessantíssima não só pelo que faz, mas por como faz. No lugar de tentar se aproximar da sintaxe da Microsoft ADO ou da PEAR::DB, a Creole se aproxima bastante da sintaxe da API JDBC (Java Database Connectivity).
Surgiu como um subprojeto do Propel, um framework de mapeamento objeto x relacional. Devido à bagagem Java de seus desenvolvedores (o Propel possui algumas características do Hibernate, como mapeamento em arquivos XML), e à necessidade de uma API mais bem estruturada no mundo OO, em seu site oficial http://creole.phpdb.org/ consta a informação de que eles tentaram utilizar as bibliotecas PEAR::DB, PEAR::MDB e ADOdb, mas não ficaram satisfeitos com o resultado. Por isso criaram a Creole.
Possui ainda um outro subprojeto chamado Jargon, uma extensão que adiciona alguns métodos de atalho para leitura e escrita de dados.

PDO
Ex-pacote PECL (no PHP 5.0), a PDO é um dos recursos mais aclamados do PHP 5.1. Alguns acreditam que não passa de euforia passageira, afinal, ela não inventou nada realmente novo. O fato é que se trata de uma biblioteca, que por ser posterior, pôde ser arquitetada sobre os erros e acertos das demais. Alie isso ao fato da biblioteca vir na forma de extensão distribuída em conjunto com o PHP, o que sugere maior desempenho (acompanhe o benchmark adiante). Por outro lado, ela ainda precisa evoluir em portabilidade, nesse quesito ainda está bem aquém da Metabase, ADOdb, MDB2 ou Creole.
Suporta os SGBDs MySQL, PostgreSQL, MS SQL Server, Sybase, Informix, SQLite, Oracle, Firebird e MSSQL, além do ODBC, no entanto requer uma diretiva para suportar cada banco no ato de compilação. A linha abaixo configuraria o PHP para ser instalado como módulo do Apache2, com suporte aos SGBDs MySQL, PostgreSQL e SQLite, adicionando o acesso via PDO:

# ./configure --with-apxs2=/usr/local/apache/bin/apxs --with-pgsql=/usr/local/pgsql --with-mysql=/usr/local/mysql --with-sqlite --enable-pdo --with-pdo-pgsql=/usr/local/pgsql --with-pdo-mysql=/usr/local/mysql --with-pdo-sqlite

As barras invertidas acima servem apenas para poder continuarmos o comando na linha de baixo. Perceba que além de adicionar o suporte aos três SGBDs[1], habilitamos o PDO e inserimos uma diretiva para que ela seja capaz de acessar cada banco especificamente.
O site oficial http://www.php.net/pdo recomenda que você instale o PDO como um módulo compartilhado (shared), adicionando a diretiva da seguinte forma:

--enable-pdo=shared
Assim, um arquivo pdo.so será gerado no diretório da diretiva extension_dir do php.ini. Para habilitar o PDO, será necessário adicionar ao php.ini a linha:

extension=pdo.so
Como você faria no Windows para habilitar o pdo.dll que já vem fornecido na distribuição binária do PHP. O mesmo deve ser feito para cada SGBD (--with-pdo-mysql=shared e extension=pdo_mysql.so, por exemplo).
Num PHP já instalado, é possível adicionar o suporte ao PDO através do instalador do PECL. As linhas:

# pecl install pdo
# pecl install pdo_mysql
# pecl install pdo_pgsql
# pecl install pdo_sqlite
Irão disparar os mesmos passos do instalador pear. O único pré-requisito para esse procedimento é que o PHP tenha sido instalado com suporte à biblioteca Zlib (--with-zlib) para permitir que o pecl possa baixar e descompactar os pacotes do PDO. A instalação via pecl irá gerar o módulo compartilhado pdo.so (e os demais para cada SGBD adicionado) e você deve habilitá-los no php.ini da mesma forma da instalação shared. Nesses últimos dois processos é possível atualizar o pdo também através do pecl:

# pecl upgrade pdo
Instrução idêntica à que você faria para atualizar algum pacote PEAR.

1 SQLite não é um SGBD, mas um punhado de código C capaz de ler e escrever e um arquivo sob a semântica de acesso SQL. Assim como a PDO, já vem fornecido nas fontes do PHP.