Tecnologias de Base de Dados 2005/2006 PROCESSAMENTO DE INTERROGAÇÕES EM MEMÓRIA PRINCIPAL por Vitor Manuel G. da Silva Sumário. Apresentam-se, de seguida, alguns dos algoritmos de processamento de interrogações em memória principal e análise do seu desempenho, assim como quais são os seus prós e contras tendo em conta alguns dos cenários possíveis das operações de manipulação de dados do dia a dia. Palavras-chave. Interrogações, Memória Principal, Árvores, Hash, Select, Join, Aggregation, Projection, Ordenação. 1. Introdução As interrogações SQL permitem a um utilizador colocar questões a uma base de dados de modo a obter as respostas que necessita. Desse modo faz-se com que a base de dados não seja um simples repositório de dados, mas sim uma ferramenta importante, utilizada no dia a dia pelos utilizadores que precisam de lidar com a informação nela contida. O processamento de interrogações é fundamental num Sistema de Gestão de Base de Dados. Para que o desempenho de uma base de dados seja aceitável, esta deverá responder às solicitações dos utilizadores no menor espaço de tempo possível. Todas as interrogações têm de ser executadas em memória principal para permitir manipular os dados, de modo a obter os resultados. Contudo as bases de dados actuais ocupam um espaço de armazenamento cada vez maior já chegando aos terabytes e, apesar da memória principal já apresentar preços mais acessíveis do que há uns anos atrás, estes ainda não são comparáveis aos custos da memória secundária. Para além disto não podemos negligenciar o facto de que quanto maior a capacidade disponível para implementar sistemas de base de dados mais informação se “consegue arranjar” para preencher essa capacidade. Com base no paradigma de que a memória secundária se destina a armazenar os dados e a memória principal a processar interrogações, foram desenvolvidos diversos algoritmos que procuram (i) minimizar o tempo para fornecer os resultados de uma interrogação, e (ii) minimizar o espaço necessário, em memória principal, para produzir os resultados. [5] O modelo relacional é composto por um conjunto de operadores que podem ser implementado com recurso a diversos algoritmos, alguns mais eficientes, outros nem tanto. Iremos agora analisar alguns desses operadores. 2. Selecção (Select) A selecção é um operador relacional cujo objectivo é aceder ou extrair os tuplos de uma tabela que obedeçam a uma condição. Por exemplo, dada uma tabela de alunos, mostrar quais os nomes dos alunos que têm 24 anos de idade traduz-se numa interrogação que poderia ser escrita da seguinte forma em SQL: Select nome From Alunos Where idade = 24 Para poder processar uma interrogação, o primeiro passo é colocar os tuplos da tabela em memória principal para que possam ser manipulados. Assim deverá começar-se por identificar e escolher quais serão as melhores estruturas de dados em termos de economia de espaço e de rapidez no acesso. Estas estruturas de dados terão de permitir a “navegação” nos dados, ou seja, deverá ser possível aceder a qualquer elemento que esteja nessa estrutura de dados, bem como adicionar ou remover um desses elementos. Uma vez que a operação de selecção consiste em aceder a um ou mais tuplos que obedeçam a uma dada condição, o acesso aos dados armazenados nestas estruturas constitui uma importante porção do processamento necessário para obter os resultados. [1,3] Nesta secção iremos analisar o seguinte conjunto de algoritmos: os Arrays, as AVL Trees (Árvores Binárias), as B+ Trees, as T Trees, o algoritmo de Chained Bucket Hashing, o Extendible Hashing, o Linear Hashing, ou o algoritmo de Modified Linear Hashing. 2.1 Arrays Os Arrays constituem a estrutura de dados que melhores resultados obtém ao nível do espaço ocupado. De facto, trata-se de uma sequência ordenada e contígua de elementos de dados cuja Tecnologias de Base de Dados 1 Tecnologias de Base de Dados dimensão é dada pelo número de elementos que se pretende guardar. Cada elemento do array contém um campo de dados, sendo necessário apenas mais um ponteiro que indique a posição inicial do array. Contudo os Arrays apresentam um desempenho muito pobre ao nível da inserção e remoção de dados visto que o reordenamento do array é um processo muito lento. Em média, uma actualização num array implica mover cerca de metade dos elementos do array com vista à sua ordenação. A pesquisa de um elemento num array ordenado é uma tarefa razoavelmente eficiente e os seus tempos de resposta não são muito elevados. [1,5] 2.2 Adelson-Velsky Landis Tree (Árvores Binárias) As Árvores Binárias são estruturas de dados caracterizadas por uma representação, tal como o nome indica, em árvore, em que cada elemento, denominado nó, é constituído pelo elemento de dados que se pretende armazenar, bem como por dois ponteiros, cada um deles a apontar para outros dois elementos, tal como a Figura 1 indica. [1,5] Figura 1 - Um nó de uma árvore binária Uma árvore é binária precisamente por cada nó se dividir em dois nós, ficando a árvore final com uma representação similar à que se segue na Figura 2. 2005/2006 A inserção de elementos nesta estrutura de dados passa por criar um novo nó que deve conter os dados que se queiram guardar. De seguida, serão preenchidos os campos dos ponteiros de forma a garantir que a árvore permanecerá ordenada após a inserção de um novo elemento de dados. Esta reordenação poderá, no limite, implicar a reestruturação de toda a árvore, pelo que este é um ponto crítico da inserção, em termos de tempo. Deste modo o tempo de inserção depende da reordenação que é necessária efectuar. [1,2,5] Na remoção, o processo é análogo. Um elemento é removido quando nenhum nó na árvore tem um ponteiro a referenciá-lo. A remoção é rápida, em si, mas a reordenação que poderá ter que ser feita constituirá a grande fatia do tempo necessário para a operação. [1,2,5] No que diz respeito à operação de selecção que estamos a analisar, o que interessa é o tempo de resposta do algoritmo de pesquisa de dados na árvore binária. A pesquisa numa árvore binária começa por, a partir do nível mais alto da árvore, a raiz, verificar se esse é o elemento pretendido. Caso não o seja, a pesquisa seguirá pelo ramo da esquerda e assim sucessivamente em cada nó. Caso chegue ao final do ramo da esquerda e não encontre o elemento pretendido, começará a analisar os ramos da direita. Trata-se de uma pesquisa baseada num algoritmo de procura em profundidade que é bastante eficiente e que, tipicamente, apresenta bons resultados. [1] Apesar dos problemas já referidos ao nível do tempo de inserção e remoção de elementos, provavelmente o maior entrave à adopção desta estrutura de dados será o seu custo em termos de espaço. De facto, para cada nó, além dos dados a guardar, que poderá ser um simples ponteiro, terão de se guardar mais dois ponteiros para os nós filho. Ou seja, na melhor das hipóteses, uma árvore binária ocupará sensivelmente o triplo do espaço ocupado pelos dados que se pretende guardar. [1,2] 2.3 B+ Tree Figura 2 - Uma árvore binária balanceada Outra característica desta estrutura de dados é o facto dos seus elementos da árvore estarem ordenados de alguma forma, e da árvore ser balanceada, ou seja, cada nó que não pertença ao nível inferior da árvore (nó não folha) deve ter ligação a dois nós. [1,5] Tecnologias de Base de Dados As B+ Trees surgiram precisamente para procurar reduzir o espaço desperdiçado pelas Árvores Binárias passando cada um dos nós a armazenar mais do que um elemento (Figura 3). Desta forma reduz-se quer a dimensão da árvore quer o espaço ocupado por esta (Figura 4). [1,5] 2 Tecnologias de Base de Dados Figura 3 - Um nó de uma B+ Tree 2005/2006 propriamente lenta apresenta uma desvantagem em relação às Árvores Binárias: a pesquisa nos elementos de cada nó. Ao efectuar uma pesquisa numa Árvore Binária, ao analisar um nó para ver se este corresponde ao valor pretendido, caso este não o seja, imediatamente se segue para a análise do nó seguinte. Numa B+ Tree, ao pesquisar por um valor quando se analisa um nó tem que se analisar individualmente cada um dos elementos que compõem o nó e, caso o valor pretendido não esteja entre eles terá de se analisar o nó seguinte. Ora é precisamente para procurar optimizar esta avaliação dos elementos de um nó que surgem as T Trees. Para tal, cada nó de uma T Tree tem mais dois campos: o valor mínimo do nó e o valor máximo do nó. [1,5] Figura 4 - Exemplo de uma B+ Tree No que toca ao algoritmo de pesquisa na B+ Tree a principal diferença para as árvores binárias passa por, em cada nó se comparar os diversos elementos que o compõem com a condição que se pretende verificar. Caso o elemento pretendido não exista nesse nó prosseguirá a pesquisa pelo ramo onde haja mais hipóteses de se encontrar o elemento pretendido, ou seja, para cada elemento de um nó é feita uma comparação entre o valor pretendido e o valor desse elemento. Caso o valor pretendido seja inferior ao valor existente seguir-se-á o ramo à esquerda do elemento. Caso o valor pretendido seja superior ao valor existente seguir-se-á o ramo da direita e assim sucessivamente até alcançar o registo pretendido. [1,5] Quer o algoritmo de inserção, quer o de remoção de elementos acarretam, à semelhança da árvore binária, um custo de tempo assinalável devido à necessidade de reordenação da árvore. Contudo, uma vez que cada nó pode ter vários elementos e, um nó pode ter elementos vazios, o reordenamento poderá não ser necessário. Uma vez que a reordenação não acontece sempre que um elemento é inserido ou removido, as B+ Trees apresentam resultados melhores que as Árvores Binárias. Os algoritmos de inserção e remoção só por si não são mais eficazes ou simples, simplesmente a componente de reordenação ocorre menos vezes. [1,2,5] O objectivo das B+ Trees de reduzir o espaço ocupado pelas árvores binárias é alcançado. Pois ao invés de dois ponteiros por cada elemento de dados da Árvore Binária, numa B+ Tree só é necessário um ponteiro para referenciar o nó seguinte, mais o número de ponteiros suficiente para fazer as ligações entre os nós folhas. [1,2] 2.4 T Trees As T Trees procuram optimizar a pesquisa das B+ Trees. Se bem que a pesquisa nas B+ Trees não seja Tecnologias de Base de Dados Figura 5 - Um nó de uma T Tree Quando uma pesquisa está ser efectuada num nó compara-se o valor mínimo e máximo desse nó com o valor pretendido. Caso o valor pretendido esteja dentro do intervalo desse nó então pesquisar-se-á nesse nó pelo valor que se procura. Caso o valor pretendido não esteja nesse nó imediatamente a pesquisa avançará para o nó seguinte: se o valor a pesquisar for menor que o valor mínimo do nó a pesquisa prossegue pelo ramo da esquerda, caso o valor pretendido seja superior ao máximo desse nó a pesquisa segue para o ramo da direita. [1,5] Figura 6 – Uma T Tree As T Trees procuram juntar o melhor das Árvores Binárias e das B+ Trees: a rapidez na decisão do ramo a seguir e a capacidade de armazenamento de cada nó. Os algoritmos de inserção e remoção, tal como nas B+ Trees, não são mais eficazes só por si, mas por implicarem menos frequentemente a reordenação da árvore. Em termos de espaço, o desempenho é similar às B+ Trees, já que apesar de 3 Tecnologias de Base de Dados necessitar de guardar mais dois campos de controlo (valor máximo e mínimo do nó), não necessita dos ponteiros de ligação entre os nós folha, à semelhança das Árvores Binárias, o que acaba por “equilibrar” as contas. [1,5] 2.5 Chained Bucket Hashing Chained Bucket Hashing é uma estrutura de dados da família dos algoritmos de hashing, em que é utilizada uma função matemática para dividir o conjunto dos dados em subconjuntos mais pequenos, denominados buckets (baldes), cuja consulta seja mais rápida e eficiente. Assim, os algoritmos de hashing procuram seguir a filosofia de “dividir para conquistar”. Estes subconjuntos de dados são guardados em tabelas de hash (arrays) em que cada célula contém a identificação do subconjunto e um ponteiro para um balde que conterá os dados em concreto. Os dados, por sua vez, estão armazenados em baldes que por sua vez estão ligados entre si através de listas simplesmente ligadas, daí o nome de Chained Bucket (Baldes Ligados). [1,5] 2005/2006 desempenho da pesquisa está mais dependente da função de hash, dado que uma boa função de hash produzirá uma distribuição mais uniforme dos dados pelos diversos nós, do que da pesquisa na lista ligada. [1] No que toca ao espaço, o desempenho não é muito satisfatório. Para além do espaço ocupado pelos dados, temos o espaço ocupado pelos ponteiros para o próximo balde. Adicionalmente temos ainda o espaço ocupado pela tabela de hash. No total, tipicamente, uma tabela de hash com baldes ligados ocupa sensivelmente o dobro do espaço dos dados que se pretende guardar (caso os dados a guardar também tenham a dimensão de ponteiros). [1] Um dos grandes inconvenientes do Chained Bucket Hashing é o facto de ser uma estrutura de dados de dimensão fixa no que toca à tabela de hash. Esta tem de ser dimensionada à partida e com cuidado para obter um bom desempenho. Poucas entradas na tabela de hash farão com que cada nó tenha muitos baldes. Uma tabela de hash muito grande pode, para além do custo de espaço adicional, aumentar as hipóteses de não se conseguir obter uma distribuição uniforme. [1] 2.6 Extendible Hashing Um dos algoritmos que procura ultrapassar a limitação da dimensão fixa do Chained Bucket Hashing é o algoritmo de Extendible Hashing que implementa uma tabela de hash que cresce dinamicamente com a quantidade de dados. Figura 7 – Exemplo de uma estrutura de dados Chained Bucket Hashing O Chained Bucket Hashing tem um bom desempenho ao inserir e remover elementos ou a efectuar pesquisas, embora esta eficácia seja à custa de espaço de armazenamento. A inserção de elementos na estrutura de dados começa por verificar em que nó da tabela de hash deverá ser inserido o elemento, através da aplicação da função de hash. Depois o elemento é inserido num balde na lista ligada associada ao nó da tabela de hash. A remoção consiste em simplesmente fazer com que o ponteiro que referencia o elemento que se pretende remover deixe de o referenciar. [1,5] A pesquisa começa por identificar qual o nó da tabela de hash em que o dado pretendido estará, aplicando para tal a função de hash ao valor da condição. Uma vez identificado o nó bastará percorrer os baldes de dados até encontrar o que se pretende. Na prática, o Tecnologias de Base de Dados Uma das principais diferenças relativamente ao Chained Bucket Hashing diz respeito ao conteúdo de cada nó. Nas tabelas de hash com baldes ligados, a função de hash pode ser uma qualquer que subdivida os dados. No Extendible Hashing é utilizada a representação binária dos elementos de dados. Por exemplo, se o elemento de dados a inserir for o número 9, a sua representação binária será 1001. Este elemento poderá então ser inserido num balde que contenha elementos cuja representação binária termine em 01, por exemplo. Ao utilizar a representação binária, a probabilidade de baldes vazios é menor porque só existem dois algarismos possíveis, em vez dos dez da representação decimal ou dos vinte e seis da representação alfabética. [1,5] O número de elementos que cada um dos baldes pode conter é limitado, pelo que quando existe a necessidade de adicionar um elemento e o balde já se encontra totalmente preenchido, acontece um overflow. Quando acontece um overflow, o balde sem mais espaço é dividido em dois e os elementos de dados são redistribuídos pelos dois de acordo com a função de hash. A operação de overflow poderá ainda 4 Tecnologias de Base de Dados necessitar de aumentar a tabela de hash de modo a comportar os novos baldes. [1,3,5] 2005/2006 Figura 9 – Estrutura de dados após a inserção do elemento 20 Para inserir o elemento de dados 9 na estrutura de dados da Figura 9 irá acontecer um novo overflow, no balde B. Este terá de ser divido em dois (B e B2) e os elementos de dados redistribuídos. Novamente passarão a ser contabilizados três dígitos (001 e 101) em vez de apenas dois, mas desta vez a tabela de hash não precisará de ser redimensionada. A Figura 10 mostra o resultado da inserção do elemento 9. [3] Figura 8 – Uma estrutura de dados Extendible Hashing A Figura 8 representa um exemplo de uma estrutura de dados Extendible Hashing com alguns elementos de dados já inseridos. Se tentarmos inserir o elemento de dados 20, cuja representação binária é 10100, irá acontecer um overflow porque o balde A já se encontra totalmente preenchido. O balde A terá de ser dividido (A e A2) e os dados terão de ser redistribuídos pelos dois baldes. Para que os dados sejam redistribuídos já não bastará olhar para dois dígitos da representação binária, sendo necessário olhar para três dígitos (32 = 100000, 4 = 100). Contudo a tabela de hash apenas comporta dois dígitos (00, 01, 10 e 11) pelo que terá de ser aumentada. A Figura 9 mostra o resultado dessa operação tendo a tabela de hash duplicado e o balde A dividido em dois. [3] Figura 10 – Estrutura de dados após a inserção do elemento 9 A pesquisa no Extendible Hashing é feita de forma similar às tabelas de hash com baldes ligados mas tenderá a apresentar melhores resultados com quantidades de dados elevadas pois os baldes são de dimensão fixa e tipicamente inferior à dimensão dos baldes ligados. [1,3,5] No que toca à inserção e remoção de elementos a tarefa de redimensionar a tabela de hash inflacionará o tempo e influenciará o desempenho do Extendible Hashing. Deste modo, o Extendible Hashing apresenta um desempenho bastante bom quer ao nível da pesquisa quer ao nível das suas actualizações, desde que estas não signifiquem muitos redimensionamentos da tabela de hash. Contudo este desempenho paga-se em termos de espaço. O redimensionamento da tabela de hash é uma tarefa com um custo elevado ao nível de armazenamento dado que um nó e um balde, em caso de overflow, dão origem a pelo menos dois baldes, e em caso de aumento da tabela de hash, a dois nós, que poderão não vir a ser utilizados na sua totalidade. [1,3,5] Tecnologias de Base de Dados 5 Tecnologias de Base de Dados 2.7 Linear Hashing Uma outra variante dos algoritmos de hash é o Linear Hashing. O conceito desta estrutura de dados passa por, uma vez que a tarefa de redimensionamento tem custos computacionalmente elevados, procurar estabelecer critérios adicionais para o redimensionamento da estrutura de dados ao nível de armazenamento e não apenas por overflow de um balde. A ideia é efectuar o redimensionamento não apenas por acontecer um overflow num baldes, mas sim efectuá-lo de uma forma mais eficaz, em termos de espaço, ou seja, quando o número de overflow tornar conveniente um novo balde. [1,5] Outra característica que diferencia o algoritmo de Linear Hashing dos restantes algoritmos de hash é o facto de não ser utilizada uma tabela de hash. Os baldes são colocados em memória de forma consecutiva. Deste modo é possível calcular a localização de qualquer balde a partir do endereço inicial, um pouco à semelhança de um array. Tornase também mais simples analisar o crescimento da estrutura de dados e dos custos de armazenamento. [1,3,5] 2005/2006 os dados. Além disso, esperar o momento em que é conveniente criar um novo balde acarreta custos em termos de espaço, dado que é necessário guardar as páginas de overflow que contém os elementos de dados que serão inseridos nos baldes. [1,3,5] 2.8 Modified Linear Hashing O Linear Hashing, como explicado na secção 2.7, procura optimizar o espaço de armazenamento mas à custa do seu desempenho. Para procurar “corrigir” esse problema existe o Modified Linear Hashing que procura utilizar o critério do desempenho para o redimensionamento. O Modified Linear Hashing recorre novamente a tabelas de nós que referenciam os baldes (tabela de hash) que se encontram simplesmente ligados, procurando juntar conceitos do Chained Bucket Hashing com conceitos do Extendible Hashing. O Modified Linear Hashing apresenta uma estrutura de baldes ligados, mas cada nó utiliza a representação binária para referenciar os dados. A Figura 12 ilustra uma tabela do Modified Linear Hashing. [1,5] A Figura 11 representa o aumento do número de baldes de uma estrutura de Linear Hashing. Já existem overflows suficientes para dividir o balde 00 em dois (000 e 100). [1,3,5] Figura 12 – Uma estrutura de dados Modified Linear Hashing O critério para redimensionar a tabela de hash é a dimensão das listas de baldes, dado que é o factor que influencia o desempenho das pesquisas e actualizações de dados numa estrutura de dados com baldes ligados. [1,5] Figura 11 – Crescimento da estrutura de dados Linear Hashing A pesquisa em estruturas Linear Hashing é eficaz porque os baldes estão ordenados sequencialmente e o algoritmo de pesquisa tem acesso à informação necessária para calcular a localização do balde que contém o elemento de dados pretendido a partir da localização inicial. [1,3,5] Desta forma o desempenho do algoritmo nas pesquisas é bastante bom. Quanto às actualizações, assim que o volume de dados a guardar começa a aumentar e a implicar muitos redimensionamentos o desempenho começa a degradar-se. O volume da informação a guardar é determinante para o desempenho da estrutura de dados. [1,5] No que toca ao espaço de armazenamento o resultado é similar ao do Chained Bucket Hashing, ou seja, o Modified Linear Hashing poderá chegar a ocupar cerca do dobro do espaço ocupado somente pelos dados (caso os dados também sejam da dimensão de um ponteiro). [1,5] Na prática, os resultados alcançados por esta estrutura de dados são insatisfatórios, devido ao seu custo de actualização dos dados. Para manter uma utilização do espaço eficaz, é perdido muito tempo a reorganizar Tecnologias de Base de Dados 6 Tecnologias de Base de Dados 2005/2006 2.9 Comparação do desempenho das Estruturas de Dados 3. Junção (Join) Depois de analisar individualmente cada uma destas estruturas de dados é importante fazer uma comparação entre os seus desempenhos. Existe uma primeira divisão entre os algoritmos de funções de hash e os algoritmos de árvores. Os arrays são colocados de parte dado que, apesar de serem a estrutura de dados com melhor desempenho em termos de armazenamento, o desempenho em termos de velocidade de pesquisa e principalmente de actualização não os torna viáveis para serem adoptados como estrutura de dados eficiente, salvo em casos em que o volume de dados a guardar seja mesmo baixo. [1] No que toca aos algoritmos de manipulação de funções de hash, qualquer um deles apresenta um bom desempenho ao efectuar uma pesquisa. No que toca às actualizações, à excepção do Linear Hashing são eficientes. No caso do Linear Hashing, a sua optimização em termos de espaço tem como custo o seu desempenho em termos de velocidade de actualização. Em termos de espaço de armazenamento, a estrutura de dados que ocupa menos espaço é precisamente o Linear Hashing. O espaço ocupado pelas restantes estruturas de dados deixa algo a desejar podendo ocupar o dobro do espaço realmente ocupado pelos dados. [1] A análise aos algoritmos de árvores mostra que os desempenhos alcançados pelas B+ Trees e T Trees quer em termos de pesquisa, quer em termos de actualização e custo de armazenamento, são bons. Apenas as Árvores Binárias apresentam um desempenho fraco em termos do custo de armazenamento, podendo ficar a ocupar o triplo do espaço ocupado pelos dados nela guardados. [1] Não se pode dizer, com certeza, que uma estrutura de dados é melhor que outra em todas as situações possíveis. Os Sistemas de Gestão de Base de Dados, ao processar uma interrogação terão de optar pelo que indicie melhor desempenho, tendo de ser ponderados certos parâmetros como a quantidade de dados a guardar e/ou a pesquisar, o espaço disponível, entre outros. Por último temos o facto de que uma interrogação SQL com um Select pode ser de dois tipos: de igualdade ou desigualdade. É consensual que os algoritmos de funções de hash apresentam melhor desempenho para tratar igualdades. Os algoritmos de árvores são mais adequados a desigualdades. Tecnologias de Base de Dados Outro tipo de interrogações SQL é a junção, que consiste em seleccionar informação de duas tabelas ou mais, de acordo com pelo menos um atributo comum, como por exemplo: Select A.nome, N.nota From Alunos A, NotasTBD N Where a.numero = n.numeroAluno Para tal existem diversos algoritmos entre os quais se incluem: o Nested Loops Join, o Sort Merge Join ou o Tree Merge Join, o Hash Join ou o Tree Join. Nesta secção analisaremos o desempenho destes algoritmos ao nível do tempo de cálculo dos resultados. Para tal, são tidos em consideração aspectos como por exemplo a dimensão das tabelas, variando a dimensão das relações, a percentagem de duplicados existente nas tabelas e a sua distribuição (uniforme ou enviesada). 3.1 Nested Loops Join A primeira aproximação possível ao cálculo de resultados de junções é o Nested Loops Join que consiste em, para cada um dos tuplos de uma tabela, percorrer todos os tuplos da outra tabela e verificar um a um quais os tuplos que obedecem à condição de igualdade. [1] Este algoritmo tem um tempo de execução máximo para a situação em que ambas as tabelas têm o mesmo número de tuplos, de O (N²) em que N é o número de tuplos de uma tabela. Mesmo para relações com poucos tuplos (da ordem das centenas ou milhares) o tempo dispendido no processamento não permite que o Nested Loops Join seja considerado como muito eficiente. [1] 3.2 Sort Merge Join A ideia base do Sort Merge Join é a ordenação prévia das relações para procurar que tuplos com valores iguais fiquem contíguos. Deste modo os valores iguais ficam agrupados em partições. Assim poder-seão comparar as partições em vez dos tuplos individuais. Os benefícios desta aproximação só serão sentidos se as tabelas contiverem valores duplicados. Para efectuar o Sort Merge Join recorre-se a arrays e ordenam-se as relações recorrendo ao algoritmo de ordenação em memória principal quicksort. [1] 7 Tecnologias de Base de Dados Dadas duas relações R e S, o algoritmo começa por ordenar as duas relações e identificar as partições. De seguida, é feita uma pesquisa com o primeiro tuplo de cada relação. Vai se avançando na relação R enquanto o tuplo de R for menor que o tuplo de S, aproveitando-se assim a ordenação previamente efectuada. Analogamente, avança-se na relação S enquanto o valor do atributo de join for menor que o valor de R. Vai se alternando a pesquisa até se encontrar os valores pretendidos. [1,3,4] O algoritmo de Sort Merge Join mostra-se eficiente para tratar junções de relações com um valor elevado de duplicados, quer estes existam numa distribuição uniforme, quer numa distribuição enviesada. Esta característica deve-se ao facto de se agruparem os valores duplicados em partições. Em relações em que o número de valores duplicados não seja muito significativo, o desempenho em termos de tempo do Sort Merge Join não é tão bom devido à ordenação das relações, que acaba por ser a tarefa mais demorada. [1,4] Uma vez que a estrutura de dados utilizada no Sort Merge Join é o Array, o desempenho em termos de espaço de armazenamento é eficiente porque o espaço ocupado por um Array corresponde ao espaço ocupado pelos elementos de dados. [1] 3.3 Tree Merge Join O Tree Merge Join, conceptualmente, é semelhante ao Sort Merge Join com a diferença de que ao invés de utilizar arrays como estrutura de dados se recorre a uma T Tree. 2005/2006 3.4 Hash Join Para efectuar junções podemos também recorrer ao Hash Join. O Hash Join começa por particionar as tabelas, à semelhança do Sort Merge Join, mas em vez de recorrer a um algoritmo de ordenação recorre a uma função de hash. De seguida, na chamada fase de probing, aplica-se uma segunda função de hash às partições das relações. Desta forma teremos a garantia que os elementos de dados existentes na partição de uma relação farão junção com os elementos de dados da partição com o mesmo valor da outra relação. Na Figura 13 temos um exemplo da fase de probing, em que é aplicada uma função de hash h2. Após a aplicação de h2 se R e S tiverem uma partição com o mesmo valor de hash, isso significa que os elementos de dados dessa partição poderão ser juntos. [1,3] Figura 13 – Fase de Probing do Hash Join O Hash Join tem um desempenho, regra geral, inferior ao Tree Merge Join se já estiverem criadas as estruturas de dados T Tree. Contudo, se não existir pelo menos uma das T Tree o algoritmo de Hash Join demonstra ser o mais eficiente, porque constrói-se mais rapidamente uma tabela de hash que uma T Tree. O Hash Join tem, no entanto, uma limitação que é o facto de apenas ser aplicável a junções naturais. [1,2,3,4] O Tree Merge Join obtém um melhor desempenho que o Sort Merge Join, desde que o número de duplicados não seja muito elevado, dado que num Array os valores duplicados ficam contíguos, enquanto numa estrutura em árvore terão de ser seguidos os ponteiros que ligam os elementos de dados. Outro resultado importante do algoritmo de Tree Merge Join é que não é afectado pelo facto da relação interior ser muito maior que a relação exterior, ou vice-versa. [1] No que toca ao espaço de armazenamento as tabelas de hash do Hash Join são eficientes. Existe uma grande desvantagem na utilização do Tree Merge Join que é o facto de apenas apresentar melhores resultados caso as estruturas de dados já estejam construídas porque o tempo de construção de uma T Tree é substancialmente superior ao tempo de construção dos Arrays do Sort Merge Join. [1] Em termos de desempenho, o Tree Join é inferior ao Hash Join e, por vezes inferior ao desempenho do Sort Merge Join com excepção de uma situação. Se pelo menos uma das relações já estiver inserida numa T Tree e a outra relação não for muito grande (cerca de metade dos elementos de dados na T Tree), então o Tree Join será mais rápido que o Hash Join. O tempo de construção da segunda T Tree será inferior ao tempo de construção das duas tabelas de hash. [1] Em termos de espaço de armazenamento o desempenho das T Trees não é tão eficiente como os Arrays, mas é aceitável. [1] 3.5 Tree Join O Tree Join é um algoritmo similar ao Hash Join, apenas diferindo no facto de usar uma T Tree como estrutura de dados. Novamente o armazenamento Tecnologias de Base de Dados desempenho do está directamente espaço ligado de ao 8 Tecnologias de Base de Dados desempenho das T Trees, que apesar de não serem as melhores, também não são comprometedoras. 3.6 Comparação No que toca à operação de junção, tal como com a selecção, não se pode dizer que exista um algoritmo que obtenha um desempenho superior a todos os outros em qualquer situação, mas é possível caracterizar melhor quais os pontos fortes de cada algoritmo. Caso já existam estruturas de dados com os elementos de dados das relações envolvidas na junção tipicamente o algoritmo com melhor desempenho é o Tree Merge. Se isso não acontecer o Hash Join será a melhor escolha porque o tempo de construir uma estrutura de hash é inferior ao tempo de construção de uma árvore. O Sort Merge Join mostra-se uma opção viável em duas situações: (i) se o número de duplicados for elevado, (ii) se o volume de tuplos a analisar for elevado; dado que o Sort Merge Join é o algoritmo que lida melhor com quantidades grandes de informação. [1] 2005/2006 duplicados. Enquanto o volume de duplicados for reduzido, o desempenho é directamente influenciado pelo tempo de construção da estrutura de dados. O algoritmo de Hashing tem um custo de construção linear em relação ao volume de dados. O algoritmo de Sorting tem um custo de O (|R| log|R|). Quando o volume de elementos duplicados se torna elevado e o tempo de construção da estrutura de dados deixa de ser o factor preponderante, o melhor desempenho passa a ser alcançado pelo algoritmo de Hashing. O Hashing elimina os duplicados logo durante a fase de construção da tabela de hash enquanto o Sorting apenas o faz após o array já estar ordenado. [1,3] 5. Agregação (Aggregate) Temos ainda um tipo de interrogações SQL que é a agregação. Um exemplo simples de uma interrogação com recurso à agregação é: Select Avg (idade) From Alunos Group By curso Neste exemplo procura-se obter a média de idades dos alunos agrupados por curso. 4. Projecção (Projection) A projecção é outro tipo de interrogações SQL, do qual se segue um exemplo: Select Distinct A.nome From Alunos A Em que se procura saber quais os nomes, sem repetição, dos alunos da relação Alunos, sendo que a tabela Alunos contém mais atributos para além do nome. A implementação da projecção passa por duas fases: (i) remoção dos atributos não desejados, (ii) eliminação de tuplos duplicados que daí resultem. Esta segunda parte da remoção de duplicados é a fase crítica em termos de desempenho. [1,3] Existem dois tipos de algoritmos que podem ser utilizados para efectuar projecções: Sorting e Hashing. O Sorting consiste em introduzir todos os elementos num array, ordená-los e de seguida remover os duplicados. O Hashing recorre a uma função de hash para colocar os elementos numa estrutura de hash, eliminando logo aí os elementos duplicados. [1,3] O critério que decide qual destes dois algoritmos obterá melhor desempenho é a quantidade de Tecnologias de Base de Dados O algoritmo da agregação passa por processar todos os tuplos da relação e ir guardando informação adicional que permita calcular o valor final após o processamento de todos os dados. [1,3] Para interrogações com group by, a fase de agrupamento pode ser efectuada com recurso aos algoritmos de sorting e hashing, com os prós e contras já referidos na secção 5. [1,3] 6. Conclusões O objectivo deste artigo é dar a conhecer algumas das alternativas existentes para processar as interrogações em memória principal. Estas são algumas das técnicas à disposição dos programadores de Sistemas de Gestão de Bases de Dados em termos dos operadores de interrogações SQL, bem como explicitar alguns dos pontos fortes e fracos dos algoritmos. Estas técnicas foram analisadas apenas do ponto de vista do desempenho e custos de armazenamento partindo de uma situação em que os dados já estavam em memória principal. Mas se considerarmos a tarefa de colocar em memória principal os dados que estiverem em memória secundária como algo 9 Tecnologias de Base de Dados constante, alguns destes resultados continuarão a ser verdade. Apesar de alguns destes algoritmos já terem décadas ainda hoje, com a capacidade tecnológica à nossa disposição, ainda são bastante eficazes. A identificação dos pontos fortes e fracos (tolerância a valores duplicados, dimensão das relações, etc.) dos algoritmos é muito importante. Se conseguirmos identificar quais os factores que devem ser analisados aquando da decisão do algoritmo a implementar isso poderá levar-nos a desenhar planos de processamento de interrogações mais eficazes. Se conseguirmos escolher os melhores planos, estaremos a optimizar o desempenho quer das interrogações, quer das Bases de Dados no seu todo. Referências 1. “Query Processing in Main Memory Database Management Systems” - Tobin J. Lehman & Michael J. Carey, 1986 2. “Implementation Techniques for Main Memory Database Systems” - David J. de Witt, Randy H. Katz, Frank Olken, Leonard D. Shapiro, Michael R. Stonebraker, David Wood, 1984 3. “Database Management Systems”, 2nd Edition Mcgraw Hill 4. “Join Processing in Database with Large Main Memories” - Leonard D. Shapiro, 1986 5. “A Study of Index Structures for Main Memory Database Management Systems” - Tobin J. Lehman, Michael J. Carey Tecnologias de Base de Dados 10 2005/2006