Armazenamento de Informação em HSQL Bruno Azenha Instituto Superior Técnico, Universidade Técnica Lisboa Taguspark, Portugal [email protected] Resumo: Neste artigo é analisado o armazenamento de informação no SGBD HSQL. Este SGBD foi lançado em 1998 por um grupo de entusiastas que quiseram explorar a linguagem Java para desenvolver um SGBD que fosse leve e simples. A sua versão actual (1.8.0) suporta um grande conjunto dos standards SQL 92 e 200n, acrescentado algumas extensões proprietárias. Apresenta três modos principais de funcionamento “in-memory”, “standalone” e “client/server” – sendo as ligações feitas através de um driver JDBC. Apesar das suas limitações, o facto de ser OpenSource, freeware e apresentar bons desempenhos para um sistema implementado em Java, levaram a que tenha sido usado em alguma aplicações bastante conhecidas, tais como o Mathematica, OpenOffice, JBoss e outras. Palavras-chave: armazenamento de informação, bases de dados relacional, bases de dados Java, HSQL 1. Introdução O HSQL é um SGBD relacional escrito em Java, que disponibiliza um driver JDBC, suportando um grande conjunto das especificações ANSI-92 SQL, bem como algumas das melhorias introduzidas pelo SQL 99 e 2003, acrescentando também algumas extensões proprietárias. Caracteriza-se por ser um SGBD pequeno e rápido, que suporta tabelas em disco e em memória [1]. O desenvolvimento iniciou-se em 1998 por um grupo de entusiastas que lançaram o projecto Hypersonic SQL, no seguimento da euforia que se vinha a gerar em torno da linguagem Java. Em 2000, o projecto foi encerrado, no entanto um grupo de antigos programadores decidiram-se juntar via Internet e formar o “HSQLDB Development Group”, lançando a sua primeira versão do HSQL (1.6) em Abril de 2001. Desde essa altura que novas versões e funcionalidades vêm sido adicionadas periodicamente. As aplicações do utilizador só podem aceder ao servidor através da interface JDBC, sendo que este apresenta três casos de uso: aplicação e BD na mesma máquina virtual Java (JVM), ligação HTTP através da Internet com recurso a um WebServer ou a uma Servlet ou ligação TCP/IP (Intranet) usando um servidor dedicado. Conforme os modos de funcionamento, o SGBD disponibiliza ou não diversas funcionalidades e graus de desempenho. Apesar de ser um SGBD algo limitado qando em comparação com outros sistemas comerciais, o facto de ser OpenSource, freeware e apresentar bons desempenhos para um sistema implementado em Java, levaram a que venha a ser usado com sucesso em alguma aplicações bastante conhecidas, tais como o Mathematica, OpenOffice, JBoss, JFox, TrackStudio e outras. 1 O objectivo deste artigo é descrever o modo como se efectua o armazenamento de informação no SGBD HSQL. Na Secção 2, procura-se dar uma visão geral do SGBD. Na Secção 3, é analisada a forma de implementação física das BDs no sistema de ficheiros do sistema operativo (SO). A Secção 4 apresenta uma análise aos tipos de tabelas disponibilizados e as suas vantagens e desvantagens, bem como a forma como estas são implementadas. A Secção 5 expõe o modo como os dados são armazenados, quer em disco quer em memória. Na Secção 6 apresenta-se a capacidade do HSQL em lidar com tipos de dados binários. Finalmente, na Secção 7, é feita uma breve análise e discussão sobre o SGBD. 2. Visão global do sistema O HSQL apresenta uma arquitectura relativamente simples. Além de um servidor, disponibiliza também dois tipos de interfaces gráficas. A sua execução em Sistemas Windows ou Linux efectua-se através de “shell scripts” existentes no directório “demo”. Existem três modos de funcionamento neste SGBD: Server Mode O motor encontra-se a correr numa JVM, à espera de ligações de programas na mesma máquina e de máquinas ligadas na rede. Os clientes ligam-se através de um driver HSQL JDBC. Dependendo do modo de servidor, é possível disponibilizar acessos até 10 bases de dados, sendo estas especificadas na altura do arranque. In-Process ou Standalone Mode Este modo lança o motor da BD como parte da aplicação na mesma JVM. Para a maioria das aplicações, este modo poderá ser o mais rápido pois os dados não precisam de ser convertidos e enviados através da rede. A desvantagem consiste em tornar-se impossível neste modo ligar à BD fora da aplicação que lhe está a aceder. Memory-Only Databases É possível executar o HSQL num modo em que a base de dados não é persistente e existe unicamente em memória. Como nenhuma informação é escrita para disco, este modo é recomendado apenas para processamento interno de dados, em “applets” ou em aplicações muito específicas. O modo de interacção dos diferentes componentes é bastante simples, como se pode observar na Figura 1. Quer as ferramentas que vêm com o HSQL (interfaces gráfica e ferramentas de gestão/exploração de dados), quer os programas terceiros, acedem ao motor da BD através da interface JDBC. Esta por sua vez permite diferentes tipos de uso: ligações HTTP via Internet usando um Web Server ou uma servlet; standalone em que a aplicação e a BD estão na mesma JVM; ligação TCP/IP numa intranet em que se recorre ao uso de um servidor dedicado. 2 Demonstration programs and Tools JDBC Interface HTTP Connection WebServer Standalone HSQL Connection Servlet Server Database Engine Figura 1 – Arquitectura HSQL 3. Suporte Físico Quando o servidor é lançado ou quando é estabelecida uma ligação uma BD, é sempre criada uma nova BD vazia se ainda não existir nenhuma no caminho especificado. Assim sendo, caso seja cometido um erro a especificar o caminho, é estabelecida à mesma uma ligação, podendo o utilizador não se aperceber do facto e estranhar por ver uma BD vazia. Para evitar isto, é possível definir a propriedade “ifexists = true” no ficheiro “.properties” para permitir que a ligação seja feita apenas a uma BD que já exista, evitando assim a criação de uma nova BD vazia. Caso esta não exista, é lançada uma excepção. Nos casos em que o HSQL se executa em modo “standalone” ou “server”, cada base de dados irá consistir num conjunto de 2 a 5 ficheiros todos com o mesmo nome mas com diferentes extensões, localizados na mesma directoria (./data). Por exemplo, uma BD com o nome “teste”, consistiria nos seguintes ficheiros: teste.properties – contém configurações genéricas da BD (versão, compatibilidades, tamanho do ficheiro de log, etc.) teste.script – contêm a definição em SQL das tabelas, dos dados e de outros objectos da BD, para as tabelas que não são “cached”1 teste.log – contém as alterações recentes à BD teste.data – contém os dados para as tabelas “cached” teste.backup – é uma cópia de segurança comprimida do último estado consistente conhecido para o ficheiro “.data” Se a BD não tiver tabelas “cached”, os ficheiros teste.data e teste.backup não existirão. Adicionalmente, o SGBD pode ligar a outros ficheiros de texto formatados, por exemplo ficheiros CSV2, existentes em qualquer directório do disco. Cached porque quando os dados que se encontram em disco no ficheiro “.data” são necessários, o SGBD carrega-os para memória 2 CVS significa “Comma Separated Values”. É um formato de ficheiro usado para portabilidade de BDs. Cada linha representa um túplo e cada campo encontra-se separado por um “;” 1 3 Enquanto a BD “teste” estiver a ser acedida, o ficheiro “teste.log” é usado para guardar as alterações feitas aos dados. Este ficheiro é removido quando se executa o encerramento da BD através do comando “SHUTDOWN”, ou seja, as operações são guardadas no ficheiro “.script” ou “.data” conforme o conteúdo e as propriedades da BD. Caso isso não se verifique (se terminar devido a uma falha), o ficheiro é usado da próxima vez que a BD fôr iniciada para repôr o último estado consistente. Existe ainda um ficheiro teste.lck que é usado para assinalar que a BD está aberta, e que é eliminado após um comando de SHUTDOW. Para efeitos de recuperação, em algumas circunstâncias são criadas cópias dos vários ficheiros com extensão “.old” e “.new”. Estes ficheiros são usados num processo de recuperação que se assemelha ao “shadow paging”. Mais informações poderão ser obtidas em [1] (Appendix C). 4. Estrutura e Implementação das Tabelas Nesta Secção pretende-se expôr a forma como o HSQL permite armazenar os dados nas tabelas, quer ao nível dos tipos de tabelas que disponibiliza, quer a forma como estas se encontram implementadas. Tipos de Tabelas O HSQL define vários tipos de tabelas, as tabelas temporárias (TEMP) e 3 tipos de tabelas persistentes: em memória (MEMORY), em disco (CACHED) e de texto (TEXT). As tabelas temporárias só duram enquanto a ligação está activa. Já para as tabelas em memória, sempre que a BD é iniciada são lidos todos os dados existentes no ficheiro “.script”. Quando é executado o comando de SHUTDOW, todos os dados são escritos novamente de memória para o ficheiro “.script”. As vantagens deste tipo de tabelas sobre as tabelas em disco são tempos de acesso menores. No entanto, este tipo de tabelas só é adequado para pequenos conjuntos de dados. No caso das tabelas em disco, os dados só são lidos do ficheiro “.data” quando a tabela é acedida. Assim, só são mantidos em memória parte dos dados e dos índices, donde vem o nome deste tipo de tabelas – CACHED (a quantidade de dados a serem mantidos em cache é configurável). Quando se executa o comando SHUTDOW, os dados em memória que sofreram alterações são escritos para disco e são efectuadas cópias de segurança de todas as tabelas em disco. Este tipo de tabelas são ideais para grandes conjuntos de dados, visto que se acelera o acesso aos mesmos recorrendo à cache, ao mesmo tempo que permite a manipulação de grandes conjuntos de dados que não caberiam todos em memória usando tabelas do tipo MEMORY. Existe ainda um terceiro tipo de tabelas, as tabelas de texto. Este tipo de tabelas usa ficheiros CSV como fonte de dados. O uso de memória é mais eficiente pois assenta no carregamento para memória da totalidade dos índices e apenas uma pequena parte dos dados. Têm também a vantagem da portabilidade e legibilidade do formato, que praticamente todas as folhas de cálculo, demais SGBDS e processadores de texto podem ler e modificar. Ficam a perder em termos de desempenho para os outros dois tipos de tabelas persistentes, já que cada acesso a dados implica um potencial acesso a disco, com consequentes overheads3. 3 Overhead é um custo adicional em processamento ou armazenamento que, como consequência, piora o desempenho de um programa ou de um dispositivo de processamento. São custos adicionais indesejáveis, que deveriam ou poderiam ser evitados. 4 Estrutura das tabelas Em termos de implementação das tabelas, estas são implementadas como objectos Java. Na Figura 2 encontra-se uma representação simplificada da estrutura de uma tabela. Cada tabela contém um nome (que se encontra protegido), um conjunto de colunas (cada objecto coluna contém o nome, tipo dados, etc.), um ou mais índices (os túplos só são acessíveis através de índices pelo que estes são indispensáveis), uma cache (objecto org.hsqldb.persist.DataFileCache) para gerir o acesso a dados se a tabela fôr do tipo MEMORY ou CACHED, e restrições de integridade (se existirem chaves estrangeiras). Se o utilizador não criar uma chave primária, são criados automaticamente uma coluna e índices ocultos afim de possibilitar os acessos ao túplo. Para além destes campos, que são os mais relevantes, existem ainda alguns campos para optimizar acessos, de propriedades da tabela, e mais alguns dependendo depois do subtipo de tabela a considerar (MEMORY, TEXT, etc.). Na “package” org.hsqldb encontra-se o ficheiro Table.java contendo a implementação. Table Column (Vector) Index (Vector) Cache Constraint (Vector) Figura 2 – Estrutura de uma tabela 5. Armazenamento e Acesso a dados Os dados em HSQL só são acessíveis através de índices (Index). Os índices (definidos na “package” org.hsqldb) são uma implementação de uma árvore AVL4 em que cada nó tem um apontador para o nó pai afim de permitir navegação bi-direccional (raiz para as folhas e vice versa), para além do nó esquerdo e nó direito. Na Figura 3 pretende-se mostrar a forma como os índices se encontram estruturados. As árvores binárias simples apresentam um pior caso quando são inseridos dados com determinada ordenação e que levam a que estas fiquem muito desequilibradas. Nessas situações teríamos uma complexidade de O(n) nas procuras e inserções. As árvores AVL eliminam o pior caso, garantindo que teremos sempre O(lg n) em procuras 4 Árvores binárias de procura, auto-ajustáveis e balanceadas em altura, que receberam o nome dos seus inventores: Adelson-Velskii e Landis 5 inserções, devido a forma como é feito o balanceamento em cada nó. Em [3] encontrase uma explicação detalhadas das características das árvores AVL. Cada nó está ligado a um túplo, existindo algumas diferenças entre as várias subclasses de “Node”. Conforme temos tabelas do tipo MEMORY, CACHED ou TEXT, existem variações à forma como os nós mantêm referências para outros nós da árvore ou para os túplos, já que as tabelas apresentam diferentes formas de aceder aos seus dados. Os dados contidos no túplo são vectores de objectos Java. No índice existe um nó raiz que se encontra ligado aos restantes nós através de referências a objectos Java ou apontadores de ficheiros, dependendo da implementação do nó. O nó raiz toma o valor “null” se a tabela estiver vazia. Um objecto “Index” contém ainda informação sobre as colunas da tabela que referencia. Index Node [root] Node [parent] Node [left] Node [right] Row [data] Table Object (array) [data] Figura 3 – Estrutura de um índice O objecto tabela (ver org.hsql.Table), representado na Figura 2, contém um objecto cache do tipo “DataFileCache” (ver org.hsqldb.persist.DataFileCache) que, recorrendo a mais alguns objectos, gere o acesso aos dados e trata as transferências disco/memória. A cache funciona como uma “hashtable” em que os túplos se encontram ligados através de listas duplamente ligadas (ver org.hsqldb.Row e org.hsqldb.CachedRow). Quando é necessário arranjar espaço para ler mais dados para memória, os mais antigos são guardados (se necessário) e removidos da cache. A Figura 4 dá uma percepção da relação entre os diferentes componentes da cache. Em cada momento existem alguns túplos (Row) na cache, todos eles interligados através da lista duplamente ligada. O array “Data” contém em cada “bucket” um apontador para o primeiro túplo (caso exista) mapeado pela função de hash para esse “bucket”. Por outro lado, a cache contém também uma ligação para o ficheiro “.data”, o local onde os dados da BD se encontram guardados de forma persistente. Com esta estrutura não só se torna fácil aceder de forma aleatória aos túplos graças à “hastable”, mas também é possível efectuar acessos do tipo “scan” a todos os dados em cache, já que estes se encontram acessíveis através da lista duplamente ligada. 6 A lista permite ainda que as inserções e remoções de túplos da cache seja relativamente simples. Cache Data (array) Row .data: [contains the binary records and empty blocks] Figura 4 – Arquitectura da cache 6. Tipos de dados especiais O HSQL, para além dos tipos tradicionais de dados, apresenta ainda suporte para tipos de dados especiais, os tipos OTHER e OBJECT, que são mapeados para java.lang.Object. Isto torna possível o armazenamento de dados binários nas colunas, tais como fotografias, ficheiros áudio ou objectos Java serializados. A ferramenta “SQL Tool”, que vem com o SGBD, mantém um buffer binário para onde é possível ler ficheiros recorrendo ao comando ‘\bl’ e ao uso de “prepared statments”. Quando os dados estão no buffer, podem então ser escritos para uma coluna da BD. Seguidamente, apresenta-se um exemplo em que se guarda na tabela ‘musictbl’ um ficheiro mp3: $ \bl /tmp/favoritesong.mp3 $ \bp INSERT INTO musictbl (id, stream) VALUES (3112, ?); Quando se quisesse obter os dados outra vez num formato de ficheiro, bastaria executar os seguintes comandos: $ SELECT stream FROM musictbl WHERE id = 3112; $\bd /tmp/favoritesong.mp3 O tamanho máximo do objecto é ditado pela memória disponível. Na prática, SGBDS de produção têm suportado com sucesso objectos até um megabyte. 7. Discussões e Conclusões O HSQL enquanto SGBD não pode ser considerado com uma alternativa a SGBDs já estabelecidos e direccionados a uso empresarial e/ou em larga escala, como o 7 Oracle, SQLServer ou até o MySQL devido às limitações que ainda apresenta. Ainda assim, para armazenamento de pequenos conjuntos de dados, que não requeiram grandes tratamentos ou para dar suporte a aplicações com poucas exigências de manipulação de dados, este SGBD poderá ser uma hipótese a considerar pela sua simplicidade e relativa rapidez. O suporte de armazenamento de dados tem vindo a sofrer sucessivas evoluções, acrescentando cada vez mais funcionalidades e começa nesta altura a revelar-se interessante, quer pelo que já disponibiliza quer pelas funcionalidades que estão em desenvolvimento (suporte para “datasets” entre 10MB e 2GB, acessos concorrentes, etc). É portanto, um sistema que poderá ser acompanhado com algum interesse, até por possibilitar um aprendizagem académica do funcionamento de um SGBD, mesmo que bastante simplificado. 8. Referências [1] HSQL Development Group, Hsqldb User Guide, www.hsqldb.org, 2005 [2] HSQL Development Group, Hypersonic SQL Arquitecture, www.hsqldb.org, 2005 [3] David Carlson, AVL Trees, http://cis.stvincent.edu/swd/avltrees/avltrees.html, 2004 8