246
74 Introdução Um Sistema Gerenciador de Banco de Dados (SGBD) é constituído por um conjunto de dados associados a um conjunto de programas para acesso a esses dados. O conjunto de dados, comumente chamado banco de dados, contém informações sobre uma empresa em particular. O principal objetivo de um SGBD é proporcionar um ambiente tanto conveniente quanto eficiente para a recuperação e armazenamento das informações dos bancos de dados. Sistemas de banco de dados são projetados para gerir grandes volumes de informações. O gerenciamento de informações implica a definição das estruturas de armazenamento de informações e a definição dos mecanismos para a manipulação dessas informações. Ainda, um sistema de banco de dados deve garantir a segurança das informações armazenadas contra eventuais problemas com o sistema, além de impedir tentativas de acesso não autorizadas. Se os dados são compartilhados por diversos usuários, o sistema deve evitar a ocorrência de resultados anômalos. A importância da informação na maioria das organizações – que estabelece o valor do banco de dados – tem determinado o desenvolvimento de um grande conjunto de conceitos e técnicas para a administração eficaz desses dados. Objetivos de um Sistema de Banco de dados Considere a área de um banco responsável por todas as informações de seus cliente e de suas contas- poupança. Um modo de guardar as informações no computador é armazená-las em sistemas de arquivos permanentes. Para permitir aos usuários a utilização dessas informações, o sistema deve apresentar um conjunto de programas de aplicações que tratam esses arquivos, incluindo: Um programa para débito e crédito na contabilidade. Um programa para incluir novos registros na contabilidade. Um programa para balanço da contabilidade. Um programa para gerar relatórios mensais. Essas aplicações foram desenvolvidas por programadores a fim de atender às necessidades das organizações bancárias. Novos programas foram incorporados a esses sistemas para atender a necessidades que foram surgindo. Por exemplo, suponha que novas regras sejam promulgadas pelo governo obrigando que os bancos ofereçam meios para a checagem de suas contas. Com isso, novos arquivos permanentes serão criados contendo dados para checagem de todas as contas mantidas pelo banco e novos programas de aplicações serão necessários a fim de adequar-se a nova situação, que de fato não foi originada pela caderneta de poupança, como o tratamento de saldos negativos. Assim, conforme passa o tempo, mais arquivos e mais programas de aplicações são adicionados ao sistema. O sistema de processamento de arquivos típico que acabamos de descrever pode ser aceito pelos sistemas operacionais convencionais. Registros permanentes são armazenados em vários arquivos e diversos programas de aplicação são escritos para extrair e gravar registros nos arquivos apropriados. Antes do advento dos SGBDs, as organizações usavam esses sistemas para armazenar informações. Obter informações organizacionais em sistemas de processamento de arquivos apresenta numerosas desvantagens: Inconsistência e redundância de dados. Já que arquivos e aplicações são criados mantidos por diferentes programadores, em geral durando longos períodos de tempo, é comum que os arquivos possuam formatos diferentes e os programas sejam escritos em diversas linguagens de programação. Ainda mais, a mesma informação pode ser repetida em diversos lugares (arquivos). Por exemplo, o endereço e o telefone de um cliente em particular pode aparecer tanto no arquivo de contas quanto no arquivo de checagem de contas. Esta redundância aumenta os custos de armazenamento e acesso. Ainda, pode originar inconsistência de dados; isto é, as várias cópias dos dados poderão divergir ao longo do tempo.

Banco Dados Completa

  • Upload
    sancrey

  • View
    528

  • Download
    5

Embed Size (px)

Citation preview

74

Introdução

Um Sistema Gerenciador de Banco de Dados (SGBD) é constituído por um conjunto de dados associados a

um conjunto de programas para acesso a esses dados. O conjunto de dados, comumente chamado banco de

dados, contém informações sobre uma empresa em particular. O principal objetivo de um SGBD é proporcionar

um ambiente tanto conveniente quanto eficiente para a recuperação e armazenamento das informações dos

bancos de dados.

Sistemas de banco de dados são projetados para gerir grandes volumes de informações. O gerenciamento

de informações implica a definição das estruturas de armazenamento de informações e a definição dos

mecanismos para a manipulação dessas informações. Ainda, um sistema de banco de dados deve garantir a

segurança das informações armazenadas contra eventuais problemas com o sistema, além de impedir tentativas

de acesso não autorizadas. Se os dados são compartilhados por diversos usuários, o sistema deve evitar a

ocorrência de resultados anômalos.

A importância da informação na maioria das organizações – que estabelece o valor do banco de dados –

tem determinado o desenvolvimento de um grande conjunto de conceitos e técnicas para a administração eficaz

desses dados.

Objetivos de um Sistema de Banco de dados

Considere a área de um banco responsável por todas as informações de seus cliente e de suas contas-

poupança. Um modo de guardar as informações no computador é armazená-las em sistemas de arquivos

permanentes. Para permitir aos usuários a utilização dessas informações, o sistema deve apresentar um conjunto

de programas de aplicações que tratam esses arquivos, incluindo:

• Um programa para débito e crédito na contabilidade.

• Um programa para incluir novos registros na contabilidade.

• Um programa para balanço da contabilidade.

• Um programa para gerar relatórios mensais.

Essas aplicações foram desenvolvidas por programadores a fim de atender às necessidades das

organizações bancárias.

Novos programas foram incorporados a esses sistemas para atender a necessidades que foram surgindo.

Por exemplo, suponha que novas regras sejam promulgadas pelo governo obrigando que os bancos ofereçam

meios para a checagem de suas contas. Com isso, novos arquivos permanentes serão criados contendo dados

para checagem de todas as contas mantidas pelo banco e novos programas de aplicações serão necessários a fim

de adequar-se a nova situação, que de fato não foi originada pela caderneta de poupança, como o tratamento de

saldos negativos. Assim, conforme passa o tempo, mais arquivos e mais programas de aplicações são adicionados

ao sistema.

O sistema de processamento de arquivos típico que acabamos de descrever pode ser aceito pelos

sistemas operacionais convencionais. Registros permanentes são armazenados em vários arquivos e diversos

programas de aplicação são escritos para extrair e gravar registros nos arquivos apropriados. Antes do advento

dos SGBDs, as organizações usavam esses sistemas para armazenar informações.

Obter informações organizacionais em sistemas de processamento de arquivos apresenta numerosas

desvantagens:

• Inconsistência e redundância de dados. Já que arquivos e aplicações são criados mantidos por diferentes

programadores, em geral durando longos períodos de tempo, é comum que os arquivos possuam

formatos diferentes e os programas sejam escritos em diversas linguagens de programação. Ainda mais, a

mesma informação pode ser repetida em diversos lugares (arquivos). Por exemplo, o endereço e o

telefone de um cliente em particular pode aparecer tanto no arquivo de contas quanto no arquivo de

checagem de contas. Esta redundância aumenta os custos de armazenamento e acesso. Ainda, pode

originar inconsistência de dados; isto é, as várias cópias dos dados poderão divergir ao longo do tempo.

75

Por exemplo, a mudança de endereço de um cliente pode refletir nos arquivos de contas, mas não ser

alterada no sistema como um todo.

• Dificuldade de acesso aos dados. Suponha que um dos empregados da empresa precise de uma relação

com os nomes de todos os cliente que moravam em determinada área de acidade cujo CEP é 78733. O

empregado pede, então, ao departamento de processamento de dados que crie tal relação. Como esse

tipo de solicitação não foi prevista no projeto do sistema não há nenhuma aplicação disponível para

atende-la. No entanto, há uma aplicação para gerar a relação de todos os clientes da empresa. Assim, o

empregado tem duas alternativas: separar manualmente da lista de todos os clientes aqueles de que

necessita ou requisitar ao departamento de processamento de dados um programador para escrever o

programa necessário. Ambas as alternativas são, obviamente, insatisfatórias. Suponha que o tal programa

seja escrito e que dias depois o mesmo empregado necessite selecionar os clientes que possuem saldo

superior a dez mil dólares. Como esperado, tal programa não existe. Novamente o empregado tem as

mesmas duas opções, nenhuma delas insatisfatória. O fato é que um ambiente com um sistema de

processamento de arquivos convencional não atende às necessidades de recuperação de informações de

modo eficiente. Sistemas mais efetivos (com respostas mais rápidas e adequadas) para a recuperação de

informações precisam ser desenvolvidos.

• Isolamento de dados. Como os dados estão dispersos em vários arquivos, e estes arquivos podem

apresentar diferentes formatos, é difícil escrever novas aplicações para recuperação apropriada dos

dados.

• Problema de integridade. Os valores dos dados atribuídos e armazenados em um banco de dados devem

satisfazer certas restrições para manutenção da consistência. Por exemplo, o balanço de uma conta

bancaria não pode cair abaixo de um determinado valor. Os programadores determinam o cumprimento

dessas restrições por meio da adição de código apropriado aos vários programas de aplicações.

Entretanto, quando aparecem novas restrições é difícil alterar todos os programas para incrementá-las. O

problema é ampliado quando as restrições atingem diversos itens de dados em diferentes arquivos.

• Problemas de atomicidade. Um sistema computacional, como qualquer outro dispositivo mecânico ou

elétrico, está sujeito a falhas. Em muitas aplicações é crucial assegurar que, uma vez detectada uma falha,

os dados sejam salvos em seu último estado consistente, anterior a ela. Considere um programa para

transferir 50 dólares da conta A para uma conta B. Se ocorrer falha no sistema durante sua execução, é

possível que os 50 dólares sejam debitados da conta A sem serem creditados na conta B, criando um

estado inconsistente no banco de dados. Logicamente, é essencial para a consistência do banco de dados

que ambos, débito e crédito, ocorram ou nenhum deles seja efetuado. Isto é, a transferência de fundos

deve ser uma operação atômica – deve ocorrer por completo ou não ocorrer. É difícil garantir essa

propriedade em um sistema convencional de processamento de arquivos.

• Anomalias no acesso concorrente. Muitos sistemas permitem atualizações simultâneas nos dados para

aumento do desempenho do sistema como um todo e para melhores tempos de resposta. Nesses tipos

de ambiente, a interação entre atualizações concorrentes pode resultar em inconsistência de dados.

Suponha que o saldo de uma conta bancária A seja 500 dólares. Se dois clientes retiram fundos da conta

A (digamos 50 e 100 dólares, respectivamente), essas operações, ocorrendo simultaneamente, podem

resultar em erro (ou gerar inconsistência). Suponha que, na execução dos programas, ambos os clientes

leiam o saldo antigo e retirem, cada um, seu valor correspondente, sendo o resultado armazenado. Os

dois programas concorrendo, ambos leem o valor 500 dólares, resultado, em 450 e 400 dólares,

respectivamente. Dependendo de qual deles registre seu resultado primeiro, o saldo da conta A será 450

ou 400 dólares, em vez do valor correto de 350 dólares. Para resguardar-se dessa possiblidade, o sistema

deve manter algum tipo de supervisão. Como os dados podem sofrer acesso de diferentes programas, os

quais não foram coordenados previamente, a supervisão é bastante dificultada.

• Problemas de segurança. Nem todos os usuários de banco de dados estão autorizados ao acesso a todos

os dados. Por exemplo, em um sistema bancário, os funcionários do departamento pessoal devem ter

76

acesso apenas ao conjunto de pessoas que trabalham no banco. Uma vez que os programas de aplicação

são inseridos no sistema como um todo, é difícil garantir a efetividade das regras de segurança.

Estas dificuldades, entre outras, provocaram o desenvolvimento dos SGBDs. A seguir mostraremos os

conceitos e algoritmos que foram desenvolvidos para os sistemas de banco de dados que resolvem os problemas

mencionados anteriormente. Uma aplicação típica em banco de dados armazena um grande número de registros,

sendo que estes registros são frequentemente simples e pequenos.

Visão dos Dados

Um SGBD é uma coleção de arquivos e programas inter-relacionados que permitem ao usuário o acesso

para consultas e alterações desses dados. O maior benefício de um banco de dados é proporcionar ao usuário

uma visão abstrata dos dados. Isto é, o sistema acaba por ocultar determinados detalhes sobre a forma de

armazenamento e manutenção desses dados.

Abstração de Dados

Para que se possa usar um sistema, ele precisa ser eficiente na recuperação de informações. Esta

eficiência está relacionada à forma pela qual foram projetadas as complexas estruturas de representação desses

dados no banco de dados. Já que muitos dos usuários dos sistemas de banco de dados não são treinados em

computação, os técnicos em desenvolvimento de sistemas omitem essa complexidade desses usuários por meio

dos diversos níveis de abstração de modo a facilitar a interação dos usuários com o sistema:

• Nível físico. É o mais baixo nível de abstração que descreve como esses dados estão de fato

armazenados. No nível físico, estruturas de dados complexas de nível baixo são descritas em detalhes.

• Nível lógico. Este nível médio de abstração descreve quais dados estão armazenados no banco de dados

e quais os inter-relacionamentos entre eles. Assim, o banco de dados como um todo é descrito em

termos de um número relativamente pequeno de estruturas simples. Embora a implementação dessas

estruturas simples no nível lógico possa envolver estruturas complexas no nível físico, o usuário do nível

lógico não necessariamente precisa estar familiarizado com essa complexidade. O nível lógico de

abstração é utilizado pelos administradores do banco de dados que precisam decidir quais informações

devem pertencer ao banco de dados.

• Nível de visão. O mais alto nível de abstração descreve apenas parte do banco de dados. A despeito das

estruturas simples do nível lógico, alguma complexidade permanece devido ao tamanho dos banco de

dados. Muitos dos usuários do banco de dados não precisam conhecer todas as suas informações. Pelo

contrário, os usuários normalmente utilizam apenas parte do banco de dados. Assim, para que estas

interações sejam simplificadas, um nível de visão é definido. O sistema pode proporcionar diversas visões

do mesmo banco de dados.

77

O inter-relacionamento entre esses três níveis de abstração está ilustrado na fig. 1.1.

Uma analógica com o conceito de tipos de dados em linguagens de programação pode ajudar a esclarecer

a distinção entre os níveis de abstração. As linguagens de programação de mais alto nível dão suporte à noção de

tipos de dados. Por exemplo, nas linguagens semelhantes ao Pascal, podemos declarar um registro como se

segue:

Esse código define um novo registro chamado cliente com quatro campos. Cada campo tem um nome e

um tipo a ele associado. Um banco pode ter diversos tipos de registro, incluindo:

• conta, com os campos número_conta e saldo;

• empregado, com os campos nome_empregado e salario.

No nível físico, um registro de cliente, conta ou empregado pode ser descrito como um bloco consecutivo

de memória (p.e., palavras ou bytes). O compilador esconde esse nível de detalhes dos programadores.

Analogamente, o sistema de banco de dados esconde muitos dos detalhes de armazenamento em nível mais

baixo dos programadores do banco de dados. Os administradores de banco de dados podem estar familiarizados

com certos detalhes da organização física dos dados.

No nível lógico, cada registro é descrito por um tipo definido, como ilustrado no segmento de código de

programa visto, assim como é definida a inter-relação entre esses tipos de registros. Os programadores trabalham

com a linguagem de programação nesse nível de abstração. Da mesma forma, os administradores de banco de

dados, usualmente, trabalham nesse nível de abstração.

Finalmente, no nível de visão, os usuários do computador veem um conjunto de programas de aplicação

que esconde os detalhes dos tipos de dados. Nesse nível, algumas visões do banco de dados são definidas e os

usuários têm acesso a essas visões. Mais do que esconder detalhes próprios do nível lógico, essas visões também

fornecem mecanismos de segurança, de modo a restringir o acesso dos usuários a determinadas partes do banco

de dados. Por exemplo, em um banco, as telefonistas devem ter acesso apenas às informações dos extratos

bancários dos clientes, não devem ter acesso a informações salariais dos empregados do banco.

Instâncias e Esquemas

Um banco de dados muda ao longo do tempo por meio das informações que neles são inseridas ou

excluídas. O conjunto de informações contidas em determinado banco de dados, em um dado momento é

chamado instância do banco de dados. O projeto geral do banco de dados é chamado esquema. Os esquemas são

alterados com pouca frequência.

Analogias com conceitos de linguagem de programação, como tipos de dados, variáveis e valores, são

úteis aqui. Voltando à definição do registro clientes, note que, na declaração de seu tipo, não definimos qualquer

variável. Para declarar uma variável em linguagens semelhantes ao Pascal, escrevemos

var cliente1: cliente;

A variável cliente1 corresponde agora a uma área de memória que contém um registro tipo cliente.

Um esquema de banco de dados corresponde à definição do tipo em uma linguagem de programação.

Uma variável de um tipo tem um valor em particular em dado instante. Assim, esse valor corresponde a uma

instância do esquema do banco de dados.

Os sistemas de banco de dados apresentam diversos esquemas, referentes aos níveis de abstração que

discutimos. No nível mais baixo há o esquema físico; no nível intermediário, o esquema lógico; e no nível mais

78

alto, os subesquemas. Em geral, os sistemas de banco de dados dão suporte a um esquema físico, um esquema

lógico e vários subesquemas.

Independência de Dados

Os sistemas de banco de dados apresentam diversos esquemas, referentes ao níveis de abstração já

discutidos. No nível mais baixo há o esquema físico; no nível intermediário, o esquema lógico; e no nível mais

alto, os subesquemas. Em geral, os sistemas de banco de dados dão suporte a um esquema físico, um esquema

lógico e vários subesquemas.

Independência de Dados

A capacidade de modificar a definição dos esquemas em determinado nível, sem afetar o esquema

superior, é chamado independência de dados. Existem dois níveis de independência de dados:

1. Independência de dados física é a capacidade de modificar o esquema físico sem que, com isso, qualquer

programa de aplicação precise ser reescrito. Modificações no nível físico são necessárias, ocasionalmente,

para aprimorar o desempenho.

2. Independência de dados lógica é a capacidade de modificar o esquema lógica sem que, com isso,

qualquer programa de aplicação precise ser reescrito. Modificações no nível lógico são necessárias

sempre que uma estrutura lógica do banco de dados é alterada (p.e., quando novas moedas são inseridas

no sistema de um banco).

A independência de dados lógica é mais difícil de ser alcançada que a independência física, uma vez que

os programas de aplicação são mais fortemente dependentes da estrutura lógica dos dados do que de seu acesso.

O conceito de independência de dados é de várias formas similar ao conceito de tipo de dados

empregado nas linguagens modernas de programação. Ambos os conceitos omitem detalhes de implementação

do usuário, permitindo que ele se concentre em sua estrutura geral em vez de se concentrar nos detalhes

tratados no nível mais baixo.

Modelo de Dados

Sob a estrutura do banco de dados está o modelo de dados: um conjunto de ferramentas conceituais

usadas para a descrição dos dados, relacionamentos entre dados, semântica de dados e regras de consistência.

Os vários modelos que vêm sendo desenvolvidos são classificados em três diferentes grupos: modelos lógicos

com base em objetos, modelos lógicos com base em registros e modelos físicos.

Modelos Lógicos com Base em Objetos

Os modelos lógicos com base em objetos são usados na descrição do nível lógico e de visões. São

caracterizados por dispor de recursos de estruturação bem mais flexíveis e por viabilizar a especificação explícita

das restrições de dados. Existem vários modelos nessa categoria, e outros ainda estão por surgir. Alguns são

amplamente conhecidos, como:

• Modelo entidade-relacionamento.

• Modelo orientado a objeto.

• Modelo semântico de dados.

• Modelo funcional de dados.

Modelo Entidade-Relacionamento

O modelo de dados entidade-relacionamento (E-R) tem por base a percepção do mundo real como um

conjunto de objetos básicos, chamados entidades, e do relacionamento entre eles. Uma entidade é uma “coisa”

ou um “objeto” do mundo real que pode ser identificado por outros objetos. Por exemplo, cada pessoa é uma

entidade, as contas dos clientes de um banco também podem ser consideradas entidades. As entidades soa

79

descritas no banco de dados por meio de seus atributos. Por exemplo, os atributos número_conta e saldo

descrevem uma conta bancária em particular. Um relacionamento é uma associação entre entidades. Por

exemplo, um relacionamento depositante associa um cliente a cada conta que ele possui. O conjunto de todas as

entidades de um mesmo tipo, assim como o conjunto de todos os relacionamentos de mesmo tipo são

denominados conjunto de entidades e conjunto de relacionamentos, respectivamente.

Além das entidades e dos relacionamento, o modelo E-R representa certas regras, as quais o conteúdo do

banco de dados precisa respeitar. Uma regra importante é o mapeamento das cardinalidades, as quais expressam

o número de entidades às quais a outra entidade se relaciona por meio daquele conjunto de relacionamentos.

Toda estrutura lógica do banco de dados pode ser expressa graficamente por meio do diagrama E-R, cujo

construtores dos seguintes componentes são:

• Retângulos, que representam os conjuntos de entidades.

• Elipses, que representam os atributos.

• Losangos, que representam os relacionamentos entre os conjuntos de entidades.

• Linhas, que unem os atributos aos conjuntos de entidades e o conjunto de entidades aos seus

relacionamentos.

Cada componente é rotulado com o nome da entidade ou relacionamento que representa. Como ilustração,

considere uma parte do sistema bancário composta pelos clientes e suas respectivas contas. O diagrama E-R

correspondente é mostrado na fig. 1.2.

Modelo Orientado a Objetos

Como o modelo E-R, o modelo orientado a objetos tem por base um conjunto de objetos. Um objeto

contém valores armazenados em variáveis instâncias dentro do objeto. Um objeto também contém conjuntos de

códigos que operam esse objeto. Esses conjuntos de códigos são chamados métodos.

Os objetos que contêm os mesmos tipos de valores e os mesmos métodos são agrupados em classes.

Uma classe pode ser vista como uma definição de tipo para objetos. Essa combinação compacta de dados e

métodos abrangendo uma definição de tipo é similar ao tipo abstrato em uma linguagem de programação.

O único modo pelo qual um objeto pode conseguir acesso aos dados de outro objeto é por meio do

método desse outro objeto. Essa ação é chamada de enviar mensagem ao objeto. Assim, a interface de métodos

de um objeto define a parte externa visível de um objeto. A parte interna de um objeto – as instâncias variáveis e

o código do método – não são visíveis externamente. O resultado são dois níveis de abstração de dados.

Modelos Lógicos com Base em Registros

Modelos lógicos com base em registros são usados para descrever os dados no nível lógico e de visão. Em

contraste com os modelos com base em objetos, este tipo de modelo é usado tanto para especificar a estrutura

lógica do banco de dados quanto para implementar uma descrição de alto nível.

80

Os modelos com base em registro são assim chamados porque o banco de dados é estruturado por meio

de registros de formato fixo de todos os tipos. Cada registro define um número fixo de campos ou atributos, e

cada atributo tem um tamanho fixo.

Modelo Relacional

O modelo relacional usa um conjunto de tabelas para representar tanto os dados com a relação entre

eles. Cada tabela possui múltiplas colunas e cada uma possui um nome único. A fig. 1.3 apresenta um exemplo de

banco de dados relacional condensado em duas tabelas: uma mostrando os clientes do banco e a outra, suas

contas.

Modelo de Rede

Os dados no modelo de rede são representados por um conjunto de registros (como no Pascal) e as

relações entre estes registros são representados por links (ligações), as quais podem ser vistas pelos ponteiros. Os

registros são organizados no banco de dados por um conjunto arbitrário de gráficos. A fig. 1.4 apresenta um

exemplo de banco de dados em rede, usando as mesmas informações da fig. 1.3.

Modelo Hierárquico

O modelo hierárquico é similar ao modelo em rede, pois os dados e suas relações são representados,

respectivamente, por registros e links. A diferença é que no modelo hierárquico os registros estão organizados em

arvores em vez de gráficos arbitrários. A fig. 1.5 apresenta um exemplo de modelo de banco de dados

hierárquico, usando as mesmas informações da fig. 1.4.

81

Diferença entre Modelos

O modelo relacional difere dos modelos hierárquico e em rede por não usar nem ponteiros nem links. Ele

relaciona os registros por valores próprios a eles. Como não é necessário o uso de ponteiros, houve a

possibilidade do desenvolvimento de fundamentos matemáticos para sua definição.

82

Modelos Físicos de Dados

Os modelos físicos de dados são usados para descrevê-los no nível mais baixo. Em contraste com os

modelos lógicos, há poucos modelos físicos de dados em uso. Dois deles são amplamente conhecidos: o modelo

unificado (unifying model) e o modelo de partição de memória (frame-memory model).

Os modelos físicos captam os aspectos de implementação do sistema de banco de dados.

Linguagens de Banco de Dados

Um sistema de banco de dados proporciona dois tipos de linguagens: uma específica para os esquemas

do banco de dados e outra para expressar consultas e atualizações.

Linguagens de Definicao de Dados

Um esquema de dados é especificado por um conjunto de visões expressas por uma linguagem especial

chamada linguagem de definição de dados (data-definition language – DDL). O resultado da compilação dos

parâmetros DDLs é armazenado em um conjunto de tabelas que constituem um arquivo especial chamado

dicionário de dados ou diretório de dados.

Um dicionário de dados é um arquivo de metadados – isto é, dados a respeito de dados. Em um sistema

de banco de dados, esse arquivo ou diretório é consultado antes que o dado real seja modificado.

A estrutura de memória e o método de acesso usados pelo banco de dados são especificados por um

conjunto de definições em um tipo especial de DDL, chamado linguagem de definição e armazenamento de dados

(data storage and definition language). O resultado da compilação dessas definições é um conjunto de instruções

para especificar os detalhes de implementação dos esquemas do banco de dados – os detalhes normalmente são

ocultados dos usuários.

Linguagem de Manipulação dos Dados

Os níveis de abstração já discutidos não se aplicam apenas à definição ou à estrutura dos dados, mas

também a sua manipulação.

Por manipulação de dados entendemos:

83

• A recuperação das informações armazenadas no banco de dados.

• Inserção de novas informações no banco de dados.

• A remoção de informações do banco de dados.

• A modificação das informações do banco de dados.

No nível físico, precisamos definir algoritmos que permitam o acesso eficiente aos dados. Nos níveis mais

altos de abstração, enfatizamos a facilidade de uso. O objetivo é proporcionar uma interação eficiente entre

homens e sistema.

A linguagem de manipulação de dados (DML) é a linguagem que viabiliza o acesso a manipulação dos

dados de forma compatível ao modelo de dados apropriado. São basicamente dois tipos:

• DMLs procedurais exigem que o usuário especifique quais dados são necessários e como obtê-los.

• DMLs não procedurais exige que o usuário especifique quais dados são necessários, sem especificar

como obtê-los.

As DMLs não procedurais são normalmente mais fáceis de aprender e de usar. Entretanto, como o

usuário não especifica como obter os dados, essas linguagens pode gerar código menos eficiente que os gerados

por linguagens procedurais. Podemos resolver esse tipo de problema por meio de várias técnicas de otimização.

Uma consulta é uma solicitação para recuperação de informações. A parte de uma DML responsável pela

recuperação de informação é chamada linguagem de consultas (query language). Embora tecnicamente incorreto,

é comum o uso do termo linguagem de consultas como sinônimo de linguagem de manipulação de dados.

Gerenciamento de Transações

Frequentemente, muitas operações em um banco de dados constituem uma única unidade lógica de

trabalho. Voltamos ao exemplo usado na transferência de fundos entre contas bancárias, responsável pelo débito

na conta A e crédito na conta B. Antes de mais nada, é essencial que ocorram ambas as operações, de crédito e

débito, ou nenhuma delas deverá ser realizada. Isto é, ou a transferência de fundos acontece como um todo ou

nada deve ser feito. Esse tudo-ou-nada é chamado atomicidade. Ainda mais, é necessário que a transferência de

fundos preserve a consistência do banco de dados. Ou seja, a soma de A+B deve ser preservada. Essas exigências

de corretismo são chamadas de consistência. Finalmente, depois da execução com sucesso da transferência de

fundos, os novos valores de A e B devem persistir, a despeito das possibilidades de falhas no sistema. Esta

persistência é chamada durabilidade.

Uma transação é uma coleção de operações que desempenha uma função lógica única dentro de uma

aplicação do sistema de banco de dados. Cada transação é uma unidade de atomicidade e consistência. Assim,

exigimos que as transações não violem nenhuma das regras de consistência do banco de dados. Ou seja, o banco

de dados estava consistente antes do início da transação e deve permanecer consistente após o término com

sucesso de uma transação. Entretanto, durante a execução de uma transação, será necessário aceitar

inconsistências temporariamente. Essa inconsistência temporária, embora necessária pode gerar problemas caso

ocorra uma falha.

É responsabilidade do programador definir, de modo apropriado, as diversas transações, tais que cada

uma preserve a consistência do banco de dados. Por exemplo, a transação para a transferência de fundos da

conta A para a conta B poderia ser composta por dois programas distintos: um para débito na conta A e outro

para crédito na conta B. A execução destes dois programas um após o outro irá manter a consistência do banco

de dados. Entretanto, cada programa executado isoladamente não leva o banco de dados de um para outro

estado inconsistente. Logo, esses programas separados não são transações.

Assegurar as propriedades de atomicidade e durabilidade é também responsabilidade do sistema de

banco de dados – especialmente, os componentes de gerenciamento de transações. Na ausência de falhas, todas

as transações se completam com sucesso e a atomicidade é garantida. No entanto, devido aos vários tipos de

falhas possíveis, uma transação pode não se completar com sucesso. Se estivermos empenhados em garantir a

84

atomicidade, uma transação incompleta não poderá comprometer o estado do banco de dados. Assim, o banco

de dados precisa retornar ao estado anterior em que se encontrava antes do início dessa transação.

É responsabilidade do sistema de banco de dados detectar as falhas e recuperar o banco de dados,

garantindo seu retorno a seu último estado consistente.

Por fim, quando muitas transações atualizam o banco de dados concorrentemente, a consistência do

banco de dados pode ser violada, mesmo que essas transações, individualmente, estejam corretas. É

responsabilidade do gerenciador de controle de concorrência controlar a interação entre transações concorrentes

de modo a garantir a consistência do banco de dados.

Os sistemas de banco de dados projetados para o uso em computadores pessoais podem não apresentar

todas essas funções.

Administração de Memória

Normalmente, os banco de dados exigem um grande volume de memória. Um banco de dados

corporativo é usualmente medido em termos de gigabytes ou, para banco de dados de grande porte (largest

database), terabytes. Um gigabyte corresponde a 1000 megabytes (1 bilhão de bytes) e um terabyte é 1 milhão

de megabytes (1 trilhão de bytes). Já que a memória do computador não pode armazenar volumes tão grandes de

dados, as informações são armazenadas em discos. Os dados são transferidos dos discos para a memória quando

necessário. Uma vez que essa transferência é relativamente lenta comparada à velocidade do processador, é

imperativo que o sistema de banco de dados estruture os dados de forma a minimizar a necessidade de

movimentação entre disco e memória.

O objetivo de um sistema de banco de dados é simplificar e facilitar o acesso aos dados. Visões de alto

nível ajudam a alcançar esses objetivos. Os usuários do sistema não devem desnecessariamente importunados

com detalhes físicos relativos à implementação do sistema. Todavia, um dos fatores mais importantes de

satisfação ou insatisfação do usuário com um sistema de banco de dados é justamente seu desempenho. Se o

tempo de resposta é demasiado, o valor do sistema diminui. O desempenho de um sistema de banco de dados

depende da eficiência das estruturas usadas para a representação dos dados, e do quanto esse sistema está apto

a operar essas estruturas de dados. Como acontece com outras áreas dos sistemas computacionais, não se trata

somente do consumo de espaço e tempo, mas também da eficiência de um tipo de operação sobre outra.

Um gerenciador de memória é um módulo de programas para interface entre o armazenamento de dados

em um nível baixo e consultas e programas de aplicação submetidos ao sistema. O gerenciamento de memória é

responsável pela interação com o gerenciamento de arquivos. Uma linha de dados é armazenada no disco usando

os sistema de arquivos que, convencionalmente, é fornecido pelo sistema operacional. O gerenciador de memória

traduz os diversos comandos DML em comandos de baixo nível de sistema de arquivos. Assim, o gerenciador de

memória é responsável pelo armazenamento, recuperação e atualização de dados no banco de dados.

O Administrador de Banco de Dados

Uma das principais razoes que motivam o uso do SGBDs é o controle centralizado tanto dos dados quanto

dos programas de acesso a esses dados. A pessoa que centraliza esse controle do sistema é chamado

administrador de dados (DBA). Dentre as funções de um DBA destacamos as seguintes:

• Definicao do esquema. O DBA cria o esquema do banco de dados original escrevendo um conjunto de

definições que são transformadas pelo compilador DDL em um conjunto de tabelas armazenadas de

modo permanente no dicionário de dados.

• Esquema e modificações na organização física. Os programadores realizam relativamente poucas

alterações no esquema do banco de dados ou na descrição da organização física de armazenamento por

meio de um conjunto de definições que serão usadas ou pelo compilador DDL ou pelo compilador de

armazenamento de dados e definição de dados, gerando modificações na tabela apropriada, interna ao

sistema (p.e., no dicionário de dados).

85

• Fornecer autorização de acesso ao sistema. O fornecimento de diferentes tipos de autorização no acesso

aos dados permite que o administrador de dados regule o acesso dos diversos usuários às diferentes

partes do sistema. Os dados referentes à autorização de acesso são armazenados em uma estrutura

especial do sistema que é consultada pelo sistema de banco de dados toda vez que o acesso àquele dado

for solicitado.

• Especificação de regras de integridade. Os valores dos dados armazenados no banco de dados devem

satisfazer certas restrições para manutenção de sua integridade. Por exemplo, o número de horas que um

empregado pode trabalhar durante uma semana não deve ser superior a um limite especificado

(digamos, 40 horas). Tal restrição precisa ser explicitada pelo administrador de dados. As regras de

integridade são tratadas por uma estrutura especial do sistema que é consultada pelo sistema de banco

de dados sempre que uma atualização está em curso no sistema.

Usuários de Banco de Dados

A meta básica de um sistema de banco de dados é proporcionar um ambiente para recuperação de

informações e para o armazenamento de novas informações no banco de dados. Há quatro tipos de usuários de

sistemas de banco de dados, diferenciados por suas expectativas de interação com o sistema.

• Programadores de aplicação: são profissionais em computação que interagem com o sistema por meio

de chamadas DML, as quais são envolvidas por programas escritos na linguagem hospedeira (p.e., COBOL,

PL/1, C). Esses programas são comumente referidos como programas de aplicação. Exemplos em um

sistema bancário incluem programas para gerar relação de cheques pagos, para crédito em contas, para

débitos em conta ou para transferência de fundos entre contas.

Uma vez que a sintaxe da DML é, em geral, completamente diferente de uma linguagem hospedeira, as chamadas

DML são, normalmente, precedidas por um caractere especial antes que o código apropriado possa ser gerado.

Um pré-processamento, chamado pré-compilador DML, converte os comandos DML para as chamadas normais

em procedimentos da linguagem hospedeira. O programa resultante é, então, submetido ao compilador da

linguagem hospedeira, a qual gera o código de objeto apropriado.

Existem tipos especiais de linguagem de programação que combinam estruturas de controle de

linguagens semelhantes ao Pascal com estruturas de controle para manipulação dos objetos do banco de dados

(p.e., relações). Estas linguagens, muitas vezes chamadas linguagens de quarta geração, frequentemente incluem

recursos especiais para facilitar a geração de formulários e a apresentação de dados no monitor. A maior parte

dos sistemas de banco de dados comerciais inclui linguagens de quarta geração.

• Usuários sofisticados: interagem com o sistema sem escrever programas. Formulam suas solicitações ao

banco de dados por meio de linguagens de consultas. Cada uma dessas solicitações é submetida ao

processador de consultas cuja função é quebrar as instruções DML em instruções que o gerenciador de

memória entenda. Os analistas que submetem consultas para explorar dados no banco de dados caem

nessa categoria.

• Usuários especialistas: são usuários sofisticados que escrevem aplicações tradicionais especializadas de

banco de dados que não podem ser classificadas como aplicações tradicionais em processamento de

dados. Dentre elas estão os sistemas para projetos auxiliados por computador, sistemas especialistas e

sistemas de base de conhecimento, sistemas que armazenam dados de tipos complexos (por exemplo,

dados gráficos e de áudio) e sistemas para modelagem de ambiente (environment-modeling systems).

• Usuários navegantes: são usuários comuns que interagem com o sistema chamando um dos programas

aplicativos permanentes já escritos, como, por exemplo, um usuário que pede a transferência de 50

dólares da conta A para a B por telefone, usando para isso um programa chamado transfer. Esse

programa pede ao usuário o valor a ser transferido, o número da conta para crédito e o número da conta

para débito.

Visão Geral da Estrutura do Sistema

86

Um sistema de banco de dados está dividido em módulos específicos, de modo a atender a todas as

funções do sistema. Algumas das funções do sistema de banco de dados podem ser fornecidas pelo sistema

operacional. Na maioria das vezes, o sistema operacional do computador fornece apenas as funções essenciais, e

o sistema de banco de dados deve ser construído nessa base. Assim, o projeto do banco de dados deve considerar

a interface entre o sistema de banco de dados e o sistema operacional.

Os componentes funcionais do sistema de banco de dados podem ser divididos pelos componentes de

processamento de consultas e pelos componentes de administração de memória. Os componentes de

processamento de consultas incluem:

• Compilador DML, que traduz comando DML da linguagem de consulta em instruções de baixo nível,

inteligíveis ao componente de execução de consultas. Além disso, o compilador DML tenta transformar a

solicitação do usuário em uma solicitação equivalente, mas mais eficiente, buscando, assim, uma boa

estratégia para execução da consulta.

• Pré-compilador para comandos DML inseridos em programas de aplicação, que convertem comandos

DML em chamadas de procedimentos normais da linguagem hospedeira. O pré-compilador precisa

interagir com o compilador DML de modo a gerar o código apropriado.

• Interpretador DDL, que interpreta os comandos DDL e registra-os em um conjunto de tabelas que

contêm metadados.

• Componentes para o tratamento de consultas, que executam instruções de baixo nível geradas pelo

compilador DML.

Os componentes para administração do armazenamento de dados proporcionam a interface entre os dados de

baixo nível, armazenados no banco de dados, os programas de aplicações e as consultas submetidas ao sistema.

Os componentes de administração de armazenamento de dados incluem:

• Gerenciamento de autorizações e integridade, que testam o cumprimento das regras de integridade e a

permissão ao usuário no acesso ao dado.

• Gerenciamento de transações, que garante que o banco de dados permanecerá em estado consistente

(correto) a despeito de falhas no sistema e que transações concorrentes serão executadas sem conflitos

em seus procedimentos.

• Administração de arquivos, que gerencia a alocação de espaço no armazenamento em disco e as

estruturas de dados usadas para representar estas informações armazenadas em disco.

• Administração de buffer, responsável pela intermediação de dados do disco para a memória principal e

pela decisão de quais dados colocar em memória cache.

Além disso, algumas estruturas de dados são exigidas como parte da implementação física do sistema:

• Arquivo de dados, que armazena o próprio banco de dados.

• Dicionário de dados, que armazena os metadados relativos à estrutura do banco de dados. O dicionário

de dados é muito usado. Portanto, grande ênfase é dada ao desenvolvimento de um bom projeto com

uma implementação eficiente do dicionário.

• Índices, que proporcionam acesso rápido aos itens de dados que são associados a valores determinados.

• Estatísticas de dados, armazenam as informações estatísticas relativas aos dados contidos no banco de

dados. Essas informações são usadas pelo processador de consultas para seleção de meios eficientes para

execução de uma consulta.

87

Modelo Entidade-Relacionamento

O modelo entidade-relacionamento (E-R) tem por base a percepção de que o mundo real é formado por

um conjunto de objetos chamados entidades e pelo conjunto dos relacionamentos entre esses objetos. Foi

desenvolvido para facilitar o projeto do banco de dados, permitindo a especificação do esquema da empresa, que

representa toda estrutura lógica do banco de dados. O modelo E-R é um dos modelos com maior capacidade

semântica; os aspectos semânticos do modelo se referem à tentativa de representar o significado dos dados. O

modelos E-R é extremamente útil para mapear, sobre um esquema conceitual, o significado e interações das

empresas reais. Devido a essa utilidade, muitas das ferramentas de projeto foram concebidas para o modelo E-R.

Conceitos Básicos

Existem três noções básicas empregadas pelo modelo E-R: conjunto de entidades, conjunto de

relacionamentos e os atributos.

Conjunto de Entidades

Um entidade é uma “coisa” ou um “objeto” no mundo real que pode ser identificada de forma unívoca

em relação a todos os outros objetos. Por exemplo, cada pessoa na empresa é uma entidade. Uma entidade tem

um conjunto de propriedades, e os valores para alguns conjuntos dessas propriedades devem ser únicos. Por

exemplo, o número social 677-89-9011 identifica uma única pessoa na empresa. Também, pode-se pensar em

empréstimos como entidades, e o empréstimo número L-15 referente à agência Perryridge identifica

univocamente uma entidade empréstimo. Uma entidade pode ser concreta, como uma pessoa ou um livro, ou

pode ser abstrata, como um empréstimo, uma viagem de férias ou um conceito.

Um conjunto de entidades é um conjunto que abrange entidades de mesmo tipo que compartilham as

mesmas propriedades: os atributos. O conjunto de todas as pessoas que são clientes de um banco de dados, por

exemplo, pode ser definido como o conjunto de entidades clientes. Analogamente, o conjunto de entidades

empréstimo poderia representar o conjunto de todos os empréstimos que o banco em questão viabiliza. As

entidades individuais que constituem um conjunto são chamadas extensões do conjunto de entidades. Assim,

todos os clientes do banco são as extensões do conjunto de entidades cliente.

Os conjuntos de entidades não são necessariamente separados. Por exemplo, é possível definir um

conjunto de entidades com todos os empregados do banco (empregado) e um conjunto de entidades com todos

os clientes do banco (cliente). A entidade pessoa pode pertencer ao conjunto de entidades empregado, ou ao

conjunto de entidades cliente, ou a ambos, ou a nenhum.

Uma entidade é representada por um conjunto de atributos. Atributos são propriedades descritivas de

cada membro de um conjunto de entidades. A designação de um atributo para um conjunto de entidades

expressa que o banco de dados mantém informações similares de cada uma das entidades do conjunto de

entidades; entretanto, cada entidade pode ter seu próprio valor em cada atributo. Atributos possíveis ao

conjunto de entidades clientes são nome_cliente, seguro_social, rua_cliente, e cidade_cliente. Atributos possíveis

para o conjunto de entidades empréstimo são número_empréstimo e conta. Para cada atributo existe um

conjunto de valores possíveis, chamado domínio, ou conjunto de valores, daquele atributo. O domínio do atributo

nome_cliente pode ser o conjunto de todos os textos string de um certo tamanho. Similarmente, o domínio do

atributo número_empréstimo pode ser o conjunto de todos os inteiros positivos.

Desse modo, um banco de dados inclui uma coleção de conjuntos de entidades, cada qual contendo um

número de entidades de mesmo tipo. A fig. 2.1 mostra parte do banco de dados de uma empresa bancária

contendo dois conjuntos entidades: cliente e empréstimo.

88

Formalmente, um atributo de um conjunto de entidades é uma função que relaciona o conjunto de

entidades a seu domínio. Desde que um conjunto de entidades possua alguns atributos, cada entidade pode ser

descrita pelo conjunto formado pelos pares (atributos, valores de dados), um par para cada atributo do conjunto

de entidades. Por exemplo, uma entidade em particular de cliente pode ser descrita pelo conjunto {(nome,

Hayes), (seguro_social, 677-89-9011), (rua_cliente, Main), (cidade_cliente, Harrison)}, o que significa que essa

entidade descreve o cliente Hayes, que possui o seguro social número 677-89-90211 e mora na Rua Main em

Harrison. Podemos notar, a esta altura, uma integração entre o esquema abstrato e a empresa real que está

sendo modelada. Os valores dos atributos que descrevem as entidades constituem uma porção significativa dos

dados que serão armazenados no banco de dados. Um atributo, como é usado no modelo E-R, pode ser

caracterizado pelos seguintes tipos:

• Atributos simples ou compostos. Em nosso exemplo anterior, todos os atributos eram simples: isto é, não

eram divididos em partes. Os atributos compostos, por outro lado, podem ser divididos em partes (isto é,

outros atributos). Por exemplo, nome_cliente pode ser estruturado em prenome, nome_intermediario e

sobrenome. O uso de atributos compostos ajudam-nos a agrupar atributos correlacionados, tornando o

modelo mais claro.

Note que os atributos compostos podem estar hierarquizados. Retornando o exemplo do atributo

composto endereço_cliente, seu atributo rua pode vir a ser subdividido posteriormente em número_rua,

nome_rua, e número_apto. Esses exemplos de atributos compostos para clientes são apresentados na fig. 2.2.

• Atributos monovalorados ou multivalorados. Os atributos usados em nossos exemplos foram todos de

valores simples para uma entidade em particular. Ou seja, o atributo número_empréstimo de uma

entidade específica refere-se apenas a um número de empréstimo. Esses atributos são chamados

monovalorados. Pode haver instâncias em que um atributo possua um conjunto de valores para uma

única entidade. Considere o conjunto de entidades empregado com o atributo nome_dependente.

Qualquer empregado em particular pode ter um, nenhum ou vários dependentes; entretanto, diferentes

entidades empregado dentro do conjunto e empregados terão diferentes números de valores para o

atributo nome_dependente. Esse tipo de atributo é dito multivalorado. Quando necessário, pode-se

estabelecer limites inferiores e superiores para o número de ocorrências em um atributo multivalorado.

Por exemplo, um banco pode ter um número limite de registros de endereços para um cliente normal,

um ou dois endereços. O estabelecimento de limites, neste caso, exprime que o atributo

endereço_cliente do conjunto de entidades cliente pode possuir de zero a dois valores.

• Atributos nulos. Um atributo nulo é usado quando uma entidade não possui valor para determinado

atributo. Por exemplo, se um empregado em particular não possui dependentes, o valor do atributo

89

nome_dependente para esse dependente deverá ser nulo, e isso significa que esse atributo “não é

aplicável”. Nulo também pode significar que o valor do atributo é desconhecido. Um valor desconhecido

pode caracterizar omissão (o valor existe de fato, mas não temos essa informação) ou não conhecimento

(não sabemos se o valor existe de fato). Por exemplo, se o valor do seguro_social de determinado cliente

é nulo, assume-se que seu valor foi omitido, já que é exigido para efeitos de impostos. Um valor nulo para

o atributo número_apartamento pode significar que o número do apartamento foi omitido, ou que o

número existe mas não sabemos qual é, ou que o endereço não é um prédio de apartamentos e,

portanto, não faz parte do endereço do cliente.

• Atributo derivado. O valor desse tipo de atributo pode ser derivado de outros atributos ou entidades a

ele relacionados. Por exemplo, digamos que o conjunto de entidades cliente possui o atributo

empréstimos_tomados, que representa o número de empréstimos tomados do banco por um cliente.

Podemos derivar o valor desse atributo contando o número das entidades empréstimos associadas ao

cliente em questão. Como outro exemplo, consideremos que o conjunto de entidades empregado está

relacionado aos atributos data_contratação e tempo_de_casa, os quais representam o primeiro dia de

emprego no banco e o tempo total que o empregado está trabalhando, respectivamente. O valor do

tempo_de_casa pode ser derivado do valor da data_contratação e tempo_de_casa, os quais representam

o primeiro dia de emprego no banco e o tempo total que o empregado está trabalhando,

respectivamente. O valor do tempo_de_casa pode ser derivado do valor data_contratação e da

data_corrente. Neste caso, a data_contratação pode ser referida como um atributo da base ou um

atributo armazenado.

Um banco de dados para uma empresa bancária pode incluir um número diferente de conjuntos de entidades.

Por exemplo, aliado ao que foi dito sobre clientes e empréstimos, um banco também possui contas, que estão

representadas pelo conjunto de entidades conta com os atributos número_conta e saldo. Também, se um banco

tem um número diferente de agências, então deveríamos captar informações sobre todas essas agências. Cada

conjunto de entidades agência pode ser descrito pelos atributos nome_agência, cidade_agência e fundos.

Conjuntos de Relacionamentos

Um relacionamento é uma associação entre uma ou várias entidades. Por exemplo, podemos definir um

relacionamento que associa o cliente Hayes com o empréstimo L-15. Esse relacionamento especifica que o cliente

Hayes é cliente com o empréstimo número L-15.

Um conjunto de relacionamentos é um conjunto de relacionamentos do mesmo tipo. De modo formal, é a

relação matemática com n≥2 conjunto de entidades (podendo ser não-distintos). Se E1, E2, ..., Em são conjuntos

de entidades, então um conjunto de relacionamentos R é um subconjunto de

em que (e1, e2, ..., en) são relacionamentos.

Considere dois conjuntos de entidades da fig. 2.1, cliente e empréstimo. Definimos o conjunto de

relacionamentos devedor para denotar a associação entre clientes e empréstimos bancários contraídos pelo

clientes. Essa associação é apresentada na fig. 2.3.

90

Como exemplo, consideremos dois tipos de conjuntos de entidades, empréstimo e agência. Podemos

definir o conjunto de relacionamento agência_empréstimo denotando a associação entre um empréstimo

bancário e a agência onde esse empréstimo é mantido.

A associação entre os conjuntos de entidades é referida como uma participação; isto é, o conjunto de

entidade E1, E2, ..., En participa do conjunto de relacionamentos R. Uma instância de relacionamento em um

esquema E-R representa a existência de uma associação entre essa entidade e o mundo real no qual insere a

empresa que está sendo modelada. Ilustramos a entidade cliente chamada Hayes, que possui o seguro social

número 677-89-9011, e a entidade empréstimo L-15 participam na instância do relacionamento devedor.

Essa instância do relacionamento representa que, no mundo real da empresa, uma pessoa chamada

Hayes que possui o seguro social número 677-89-9011 tomou um empréstimo que tem o número L-15.

A função que uma entidade desempenha em um relacionamento é chamada papel. Uma vez que os

conjunto de entidades participantes em um conjunto de relacionamentos são geralmente distintos, papéis são

implícitos e não são, em geral, especificados. Entretanto, são uteis quando o significado de um relacionamento

precisa ser esclarecido. Este é o caso quando os conjuntos de entidades e os conjuntos de relacionamentos mais

de uma vez, em diferentes papéis.

Nesse tipo de conjunto de relacionamentos, que algumas vezes é chamado conjunto de relacionamentos

recursivos, nomes explícitos de papéis são necessários para especificar como uma entidade participa de uma

instância de relacionamento. Por exemplo, considere o conjunto de entidades empregado que mantém

informações sobre todos os empregados do banco. Podemos ter um conjunto de relacionamentos trabalha_para

que é modelado para ordenar os pares de entidades de empregado. O primeiro empregado de um par tem o

papel de gerente, enquanto o outro tem o papel de empregado. Deste modo, todos os relacionamentos de

trabalha_para são caracterizados pelos pares (gerente, empregado); os pares (empregado, gerente) são

excluídos.

Um relacionamento também pode ter atributos descritos. Considere o conjunto de relacionamentos

depositante com o conjunto das entidades cliente e conta. Poderemos associar o atributo data_acesso a essa

relação para especificar a data do último aceso feito pelo cliente em sua conta. O relacionamento depositante

entre as entidades correspondentes ao cliente Jones e à conta A-217 é descrita por {(data-acesso, 23 de maio de

2013)}, a qual significa que o mais recente acesso que Jones fez a sua conta A-217 foi em 23 de maio de 2013.

O conjunto de relacionamentos devedor e agência_empréstimo é um exemplo de conjunto de

relacionamentos binário – isto é, um relacionamento que envolve dois conjuntos de entidades. A maior parte dos

conjuntos de relacionamentos nos sistemas de banco de dados são binários. Ocasionalmente, entretanto, os

conjuntos de relacionamentos envolvem mais de dois conjuntos de entidades. Como exemplo, podemos

combinar os conjuntos de relacionamentos devedor e agência_empréstimo formado o conjunto de

91

relacionamentos CEA, envolvendo os conjuntos de entidades cliente, empréstimo e agência. Assim, o

relacionamento ternário entre as entidades correspondente ao cliente Hayes, o empréstimo número L-15, e a

agência Perryridge especifica que o cliente Hayes tem o empréstimo L-15 na agência Perryridge.

O número de conjuntos de entidades que participa de um conjunto de relacionamento é também o grau

desse conjunto de relacionamento. Um conjunto de relacionamento binário é de grau dois; um relacionamento

ternário é de grau três.

Metas de Projeto

Um conjunto de entidades e um conjunto de relacionamento não são noções precisas e é possível definir

um conjunto de entidades e de relacionamentos entre eles de várias formas diferentes.

Uso de Conjuntos de Entidades ou Atributos

Considere o conjunto de entidades empregado com os atributos nome_empregado e número_telefone.

Pode ser facilmente verificado que o telefone é uma entidade sujeita a seus próprios atributos, como

número_telefone e localização (o escritório onde o telefone está instalado). Sob esse ponto de vista, o conjunto

de entidades deve ser redefinido, conforme segue:

• O conjunto de entidades empregado com o atributo nome_empregado.

• O conjunto de entidades telefone com os atributos número_telefone e localização.

• O conjunto de relacionamentos emp_telefone, o qual denota a associação entre os empregados e os

telefones que podem ter.

Qual é, então, a principal diferença entre essas duas definições de um empregado? No primeiro caso, a definição

implica que todo empregado possui, precisamente, um número de telefone a ele associado. No segundo caso,

entretanto, a definição estabelece que o empregado pode ter vários números de telefones (incluindo zero) a ele

associados. Assim, a segunda definição é mais geral que a primeira e pode refletir com maior precisão as

situações reais.

Mesmos se nos for dado que cada empregado tem, precisamente, um número de telefone a ele

associado, a segunda definição pode, ainda assim, ser mais apropriada, caso um mesmo telefone possa ser

compartilhado por diversos empregados.

Não seria apropriado, no entanto, aplicar a mesma técnica ao atributo nome_empregado; é difícil

sustentar que nome_empregado seja uma entidade por si só (em contraste com telefone). Assim, é apropriado

manter nome_empregado como atributo do conjunto de entidades empregado.

Duas questões aparecem naturalmente: o que constitui um atributo e o que constitui um conjunto de

entidades? Infelizmente, não existe uma resposta simples. As distinções dependem, principalmente, da estrutura

real da empresa que está sendo modelada e da semântica associada aos atributos em questão.

Uso dos Conjuntos de Entidades e Conjunto de Relacionamentos

Nem sempre fica claro se um objeto é melhor expresso por um conjunto de entidades ou por um

conjunto de relacionamentos. Já assumimos anteriormente que um empréstimo bancário é modelado como uma

entidade. Uma alternativa é modelar o empréstimo não como uma entidade, mas como um relacionamento entre

clientes e agências, com número_empréstimo e conta como atributos descritivos. Cada empréstimo é

representado por um relacionamento entre um cliente e uma agência.

Se todo empréstimo é tomado por exatamente um cliente e está associado a exatamente uma agência,

podemos resolver o projeto de modo satisfatório caso o empréstimo seja representado como relacionamento.

Entretanto, com um projeto assim, não poderemos representar convenientemente uma situação na qual vários

clientes tomam um empréstimo conjunto. Precisaremos definir um relacionamento em separado para cada

componente de um empréstimo conjunto. Então, precisaremos replicar os valores dos atributos descritivos,

número_empréstimo e conta, para cada um dos relacionamentos. Dois problemas são consequência dessa

92

replicação: (1) os dados são armazenados diversas vezes, desperdiçando espaço em memória; e (2) as

atualizações deixam, potencialmente, os dados em um estado inconsistente, quando os valores diferem nos

atributos de dois relacionamentos que deveriam, supostamente, possuir valores iguais. O meio pelo qual se

evitam tais replicações é aplicado formalmente pela teoria da normalização.

Uma linha mestra possível na opção pelo uso de um conjunto de entidades ou pelo uso de um conjunto

de relacionamentos é recorrer ao conjunto de relacionamentos para descrever uma ação que ocorre entre

entidades. Essa abordagem pode ser útil também para decidir se certos atributos podem ser expressos de

maneira mais apropriada como relacionamentos.

Conjunto de relacionamentos Binários versus n-ésimos

É sempre possível recompor um conjunto de relacionamentos não-binários (n-ésimos, com n>2) por um

número de conjuntos de relacionamentos binários distintos. Para simplificar, considere o conjunto de

relacionamento ternário (n=3) abstrato R, relacionados aos conjuntos de entidades A, B e C. Poderemos recompor

o conjunto R em um conjunto de entidades E, e criar três conjuntos de relacionamentos:

• RA, relacionando E e A

• RB, relacionando E e B

• RC, relacionando E e C

Se o conjunto de relacionamentos R possui quaisquer atributos, estes são designados pelo conjunto de entidades

E (já que todo o conjunto de entidades deve ter ao menos um atributo para distinguir seus membros). Para cada

relacionamento (ai, bi, ci) do conjunto de relacionamentos R, podemos criar uma nova entidade ei no conjunto de

entidades E. Então, em cada um dos três novos conjuntos de relacionamentos, inserimos um relacionamento,

como segue:

• (ei, ai) em RA

• (ei, bi) em RA

• (ei, ci) em RA

Podemos generalizar esse processo de modo direto para os conjuntos de relacionamentos n-ésimo.

Assim, conceitualmente podemos restringir o modelo E-R para conter apenas conjuntos de relacionamentos

binários. Entretanto, essa restrição nem sempre é desejável.

• Pode ser que seja necessária a criação de um atributo de identificação para o conjunto de entidades

criado para substituir o conjunto de relacionamentos. Este atributo, juntamente com o conjunto extra de

relacionamentos criados, aumenta a complexidade do projeto e as necessidades de armazenamento

como um todo.

• Um conjunto de relacionamentos n-ésimo mostra claramente todos os conjuntos de entidades que

participam de uma determinada relação. O projeto correspondente, usando somente conjunto de

relacionamentos binários, torna mais difícil estabelecer as restrições dessa participação.

Mapeamento de Restrições

O esquema E-R de uma empresa pode definir certas restrições, as quais o conteúdo do banco de dados

deve respeitar.

Mapeamento das Cardinalidades

O mapeamento das cardinalidades, ou rateio de cardinalidades, expressa o número de entidades às quais

outra entidade pode estar associada via um conjunto de relacionamentos.

O mapeamento de cardinalidades é mais útil na descrição dos conjuntos de relacionamentos binários,

embora, ocasionalmente, possam contribuir para a descrição de conjuntos de relacionamentos que envolvam

mais de dois conjuntos de entidades.

93

Para um conjunto de relacionamentos R binário entre os conjuntos de entidades A e B, o mapeamento

das cardinalidades deve seguir uma das instruções abaixo:

• Um para um. Uma entidade em A está associada no máximo a uma entidade em B, e uma entidade em B

está associada a no máximo uma entidade em A (fig. 2.4a).

• Um para muitos. Uma entidade em A está associada a várias entidades em B. Uma entidade em B,

entretanto, pode estar associada a qualquer número de entidades em B e uma entidade em B está

associada a um número qualquer de entidades em A (fig. 2.5a).

O mapeamento apropriado de cardinalidades para um conjunto de relacionamentos em particular é,

obviamente, dependente das situações reais que estão sendo modeladas pelo conjunto de relacionamentos.

Como ilustração, considere o conjunto de relacionamentos devedor. Se, em um banco em particular, um

empréstimo pode se destinar a apenas um cliente e um cliente pode contrair diversos empréstimos, então o

conjunto de relacionamentos entre cliente e empréstimo é de um para muitos. Esse tipo de relacionamento é

apresentado na fig. 2.3. Se um empréstimo puder ser tomado por mais de um cliente (como normalmente

acontece com os vários sócios de um negócio), o relacionamento seria de muitos para muitos.

O rateio de cardinalidades de um relacionamento pode afetar a colocação dos atributos nos

relacionamentos. Atributos em conjuntos de relacionamentos um para um ou um para muitos deve ser

associados a um dos conjuntos de entidades participantes, em vez de serem associados ao conjunto de

relacionamentos. Por exemplo, consideremos depositante como um conjunto de relacionamentos um para

muitos, tal que um cliente pode possuir diversas contas, mas cada conta está vinculada a apenas um cliente.

Nesse caso, o atributo data_acesso poderia estar associado ao conjunto de entidades conta, como mostrados na

94

fig. 2.6; de modo a tornar a figura mais clara, são apresentados apenas alguns dos atributos dos dois conjuntos de

entidades. Já que cada entidade conta participa de um relacionamento com no máximo uma instância de cliente,

fazer esta designação de atributo pode ter o mesmo significado que colocar data_acesso no conjunto de

relacionamentos depositante. Atributos de conjuntos de relacionamentos um para muitos podem apenas ser

reposicionados no conjunto de entidades do lado “muitos” desse relacionamento. Em conjuntos de

relacionamentos um para um, o atributo do relacionamento pode ser associado a qualquer uma das entidades

participantes.

As decisões de projeto, como decidir onde colocar atributos descritivos – como um atributo de entidade

ou relacionamento – devem refletir as características da empresa que está sendo modelada. O projetista pode

optar por manter data_acesso como um atributo de depositante para explicitar que um acesso ocorreu em uma

interação entre os conjuntos de entidades cliente e conta.

A escolha de onde colocar um atributo é mais clara quando se trata de conjuntos de relacionamentos

muitos para muitos. Retornando ao exemplo, especifiquemos o que talvez seja um dos mais realísticos casos de

conjuntos de relacionamentos muitos para muitos, depositante, que expressa que um cliente pode ter uma ou

mais contas e que uma conta pode estar vinculada a um ou mais clientes.

Se quisermos expressar a data do último acesso de um cliente a uma dada conta, o atributo data_acesso

deverá ser atribuído ao conjunto de relacionamentos depositante, em vez de ser alocado a uma das duas

entidades participantes. Se data_acesso fosse atributo de conta, não poderíamos determinar qual dos clientes é

responsável pelo acesso mais recente à conta em questão.

Quando um atributo é determinado pela combinação dos conjuntos de entidades participantes em vez de

estar associado a cada uma das entidades, separadamente, esse atributo precisa ser associado ao conjunto de

relacionamentos muitos para muitos. A colocação de data_acesso como atributo do conjunto de relacionamentos

é mostrada na fig. 2.7; novamente, para tornar a figura mais simples, são apresentados apenas alguns dos

atributos dos dois conjuntos de entidades.

95

Dependência de Existência

Outra classe importante de restrições é a dependência de existência. Especificamente, se a existência da

entidade x depende da existência da entidade y, então x é dito dependente da existência de y. Operacionalmente,

se y for excluído, o mesmo deve acontecer com x. A entidade y é chamada entidade dominante e a x é chamada

entidade subordinada. Como ilustração, considere o conjunto de entidades empréstimo e o conjunto de

entidades pagamento, que mantém todas as informações dos pagamentos realizados para um determinado

empréstimo. O conjunto de entidades pagamento é descrito pelos atributos número_pagamento,

data_pagamento e total_pagamento. Criamos um conjunto de relacionamentos pagamento_empréstimo entre

esses dois conjuntos de entidades pagamento é descrito pelos atributos número_pagamento, data_pagamento e

total_pagamento. Criamos um conjunto de relacionamentos pagamento_empréstimo entre estes dois conjuntos

de entidade que é de um para muitos do empréstimo para o pagamento. Toda entidade pagamento está

associada a uma entidade empréstimo. Se uma entidade empréstimo é excluída, então todas as entidades

pagamento a ela associadas deverão ser excluídas também. Em contraste, uma entidade pagamento pode ser

excluída do banco de dados sem afetar em nada qualquer empréstimo. O conjunto de entidades empréstimo,

portanto, é dominante e pagamento é subordinado ao conjunto de relacionamentos pagamento_empréstimo.

A participação de um conjunto de entidades E no conjunto de relacionamentos R é dita total se todas as

entidades em E participam de pelo menos um relacionamento R. Se somente algumas entidades em E participam

do relacionamento R, a participação do conjunto de entidades em E participam do relacionamento R, a

participação do conjunto de entidades E no relacionamento R é dito parcial. A participação total está

estreitamente relacionada à existência de dependência. Por exemplo, desde que toda entidade pagamento esteja

associada a alguma entidade empréstimo pelo relacionamento pagamento_empréstimo, a participação de

pagamento no conjunto de relacionamentos pagamento_empréstimo é total. Por outro lado, um indivíduo pode

ser cliente de um banco, tendo ou não contraído um empréstimo nele. Daí, é possível que apenas parte do

conjunto de entidades cliente esteja relacionada ao conjunto de entidades empréstimo e a participação de cliente

no conjunto de relacionamentos devedor é, portanto, parcial.

Chaves

96

É importante especificar como as entidades dentro de um dado conjunto de entidades e os

relacionamentos dentro de um conjunto de relacionamentos podem ser identificados. Conceitualmente,

entidades e relacionamentos individuais são distintos, entretanto, na perspectiva do banco de dados, a diferença

entre ambos deve ser estabelecida em termos de seus atributos. O conceito de chave permite-nos fazer tais

distinções.

Conjunto de Entidades

Uma superchave é um conjunto de um ou mais atributos que, tomados coletivamente, nos permitem

identificar de maneira unívoca uma entidade em um conjunto de entidades. Por exemplo, o atributo

seguro_social do conjunto de entidades cliente é suficiente para distinguir uma entidade cliente de outra. Assim,

o seguro_social é uma superchave. Do mesmo modo, a combinação de nome_cliente e seguro_social é

superchave para o conjunto de entidades cliente. O atributo nome_cliente não é superchave de cliente, pois

algumas pessoas podem ter o mesmo nome.

O conceito de superchave não é suficiente para nossos propósitos, já que, como vimos, uma superchave

pode conter atributos externos. Se K é uma superchave, entoa é qualquer superconjunto de K. Nosso interesse é

por superchaves para as quais nenhum subconjunto possa ser uma superchave. Essas superchaves são chamadas

chaves candidatas. É possível que vários conjuntos diferentes de atributos possam servir como superchave.

Suponha que uma combinação de nome_cliente e rua_cliente seja suficiente para distinguir todos os membros do

conjunto de entidades cliente. Então, (seguro_social) e (nome_cliente, rua_cliente) são chaves candidatas.

Embora os atributos seguro_social e nome_cliente, juntos, possam distinguir as entidades cliente, sua

combinação não forma uma chave candidata, uma vez que seguro_social, sozinho, é uma chave candidata.

Chaves candidatas precisam ser escolhidas com cuidado. Como notamos, obviamente o nome de uma

pessoa não é suficiente, já que homônimos são possíveis. Nos Estados Unidos, o número de seguro_social pode

ser uma chave candidata. Em outros países onde os habitantes normalmente não possuem número de seguro

social, as empresas podem gerar seu próprio número de identificação, como número do cliente ou número de

identificação de estudantes ou número de identificação, como número do cliente ou número de identificação de

estudantes ou qualquer outra combinação única de outros atributos como chave. Uma das combinações mais

frequentemente usadas é o nome, data de nascimento e endereço, já que é extremamente difícil que mais de

uma pessoa tenha os mesmos valores para todos esses atributos.

Podemos usar o termo chave primária para caracterizar a chave candidata que é escolhida pelo projetista

do banco de dados como de significado principal para a identificação de entidades dentro de um conjunto de

entidades. Uma chave (primária, candidata e super) é duas entidades individuais em um conjunto não podem ter,

simultaneamente, mesmos valores em seus atributos-chave. A especificação de uma chave representa uma

restrição ao mundo real da empresa que está sendo modelada.

Conjuntos de Relacionamentos

A chave primária de um conjunto de entidades permite-nos distinguir as várias entidades de um conjunto.

Precisamos, de modo similar, de um mecanismo para a identificação dos vários relacionamentos em um conjunto

de relacionamentos.

Seja R um conjunto de relacionamentos envolvendo os conjuntos de entidades E1, E2, ..., En. Seja uma

chave_primária (Ei) denotando o conjunto de atributos que formam a chave primárias sejam únicos (se não

forem, use um esquema apropriado para rebatizá-los). A composição da chave primária para um conjunto de

relacionamentos depende de uma estrutura de atributos associada ao conjunto de relacionamentos R.

Se o relacionamento R não possui atributo, então o conjunto de atributos:

descreve um relacionamento individual do conjunto R.

Se o conjunto de relacionamento r possui os atributos a1, a2, ..., an a ele associados, então o conjunto de

atributos:

97

descreve um relacionamento em particular do conjunto R.

Em ambos os casos acima, o conjunto de atributos:

forma uma superchave do conjunto de relacionamentos.

A estrutura da chave primária para o conjunto de relacionamentos depende do mapeamento da

cardinalidade do conjunto de relacionamentos. Como ilustração, considere o conjunto de entidades cliente e

empregado e um conjunto de relacionamentos cliente_bancário que representa a associação entre um cliente e

um bancário (uma entidade empregado). Suponha que um conjunto de relacionamentos seja de muitos para

muitos, suponha também que o conjunto de relacionamentos possui o atributo tipo a ele associado,

representando a natureza do relacionamento (como um agente de empréstimo ou como um atendente pessoal).

A chave primária cliente_bancário constitui-se da união das chaves primárias de cliente e empregado. Entretanto,

se um cliente pode ser atendido exclusivamente por um bancário – isto é, se um relacionamento cliente_bancário

é muitos para um – então, a chave primária de cliente_bancário é simplesmente a chave primária de cliente. Para

relacionamentos um para um, qualquer uma das chaves primárias pode ser usada.

A designação de chaves primárias é mais complicada para relacionamentos não-binários.

Diagrama Entidade-Relacionamento

Toda estrutura lógica do banco de dados pode ser expressa graficamente pelo diagrama E-R. A relativa

simplicidade e clareza desta técnica de diagramação pode explicar, em grande parte, a ampla disseminação do

uso do modelo E-R.

A seguir são apresentados seus principais componentes:

• Retângulos, que representam os conjuntos de entidades.

• Elipses, que representam os atributos.

• Losangos, que representam os conjuntos de relacionamentos.

• Linhas, que unem os atributos aos conjuntos de entidades e os conjuntos de entidades aos conjuntos de

relacionamentos.

• Elipses duplas, que representam atributos multivalorados.

• Linhas duplas, que indicam a participação total de uma entidade em um conjunto de relacionamentos.

Como é mostrado na fig. 2.8, os atributos de um conjunto de entidades que são membros de uma chave

primária devem ser sublinhados.

Considere o diagrama entidade-relacionamento da fig. 2.8, que consiste de dois conjuntos de entidades,

cliente e empréstimo, relacionados pelo conjunto de relacionamentos devedor. Os atributos associados a cliente

são nome_cliente, seguro_social, rua_cliente e cidade_cliente. Os atributos associados a empréstimo são

número_empréstimo e total.

O conjunto de relacionamentos devedor pode ser muitos para muitos, um para um, muitos para um ou

um para um. Para fazer a distinção entre esses tipos, desenhamos uma linha direcionada (�) ou uma linha sem

direcionamento (-) entre o conjunto de relacionamentos e o conjunto de entidades em questão.

• Uma linha direcionada do conjunto de relacionamentos devedor para o conjunto de entidades

empréstimo especifica que devedor é um conjunto de relacionamentos um para um ou muitos para um,

de cliente para empréstimo; devedor não pode ser um conjunto de relacionamentos muitos para muitos

ou um para muitos, de cliente para empréstimo.

• Uma linha não direcionada do conjunto de relacionamentos devedor para o conjunto de entidades

empréstimo especifica que devedor é um conjunto de relacionamentos muitos para muitos ou um para

muitos, de cliente para empréstimo.

98

Voltando ao diagrama E-R da fig. 2.8, podemos ver que o conjunto de relacionamentos devedor é muitos

para muitos. Se o conjunto de relacionamentos dever for um para muitos, de cliente para empréstimo, então a

linha de devedor para cliente deveria ser direta, com a seta apontando para o conjunto de entidades cliente (fig.

2.9a). Similarmente, se o conjunto de relacionamentos devedor for muitos para um, de cliente para empréstimo,

então a linha de devedor para empréstimo deveria ser uma seta pontando para o conjunto de entidades

empréstimo (fig. 2.9b).

Finalmente, se o conjunto de relacionamentos devedor for um para um, então ambas as linhas de

devedor deveriam ser setas: uma apontando para o conjunto de entidades empréstimo e outra apontando para o

conjunto de entidades clientes (fig. 2.10).

Se um conjunto de relacionamentos também tem atributos a ele relacionados, então deveremos fazer a

ligação desses atributos com o conjunto de relacionamentos. Por exemplo, na fig. 2.11, temos o atributo descrito

data_acesso atrelado ao conjunto de relacionamentos depositante para especificar a data mais recente de acesso

do cliente à conta.

Indicamos os papeis desempenhados no diagrama E-R por meio da denominação nas linhas que ligam os

losangos aos retângulos. A fig. 2.12 mostra os papeis desempenhados, gerente e empregado, entre o conjunto de

entidades empregado e o conjunto de relacionamentos trabalha_para.

Conjuntos de relacionamentos não-binários podem ser facilmente especificados no diagrama E-R. A fig.

2.13 apresenta três conjuntos de entidades, cliente, empréstimo e agência, interligados pelo conjunto de

relacionamentos CEA.

Esse diagrama especifica que um cliente pode contrair diversos empréstimos e que um empréstimo pode

pertencer a diferentes clientes.

Esse diagrama especifica que um cliente pode contrair diversos empréstimos e que um empréstimo pode

pertencer a diferentes clientes.

A seguir, vemos uma seta apontando para agência indicando que cada par empréstimo-cliente está

associado a uma agência bancária específica. Se o diagrama tem uma seta apontando para cliente e outra

apontando para agência, o diagrama especificaria que cada empréstimo está associado a um cliente específico de

uma determinada agência bancária.

99

100

Conjunto de Entidades Fracas

Um conjunto de entidades pode não ter atributos suficientes para formar uma chave primária. Esse tipo

de conjunto de entidades é denominado conjunto de entidades fracas. Um conjunto de entidades que tem uma

chave primária é chamado um conjunto de entidades fortes. Como ilustração, considere o conjunto de entidades

pagamento, com três atributos: número_pagamento, data_pagamento e total_pagamento. Embora cada

101

entidade pagamento seja distinta, os pagamentos de empréstimos não tem uma chave primária; este é um

conjunto de entidades fracas. Para um conjunto de entidades fracas ser significativo, ele deve fazer parte de um

conjunto de relacionamentos um para muitos. Esse conjunto de relacionamentos não deve ter nenhum atributo

significativo, já que qualquer atributo exigido pelo conjunto de relacionamentos não deve ter nenhum atributo

significativo, já que qualquer atributo exigido pelo conjunto de relacionamentos pode ser associado ao conjunto

de entidades fracas.

Os conceitos de conjuntos de entidades fortes e fracas não possuir chave primária, precisamos, todavia,

de um significado para a distinção entre todas aquelas entidades em um conjunto de entidades que dependem de

uma entidade forte em particular. O identificador de um conjunto de entidades fracas é um conjunto de atributos

que permite que essa distinção seja feita. Por exemplo, o identificador do conjunto de entidades fracas

pagamento é o atributo número_pagamento, assim, para cada empréstimo, um número de pagamento identifica

um determinado pagamento para aquele empréstimo. O identificador de um conjunto de entidades fracas é

também chamado de chave parcial de um conjunto de entidades fracas é também de chave parcial de um

conjunto de entidades.

A chave primaria de um conjunto de entidades fracas é formado pela chave primária, precisamos, todavia,

de um significado para a distinção entre todas aquelas entidades fortes ao qual a existência do conjunto de

entidades fracas está vinculada mais o identificador do conjunto de entidades fracas. No caso do conjunto de

entidades pagamento, sua chave primária é {número_empréstimo, número_pagamento}, em que

número_empréstimo identifica a entidade dominante pagamento e número_pagamento identifica a entidade

pagamento dentro de determinado empréstimo.

O conjunto de entidades dominantes de identificação é dito proprietário do conjunto de entidades fracas

por ele identificada. O relacionamento que associa o conjunto de entidades fracas a seu proprietário é o

relacionamento identificador. Em nosso exemplo, pagamento_empréstimo é o relacionamento identificador de

pagamento.

Um conjunto de entidades fracas é identificado no diagrama E-R pela linha dupla usada no retângulo e no

losango do relacionamento correspondente. Na fig. 2.14, o conjunto de entidades fracas pagamento é

dependente do conjunto de entidades fortes empréstimo pelo conjunto de relacionamentos

pagamento_empréstimo. A figura também apresenta o uso de linhas duplas para identificar participação total – a

participação do conjunto de entidades (fracas) pagamento no relacionamento pagamento_empréstimo é total,

significando que todo pagamento precisa estar relacionado via pagamento_empréstimo a alguma conta.

Finalmente, a seta de pagamento_emprestimo para empréstimo indica que cada pagamento é para um único

empréstimo. O identificador de um conjunto de entidades fracas. Mesmo que um conjunto de entidades fracas

tenha sempre sua existência dependente de uma entidade dominante, a dependência de existência não resulta,

necessariamente, em um conjunto de entidades fracas; isto é, o conjunto de entidades subordinadas pode ter

uma chave primária.

102

Em alguns casos, o projetista do banco de dados pode optar por expressar um conjunto de entidades

fracas como um atributo composto multivalorado do conjunto de entidades proprietário. Em nosso exemplo, essa

alternativa exigiria que o conjunto de entidades empréstimo tivesse um atributo composto multivalorado

pagamento, consistindo de número_pagamento, data_pagamento e total_pagamento. Um conjunto de entidades

fracas pode ser modelado de forma mais apropriada, como um atributo, se ele apenas participar da identificação

do relacionamento e ele tiver poucos atributos. Por outro lado, será mais conveniente optar por um conjunto de

entidades fracas quando os conjuntos participantes no relacionamento não constituírem o relacionamento

identificador e quando o conjunto de entidades fracas possuir diversos atributos.

Recursos de Extensão do E-R

Apesar de ser possível modelar a maioria dos banco de dados apenas com os conceitos básicos do E-R,

alguns aspectos de um banco de dados podem ser expressos de modo mais conveniente por meio de algumas

extensões do modelo básico do E-R.

Especialização

Um conjunto de entidades pode conter subgrupos de entidades que são, de alguma forma, diferentes de

outras entidades do conjunto. Por exemplo, um subconjunto de entidades dentro de um conjunto de entidades

pode possuir atributos que não são compartilhados pelas demais entidades do conjunto. O modelo E-R

proporciona um significado para a representação desses agrupamentos distintos entre as entidades.

Considere o conjunto de entidades conta com os atributos número_conta e saldo. Um saldo é

futuramente classificado como um dos seguintes grupos:

• conta_poupança

• conta_movimento

Cada um desses tipos de contas é descrito como um conjunto de atributos que, além de todos os

atributos do conjunto de entidades conta, possui outros atributos adicionais. Por exemplo, as entidades

conta_poupança podem ser descritas pelo atributo taxa_juros, enquanto as entidades conta_movimento poderão

possuir o limite_cheque_especial. O processo de especialização de conta permite-nos distinguir os tipos de

contas.

Um conjunto de entidades pode ser especializado por mais de uma característica de diferenciação. Em

nosso exemplo, a distinção entre as entidades conta é seu tipo. Em nosso exemplo, a distinção entre as entidades

conta é seu tipo. Outra especialização coexistente poderia ser estabelecida entre os cheques especiais, resultando

nos conjuntos de entidades pessoa_física e pessoa_juridica. Quando mais de um tipo de especialização é formado

em um conjunto de entidades, uma entidade em particular pode pertencer a pessoa_fisica e conta_movimento.

Podemos aplicar o agrupamento por especialização, repetidamente, para refinar o esquema que está

sendo projetado. Por exemplo, um banco pode oferecer os três tipos de conta movimento:

1. Conta movimento padrão com taxa de três dólares mensais e 25 folhas de cheques por mês gratuitas.

Para essas contas, o banco emite o extrato bancário mensal com os números dos cheques.

2. Conta movimento especial que exige um saldo mínimo de mil dólares, pagando 2% em juros, e sem

limites na emissão de cheques. Neste caso, o banco monitora o saldo mínimo e os juros mensais pagos.

3. Conta movimento sênior para clientes com idade superior a 65 anos que não pagam taxa de serviços e

permite uso ilimitado de cheques, sem custo. Um registro sobre a data de aniversario do cliente é

associado a esse tipo de conta.

A especialização da conta_movimento pelo tipo de conta cria os seguintes conjuntos de entidades:

1. Padrão, com o atributo número_cheque.

2. Especial, com o atributo saldo_mínimo e taxa_juros.

3. Sênior, com o atributo data_aniversário.

103

Em termos de diagrama E-R, a especialização é representada pelo triângulo rotulado de ISA, como demonstrado

na fig. 2.15. Este rótulo-padrão ISA indica que, por exemplo, uma conta poupança “é uma” conta. Este

relacionamento ISA pode ser também entendido como um relacionamento de super ou subclasse. Os conjuntos

de entidades em nível superior e inferior são representados do mesmo modo que os conjuntos de entidades

regulares, ou seja, por um retângulo contendo o nome do conjunto de entidades.

Generalização

O refinamento do conjunto de entidades em níveis sucessivos de subgrupos indica um processo top-down

de projeto, no qual as diferenciações são feitas de modo explícito. O projeto pode ser realizado de modo bottom-

up, no qual vários conjuntos de entidades são sintetizados em um conjunto de entidades em alto nível, com base

em atributos comuns. O projetista do banco de dados poderia identificar, em uma primeira modelagem, o

conjunto de entidades conta_padrão com os atributos número_conta, saldo e saldo_negativo e o conjunto de

entidades conta_poupanca com os atributos número_conta, saldo e taxa_juros.

Existem similaridades entre o conjunto de entidades conta_movimento e o conjunto de entidades

conta_poupança, já que possuem atributos comuns. Esse compartilhamento de atributos pode ser expresso pela

generalização, que exprime o relacionamento existente entre os conjuntos de entidades de nível superior e um

ou mais conjuntos de entidades de nível inferior. E no nosso exemplo, conta é um conjunto de entidades de nível

superior e conta_poupança e conta)movimento são conjuntos de entidades de nível inferior. Conjuntos de

entidades superiores e inferiores podem também ser designados em termos de super e subclasses,

respectivamente. O conjunto de entidades conta é uma superclasse de conta_poupanca e conta_movimento são

subclasses.

Na prática, a generalização é simplesmente o inverso da especialização. Aplicaremos ambos os processos,

combinados, ao longo do projeto do esquema E-R de uma empresa. Em termos do diagrama E-R propriamente

104

dito, não faremos distinção entre a especialização e a generalização. Novos níveis de representação de entidades

serão diferenciadas (especialização) ou sintetizadas (generalização) de modo que o esquema possa expressar

totalmente a aplicação do banco de dados e atender às necessidades de seus usuários. As diferenças das duas

abordagens podem ser caracterizadas pelo ponto de partida e seus objetivos gerais.

A especialização parte de um único conjunto de entidades; ela enfatiza as diferenças entre as entidades

pertencentes ao conjunto por meio do estabelecimento das diferenças expressas nos conjuntos de entidades de

nível inferior. Estes conjuntos de entidades de nível inferior podem possuir atributos, ou mesmo participar de

relacionamentos que não podem ser aplicados a todas as entidades do conjunto de entidades de nível superior.

De fato, o projetista usa especialização justamente para representar tais distinções. Se conta_poupança e

conta_movimento não possuem atributos únicos; não há necessidade de especializar o conjunto de entidades

conta.

O uso da generalização procede para o reconhecimento de um número de conjunto de entidades que

compartilham características comuns (são descritos pelos mesmos atributos e participam dos mesmos conjuntos

de relacionamentos). Com base nessas características comuns, a generalização sintetiza esses conjuntos de

entidades de nível superior. A generalização é suada para enfatizar as similaridades entre os conjuntos de

entidades de nível inferior, omitindo suas diferenças; isso permite também uma representação mais econômica,

evitando repetições de atributos compartilhados.

Herança de Atributos

Uma propriedade decisiva das entidades de níveis superior e inferior criadas pela especialização e pela

generalização é a herança de atributos. Os atributos dos conjuntos de entidades de nível superior são herdados

pelos conjuntos de entidade de nível inferior. Por exemplo, conta_poupança e conta_movimento herdam os

atributos de conta. Assim, conta_poupança é identificado por seus atributos número_conta, saldo e taxa_juros;

conta_movimento é identificado por seus atributos número_conta, saldo e limite_cheque_especial. Os conjuntos

de entidade de nível inferior (ou subclasses) também herdam a participação em conjuntos de relacionamentos

dos quais participam seus conjuntos de entidades de nível superior(ou superclasse). Tanto que o conjunto de

entidades conta_poupança e conta_movimento participam do conjunto de relacionamentos depositante. Os

conjuntos de entidades padrão, especial e sênior de nível inferior herdam os atributos e a participação nos

relacionamentos de conta_movimento e conta.

Se uma dada porção do modelo E-R chegou à especialização ou à generalização, os resultados são

basicamente os mesmos:

• Conjuntos de entidades de nível superior com atributos e relacionamentos que são aplicados a todos os

seus conjuntos de entidades de nível inferior.

• Conjunto de entidades de nível inferior com características distintas que são apenas aplicadas a um

conjunto de entidades de nível inferior em particular.

Como se percebe, embora frequentemente nos refiramos apenas à generalização, as propriedades que

discutimos pertencem totalmente a ambos os processos.

A fig. 2.15 apresenta uma hierarquia nos conjuntos de entidades. Na figura, conta_movimento é um

conjunto de entidades de nível superior aos conjuntos de entidades padrão, especial e sênior. Hierarquicamente,

um dado conjunto de entidades somente poderá ser envolvido, como um conjunto de entidades de nível inferior,

por meio de um relacionamento ISA. Se um conjunto de entidades é um conjunto de entidades de nível inferior

em mais de um relacionamento ISA, a estrutura resultante é chamada reticulada.

Restrições de Projeto

Para modelagem mais apurada de uma empresa, o projetista do banco de dados pode optar por definir

algumas restrições em uma generalização em particular. Um tipo de restrição envolve a determinação das

105

entidades que podem participar de um dado conjunto de entidades de nível inferior. Tais escolhas podem ser

uma das seguintes:

• Definida por condição. Um conjunto de entidades de nível inferior definido por condição é selecionado

com base na satisfação ou não de condições ou predicados preestabelecidos. Por exemplo, considere que

o conjunto de entidades de nível superior conta possui o atributo tipo_conta. Todas as entidades conta

evoluem para um dos atributos tipo_conta. Somente aquelas entidades que satisfaçam a condição

tipo_conta = “conta_poupança”, podem pertencer ao conjunto de entidades de nível inferior

conta_poupança. Todas as entidades que satisfaçam a condição tipo_conta = “conta_movimento” são

incluídas em conta movimento. Desde que todas as entidades de nível inferior sejam classificadas com

base nos mesmos atributos (neste caso, tipo_conta), esse tipo de generalização é chamado

definida_por_atributo.

• Definida pelo usuário. Um conjunto de entidades de baixo nível definido pelo usuário não tem seus

membros classificados por uma condição; as entidades são designadas a um determinado conjunto de

entidades por usuários do banco de dados. Por exemplo, suponhamos que, depois de três meses de

trabalho, os empregados do banco sejam convocados para compor um dentre os quatro grupo de

trabalho existentes. Esses grupos são representados por quatro conjuntos de entidades de baixo nível,

derivados do conjunto de entidades de alto nível empregado. A escolha de um determinado empregado

para participar de um conjunto de entidades de um grupo específico não é originada automaticamente

pela definição de uma condição explícita. Pelo contrário, a escolha para compor determinado grupo é

feita por critérios individuais, pesando na decisão a opinião do usuário, e sua implementação é feita por

uma operação que adiciona a entidade ao conjunto de entidades.

O segundo tipo de restrição determina se uma entidade pode ou não pertencer a mais de um conjunto de

entidades de nível inferior dentro de uma generalização simples. Os conjuntos de entidades de nível inferior

podem ser um dos seguintes:

• Manutenção exclusivos. Restrições mutuamente exclusivas exigem que uma entidade pertença a apenas

um conjunto de entidades de nível inferior. Em nosso exemplo, uma entidade conta pode satisfazer a

apenas uma condição para o atributo tipo_conta; uma entidade pode ser tanto uma conta poupança

como uma conta movimento, mas nunca ambas.

• Sobrepostos. Em generalizações sobrepostas, uma mesma entidade pode pertencer a mais de um

conjunto de entidades de nível inferior dentro de uma generalização simples. Para ilustração, retornemos

ao exemplo dos grupos de trabalho dos empregados do banco e suponhamos que determinados gerentes

participem de mais de um desses grupos. Um determinado empregado pode, portanto, pertencer a mais

de um conjunto de entidades formadas pelos grupos de trabalho.

Conjuntos de entidades de nível inferior com sobreposição é o caso-padrão; restrições mutuamente

exclusivas devem ser apresentadas explicitamente em uma generalização (ou especialização).

Uma restrição final, a restrição de totalidade em uma generalização, determina se uma entidade de nível

superior pertence ou não a, no mínimo, um dos conjuntos de entidades de nível inferior dentro da generalização.

Essa restrição pode ser uma das seguintes:

• Total. Cada entidade do conjunto de entidades de nível superior deve pertencer a um conjunto de

entidades de nível inferior.

• Parcial. Qualquer entidade de nível superior pode pertencer a qualquer um dos conjuntos de entidades

de nível inferior.

A generalização conta é total: todas as entidades contas devem ser contas de poupança ou contas movimento.

Como os conjuntos de entidades de alto nível de uma generalização são, geralmente, compostos somente pelas

entidades de nível inferior. O conjunto de entidades dos grupos de trabalho ilustram uma especialização parcial.

106

Desde que os empregados participem de um dos grupos somente após três meses de trabalho, algumas

entidades empregados podem não ser membros de qualquer um dos conjuntos de entidades de grupos de nível

inferior.

Podemos caracterizar os conjuntos de entidades de grupos de modo completo como especialização

parcial e sobreposta de empregado. A generalização conta_movimento e conta_poupança de conta é total,

generalização mutuamente exclusiva. As restrições de totalidade e de sobreposição, entretanto, ano são

dependentes uma das outra. Os padrões de generalização podem ser parcial-mutuamente-exclusivas e total-

sobrepostas.

Podemos ver que certas exigências para inserções e exclusões seguem restrições que são aplicadas a

dadas generalizações ou especializações. Por exemplo, quando uma restrição total é aplicada, uma entidade

inserida em um conjunto de entidades de nível superior deverá ser inserida em pelo menos um de seus conjuntos

de entidades de nível inferior. Em restrições definidas por condição, todas as entidades de nível superior que

satisfaçam tal condição devem ser inseridas nos conjuntos de entidades de nível inferior. Finalmente, uma

entidade excluída de um conjunto de entidades de nível superior também deverá ser excluída de todos os

conjuntos de entidades de nível inferior às quais pertencem.

Agregação

Uma das limitações do modelo E-R é que não é possível expressar relacionamentos entre

relacionamentos. Para ilustrar a necessidade desse tipo de construtor, consideremos novamente um banco de

dados descrevendo informações sobre clientes e seus empréstimos. Suponha que cada par empréstimo-cliente

possui um bancário, ou agente-empréstimo, responsável pelo acompanhamento de determinado empréstimo.

Usando os construtores do modelo E-R básico, obteríamos o diagrama apresentado na fig. 2.16. Nota-se que o

conjunto de relacionamentos devedor e agente_empréstimo poderia ser combinado em um único conjunto de

relacionamentos. No entanto, não podemos fazê-lo, porque isso tornaria obscura a estrutura lógica desse

esquema. Por exemplo, se combinarmos os conjuntos de relacionamentos devedor e agente_emprestimo, essa

combinação deveria especificar que um agente-empréstimo específico é responsável por uma par empréstimo-

cliente, o que não ocorre. A separação em dois conjuntos de relacionamentos distintos resolve esse problema.

Entretanto, existe redundância de informações na figura resultante, uma vez que todo par empréstimo-

cliente em agente_empréstimo está também em devedor. Se o agente_empréstimo fosse um valor, em vez de

uma entidade de empregado, poderíamos fazer de agente_empréstimo um atributo multivalorado do

relacionamento devedor. Mas, assim, seria ainda mais difícil (nos custos de lógica e execução) encontrar, por

exemplo, o par empréstimo-cliente pelo qual determinado bancário é responsável.

Já que um agente de empréstimo é uma entidade empregado, essa alternativa é descartada em qualquer

caso.

107

O melhor modo de modelar a situação acima descrita é usando a agregação. Agregação é a abstração por

meio da qual os relacionamentos são tratados como entidades de nível superior. Assim, em nosso exemplo,

simbolizamos o conjunto de relacionamentos devedor e o conjunto de entidades cliente e empréstimo como um

conjunto de entidades de nível superior, chamado devedor.

Como é um conjunto de entidades, é tratado da mesma forma que qualquer outro conjunto de entidades.

A notação mais comum para a agregação é mostrada na fig. 2.17.

108

Projeto de um Esquema de Banco de Dados E-R

O modelo de dados E-R nos dá uma flexibilidade substancial par ao projeto de um esquema de banco de

dados na modelagem de determinada empresa. Vamos agora considerar quais as opções possíveis para o

projetista dentre o grande número de possibilidades. Algumas das possíveis opções são:

• Optar pelo uso de um atributo ou de um conjunto de entidades para representação de um objeto.

• Se uma concepção real é expressa de modo mais preciso por um conjunto de entidades ou por um

conjunto de relacionamentos.

• Optar por um conjunto de relacionamentos ternário ou por um par de relacionamento binário.

• Se se deve usar um conjunto de entidades forte. Um conjunto de entidades forte e seus conjuntos de

entidades fracas dependentes podem ser visto como um único “objeto” do banco de dados, já que as

entidades fracas têm sua existência vinculada à de uma entidade forte.

• Se o uso da generalização é apropriado. A generalização, ou uma hierarquia de relacionamentos ISA,

contribui para a modularidade, já que atributos comuns a conjuntos de entidades similares podem ser

representados em apenas um local do diagrama E-R.

• Se o uso da agregação for apropriado, agregar grupos de uma parte do diagrama E-R em um conjunto de

entidades simples, permitindo-nos tratar do conjunto de entidades agregadas como uma unidade

simples, sem abordar os detalhes de suas estruturas internas.

Podemos notar que um projetista de banco de dados necessidade de um bom entendimento da empresa

que está sendo modelada para que possa tomar essas decisões.

Fases de Projeto

109

Um modelo de dados de alto nível proporciona ao projetista uma base conceitual na qual se pode

especificar, de modo sistemático, quais as necessidades dos usuários do banco de dados e como este banco de

dados será estruturado para atender plenamente a todas estas necessidades. A fase inicial do projeto é

caracterizar todos os dados necessários na perspectiva do usuário. O resultado dessa fase é a especificação das

necessidades do usuário (ou levantamento de requisitos).

A seguir, o projetista escolhe o modelo de dados e, por meio da aplicação de seus conceitos, transcreve as

necessidades especificadas em um esquema conceitual de banco de dados. O esquema desenvolvido nessa fase é

chamado projeto conceitual e proporcional uma visão detalhada da empresa. Como só estudamos o modelo E-R

até agora, iremos usá-lo para o desenvolvimento do esquema conceitual. Empregando os termos do modelo E-R,

o esquema deve especificar todos os conjuntos de entidades, relacionamentos, atributos e o mapeamento das

restrições. O projetista revê o esquema para confirmar se todos os dados exigidos estão de fato representados e

se não há conflitos entre eles. Ele deverá também examinar o projeto para remover qualquer tipo de

redundância. Neste momento, seu enfoque é descrever os dados e seus relacionamentos, em vez de especificar

os detalhes físicos de armazenamento.

O desenvolvimento completo do esquema conceitual irá indicar também as necessidades funcionais da

empresa. Na especificação das necessidades funcionais, os usuários descrevem os tipos de operações (ou

transações) que serão realizados nos dados. Os exemplos dessas operações incluem modificação para atualização

dos dados, pesquisa para recuperação de um determinado dado e remoção de dados. Deverá ser feita, nesse

estágio do projeto conceitual, uma revisão do esquema dos dados em função das necessidades funcionais.

O transporte do modelo de dados abstrato, para sua implementação, ocorre nas duas fases finais do

projeto. Na fase de projeto lógico, o esquema conceitual de alto nível é mapeado para o modelo de

implementação de dados do SGBD que será usado. O esquema de dados resultante é usado para a fase

subsequente, que é a do projeto físico, especificamente dependente dos recursos do SGBD usado. Esses recursos

incluem as formas de organização de arquivos e estruturas internas de armazenamento.

Vamos abordar somente os conceitos do modelo E-R como usados na fase do projeto do esquema

conceitual.

Dados Necessários a uma Empresa da Área Bancária

A especificação dos requisitos dos usuários pode ser apurada por meio de entrevistas unidas à própria

avaliação do projetista sobre a empresa.

A descrição resultante dessa fase do projeto é a base para a especificação da estrutura conceitual do

banco de dados. A lista de itens que se segue apresenta as principais necessidades de uma empresa da área

bancária.

• Um banco é organizado em agências. Cada agência é localizada em uma cidade e é identificada por um

nome único. O banco controla os fundos de cada uma dessas agências.

• Os clientes do banco são identificados pelo número do seu seguro social. O banco mantém dados como

nome, rua, e cidade do cliente. Os clientes podem possuir contas e contrair empréstimos. O cliente pode

estar associado a um bancário específico que cuida de seus negócios ou atua como um agente de

empréstimos.

• Os empregados do banco também são identificados por meio do seu seguro social. A administração do

banco mantém o nome e o número do telefone de cada um de seus empregados, os nomes de seus

dependentes e o número do seguro social de seu gerente. O banco também possui a data de contrata do

empregado e, com isso, seu tempo de trabalho.

• O banco oferece dois tipos de contas – contas poupança e contas movimento. As contas movimento

podem possuir mais de um correntista, e um correntista pode possuir mais de uma conta. Cada conta

possui um único número. O banco controla o saldo de cada conta, assim como a data mais recente de

acesso a essa conta. Por outro lado, cada conta poupança possui a taxa de juros associada, assim como

são também registrados os excessos nos limites da conta movimento.

110

• Um empréstimo originado em uma agência em particular pode ter sido obtido por um ou mais clientes.

Um empréstimo é identificado por um número único. Para cada empréstimo o banco mantém o

montante emprestado, assim como os pagamentos das parcelas. Embora o número das parcelas de um

empréstimos não identifique de modo único um pagamento específico dentre os muitos realizados no

banco, o número da parcela identifica um pagamento específico dentre os muitos realizados no banco, o

número da parcela identifica um pagamento efetuado para um empréstimo em particular. A data e o

montante são registrados no pagamento de cada parcela.

Em um banco real, poderia ser de interesse manter informações sobre depósitos e retiradas tanto para as contas

poupança quanto para as contas movimento, assim como se mantêm informações sobre o pagamento de

parcelas dos empréstimos. Uma vez que a modelagem dos requisitos dos usuários nas necessidades descritas a

pouco são semelhantes àquelas feitas no início, podemos optar por um exemplo de aplicação mais reduzido, não

fazendo, em nosso modelo, o acompanhamento dos depósitos e das retiradas.

Designação de Conjuntos de Entidades em Empresas Bancárias

Nossa especificação dos requisitos de dados servem como base inicial para a construção de um esquema

conceitual do banco de dados.

Para as especificações relacionadas, começamos por identificar os conjuntos de dados e seus atributos.

• O conjunto de entidades agência, com os atributos:

nome_agência, cidade_agência, e fundos

• O conjunto de entidades cliente, com os atributos:

nome_cliente, seguro_social, rua_cliente e cidade_cliente

Com a possibilidade do atributo adicional nome_bancário.

• O conjunto de entidades empregado, com os atributos:

seguro_social_empregado, nome_empregado, número_telefone, salario e gerente.

Recursos adicionais descritivos são os atributos multivalorados nome_dependentes, o atributo básico

data_início e o atributo descritivo tempo_de_trabalho.

• Dois conjuntos de entidades contas – conta_poupança e conta_movimento – com os atributos comuns

número_conta e saldo; também, conta_poupança possui o atributo taxa_de_juros e conta_movimento, o

atributo limite_cheque_especial.

• O conjunto de entidades empréstimo, com os atributos:

número_empréstimo, total e agência_origem

São possíveis os seguintes atributos adicionais: pagamento_empréstimo, composto multivalorado; com os

seguintes atributos componentes:

número_pagamento, data_pagamento e total_pagamento

Designação dos conjuntos de Relacionamentos de uma Empresa da Área Bancária

Retornemos agora ao esquema do projeto descrito para especificar os seguintes conjuntos de

relacionamentos e mapeamentos de cardinalidades:

• devedor, como conjunto de relacionamentos muitos para um que indica qual a agência responsável pelo

empréstimo.

• pagamento_empréstimo, relacionamento um para muitos entre empréstimo e pagamento, que

documento que um pagamento está sendo feito para um determinado empréstimo.

• depositante, com os atributos de relacionamento data_acesso, um conjunto de relacionamentos muitos

para muitos entre cliente e conta, indicando que um cliente possui uma conta.

111

• agente_cliente, com o atributo de relacionamento tipo, um conjunto de relacionamentos muitos para um

expressando que um cliente pode ser atendido por determinado empregado do banco e que um

empregado do banco pode atender a um ou mais clientes.

• trabalha_para, conjunto de relacionamentos entre entidades empregado que determina se se trata de

gerente ou empregado; o mapeamento da cardinalidade expressa que um empregado trabalha para um

gerente específico e que um gerente supervisiona um ou mais empregados.

Note que trocamos o atributo nome_agente do conjunto de entidades cliente pelo conjunto de relacionamentos

agente_cliente, e o atributo gerente do conjunto de entidades empregado pelo conjunto de relacionamentos

trabalha_para. Optamos por manter o conjunto de entidades empréstimo. O conjunto de relacionamentos

agência_empréstimo e pagamento_empréstimo foi substituído, respectivamente, pelos atributos agência_origem

e pagamento_empréstimo do conjunto de entidades empréstimo.

Digrama E-R de uma Empréstimo de uma Empresa da Área Bancária

Esquematizando, apresentamos agora o diagrama E-R completo para nosso exemplo de empresa da área

bancária. A fig. 2.18 mostra a representação de um modelo conceitual de um banco, expresso nos termos

conceituais de E-R. O diagrama inclui os conjuntos de entidades, atributos, conjuntos de relacionamentos e o

mapeamento das cardinalidades concluídas durante o processo descrito anteriormente, e já refinado.

112

Redução de um Esquema E-R a Tabelas

Um banco de dados em conformidade com o esquema de banco de dados E-R pode ser representado por

uma coleção de tabelas. Para cada conjunto de entidades e para cada relacionamentos, dentro de um banco de

dados, existe uma tabela única registrando o nome do conjunto de entidades ou relacionamentos

correspondentes. Cada tabela possui várias colunas, cada uma delas com um único nome.

Tanto o modelo E-R quanto o modelo relacional são abstratos, ou seja, representações lógicas de

empresas reais. Como esses dois modelos empregam princípios de projetos similares, podemos converte o

projeto E-R em projeto relacional. Converter a representação de um banco de dados de um diagrama E-R para um

formato de tabela é a base para a derivação de um diagrama E-R de um projeto a partir de um banco de dados

relacional. Embora existam importantes diferenças entre uma relação e uma tabela, informalmente, uma relação

pode ser considerada uma tabela de valores.

Representação Tabular dos Conjuntos de Entidades Fortes

113

Seja E um conjunto de entidades fortes descrito pelos atributos a1, a2, ..., an. Representamos essa

entidade por uma tabela chamada E com n colunas distintas, cada uma delas correspondendo a um dos atributos

de E. Cada linha da tabela corresponde a uma entidade do conjunto de entidades E.

Como ilustração, considere o conjunto de entidades empréstimo do diagrama E-R mostrado na fig. 2.8.

Esse conjunto de entidades possui dois atributos: número_empréstimo e total. Representaremos esse conjunto

de entidades pela tabela chamada empréstimo, com duas colunas, como mostra a fig. 2.19. A linha (L-17, 1000)

da tabela empréstimo significa que o empréstimo de número L-17 é de mil dólares. Podemos adicionar uma nova

entidade ao banco de dados pela inserção de uma linha na tabela. Também podemos excluir ou modificar as

linhas.

Denotaremos como D1 o conjunto de todos os números de empréstimos e D2 o conjunto de todos os

saldos. Qualquer linha da tabela empréstimo deve consistir de uma 2-tupla (v1, v2), em que v1 é um empréstimo

(isto é, v1 está no conjunto D1) e v2 é um total (isto é, v2 está no conjunto D2). No geral, a tabela empréstimo

conterá somente o subconjunto de todas as linhas possíveis. Iremos nos referir ao conjunto de todas as linhas

possíveis de empréstimo como o produto cartesiano de D1 e D2, denotado por: D1xD2.

No geral, se tivermos uma tabela com n colunas, denotaremos o produto cartesiano de D1, D2, ..., Dn por:

Como outro exemplo, considere o conjunto de entidades cliente do diagrama E-R mostrado na fig. 2.8.

Esse conjunto de entidades tem os atributos nome_cliente, seguro_social, rua_cliente e cidade_cliente. A tabela

correspondente a cliente tem quatro colunas, como mostra a fig. 2.20.

Representação Tabular dos Conjuntos de Entidades Fracas

Seja A um conjunto de entidades fracas com os atributos a1, a2, ..., an. Seja B um conjunto de entidades

fortes, do qual A é dependente. Seja a chave primária de B composta pelos atributos b1, b2, ..., bn. Representamos

o conjunto de entidades A pela tabela chamada A com uma coluna para cada um dos atributos do conjunto:

114

Como ilustração, considere o conjunto de entidades pagamento mostrado no diagrama E-R da fig. 2.14.

Esse conjunto de entidades tem três atributos: número_pagamento, data_pagamento e total_pagamento. A

chave primária do conjunto de entidades empréstimo, do qual pagamento é dependente, é o

número_empréstimo. Assim, pagamento é representado por uma tabela com quatro colunas chamadas

número_empréstimo, número_pagamento, data_pagamento e total_pagamento, como mostrado na fig. 2.21.

Representação Tabular do Conjunto de Relacionamentos

Seja R um conjunto de relacionamentos; seja a1, a2, ..., an o conjunto de atributos formado pela união das

chaves primarias de cada um dos conjuntos de entidades participantes de R; e seja os atributos descritivos b1, b2,

..., bn (se houver) de R. Representamos esse conjunto de relacionamentos pela tabela chamada R, com uma

coluna para cada atributo do conjunto:

Para ilustrar, considere o conjunto de relacionamentos devedor no diagrama E-R da fig. 2.8. Esse conjunto

de relacionamentos envolve os dois conjuntos de entidades seguintes:

• cliente, com a chave primária seguro_social.

• Empréstimo, com a chave primária número_empréstimo.

Uma vez que o conjunto de relacionamentos não possui atributos próprios, a tabela devedor possui duas colunas,

a de seguro_social e número_empréstimo, como mostra a fig. 2.22.

Tabelas Redundantes

115

O caso do conjunto de relacionamentos unindo um conjunto de entidades fracas ao seu conjunto de

entidades fortes correspondente é um caso especial. Como pudemos ver anteriormente, esses relacionamentos

são muitos para um e não possuem atributos descritivos. Além disso, a chave primária de um conjunto de

entidades fracas inclui a chave primária do conjunto de entidades fortes. No diagrama E-R da fig. 2.14, o conjunto

de entidades fracas pagamento é dependente do conjunto de entidades fortes empréstimo por meio do conjunto

de relacionamentos pagamento_empréstimo. A chave primária de pagamento é {número_empréstimo,

número_pagamento} e a chave primária de empréstimo é {número_empréstimo}. Desde que

pagamento_empréstimo não possui atributos descritivos, a tabela para pagamento_empréstimo poderia ter duas

colunas, número_empréstimo e número_pagamento. A tabela para o conjunto de entidades pagamento tem

quatro colunas, número_empréstimo, número_pagamento, data_pagamento e total_pagamento. Assim, a tabela

pagamento_empréstimo é redundante. Em geral, a tabela para o conjunto de relacionamentos unindo o conjunto

de entidades fracas com seu conjunto de entidades fortes correspondente é redundante e não precisa ser

apresentada em uma representação tabular do diagrama E-R.

Combinação de tabelas

Considere um conjunto de relacionamento muitos para um AB entre os conjuntos de entidades A e B.

Usando nosso esquema de construção de tabelas previamente descrito, teremos três tabelas: A, B e AB.

Entretanto, se existe dependência de A sobre B (isto é, para cada entidade a em A, a existência de a depende da

existência de alguma entidade b em B), então podemos combinar as tabelas A e AB para formar uma tabela

simples consistindo da união das colunas de ambas as tabelas.

Como ilustração, considere o diagrama E-R da fig. 2.23. O conjunto de relacionamentos entre conta e

agência, agência_conta, é muitos para um. Daqui para frente, uma linha dupla no diagrama E-R indica que a

participação de conta em conta_agência é total. Daí, uma conta não pode existir sem que esteja associada a uma

agência em particular. Portanto, necessitamos somente de duas tabelas:

• conta, com os atributos número_conta, saldo e nome_agência.

• agência, com atributos nome_agência, cidade_agência e fundos.

Atributos Multivalorados

Vimos que, em geral, os atributos do diagrama E-R são mapeados diretamente em colunas nas tabelas

apropriadas. Atributos multivalorados, entretanto, constituem uma exceção; novas tabelas são criadas para esses

tipos de atributos.

Para um atributo multivalorado M, criamos a tabela T com uma coluna C que corresponde a M e as

colunas correspondentes à chave primária do conjunto de entidades ou conjunto de relacionamento do qual M é

atributo. Como ilustração, considere o diagrama E-R apresentado na fig. 2.18. o diagrama inclui o atributo

multivalorado nome_dependente. Para esse atributo multivalorado, criamos a tabela nome_dependente com as

colunas nomed, referente ao atributo nome_dependente do empregado, e seguro_social_empregado,

116

representando a chave primária do conjunto de entidades empregado. Cada dependente de um empregado é

representado por uma única linha na tabela.

Representação Tabular da Generalização

Existem dois modos diferentes de transformar um diagrama E-R que contenha generalização em tabelas.

Embora nos refiramos à generalização mostrada na fig. 2.15, preferimos simplificar essa discussão incluindo

somente o primeiro grupo dos conjuntos de entidades de nível inferior – isto é, os atributos conta_poupança e

conta_movimento.

1. Criar a tabela para o conjunto de entidades de nível superior. Para cada conjunto de entidades de nível

inferior, criar uma tabela que inclua uma coluna para cada um dos coluna para cada um dos atributos

daquele conjunto de entidades mais uma coluna para cada atributo da chave primária do conjunto de

entidades mais uma coluna para cada atributo da chave primária do conjunto de entidades de nível

superior. Assim, para o diagrama da fig. 2.15, teremos três tabelas:

• conta, com os atributos número_conta e saldo.

• conta_poupança, com os atributos número_conta e taxa_juros.

• conta_movimento, com os atributos número_conta e limite_cheque_especial.

2. Se a generalização é mutuamente exclusiva e total – isto é, se nenhuma entidade é membro de mais de

um conjunto de entidades de nível imediatamente inferior ao conjunto de entidades de nível superior e

se todas as entidades do conjunto de entidades de nível inferior –, então, uma outra representação

alternativa é possível. Para cada conjunto de entidades de nível inferior, cria-se uma tabela que inclua

uma coluna para cada um dos atributos do conjunto de entidades mais uma coluna para cada atributo do

conjunto de entidades de nível superior. Então, para o diagrama E-R da fig. 2.15, teremos duas tabelas.

• Conta_poupança, com atributos número_conta, saldo e taxa_juros.

• Conta_movimento, com os atributos número_conta, saldo e limite_cheque_especial.

As relações correspondentes a conta_poupança e conta_movimento para essas tabelas têm, ambas, saldo

como chave primária.

Se o segundo método for usado para generalizações com sobreposição, alguns valores, como saldo,

podem ser armazenados duas vezes sem necessidade. Similarmente, se a generalização não for total – isto é, se

algumas contas não forem nem de poupança nem de movimento, então tais contas não poderão ser

representadas usando o segundo método.

Representação Tabular da Agregação

A transformação de um diagrama E-R com agregação para forma tabular é bastante direta. Considere o

diagrama da fig. 2.17. A tabela para o conjunto de relacionamentos agente_empréstimo inclui uma coluna para

cada atributo, uma para a chave primária do conjunto de entidades empregado e uma para o conjunto de

relacionamentos devedor. Poderia também incluir uma coluna para cada um dos atributos descritivos do

conjunto de relacionamentos agente_empréstimo, se eles existirem. Usando o mesmo procedimento anterior

para o resto do diagrama, criamos as seguintes tabelas:

• cliente, com os atributos nome_cliente, seguro_social, rua_cliente e cidade_cliente.

• empréstimo, com os atributos número_empréstimo e total.

• devedor, com os atributos seguro_social e número_empréstimo.

• empregado, com os atributos seguro_social_empregado, nome_empregado, e número_telefone.

• agente_empréstimo, com os atributos seguro_social, número_empréstimo e seguro_social_empregado.

117

Arquiteturas de Sistemas de Banco de Dados

A arquitetura de um sistema de banco de dados é fortemente influenciada pelo sistema básico

computacional sobre o qual o sistema de banco de dados é executado. Aspectos da arquitetura de computadores

– como rede, paralelismo e distribuição – têm influência na arquitetura do banco de dados.

• Rede de computadores permite que algumas tarefas sejam executadas no servidor do sistema e outras

sejam executadas no cliente. Essa divisão de trabalho tem levado ao desenvolvimento de sistemas de

banco de dados cliente-servidor.

• Processamento paralelo em um sistema de computadores permite que atividades do sistema de banco de

dados sejam realizadas com mais rapidez, reduzindo o tempo de resposta das transações e, assim,

aumentando o número de transações processadas por segundo. Consultas podem ser processadas de

forma a explorar o paralelismo oferecido pelo sistema operacional. A necessidade de processamento

paralelo de consultas tem levado ao desenvolvimento de sistemas de banco de dados paralelos.

• A distribuição de dados pelos nós da rede ou pelos diversos departamentos de uma organização

permitem que esses dados residam onde são gerados ou mais utilizados, mas, ainda assim, estejam

acessíveis para outros nós de outros departamentos. Dispor de diversas cópias de um banco de dados em

diferentes nós também permite a organizações de grande porte manter operações em seus bancos de

dados mesmo quando um nó é afetado por um desastre natural, como inundações, incêndios ou

terremotos. Sistemas de banco de dados distribuídos têm se desenvolvido para tratar dados distribuídos

geográfica ou administrativamente por diversos sistemas de banco de dados.

Vamos agora estudar a arquitetura dos sistemas de banco de dados, começando com os sistemas

centralizados tradicionais e passando por sistemas de banco de dados cliente-servidor, paralelos e distribuídos.

Sistemas Centralizados

Sistemas de banco de dados centralizados são aqueles executados sobre um único sistema computacional

que n ao interagem com outros sistemas. Tais sistemas podem ter a envergadura de um sistema de banco de

dados de um só usuário executado em um computador pessoal até sistemas de alto desempenho em sistema de

grande porte.

Um sistema computacional genérico moderno consiste em uma ou poucas CPUs e dispositivos de controle

que são conectados por meio de um bus comum que proporciona acesso à memória compartilhada (fig. 16.1). As

CPUs têm memórias cache locais que armazenam cópias de partes da memória para acesso rápido aos dados.

Cada dispositivo de controle atende a um tipo específico de dispositivo (por exemplo, um drive de disco, um

dispositivo de áudio ou de vídeo). A CPU e os dispositivos de controle podem trabalhar concorrentemente,

competindo pelo acesso à memória. A memória cache parcialmente reduz a contenção de acesso à memória,

uma vez que reduz o número de tentativas de acesso da CPU à memória compartilhada.

118

Dividimos em dois modos a forma pela qual os computadores são usados: por um sistema de um único

usuário e sistemas multiusuários. Computadores pessoais e estações de trabalho caem na primeira categoria. Um

sistema monousuário típico é uma unidade de trabalho de uma única pessoa, com uma única CPU e um ou dois

discos rígidos, com um sistema operacional que pode dar suporte a apenas um único usuário. Um sistema

multiusuário típico, por outro lado, possui um número maior de discos e área de memória, podendo ter diversas

CPUs e um sistema operacional multiusuário. Atende a um grande número de usuários que estão conectados ao

sistema por meio de terminais. Tais sistemas são frequentemente chamados de sistemas servidor.

Sistemas de banco de dados projetados para ser monousuários, como os de computadores pessoais,

normalmente não proporcionam muitos recursos comuns aos banco de dados multiusuários. Em particular, ele

não dão suporte ao controle de concorrência, o que não é necessário quando somente um usuário pode gerar

atualizações.

A recuperação de perdas nesse tipo de sistema é, senão inexistente, primitiva – por exemplo, fazendo um

backup do banco de dados antes de qualquer atualização. Muitos desses sistemas não dão suporte à SQL,

fornecendo linguagens de consulta bem mais simples, como uma variante da QBE.

Embora os sistemas de computadores de propósito geral possuam atualmente múltiplos processadores,

eles têm paralelismo de granulação-grossa, com um número limitado de processadores (entre dois e quatro,

normalmente), todos compartilhando a memória principal. Os banco de dados rodando em tais equipamentos

normalmente não promovem o particionamento de uma consulta entre processadores: ao contrário, cada

consulta roda em um único processador, permitindo que diversas consultas sejam executada concorrentemente.

Assim, tais sistemas proporcionam alto throughput, isto é, um grande número de transações é processador por

segundo, embora uma transação, individualmente, não seja necessariamente processada com maior rapidez.

Banco de dados processados em equipamento de um só processador já dispõem de recursos multitarefas,

nos quais diversos processos podem ser executados em um mesmo processador de modo compartilhado, dando

ao usuário a impressão de que diversos processos são executados em paralelo. Assim, equipamentos com

paralelismo de granulação-grossa parecem, na lógica, idênticos a um equipamento de um único processador;

sistemas de banco de dados projetados para equipamentos time-shared podem facilmente ser adaptados para

esse ambiente.

Em contrate, os equipamentos de granulação-fina têm um grande número de processadores, e os

sistemas de banco de dados rodando nesse tipo de equipamento podem processar unidades de tarefas

(consultas, por exemplo) submetidas pelos usuários em paralelo.

119

Sistemas Cliente-Servidor

Como os computadores pessoais têm se tornado mais rápidos, mais poderosos e baratos, há uma

tendência de seu uso nos sistemas centralizados. Terminais conectados a sistemas centralizados estão sendo

substituídos por computadores pessoais. Simultaneamente, interfaces com o usuário usadas funcionalmente para

manuseio direto com sistemas centralizados estão sendo adequadas ao trato com computadores pessoais.

Como resultado, sistemas centralizados atualmente agem como sistemas servidores que atendem a

solicitações de sistemas clientes. A estrutura geral de um sistema cliente-servidor é exibida na fig. 16.2.

As funcionalidades de um banco de dados podem ser superficialmente divididas em duas categorias –

front-end e back-end – como mostra a fig. 16.3. O back-end gerencia as estruturas de acesso, desenvolvimento e

otimização de consultas, controle de concorrência e recuperação. O front-end dos sistemas de banco de dados

consiste em ferramentas como formulários, gerador de relatórios e recursos de interface gráfica. A interface

entre front-end e o back-end é feita por meio da SQL ou de um programa de aplicação.

Sistemas servidores podem ser caracterizados, de modo geral, como servidores de transações e

servidores de dados.

• Sistemas servidores de transações, também chamados sistemas servidores de consultas (query-server),

proporcionam uma interface por meio da qual os clientes podem enviar pedidos para determinada ação

e, em resposta, eles executam a ação e mandam de volta os resultados ao cliente. Usuários podem

especificar pedidos por SQL ou por meio de um programa de aplicação usando um mecanismo de

chamada de procedimento remota (remote-procedure-call).

• Sistemas servidores de dados permitem que os servidores interajam com clientes que fazem solicitações

de leitura e atualização de dados em unidades como arquivos ou páginas. Por exemplo, servidores de

arquivos que proporcionam uma interface sistema-arquivo na qual os clientes podem criar, atualizar, ler e

remover arquivos. Servidores de dados para sistemas de banco de dados oferecem muito mais recursos:

dão suporte a unidades de dados – como páginas, tuplas ou objetos – menores que um arquivo.

Proporcionam meios para indexação de dados e transações, tal que os dados nunca se tornem

inconsistentes se um equipamento cliente ou processo falhar.

120

Servidores de Transações

Em sistemas centralizados, o front-end e o back-end são ambos executados dentro de um único sistema.

Entretanto, a arquitetura de servidores de transações segue a divisão funcional entre front-end e back-end.

Devido à grande exigência de processamento para código de interface gráfica e ao aumento do poder de

processamento dos computadores pessoais, o recurso para front-end é possível em computadores pessoais. Os

computadores pessoais agem como clientes de sistemas servidores, os quais armazenam grandes volumes de

dados e dão suporte aos recursos de back-end. Clientes enviam solicitações ao sistema servidor no qual essas

transações são executadas e os resultados são enviados de volta ao cliente que tem a responsabilidade de exibir

esses dados.

Padrões do tipo ODBC (open database connectivity) visam atender à interface de clientes e servidores.

ODBC são programas de aplicação de interface que possibilitam aos clientes a criação de comandos SQL que são

enviados para o servidor, no qual são executados. Qualquer cliente que use uma interface ODBC pode se conectar

a qualquer servidor que forneça essa interface. Nas primeiras gerações de sistemas de banco de dados, a falta

desse tipo de padrão levou ao uso de software de mesmo fabricante tanto para back-end quanto para front-end.

Com a difusão de padrões para interfaces, diversos fabricantes passaram a disponibilizar ferramentas de

front-end e os servidores back-end. Gupta SQL e PowerBuilder são exemplos de sistemas front-end

independentes dos servidores back-end. Logo, alguns programas de aplicação, como as planilhas eletrônicas e

pacotes para análise estatística, usarão interfaces cliente-servidor para acesso direto aos dados de um servidor

back-end. Com efeito, eles funcionam como front-ends especializados para tarefas específicas.

Interfaces cliente-servidor não ODBC são também usadas para alguns sistemas de processamento de

transações. São definidas por uma interface de programa de aplicação na qual os clientes enviam chamadas de

procedimento transacional remota (transactional remote procedure call) para o servidor. Essas chamadas

parecem chamadas de procedimentos simples feitas por programas, mas, na verdade, todas as chamadas de

procedimentos simples feitas por programas, mas, na verdade, todas as chamadas de procedimentos remotas

feitas pelo cliente são encapsuladas em uma única transação para o servidor. Assim, se a transação for abortada,

o servidor poderá reverter os resultados da chamada de procedimento remota.

Como os equipamentos pequenos e individuais apresentam atualmente custo bem menor de aquisição e

manutenção, as grandes corporações tendem a adotar o down-sizing. Muitas empresas estão substituindo seus

equipamentos de grande porte por redes de computadores com estacoes de trabalho ou computadores pessoais

conectados a equipamentos servidores back-end. Algumas das vantagens são a maior funcionalidade e o menor

custo, mais flexibilidade na disseminação, expansão e alocação dos recursos, melhores interfaces com os usuários

e manutenção mais fácil.

Servidores de dados

Sistemas servidores de dados são usados em redes locais, nas quais há conexões de alta velocidade entre

clientes e servidores, os equipamentos clientes são comparáveis, em poder de processamento, aos equipamentos

servidores e as tarefas executadas são do tipo processamento intensivo. Em tal ambiente, faz sentido o tráfego de

dados para o equipamento cliente, para o processamento local (o que pode levar certo tempo) e então o envio

dos dados de volta para o servidor. Note que essa arquitetura exige ampla funcionalidade back-end (fig. 16.3) nos

clientes. Arquiteturas de servidores de dados têm sido comuns nos sistemas de banco de dados orientados a

objetos.

A origem do interesse nesse tipo de arquitetura surge a partir do momento em que o custo, relativo ao

consumo de tempo, da comunicação entre cliente e servidor é alta comparada ao tempo de referência à memória

local (milissegundos versus menos de cem nanossegundos).

• Transmissão de páginas versus transferência de itens. A unidade de comunicação para os dados pode ser

de granularidade grossa, como uma página, ou de granularidade fina, como uma tupla (ou um objeto, no

contexto de banco de dados orientado a objetos).

121

Se a unidade de comunicação é um único item, o overhead para a troca de mensagens é alto se

comparado ao volume de dados transmitido. Quando um item é solicitado, faz sentido também enviar

outros itens que certamente serão usados em um futuro próximo. A busca de itens antes mesmo que

sejam solicitados é chamada prefetching. A transferência de páginas pode ser considerada uma forma de

prefetching se diversos itens residirem em uma mesma página, já que todos os itens na página são

transferidos quando um processo deseja ter acesso a um único item de uma página.

• Bloqueio. Os bloqueios, em geral, são utilizados pelo servidor em itens de dados transitando entre

clientes. Uma desvantagem da transferência de páginas é que as máquinas clientes precisam bloquear a

unidade da granularidade – um bloqueio de uma página implica o bloqueio de todos os itens dessa

página. Mesmo que o cliente não esteja acessando mais de um item dessa página, fica implícito o

bloqueio sobre todos os itens reservados. Outro cliente que solicite bloqueio nesses itens será impedido

desnecessariamente. Algumas técnicas para escala de liberação de bloqueio (lock deescalation) já foram

propostas, nas quais o servidor pode solicitar de volta para seus clientes a transferência dos bloqueios

dos itens reservados. Se o cliente não precisar de um item reservado, ele pode transferir o bloqueio do

item de volta ao servidor, que então pode ser bloqueado por outro cliente.

• Data caching. Os dados que navegam para um cliente durante uma transação pode ser cached no cliente,

mesmo depois de completada a transação, se houver espaço suficiente disponível. Transações sucessivas

em um mesmo cliente podem acarretar o uso de dados cached. Entretanto, o uso de cache exige certa

coerência: mesmo que uma transação ache um dado cached, é preciso ter certeza de que esse dado

esteja atualizado, uma vez que ele pode ter sido alterado por um outro cliente depois de ter sido

colocado em cache. Assim, uma mensagem precisará ainda ser trocada com o servidor para checar a

validade e conseguir um bloqueio sobre o dado.

• Bloqueio cache (lock caching). Se os dados forem bem particionados entre os clientes, com um cliente

raramente solicitando um dado ao mesmo tempo que outro, os bloqueios podem também ser

armazenados localmente (cached) no equipamento cliente. Suponha que um item de dado esteja em

cache e que o bloqueio solicitado para acesso a esse dado também esteja em cache. Então, o acesso pode

ser realizado sem qualquer comunicação com o servidor. Entretanto, o servidor precisa controlar os

bloqueios em cache: se um cliente solicita um bloqueio ao servidor, o servidor precisa recuperar todos os

bloqueios em conflito de itens de dados de qualquer outro equipamento cliente que possua bloqueio em

cache. Essa tarefa torna-se muito mais complicada quando são consideradas as falhas de equipamento.

Essa técnica difere da escala de liberação de bloqueios apenas pelo fato de ser realizada ao longo da

transação; de resto, ambas as técnicas são similares.

Sistemas Paralelos

Sistemas paralelos imprimem velocidade ao processamento e à I/O por meio do uso em paralelos de

diversas CPUs e discos. Equipamentos paralelos estão se tornando bastante comuns, fazendo com que o estudo

de sistema de bancos de dados paralelos seja também cada vez mais importante. O direcionamento das atenções

para os sistemas de banco de dados paralelos proveem da demanda de aplicações que geram consultas em banco

de dados muito grandes (da ordem de terabytes – isto é, 1012 bytes) ou que tenham de processar um volume

enorme de transações por segundo (da ordem de milhares de transações por segundo). Sistemas de banco de

dados centralizados e cliente-servidor não são poderosos o suficiente para tratar desse tipo de aplicação.

No processamento paralelo, muitas operações são realizadas simultaneamente, ao contrário do

processamento serial, no qual os passos do processamento são sucessivos. Um equipamento paralelo de

granulação-grossa consiste em poucos e poderosos processadores; um paralelismo intensivo ou de granulação-

fina usa milhares de pequenos processadores; um paralelismo intensivo ou de granulação-fina usa milhares de

pequenos processadores. A maioria das máquinas high-end, atualmente, oferece algum grau de paralelismo de

granulação-grossa: dois a quatro processadores em uma única máquina já é comum. Computadores de

paralelismo intensivo podem ser diferenciados dos equipamentos de paralelismo de granulação-grossa pelo grau

122

de paralelismo muito mais alto que podem oferecer. Computadores paralelos com centenas de CPUs e discos já

estão disponíveis comercialmente.

São duas as principais formas de avaliar o desempenho de um sistema de banco de dados. A primeira é o

throughput: o número de tarefas realizadas em um dado intervalo de tempo. A segunda é o tempo de resposta: o

tempo total que o sistema leva para completar uma única tarefa. Um sistema que processa um grande número de

pequenas transações pode aumentar o throughput por meio do processamento de diversas transações em

paralelo. Um sistema que processa um grande volume de transações pode aumentar o tempo de resposta, assim

como o throughput por meio do processamento em paralelo.

Aceleração e Escalabilidade

Duas metas são importantes no estudo do paralelismo, a aceleração e a escalabilidade. A aceleração

refere-se à redução do tempo de execução de uma tarefa por meio do aumento do grau de paralelismo. A

escalabilidade diz respeito ao manuseio de um maior número de tarefas por meio do aumento do grau de

paralelismo.

Considere uma aplicação de banco de dados rodando em um sistema paralelo com um certo número de

processadores e discos. Agora, suponha que incrementemos o número de processadores ou discos e outros

componentes do sistema. A meta é processar a tarefa em tempo inversamente proporcional aos processadores e

discos alocados. Suponha que o tempo de execução de uma tarefa em um equipamento de grande porte seja TL e

que o tempo de execução da mesma tarefa em uma máquina de menor porte seja TS. A aceleração em função do

paralelismo é definida por Ts/TL.

O sistema paralelo apresenta um comportamento de aceleração linear se a aceleração for N quando um

sistema de maior porte tiver N vezes mais recursos (CPU, disco e assim por diante) que o sistema de menor porte.

Se a aceleração é menor que N, diz-se que o sistema apresenta comportamento de aceleração sublinear. A fig.

16.4 ilustra as acelerações linear e sublinear.

A escalabilidade relaciona-se à capacidade de processar grande volume de tarefas em um mesmo

intervalo de tempo por meio do aumento dos recursos. Seja Que uma tarefa e QN uma tarefa que é N vezes maior

que Q. Suponha que o tempo de execução de Que em um determinado equipamento MS seja TS e o tempo de

execução da tarefa QN em um equipamento paralelo ML, que é N vezes maior que MS, seja TL.

A escalabilidade é, então, definida como TS /TL. o sistema paralelo ML apresenta um comportamento de

escalabilidade linear na tarefa Q se TL = TS. Se TL > TS, o sistema apresenta um comportamento de escalabilidade

sublinear. A fig. 16.5 ilustra a escalabilidade linear e sublinear. Há dois tipos de escalabilidade relevantes em

sistemas de banco de dados paralelos, dependendo de como se mede a tarefa:

• Na escalabilidade em lote (batch), o tamanho do banco de dados aumenta e as tarefas são grandes jobs

cujo tempo de execução depende do tamanho do banco de dados. Um exemplo de tarefa desse tipo é a

pesquisa em uma relação cujo tamanho é proporcional ao tamanho do banco de dados. Assim, o

tamanho do banco de dados é determinante para a medição do problema. A escalabilidade no

123

processamento em lote também vale para as aplicações científicas, como na execução de uma consulta

com refinamento de N vezes ou a execução de uma simulação com N repetições.

• Na escalabilidade de transação, a taxa de submissão das transações para o banco de dados aumenta e o

tamanho do banco de dados aumenta proporcionalmente à taxa de transações. Esse tipo de

escalabilidade é o que é relevante nos sistemas de processamento de transações nos quais as transações

são pequenas atualizações – por exemplo, um depósito ou saque de uma conta – e a taxa de transações

cresce à medida que mais contas são criadas. Esse tipo de processamento de transações é especialmente

adequado para execução em paralelo, uma vez que as transações podem ser executadas concorrente e

independentemente em processadores separados, e cada transação leva, aproximadamente, o mesmo

tempo, até mesmo se o banco de dados crescer.

A escalabilidade é a mais importante métrica para medir a eficiência de um sistema de banco de dados

paralelo. Normalmente, o objetivo do paralelismo em sistemas de banco de dados é garantir um desempenho

aceitável, mesmo se o tamanho do banco de dados e o volume de transações crescerem. O aumento da

capacidade do sistema em função do paralelismo proporciona um caminho mais suave para o crescimento de

uma empresa que a transferência do sistema centralizado para um equipamento mais rápido (mesmo supondo

que tal máquina exista).

Entretanto, devemos também dar uma olhada nos números relativos ao desempenho absoluto quando

avaliamos a escalabilidade: uma máquina cujo crescimento da escalabilidade seja linear pode resultar em um

desempenho pior que outra cuja escala de crescimento seja menor que a linear, porque se partiu de uma

premissa indevida.

Alguns fatores trabalham contra a eficiência das operações em paralelo e podem reduzir tanto a

aceleração quanto a escalabilidade.

• Custo inicial. Existe um custo inicial associado à inicialização de um processo. Em operações paralelas

consistindo de milhares de processos, o tempo de iniciação (startup time) pode se sobrepor ao tempo

real de processamento, afetando de modo negativo a aceleração.

• Interferência. Uma vez que os processos executando em um sistema paralelo frequentemente mantem

seus acessos a recursos compartilhados, alguma lentidão pode resultar da interferência de cada novo

processo, já que ele disputará os recursos comuns com os processos existentes, tais como cabos, discos

compartilhados ou mesmo bloqueios. Tanto a aceleração quanto a escalabilidade são afetadas por esse

fenômeno.

• Distorção (skew). Com a quebra de uma tarefa em um número determinado de passos paralelos

reduzimos o tamanho do passo médio. Nem sempre o tempo de serviço do passo mais lento determinará

o tempo de serviço para a tarefa como um todo. Frequentemente, é difícil dividir uma tarefa em partes

iguais e o modo de estabelecer essas partes é, portanto, distorcido. Por exemplo, se uma tarefa de

tamanho menor que 10 ou de tamanho maior que 10; mesmo que uma tarefa tenha tamanho 20, a

124

aceleração obtida executando as tarefas em paralelo é de apenas 5, em vez de 10, como poderia ser

esperado.

Interconexão de Redes

Os sistemas paralelos são conjuntos de componentes (processadores, memória, e discos) que podem se

comunicar entre si via redes interconectadas. Exemplos de interconexão de redes incluem:

• Bus. Todos os componentes do sistema podem enviar e receber dados em um único bus (cabo) de

comunicação. O bus pode ser uma ethernet ou um interconector paralelo. Arquiteturas com bus

trabalham bem com um pequeno número de processadores. Entretanto, não respondem bem ao

aumento do paralelismo, já que o bus só pode servir a uma comunicação por vez.

• Malha (mesh). Os componentes são organizados como nós de uma grade, e cada componente é

conectado na grade a todos os componentes adjacentes. Em uma malha bidimensional, cada nó é

conectado a quatro nós adjacentes, enquanto em uma malha tridimensional, cada nó é conectado a seis

nós adjacentes. Os nós que não estão diretamente interconectados podem se comunicar roteando

mensagens por meios dos nós intermediários. O número de ligações para comunicação cresce com o

crescimento do número de componentes e a capacidade de comunicação da malha, portanto responde

melhor ao aumento do paralelismo.

• Hipercúbica (hypercube). Os componentes são numerados em binários e um componente é conectado a

outro se a representação binaria de seus números diferirem em exatamente um bit. Assim, cada um dos n

componentes está conectado a log(n) outros componentes. Isso pode ser verificado quando em uma

interconexão hipercúbica uma mensagem de um componente pode alcançar outro por meio de, no

máximo, log(n) ligações. Em contraste, em uma malha, um componente pode manter √�-1 ligacoes com

algum outro componente (ou √�/2 ligacoes, se a interconexão pela malha alcançar as bordas da grade).

Assim, atrasos na comunicação em um hipercubo são significativamente menores que em uma malha.

Arquiteturas de Banco de Dados Paralelo

Há diversos modelos arquitetônicos para máquinas paralelas. Entre elas, as mais promissoras são

mostradas na fig. 16.6 (na figura, M denota memória, P, processador e os discos são representados por cilindros):

• Memória compartilhada. Todos os processadores compartilham uma mesma memória. Esse modelo é

mostrado na fig. 16.6a.

• Disco compartilhado. Todos os processador compartilham o mesmo disco. Esse modelo é mostrado na

fig. 16.6b. Sistemas com discos compartilhados são, às vezes, chamados de clusters.

• Ausência de compartilhamento. Os processadores não compartilham nem a memória nem disco. Esse

modelo é apresentado na figura 16.6c.

• Hierárquico. Esse modelo é mostrado na fig. 16.6d; ele é um hibrido das arquiteturas anteriores.

125

Memória compartilhada

Na arquitetura com memória compartilhada, os processadores e os discos acessam a memória comum,

normalmente, por meio de cabo ou por meio de rede de interconexão. A vantagem da memória compartilhada é

sua extrema eficiência na comunicação entre processadores – qualquer processador pode ter acesso aos dados

em memória compartilhada sem necessidade de ser movido por software. Um processador pode enviar

mensagens a outro processador usando a memória. Essas trocas de mensagens são bem mais rápidas

(normalmente, menos de um microssegundo) se comparadas às que usam mecanismos de comunicação. O

problema das máquinas com memória compartilhada é que a arquitetura não é adequada ao uso de mais de 32

ou 64 processadores, uma vez que o bus ou a interconexão por rede torna-se um gargalo (pois é compartilhado

por todos os processadores). Após determinado ponto, adicionar mais processadores não resolve; eles gastarão a

maior parte de seu tempo esperando sua vez de usar o bus para acesso à memória. Arquiteturas de memória

compartilhada utilizam, normalmente, grande memória cache em cada processador para que o acesso à memória

compartilhada seja evitado sempre que possível. Porém, pelo menos alguns desses dados não estarão em

memória cache e os acessos serão dirigidos à memória compartilhada. Consequentemente, máquinas com

memória compartilhada não possibilitam aumento de escala além de determinado ponto; as máquinas de

memória compartilhada, atualmente, não dão suporte a mais de 64 processadores.

Disco Compartilhado

No modelo de disco compartilhado, todos os processadores podem ter acesso direto a todos os discos, via

interconexão por rede, mas os processadores possuem memórias próprias. Há duas vantagens da arquitetura de

discos compartilhados sobre a de memória compartilhada. Primeiro, uma vez que cada processador possui

memória exclusiva, o bus de memória não representa um gargalo. Segundo, essa arquitetura oferece um modo

barato de aumentar a tolerância a falhas: se o processador (ou sua memória) falha, o outro processador pode

assumir suas tarefas, já que o banco de dados residente nos discos é acessível a todos os processadores. Podemos

fazer o subsistema de discos, ele próprio, tolerante a falhas usando a arquitetura RAID. A arquitetura de discos

compartilhados vem obtendo aceitação em diversos tipos de aplicações; os sistemas construídos sobre

arquitetura desse tipo são frequentemente chamados de clusters.

126

O principal problema dos sistemas de discos compartilhados é, novamente, o grau de crescimento.

Embora o bus de memória não represente mais um gargalo, a restrição é agora a interconexão com o subsistema

de discos; esse caso é particularmente determinante quando o banco de dados acessa muito os discos.

Comparando aos sistemas de memória compartilhada, os sistemas de discos compartilhados podem ser usados

com um número maior de processadores, mas a comunicação entre os processadores é lenta (um pouco acima de

milissegundos quando não há hardware específico para a comunicação), uma vez que ela tem de atravessar a

rede.

Cluster DEC rodando Rdb foi um dos primeiros usuários comerciais a utilizar a arquitetura de discos

compartilhados (o Rdb é atualmente propriedade da Oracle e é chamado de Oracle Rdb).

Ausência de Compartilhamento

Em um sistema sem compartilhamento, cada equipamento de um nó consiste em um processador, uma

memória e discos. O processador de um nó pode comunicar-se com outros processadores de outros nós usando

intercomunicação de rede de alta velocidade. Um nó serve de servidor dos dados alocados em seus discos. Uma

vez que as referências aos discos são atendidas em cada processador por discos locais, o modelo sem

compartilhamento transpõe os percalços de submeter todos os I/Os por meio de uma única rede de

interconexão; somente consultas, acessos a discos remotos e o resultado das relações são transportados por

meio da rede. Além disso, as redes de interconexão dos sistemas sem compartilhamento são normalmente

projetadas para permitir o crescimento de escala, o que significa que sua capacidade aumenta quanto mais nós

são acrescidos. Consequentemente, arquiteturas sem compartilhamento são mais flexíveis quanto à escala e

podem, facilmente, dar suporte a um grande número de processadores. Os principais problemas dos sistemas

sem compartilhamento são os custos de comunicação e os acessos a discos remotos, que são maiores que não

arquitetura com memória ou discos compartilhados, já que a transmissão de dados envolve interação feita por

software em ambos os lados.

A máquina de banco de dados Teradata foi um dos primeiros sistemas comerciais a usar a arquitetura de

banco de dados com ausência de compartilhamento.

Hierárquica

A arquitetura hierárquica combina as características das arquiteturas de compartilhamento de memória e

discos com a arquitetura sem compartilhamento. Em seu nível mais alto, o sistema constitui-se de nós que são

conectados por uma rede sem compartilhar discos ou memória entre si. O topo de linha é um sistema sem

compartilhamento. Cada nó do sistema pode ser um sistema com memória compartilhada entre poucos

processadores. Alternativamente, cada nó poderia ser um sistema de discos compartilhados e cada um desses

sistemas que compartilham um conjunto de discos poderia também compartilhar memória. Desse modo, o

sistema poderia ser construído obedecendo a uma hierarquia, com o compartilhamento de memória por alguns

processadores na base e sem compartilhamento algum no nível mais alto, com possivelmente uma arquitetura de

compartilhamento de discos intermediaria. A fig. 16.6d ilustra uma arquitetura hierárquica de nós com memória

compartilhada conectada por uma arquitetura com ausência de compartilhamento. Sistemas de banco de dados

paralelos comerciais atualmente rodam em diversas dessas arquiteturas.

Tentativas para redução da complexidade de programação desse tipo de sistema resultaram em

arquiteturas de memória virtual distribuída, nas quais há uma única memória lógica compartilhada, mas

fisicamente há diversos sistemas de memória separados; o acoplamento de hardware com mapeamento da

memória virtual por meio de suporte extra de memória oferece uma visa única de área de memória virtual dessas

memórias separadas.

Sistemas Distribuídos

Em um sistema de banco de dados distribuído, o banco de dados é armazenado em diversos

computadores. Os computadores de um sistema de banco de dados distribuído comunica-se com outros por

127

intermédio de vários meios de comunicações, como redes de alta velocidade ou linhas telefônicas. Eles não

compartilha memória principal ou discos. Os computadores em um sistema de banco de dados distribuído podem

variar, quanto ao tamanho e funções, desde estacoes de trabalho até sistemas de grande porte (mainframe).

Os computadores de um sistema de banco de dados distribuído recebem diversos nomes, como sites ou

nós, dependendo do contexto no qual são inseridos. Usaremos preferencialmente o termo site (local, sítio) para

enfatizar a distribuição física desses sistemas. A estrutura geral do sistema distribuído é mostrada na fig. 16.7.

As principais diferenças entre os banco de dados paralelos sem compartilhamento e os banco de dados

distribuídos são que, nos banco de dados distribuídos, há a distribuição física geográfica, administração separada

e uma intercomunicação menor. Outra importante diferença é que, nos sistemas distribuídos, distinguimos

transações locais de globais. Uma transação local acessa um único site, justamente no qual ela se inicia. Uma

transação global, por outro lado, é aquela que busca acesso a diferentes sites, ou a outro site além daquele em

que se inicia.

Exemplo Ilustrativo

Considere o sistema de uma empresa da área bancária que consiste em quatro agências em quatro

cidades diferentes. Cada agência possui seu próprio computador, com um banco de dados abrangendo todas as

contas referentes àquela agência.

Cada uma dessas instalações é, assim, um site. Há também um único site que mantem informações sobre

todas as agências do banco. Cada agência mantém (entre outras) uma relação conta (Esquema_conta), em que:

Esquema_conta = (nome_agência, número_conta, saldo)

O site que mantém informações sobre as quatro agências possui a relação agência(Esquema_agência), em

que:

Esquema_agência = (nome_agência, cidade_agência, fundos)

Há outras relações nos diversos sites; nós a ignoramos em nosso exemplo.

Para ilustrar a diferença entre os dois tipos de transações, consideramos a transação de adicionar 50

dólares à conta A-177 localizada na agência Valleyview. Se uma transação foi iniciada na agência Valleyview, ela é

então considerada local; caso contrário, será considerada global. Uma transação para transferir 50 dólares da

conta A-177 para a conta A-305, a qual está localizada na agência Hillside, é uma transação global, uma vez que

conta em sites diferentes sofrem acessos como resultado de sua execução.

O que faz dessa configuração um sistema de banco de dados distribuído são os seguintes aspectos:

128

• Os vários sites estão disponíveis entre si.

• Os sites compartilham um esquema global comum, embora algumas relações possam estar armazenadas

em alguns sites.

• Cada site proporciona um ambiente para execução tanto de transações locais quanto de transações

globais.

• Cada site roda o mesmo software para o gerenciamento de banco de dados.

Se houver sistemas gerenciadores de banco de dados diferentes rodando nos sites, torna-se difícil o

gerenciamento de transações globais. Tais sistemas são chamados de sistemas de banco de dados múltiplos

(multidatabase) ou sistemas de banco de dados distribuídos heterogêneos.

Tradeoffs

Há diversas razoes para a utilização de sistemas de banco de dados distribuídos, como o

compartilhamento de dados, a autonomia e a disponibilidade.

• Compartilhamento de dados. A maior vantagem de um sistema de banco de dados distribuído é criar um

ambiente no qual os usuários de um site podem ter acesso a dados residentes em outros sites. Por

exemplo, no sistema distribuído bancário usado como exemplo, os usuários de uma agência podem ter

acesso aos dados de outra agência. Sem essa disponibilidade, um usuário que desejasse transferir fundos

de uma agência para outra teria de recorrer a algum mecanismo externo.

• Autonomia. A primeira vantagem do compartilhamento dos dados por meio da distribuição dos dados é

que cada site pode manter certo nível de controle sobre os dados que estão armazenados localmente. Em

sistemas centralizados, o administrado do banco de dados central é quem gerencia o banco de dados. Em

sistemas de banco de dados distribuídos há um administrador geral responsável pelo banco como um

todo. Uma parte dessa responsabilidade é delegada ao administrador local de cada site. Dependendo do

projeto do banco de dados distribuído, os administradores podem possuir diferentes graus de autonomia

local é provavelmente uma das maiores vantagens dos banco de dados distribuídos.

• Disponibilidade. Se um site sai do ar em um sistema distribuído, os demais sites podem continuar em

operação. Particularmente, se os itens de dados são replicados em diversos sites, uma transação que

precise de um item de dado em particular pode encontrar tal item presente em diversos sites. Assim, a

queda de um site não implica, necessariamente, a queda do sistema.

A queda de um site precisa ser detectada pelo sistema, assim como determinadas ações devem ser

executadas para recuperá-lo da falha. O sistema não poderá mais usar os serviços do site fora do ar. Finalmente,

quando um site volta a funcionar ou quando é consertado, é necessário dispor de mecanismos para integrá-lo

paulatinamente ao sistema.

Embora a recuperação de uma falha seja mais complexa nos sistemas distribuídos que nos sistemas

centralizados, a capacidade da maioria dos sistemas manter-se em operação a despeito da falha de um site acaba

por aumentar a disponibilidade. A disponibilidade é crucial para os sistemas de banco de dados usados em

aplicações de tempo real. A perda do acesso aos dados em, por exemplo, uma companhia aérea pode fazer com

que um cliente em potencial prefira viajar com uma companhia concorrente.

A principal desvantagem dos sistemas de banco de dados distribuídos é o acréscimo de complexidade

necessário para assegurar a coordenação entre os sites. Esse aumento de complexidade toma diversas formas:

• Custo de desenvolvimento de software. É mais difícil implementar sistemas de banco de dados

distribuídos, portanto, o custo é mais alto.

• Maior possibilidade de bugs. Uma vez que os sites que constituem o sistema distribuído operam em

paralelo, é difícil assegurar a precisão dos algoritmos, especialmente durante a ocorrência e recuperação

de falha em parte do sistema. Há, potencialmente, a chance de bugs extremamente sutis.

129

• Aumento do processamento e overhead. A troca de mensagens e processamento adicional necessários à

coordenação entre sites são uma forma de overhead que não ocorre nos sistemas centralizados.

Na escolha do projeto do sistema de banco de dados, o projetista deve ponderar as vantagens e

desvantagens da distribuição dos dados. Há diversas abordagens para um projeto de banco de dados distribuído,

partindo de projetos totalmente distribuídos até os que mantêm alto nível de centralização.

Tipos de Redes

Sistemas de banco de dados distribuídos e sistemas cliente-servidor são apoiados em redes de

comunicação. Há basicamente dois tipos de redes: redes locais (local-area networks – LAN) e redes de longa

distância (wide-area networks – WAN). A principal diferença entre as duas é o modo pelo qual são distribuídas

geograficamente. Redes locais são compostas por processadores distribuídos sobre pequenas extensões

geográficas, como um único edifício ou alguns prédios próximos. Redes de longa distância, por outro lado, são

compostas por determinado número de processadores autônomos, distribuídos por uma extensa área geográfica.

Essas diferenças envolvem maiores variações na velocidade e confiabilidade das redes de comunicação e se

refletem no projeto do sistema operacional.

Redes Locais

As redes locais (LANs) apareceram no início dos anos 70 como meio de comunicação entre computadores

e para compartilhamento de dados. Percebeu-se que, em diversas empresas, numerosos pequenos

computadores, cada um com suas próprias aplicações, são mais econômicos que um único grande sistema. Como

cada pequeno equipamento provavelmente necessita de acesso a um grande número de dispositivos periféricos

(como discos e impressoras) e como a necessidade de alguma forma de compartilhamento de dados é

frequentemente em uma empresa, a consequência natural foi a conexão desses pequenos sistemas em uma rede

de computadores.

As LANs são normalmente projetadas para cobrir uma pequena área geográfica e são, geralmente, usadas

me escritórios. Todos os sites deste tipo de sistema estão próximos entre si, assim a comunicação entre eles

tende a manter velocidades mais altas e menores taxas de erro que as apresentadas pelas redes de longa

distância. O tipo de ligação mais comum entre os computadores de uma rede local é o par trançado, cabo coaxial

de banda larga e fibra ótica. A velocidade de comunicação gira em torno de um megabyte por segundo, para rede

como Appletalk e IBM, redes lentas em anel (token ring), até um gigabit por segundo, para redes óticas

experimentais. Dez megabits por segundo é bastante comum, é essa a velocidade da Ethernet. As redes de fibra

ótica FDDI (optical-fiber-based) e Fast Ethernet rodam a cem megabits por segundo. Redes com base no

protocolo chamado protocolo assíncrono (asynchronous transfer mode – ATM) podem alcançar velocidades

superiores, como 144 megabits por segundo, e estão se tornando bastante populares.

Uma LAN típica consiste em diferentes estações de trabalho, um ou mais sistemas servidores, vários

dispositivos periféricos compartilhados (como impressora a laser ou unidades de fita magnética) e um ou mais

gateways (processadores especializados) que proporcionam acesso a outras redes (fig. 16.8). o esquema Ethernet

é usado com frequência em LANs.

130

Redes de Longa Distância

As redes de longa distância (WANs) apareceram no final da década de 1960, principalmente em projetos

de pesquisas acadêmicas para comunicação eficiente entre sites, permitindo que o hardware e o software

pudessem ser compartilhados conveniente e economicamente entre a grande comunidade de usuários. Os

sistemas que permitiram a conexão de terminais remotos com um computador central via linhas telefônicas

foram desenvolvidos no início da década de 1960, embora não fossem de fato uma WAN. A primeira WAN

projetada e desenvolvida foi a Arpanet. Os trabalhos na Arpanet começaram em 1968. A Arpanet desenvolveu-se

a partir de quatro redes experimentais até chegar à rede mundial, a Internet, compreendendo milhões de

sistemas computacionais. A ligação característica da WAN são circuitos telefônicos que usam linhas de fibra ótica

e (por vezes) canais de satélite.

Como exemplo, consideremos a Internet, que conecta computadores pelo mundo. O sistema possibilita

que hosts em sites geograficamente separados comuniquem-se entre si. Os computadores hosts diferem uns dos

outros no tipo, velocidade, tamanho da palavra, sistema operacional, etc. Os hosts são, geralmente, LANs

conectadas a redes regionais. As redes regionais são interligadas a roteadores para formar a rede mundial. As

conexões entre as redes usam frequentemente o serviço de telefonia chamado T1, que oferece taxas de

transferência de cerca de 44.736 megabits por segundo. As mensagens enviadas para a rede são roteadas por

sistemas chamados de roteadores, que controlam o caminho percorrido por cada mensagem através da rede.

Esse roteamento pode ser dinâmico, para aumentar a eficiência da comunicação, ou estático, para reduzir riscos

ou permitir que a carga de comunicação seja processada mais facilmente.

Outras WANs em operação usam linhas telefônicas padrão como principal meio de comunicação. Os

modems são dispositivos que recebem sinal digital de um computador e convertem esses dados em sinais

analógicos, que são usados pelo sistema de telefonia. Um modem no site de destino converte o sinal analógico

em digital e, assim, o equipamento de destino recebe o dado. A velocidade dos modems varia de 2400 bips a 32

kilobits por segundo. Os sistemas de telefonia que aceitam o padrão Rede Digital de Serviços Integrados

(Integrated Services Digital Network – ISDN) permitem que os dados sejam transferidos ponto a ponto a altas

taxas, normalmente 128 kilobits por segundo.

A rede UNIX, UUCCP, permite que os sistemas se comuniquem uns com os outros um número limitado de

vezes (e, geralmente, predeterminado), via modems, para troca de mensagens. Essas mensagens são roteadas

para outro sistema próximo e, dessa forma, propagadas para todos os hosts da rede (mensagens publicas) ou

transferidas para seu destino (mensagens particulares).

131

As WANs são classificadas em dois tipos:

• WAN conexão descontínua, como aquelas que têm por base a UUCP, em que os hosts estão conectados à

rede somente por determinados períodos.

• WAN conexão contínua, como a Internet, cujos hosts estão conectados à rede o tempo todo.

O projeto de um sistema de banco de dados distribuído é fortemente influenciado pelo tipo de WAN de

apoio. Os verdadeiros sistemas de banco de dados distribuídos podem rodar apenas sobre as redes conectadas

continuamente – pelo menos durante as horas em que o banco de dados distribuído está operacional.

Redes que não estão continuamente conectadas, geralmente, não permitem transações entre sites, mas

tomam cópias locais dos dados remotos e atualizam periodicamente essas cópias (toda noite, por exemplo). Para

aplicações cuja consistência não seja crítica, como compartilhamento de documentos, sistemas de trabalho em

grupo (groupware systems) como o Lotus Notes, permitem atualizações em dados remotos feitos localmente e

essas atualizações retornam periodicamente ao site remoto. Há conflito potencial de atualizações entre sites que

precisa ser detectado e resolvido. Um mecanismo para detecções de atualizações conflitantes existe; o

mecanismo para resolução de conflitos de atualização, entretanto, é dependente da aplicação.

Arquitetura de sistemas de bancos de dados

INTRODUÇÃO

Agora temos condições de apresentar uma arquitetura para um sistema de bancos de dados. Nosso

objetivo ao apresentar essa arquitetura é fornecer um arcabouço sobre o qual possamos desenvolver os capítulos

subsequentes. Esse arcabouço é útil para descrever conceitos gerais de bancos de dados e explicar a estrutura de

sistemas de bancos de dados específicos — mas não afirmamos que todo sistema pode se enquadrar bem nesse

arcabouço em particular, nem queremos sugerir que essa arquitetura prevê o único arcabouço possível. Em

especial, é provável que sistemas “pequenos” (ver Capítulo 1) não ofereçam suporte para todos os aspectos da

arquitetura. Porém, a arquitetura em questão de fato parece se ajustar razoavelmente bem à maior parte dos

sistemas; além disso, ela é basicamente idêntica à arquitetura proposta pelo ANSI/SPARC Study Group on Data

Base Management Systems (a chamada arquitetura ANSI/SPARC — consulte as referências [2.1-2.2]). Contudo,

optamos por não seguir a terminologia ANSI/SPARC em todos os detalhes.

OS TRÊS NÍVEIS DA ARQUITETURA

A arquitetura ANSI/SPARC se divide em três níveis, conhecidos como nível interno, nível conceitual e nível

externo, respectivamente (ver Figura 2.1). De modo geral:

• O nível interno (também conhecido como nível físico) é o mais próximo do meio de armazenamento físico — ou

seja, é aquele que se ocupa do modo como os dados são fisicamente armazenados.

• O nível externo (também conhecido como nível lógico do usuário) é o mais próximo dos usuários — ou seja, é

aquele que se ocupa do modo como os dados são vistos por usuários individuais.

• O nível conceitual (também conhecido como nível lógico comunitário, ou às vezes apenas nível indireto, sem

qualificação) é um nível de “simulação” entre os outros dois.

Observe que o nível externo se preocupa com as percepções dos usuários individuais, enquanto o nível

conceitual está preocupado com uma percepção da comunidade de usuários. Em outras palavras, haverá muitas

“visões externas” distintas, cada qual consistindo em uma representação mais ou menos abstrata de alguma

parte do banco de dados completo, e haverá exatamente uma “visão conceitual”, consistindo em uma

representação analogamente abstrata do banco de dados em sua totalidade.* (Lembre-se de que a maioria dos

usuários não estará interessada em todo o banco de dados, mas somente em alguma porção restrita do banco de

dados.) Do mesmo modo, haverá precisamente uma “visão interna”, representando o modo como o banco de

dados está fisicamente armazenado.

132

Um exemplo ajudará a esclarecer essas ideias. A Figura 2.2 mostra a visão conceitual, a visão interna

correspondente e duas visões externas correspondentes (uma para um usuário PL/I e outra para um usuário

COBOL), todas para um simples banco de dados de pessoal. É claro que o exemplo é inteiramente hipotético —

ele não pretende se assemelhar a qualquer sistema real — e muitos detalhes irrelevantes foram deliberadamente

omitidos. Explicação:

• No nível conceitual, o banco de dados contém informações relativas a um tipo de entidade chamada

EMPREGADO. Cada empregado individual tem um NUMERO_EMPREGADO (seis caracteres), um

NUMERO_DEPARTAMENTO (quatro caracteres) e um SALARIO (cinco dígitos decimais).

• No nível interno, os empregados são representados por um tipo de registro armazenado, denominado

EMP_ARMAZENADO, com vinte bytes de comprimento. EMP_ARMAZENADO contém quatro campos

armazenados: um prefixo de seis bytes (presumivelmente contendo informações de controle, tais como flags ou

ponteiros) e três campos de dados correspondentes às três propriedades de empregados. Além disso, os registros

EMP_ARMAZENADO são indexados sobre o campo EMP# por um índice chamado EMPX, cuja definição não é

mostrada.

• O usuário PL/I tem uma visão externa do banco de dados na qual cada empregado é representado por um

registro PL/I que contém dois campos (números de departamentos não são de interesse para esse usuário, e por

isso foram omitidos da visão). O tipo de registro é definido por uma declaração de estrutura PL/I comum, de

acordo com as regras normais de PL/I.

• De modo semelhante, o usuário COBOL tem uma visão externa em que cada empregado é representado por um

registro COBOL contendo, mais uma vez, dois campos (agora, foram omitidos os salários). O tipo de registro é

definido por uma descrição comum de registro COBOL, de acordo com as regras normais do COBOL.

Observe que itens de dados correspondentes podem ter nomes diferentes em pontos diferentes. Por

exemplo, a referência ao número do empregado é chamada EMP# na visão externa de PL/I, EMPNO na visão

externa COBOL, NUMERO EMPREGADO na visão conceitual e novamente EMP# na visão interna. É claro que o

sistema deve estar ciente das correspondências; por exemplo, ele tem de ser informado de que o campo EMPNO

do COBOL é derivado do campo conceitual Por abstrata, queremos dizer nesse caso apenas que a representação

133

em questão envolve construções como registros e campos, mais orientadas para o usuário, em oposição a

construções como bits e bytes, mais orientadas para a máquina.

Observe que faz pouca diferença para as finalidades deste capítulo saber se o sistema que está sendo

considerado é relacional ou não. Contudo, pode ser útil indicar de forma resumida como os três níveis da

arquitetura são em geral percebidos especificamente em um sistema relacional:

• Primeiro, o nível conceitual em tal sistema será definitivamente relacional, no sentido de que os objetos visíveis

nesse nível serão tabelas relacionais, e os operadores serão operadores relacionais (incluindo, em especial, os

operadores de restrição e projeção examinados de forma abreviada no Capítulo 1).

• Em segundo lugar, uma determinada visão externa também será tipicamente relacional, ou algo muito próximo

disso; por exemplo, as declarações de registros PL/I ou COBOL da Figura 2.2 podem ser consideradas de maneira

informal, respectivamente, os equivalentes PL/I ou COBOL da declaração de uma tabela relacional em um sistema

relacional.

Nota: devemos mencionar de passagem que o termo “visão externa” (em geral abreviado apenas como “visão”)

infelizmente tem um significado bastante específico em contextos relacionais que não é idêntico ao significado

atribuído ao termo neste capítulo. Consulte os Capítulos 3 e 9 para ver uma explicação e uma descrição do

significado relacional.

• Terceiro, o nível interno não será relacional, porque os objetos nesse nível não serão apenas tabelas relacionais

(armazenadas) — em vez disso, eles serão os mesmos tipos de objetos encontrados no nível interno de qualquer

outra espécie de sistema (registros armazenados, ponteiros, índices, hashing etc.). De fato, o modelo relacional

em si não tem absolutamente nenhuma relação com o nível interno; ele se preocupa, como vimos no Capítulo 1,

com o modo como o banco de dados é visto pelo usuário.

Agora vamos prosseguir com o exame dos três níveis da arquitetura com um nível muito maior de

detalhes, começando pelo nível externo. Em toda a nossa descrição, faremos repetidas referências à Figura 2.3,

que mostra os principais componentes da arquitetura e seus inter-relacionamentos pelo administrador de banco

de dados (DBA) .

134

O NÍVEL EXTERNO O nível externo é o nível do usuário individual. Como foi explicado no Capítulo 1, um determinado usuário pode ser ou programador de aplicações ou um usuário final com qualquer grau de sofisticação. (O DBA é um caso especial importante; porém, diferentemente de outros usuários, o DBA também precisará estar interessado nos níveis conceitual e interno. Consulte as duas seções seguintes.)

Cada usuário tem uma linguagem à sua disposição: • Para o programador de aplicações, essa linguagem será uma linguagem de programação convencional (como PL/I, C+ +, Java) ou uma linguagem proprietária específica para o sistema em questão. Essas linguagens proprietárias são frequentemente chamadas de linguagens de “quarta geração” (L4Gs), pelo fato (muito difuso!) de que (a) o código de máquina, a linguagem assembler e a linguagem PL/I podem ser consideradas como três “gerações” mais antigas de linguagens e (b) as linguagens proprietárias representam o mesmo tipo de aperfeiçoamento em relação às linguagens de “terceira geração” (L3Gs) que essas linguagens representavam em relação à linguagem assembler e esta última, por sua vez, representava em relação ao código de máquina. • Para o usuário final, a linguagem será uma linguagem de consulta ou alguma linguagem de uso especial, talvez dirigida por formulários ou menus, adaptada aos requisitos desse usuário e com suporte de algum programa aplicativo on-line.

Para nossos propósitos, o ponto importante sobre todas essas linguagens é que elas incluirão uma sublinguagem de dados — isto é, um subconjunto da linguagem completa relacionado de modo específico aos objetos e às operações do banco de dados. A sublinguagem de dados (abreviada como DSL — data sublanguage — na Figura 2.3) é dita embutida na linguagem hospedeira correspondente. A linguagem hospedeira é responsável pelo fornecimento de diversos recursos não relacionados com bancos de dados, como variáveis locais, operações de cálculo, lógica de desvios condicionais (if-then-else), e assim por diante. Um dado sistema poderia admitir qualquer número de linguagens hospedeira e qualquer número de sublinguagens de dados; porém, uma determinada sublinguagem de dados reconhecida por quase todos os sistemas atuais é a linguagem SQL, discutida brevemente no Capítulo 1. A maioria desses sistemas permite que a SQL seja usada tanto de modo interativo como uma linguagem de consulta autônoma, quanto incorporada a outras linguagens como PL/I ou Java (consulte o Capítulo 4 para ver detalhes adicionais). Observe que, embora seja conveniente para propósitos arquiteturais distinguir entre a sublinguagem de dados e

135

a linguagem hospedeira que a contém, as duas podem de fato não ser distintas para o usuário; na verdade, talvez seja preferível sob a perspectiva do usuário que elas não sejam distintas. Se elas não forem distintas, ou se só puderem ser distinguidas com dificuldade, diremos que elas estão fortemente acopladas. Se forem clara e facilmente distinguíveis, dizemos que elas estão fracamente acopladas. Apesar de alguns sistemas comerciais (em especial sistemas de objetos — ver Capítulo 24) admitirem o acoplamento forte, a maioria não o aceita. Em particular, os sistemas SQL costumam oferecer suporte apenas para o acoplamento fraco. (O acoplamento forte oferece um conjunto de recursos mais uniforme para o usuário, mas sem dúvida envolve maior esforço por parte dos desenvolvedores de sistemas, um fato que presumivelmente contribui para o status quo.) Em princípio, qualquer sublinguagem de dados determinada é, na realidade, uma combinação de pelo menos duas linguagens subordinadas — uma linguagem de definição de dados (DDL — Data Definition Language), que dá suporte à definição ou à declaração de objetos dos bancos de dados, e uma linguagem de manipulação de dados (DML — Data Manipulation Language), que admite a manipulação ou o processamento desses objetos. Por exemplo, considere o usuário PL/I da Figura 2.2, na Seção 2.2. A sublinguagem de dados para esse usuário consiste nos recursos de PL/I utilizados para a comunicação com o SGBD: • A parte de DDL consiste nas construções declarativas de PL/I necessárias para se declararem objetos do banco de dados — a própria instrução DECLARE(DEL), certos tipos de dados de PL/I, possivelmente extensões especiais de PL/I para oferecer suporte a novos objetos não manipulados pela PL/I existente. • A parte da DML consiste nas instruções executáveis de PL/I que transferem informações de e para o banco de dados — mais uma vez incluindo, possivelmente, novas instruções especiais. Nota: mas precisamente, devemos dizer que a PL/I não inclui na realidade nenhum recurso específico de bancos de dados na época em que este livro foi escrito. Em particular, as instruções de “DML” costumam ser apenas instruções CALL de PL/I que invocam o SGBD (embora essas chamadas possam estar sintaticamente disfarçadas de algum modo, a fim de torná-las um pouco mais amistosas para o usuário — consulte a discussão sobre a SQL embutida no Capítulo 4).

Voltando à arquitetura: já indicamos que um usuário individual geralmente só estará interessado em alguma parte do banco de dados como um todo; além disso, a visão que esse usuário tem dessa parte será em geral um tanto abstrata quando comparada com o modo como os dados estão fisicamente armazenados. O termo ANSI/SPARC para a visão de um usuário individual é visão externa. Uma visão externa é, portanto, o conteúdo do banco de dados visto por algum usuário determinado (ou seja, para esse usuário a visão externa é o banco de dados). Por exemplo, um usuário do Departamento de Pessoal poderia considerar o banco de dados uma coleção de ocorrências de registros de departamentos e empregados, e ele poderia não ter nenhum conhecimento das ocorrências de registros de fornecedores e peças vistas pelos usuários do Departamento de Compras.

Nível Conceitual A visão conceitual é uma representação de todo conteúdo de informação de informações do banco de dados, mais uma vez (como no caso de uma visão externa) em uma forma um tanto abstrata, em comparação com o modo como os dados são visualizados por qualquer usuário em particular. Em termos gerais, a visão conceitual pretende ser uma visão dos dados “como eles realmente são”, em vez de forçar os usuários a vê-los pelas limitações (por exemplo) da linguagem ou do hardware que eles possam estar utilizando. A visão conceitual consiste em muitas ocorrências de cada um dos vários tipos de registros conceituais. Por exemplo, ela pode consistir em uma coleção de ocorrências de registros de departamentos, junto com uma coleção de ocorrências de registros de empregados, junto com uma coleção de ocorrências de registros de fornecedores, mais uma coleção de ocorrências de registros de peças, e assim por diante. Um registro conceitual não é necessariamente o mesmo que um registro externo, nem o mesmo que um registro armazenado. A visão conceitual é definida por meio do esquema conceitual, que inclui definições de cada um dos vários tipos de registros conceituais (mais uma vez, observe um exemplo simples da fig. 2.2). o esquema conceitual é escrito por meio de outra linguagem de definição de dados, a DDL conceitual. Se quisermos alcançar a independência física dos dados, então essas definições de DDL conceitual não deverão envolver quaisquer considerações sobre a representação física ou a técnica conceitual, não deverá haver nenhuma referência a representações de campos armazenados, sequencias de registros armazenados, índices, esquemas de hashing, ponteiros ou quaisquer outros detalhes de armazenamento e acesso. Se o esquema conceitual se tornar verdadeiramente independente de dados dessa maneira, então os esquemas externos, definidos em termos do esquema conceitual, também são independentes dos dados. Portanto, a visão conceitual é uma visão do conteúdo total do banco de dados, e o esquema conceitual é uma definição dessa visão. Porém, seria enganoso sugerir que o esquema conceitual nada mais é do que um

136

conjunto de definições muito semelhante às definições de registros simples, encontradas hoje em (por exemplo) um programa COBOL. As definições no esquema conceitual têm por finalidade incluir muitos recursos adicionais, como as restrições de segurança e integridade. Algumas autoridades no assunto chegariam até a sugerir que o objetivo final do esquema conceitual é descrever a empresa inteira – não apenas seus dados em si, mas também o modo como esses dados são usados; como eles fluem de um ponto para outro dentro da empresa, para q eles são usados em cada ponto, quais controles de auditoria ou outros controles devem ser aplicados em cada ponto, e assim por diante. No entanto, devemos enfatizar que nenhum sistema atual admite realmente um esquema conceitual q sequer se aproxime desse grau de complexidade; na maior parte dos sistemas existentes, o “esquema conceitual” é, na verdade, pouco mais que uma simples união de todos os esquemas externos individuais, juntamente com determinadas restrições de segurança e integridade. Porém, sem dúvida, é possível q sistemas futuros eventualmente se tornem muito mais sofisticados em seu suporte ao nível conceitual. O NÍVEL INTERNO

O terceiro nível da arquitetura é o nível interno. A visão interna é uma representação de baixo nível do banco de dados por inteiro; ela consiste em muitas ocorrências de cada um dos vários tipos de registros internos. “Registro interno” é o termo ANSI/SPARC que representa a construção que temos chamado de registro armazenado (e continuaremos a usar essa última forma). Assim, a visão interna ainda está muito afastada do nível físico, pois ela não lida com registros físicos — também chamados blocos ou páginas — nem com quaisquer considerações específicas de dispositivos, como tamanhos de cilindros ou trilhas. Em outras palavras, a visão interna pressupõe efetivamente um espaço de endereços linear infinito; os detalhes de como esse espaço de endereços é mapeado no meio de armazenamento físico são bastante específicos para cada sistema e foram deliberadamente omitidos da arquitetura geral. Nota: o bloco, ou a página, é a unidade de entrada e saída (E/S) — isto é, ele representa a quantidade de dados transferidos entre o meio de armazenamento secundário e a memória principal em uma única operação de E/S. Os tamanhos de páginas típicos são 1 K, 2 K ou 4 K bytes (K = 1.024). A visão interna é descrita por meio do esquema interno, que não somente define os diversos tipos de registros armazenados mas também especifica quais índices existem, como os campos armazenados estão representados, em que sequência física estão os registros armazenados, e assim por diante (mais uma vez, veja na Figura 2.2 um exemplo simples). O esquema interno é escrito usando-se ainda outra linguagem de definição de dados — a DDL interna. Nota: neste livro, usaremos normalmente os termos mais intuitivos “estrutura de armazenamento” ou “banco de dados armazenado” em vez de “visão interna”, e ainda “definição de estrutura de armazenamento” em lugar de “esquema interno”.

Para encerrar lembramos que, em certas situações excepcionais, os programas aplicativos — em particular as aplicações de natureza utilitária (ver Seção 2.11) — podem ter permissão para operar diretamente no nível interno, e não no nível externo. Não é preciso dizer que essa prática não é recomendável; ela representa um risco de segurança (pois as restrições de segurança são ignoradas) e um risco de integridade (pois também as restrições de integridade são ignoradas), e o programa terá uma inicialização dependente dos dados; porém, às vezes essa poderá ser a única maneira de obter a funcionalidade ou o desempenho exigido — do mesmo modo o usuário em uma linguagem de programação de alto nível poderia ter a necessidade ocasional de descer até a linguagem assembler para satisfazer certos objetivos de funcionalidade ou desempenho.

MAPEAMENTOS Além dos três níveis básicos, a arquitetura da Figura 2.3 envolve, em geral, certos mapeamentos — um mapeamento conceitual/interno e vários mapeamentos externos/conceituais: • O mapeamento conceitual/interno define a correspondência entre a visão conceitual e o banco de dados armazenado; ele especifica o modo como os registros e campos conceituais são representados no nível interno. Se a estrutura do banco de dados armazenado for alterada — isto é, se for efetuada uma mudança na definição do banco de dados armazenado – o mapeamento conceitual/interno terá de ser alterado de acordo, a fim de q o esquema conceitual possa permanecer invariável. (é responsabilidade do DBA, ou provavelmente, do SGBD, administrar tais alterações.) Em outras palavras, os efeitos dessas mudanças devem ser isolados abaixo do nível conceitual, a fim de preservar a independência de dados física. • Definir o esquema interno

137

O DBA também deve decidir como serão representados os dados no banco de dados armazenado. Em geral, esse processo é chamado projeto de banco de dados físico.* Tendo elaborado o projeto físico, o DBA deve então criar a definição da estrutura de armazenamento correspondente (isto é, o esquema interno), usando a DDL interna. Além disso, ele também deve definir o mapeamento conceitual/interno associado. Na prática, a DDL conceitual ou a DDL interna — mais provavelmente a primeira — deverá incluir os meios para definir esse mapeamento, mas as duas funções (criação do esquema, definição do mapeamento) devem ser claramente distinguíveis. Como no caso do esquema conceitual, o esquema interno e o mapeamento correspondente existirão tanto na forma de fonte quanto de objeto. • Ligação com usuários É tarefa do DBA fazer a ligação com os usuários, a fim de garantir que os dados de que eles necessitam estarão disponíveis, e escrever (ou ajudar os usuários a escreverem) os esquemas externos necessários, usando a DDL externa aplicável. (Como já foi dito, um dado sistema pode admitir várias DDLs externas distintas.) Além disso, os mapeamentos externos/conceituais correspondentes também devem ser definidos. Na prática, a DDL externa provavelmente incluirá os meios para especificar esses mapeamentos, mas, de novo, os esquemas e os mapeamentos devem ser claramente distinguíveis. Cada esquema externo e o mapeamento correspondente deverão existir tanto na forma de fonte quanto de objeto. Outros aspectos da função de ligação com o usuário incluem a consultoria em projeto de aplicações, o fornecimento de instrução técnica, a assistência para determinação e resolução de problemas e serviços profissionais semelhantes. • Definir restrições de segurança e integridade Como já vimos, as restrições de segurança e integridade podem ser consideradas uma parte do esquema conceitual. A DDL conceitual deve incluir recursos para a especificação de tais restrições. • Definir normas de descarga e recarga Uma vez que uma empresa esteja comprometida com um sistema de banco de dados, ela se torna dependente de modo crítico do sucesso desse sistema. Em caso de danos a qualquer parte do banco de dados — provocados por erro humano, digamos, ou por uma falha de hardware ou do sistema operacional — é essencial ser capaz de reparar os dados em questão com um mínimo de demora e com o menor efeito possível sobre o restante do sistema. Por exemplo, em condições ideais, a disponibilidade dos dados que não tenham sido danificados não deve ser afetada. O DBA tem de definir e implementar um esquema apropriado de controle de danos, em geral envolvendo (a) descarga periódica ou dumping do banco de dados para o meio de armazenamento de backup e (b) recarregamento do banco de dados quando necessário, a partir do dump mais recente. A propósito, a discussão anterior fornece uma razão pela qual seria uma boa ideia espalhar a coleção total de dados por vários bancos de dados, em vez de manter tudo em um único lugar; o banco de dados individual poderia muito bem formar a unidade para finalidades de descarga e recarregamento. Nessa linha, observe que já existem sistemas terabyte** — isto é, instalações de bancos de dados comerciais que armazenam bem mais de um trilhão de bytes de dados, em termos informais — e que os sistemas do futuro deverão ser muito maiores. E desnecessário dizer que tais sistemas VLDB (“banco de dados muito grande” — very large database) exigem administração muito cuidadosa e sofisticada, em especial se houver um requisito de disponibilidade contínua (que normalmente existe). Não obstante, por simplicidade, continuaremos a falar como se de fato houvesse um único banco de dados. * Observe a sequência: primeiro, defina que dados você deseja; depois, decida como representá-los no meio de armazenamento.

O projeto físico sempre deve ser feito depois do projeto lógico. **1.o24 bytes = um kilobyte (KB); 1.024 KB = um megabyte (MB); 1.024 MB = um gigabyte (GB); 1.024 GB um terabyte (TB); 1.024 TB = um petabyte (PB); 1.024 PB = um exabyte (EB). Observe que, informalmente, um gigabyte equivale a um bilhão de bytes (a abreviatura BB é empregada às vezes em lugar de GB).

138

• Monitorar o desempenho e responder a requisitos de mudanças Como foi indicado na Seção 1.4, o DBA é responsável pela organização do sistema de modo a

obter o desempenho que seja “o melhor para a empresa”, e por fazer os ajustes apropriados — isto é, a sintonia fina (tuning) — conforme os requisitos se alterarem. Por exemplo, poderia ser necessário reorganizar o banco de dados armazenado de tempos em tempos para assegurar que os níveis de desempenho permanecerão aceitáveis. Como já mencionamos, qualquer mudança no nível de armazenamento físico (interno) do sistema deve ser acompanhada por uma mudança correspondente na definição do mapeamento de nível conceitual/interno, de modo que o esquema conceitual possa permanecer constante. E claro que a lista anterior não esgota o assunto — ela pretende apenas dar uma ideia da extensão e da natureza das responsabilidades do DBA. O SISTEMA DE GERENCIAMENTO DE BANCOS DE DADOS

O sistema de gerenciamento de bancos de dados (SGBD) é o software que trata de todo o acesso ao banco de dados. Conceitualmente, o que ocorre é o seguinte (observe mais uma vez a Figura 2.3): 1. Um usuário faz um pedido de acesso usando uma determinada sublinguagem de dados (em geral, SQL). 2. O SGBD intercepta o pedido e o analisa. 3. O SGBD inspeciona, por sua vez, o esquema externo (ou as versões objeto desse esquema) para esse usuário, o mapeamento externo/conceitual correspondente, o esquema conceitual, o mapeamento conceitual/interno e a definição da estrutura de armazenamento. 4. O SGBD executa as operações necessárias sobre o banco de dados armazenado. Como exemplo, considere as ações relacionadas com a busca de uma determinada ocorrência de registro externo. Em geral, serão necessários campos de várias ocorrências de registros conceituais e, por sua vez, cada ocorrência de registro conceitual exigirá campos de várias ocorrências de registros armazenados. Então, conceitualmente, o SGBD deve primeiro buscar todas as ocorrências necessárias de registros armazenados, depois construir as ocorrências de registros conceituais exigidas e, em seguida, construir a ocorrência de registro externo. Em cada estágio, podem ser necessárias conversões de tipos de dados ou outras conversões. Naturalmente, a descrição anterior é muito simplificada; em particular, ela implica que todo o processo é interpretativo, à medida que sugere que os processos de análise do pedido, inspeção dos diversos esquemas etc., são todos realizados durante a execução. A interpretação, por sua vez, em geral implica um desempenho sofrível devido à sobrecarga em tempo de execução. Porém, na prática talvez seja possível fazer os pedidos de acesso serem compilados antes do momento da execução (em particular, diversos produtos atuais de SQL fazem isso — consulte as anotações relativas às referências [4.12] e [4.26] do Capítulo 4). Vamos examinar agora as funções do SGBD com um pouco mais de detalhes. Essas funções incluirão o suporte a pelo menos todos os itens a seguir (observe a Figura 2.4): • Definição de dados O SGBD deve ser capaz de aceitar definições de dados (esquemas externos, o esquema conceitual, o esquema interno e todos os mapeamentos associados) em forma fonte e convertê-los para a forma objeto apropriada. Em outras palavras, o SGBD deve incluir componentes de processador de DDL ou compilador de DDL para cada uma das diversas linguagens de definição de dados (DDLs). O SGBD também deve “entender” as definições da DDL, no sentido de que, por exemplo, ele “entende” que os registros externos EMPREGADO incluem um campo SALARIO; ele deve então ser capaz de usar esse conhecimento para analisar e responder a pedidos de manipulação de dados (por exemplo, “obtenha todos os empregados com salário < R$ 50.000,00”). • Manipulação de dados O SGBD deve ser capaz de lidar com solicitações do usuário para buscar, atualizar ou excluir dados existentes no banco de dados, ou para acrescentar novos dados ao banco de dados. Em outras

139

palavras, o SGBD deve incluir um componente processador de DML ou compilador de DML para lidar com a linguagem de manipulação de dados (DML — data manipulation language). Em geral, as solicitações de DML podem ser “planejadas” ou “não-planejadas”: a. Uma solicitação planejada é aquela para a qual a necessidade foi prevista com bastante antecedência em relação ao momento em que a solicitação é executada. O DBA provavelmente terá ajustado o projeto de banco de dados físico de modo a garantir um bom desempenho para solicitações planejadas. b. Em contraste, uma solicitação não-planejada é uma consulta ad hoc, isto é, uma solicitação cuja necessidade não foi prevista com antecedência mas, em vez disso, surgiu no último instante. O projeto de banco de dados físico pode estar ou não adaptado de forma ideal para a solicitação específica sendo considerada. Para usar a terminologia introduzida no Capítulo 1 (Seção 1.3), as solicitações planejadas são características de aplicações “operacionais” ou “de produção”, ao passo que as solicitações não-planejadas são características de aplicações para “apoio à decisão”. Além disso, as solicitações planejadas em geral serão emitidas a partir de programas aplicativos escritos previamente, enquanto as solicitações não-planejadas serão, por definição, emitidas de modo interativo por meio de algum processador de linguagem de consulta. Nota: como vimos no Capítulo 1, o processador de linguagem de consulta é na realidade uma aplicação on-line embutida, não uma parte do SGBD per se; ele foi incluído na Figura 2.4 por completitude. • Otimização e execução As requisições de DML, planejadas ou não-planejadas, devem ser processadas pelo componente otimizador, cujo propósito é determinar um modo eficiente de implementar a requisição. A otimização é discutida em detalhes no Capítulo 17. As requisições otimizadas são então executadas sob o controle do gerenciador em tempo de execução (run time). Nota: na prática, o gerenciador em tempo de execução provavelmente invocará algum tipo de gerenciador de arquivos para obter acesso aos dados armazenados. Os gerenciadores de arquivos são descritos resumidamente no final desta seção. • Segurança e integridade de dados O SGBD deve monitorar requisições de usuários e rejeitar toda tentativa de violar as restrições de segurança e integridade definidas pelo DBA (consulte a seção anterior). Essas tarefas podem ser executadas em tempo de compilação ou em tempo de execução, ou ainda em alguma mistura dos dois. • Recuperação e concorrência de dados O SGBD — ou, mais provavelmente, algum outro componente de software relacionado, cm geral chamado gerenciador de transações ou monitor de processamento de transações (monitor TP) — deve impor certos controles de recuperação e concorrência. Os detalhes desses aspectos do sistema estão além do escopo deste capítulo; consulte a Parte IV deste livro, que contém uma descrição em profundidade do assunto. Nota: o gerenciador de transações não é mostrado na Figura 2.4 porque, em geral, ele não faz parte do SGBD propriamente dito.

• Dicionário de dados O SGBD deve fornecer uma função de dicionário de dados. O dicionário de dados pode ser considerado um banco de dados em si (mas um banco de dados do sistema, não um banco de dados impõem restrições - de segurança e integridade do usuário), O dicionário contém “dados sobre os dados” (às vezes chamados metadados ou descritores) — ou seja, definições de outros objetos do sistema, em vez de “dados brutos” somente. Em particular, todos os vários esquemas e mapeamentos (externos, conceituais etc.) e todas as diversas restrições de segurança e integridade estarão armazenados, tanto na forma de fonte quanto de objeto, no dicionário. Um dicionário completo também incluirá muitas informações adicionais mostrando, por exemplo, os programas que utilizam determinadas partes do banco de dados, os usuários que exigem certos relatórios, os terminais conectados ao sistema, e assim por diante. O dicionário poderia até estar — na verdade, provavelmente deve estar — integrado ao banco de dados que define e, portanto, incluir sua própria definição. Por certo, deve haver a opção de consultar o dicionário como qualquer outro banco de dados para que, por exemplo, seja possível saber que programas e/ou usuários terão maior probabilidade de serem afetados

140

por alguma alteração proposta no sistema. Consulte o Capítulo 3 para ver uma discussão adicional sobre o assunto. Nota: estamos entrando agora em uma área sobre a qual existe muita confusão de terminologia. Algumas pessoas fariam referência ao que estamos chamando de dicionário como um diretório ou catálogo, o que implica que diretórios ou catálogos são de algum modo inferiores a um verdadeiro dicionário — e reservariam o termo dicionário para designar uma variedade específica (importante) de ferramenta para desenvolvimento de aplicações. Outros termos que também são algumas vezes empregados para designar essa última variedade de objetos são repositório de dados (ver Capítulo 13) e enciclopédia de dados.

É desnecessário dizer que o SGBD deve realizar todas as funções identificadas anteriormente de forma tão eficiente quanto possível.

Podemos resumir tudo o que foi mencionado antes afirmando que a função geral do SGBD é fornecer a interface do usuário para o sistema de banco de dados. A interface do usuário pode ser definida como a fronteira no sistema abaixo da qual tudo é invisível para o usuário. Por definição, portanto, a interface do usuário está no nível externo. Contudo, como veremos no Capítulo 9, há algumas situações em que é improvável que a visão externa seja significativamente diferente da porção relevante da visão conceitual subjacente, pelo menos nos produtos SQL comerciais de hoje.

Concluímos esta seção com uma breve comparação entre os sistemas de gerenciamento de bancos de dados discutidos anteriormente e os sistemas de gerenciamento de arquivos (gerenciadores de arquivos ou servidores de arquivos). Em linhas gerais, o gerenciador de arquivos é o componente do sistema operacional básico que administra arquivos armazenados; portanto, em termos informais, ele está “mais próximo ao disco” que o SGBD. (Na verdade, o SGBD costuma ser montado sobre algum tipo de gerenciador de arquivos.) Desse

141

modo, o usuário de um sistema de gerenciamento de arquivos será capaz de criar e destruir arquivos armazenados e de executar operações simples de busca e atualização sobre registros armazenados em tais arquivos. No entanto, em contraste com o SGBD: • Os gerenciadores de arquivos não têm conhecimento da estrutura interna dos registros armazenados e, por isso, não podem lidar com requisições que dependam de um conhecimento dessa estrutura. • Em geral, eles fornecem pouco ou nenhum suporte para restrições de segurança e integridade. • Em geral, eles fornecem pouco ou nenhum suporte para controles de recuperação e concorrência. • Não há verdadeiramente um conceito de dicionário de dados no nível do gerenciador de arquivos. • Eles proporcionam muito menos independência de dados que o SGBD. • Em geral, os arquivos não estão “integrados” ou “compartilhados” no mesmo sentido que o banco de dados (normalmente, eles são privativos de algum usuário ou alguma aplicação em particular). O GERENCIADOR DE COMUNICAÇÕES DE DADOS

Nesta seção, consideraremos resumidamente o tópico de comunicações de dados. As requisições a bancos de dados de um usuário final são, na verdade, transmitidas da estação de trabalho do usuário — que pode estar fisicamente afastada do próprio sistema de banco de dados — para alguma aplicação on-line (embutida ou não), e daí até o SGBD, sob a forma de mensagens de comunicação. De modo semelhante, as respostas do SGBD e da aplicação on-line para a estação de trabalho do usuário também são transmitidas sob a forma de mensagens. Todas essas transmissões de mensagens têm lugar sob o controle de outro componente de software, o gerenciador de comunicações de dados (gerenciador DE — data communications).

O gerenciador DE não faz parte do SGBD, mas é um sistema autônomo. Porém, como o gerenciador DE e o SGBD são claramente obrigados a trabalhar em harmonia, às vezes os dois são considerados parceiros de igual nível em um empreendimento cooperativo de nível mais alto, denominado sistema de banco de dados/comunicações de dados (sistema DB/DE), no qual o SGBD toma conta do banco de dados e o gerenciador DE manipula todas as mensagens de e para o SGBD ou, mais precisamente, de e para aplicações que utilizam o SGBD. Porém, neste livro, teremos pouco a dizer sobre o manejo de mensagens como essas (o que, por si só, é um assunto extenso). A Seção 2.12 descreve resumidamente a questão das comunicações entre sistemas distintos (ou seja, entre máquinas diferentes em uma rede de comunicações, como a Internet), mas esse é, na realidade, um tópico à parte. ARQUITETURA CLIENTE/SERVIDOR

Descrevemos até agora neste capítulo os sistemas de bancos de dados sob o ponto de vista da chamada arquitetura ANSI/SPARC. Em particular, a Figura 2.3 forneceu uma representação simplificada dessa arquitetura. Nesta seção, examinaremos os sistemas de bancos de dados sob uma perspectiva um pouco diferente. Naturalmente, o objetivo geral desses sistemas é fornecer suporte ao desenvolvimento e à execução de aplicações de bancos de dados. Portanto, sob um ponto de vista de mais alto nível, um sistema de banco de dados pode ser considerado como tendo uma estrutura muito simples em duas partes, consistindo em um servidor (também chamado back end) e um conjunto de clientes (também chamados front ends). Consulte a Figura 2.5. Explicação: • O servidor é o próprio SGBD. Ele admite todas as funções básicas de SGBDs discutidas na Seção 2.8 — definição de dados, manipulação de dados, segurança e integridade de dados, e assim por diante. Em particular, ele oferece todo o suporte de nível externo, conceitual e interno que examinamos nas Seções 2.3 a 2.6. Assim, o termo “servidor” neste contexto é tão-somente um outro nome para o SGBD. • Os clientes são as diversas aplicações executadas sobre o SGBD — tanto aplicações escritas por usuários quanto aplicações internas, ou seja, aplicações fornecidas pelo fabricante do SGBD ou por produtores independentes. No que se refere ao servidor, é claro que não existe nenhuma diferença entre aplicações escritas pelo usuário e aplicações internas — todas elas empregam a mesma interface para o servidor, especificamente a interface de nível externo discutida na Seção 2.3.

142

Nota: certas aplicações especiais chamadas “utilitários” poderiam constituir uma exceção ao que vimos antes, pois às vezes elas podem precisar operar diretamente no nível interno do sistema (conforme mencionamos na Seção 2.5). Esses utilitários devem ser considerados componentes integrais do SGBD, em vez de aplicações no sentido usual. Os utilitários serão discutidos com mais detalhes na próxima seção. Vamos aprofundar o exame da questão de aplicações escritas pelo usuário versus aplicações fornecidas pelo fabricante: • Aplicações escritas pelo usuário são basicamente programas aplicativos comuns, escritos (em geral) em uma linguagem de programação convencional de terceira geração (L3G), como C ou COBOL, ou então em alguma linguagem proprietária de quarta geração (L4G) — embora em ambos os casos a linguagem precise ser de algum modo acoplada a uma sublinguagem de dados apropriada, conforme explicamos na Seção 2.3. Banco de dados • Aplicações fornecidas por fabricante (chamadas frequentemente de ferramentas — tools) são aplicações cuja finalidade básica é auxiliar na criação e execução de outras aplicações! As aplicações criadas são aplicações adaptadas a alguma tarefa específica (elas podem não ser muito semelhantes às aplicações no sentido convencional; de fato, a finalidade das ferramentas é permitir aos usuários, em especial aos usuários finais, criar aplicações sem ter de escrever programas em uma linguagem de programação convencional). Por exemplo, uma das ferramentas fornecidas pelo fabricante será um gerador de relatórios, cuja finalidade é permitir que os usuários finais obtenham relatórios formatados a partir do sistema sob solicitação. Qualquer solicitação de relatório pode ser considerada um pequeno programa aplicativo, escrito em uma linguagem de geração de relatórios de nível muito alto (e finalidade especial). As ferramentas fornecidas pelo fabricante podem ser divididas em diversas classes mais ou menos distintas: a. Processadores de linguagem de consulta. b. Geradores de relatórios. c. Subsistemas gráficos de negócios. d. Planilhas eletrônicas. e. Processadores de linguagem natural. f. Pacotes estatísticos. g. Ferramentas para gerenciamento de cópias ou “extração de dados”. h. Geradores de aplicações (inclusive processadores L4G). i. Outras ferramentas para desenvolvimento de aplicações, inclusive produtos de engenharia de software auxiliada pelo computador (CASE — computer-aided software engineering). Os detalhes dessas ferramentas e de várias outras estão além do escopo deste livro; entretanto, observamos que, tendo em vista que (como foi dito antes) toda a importância de um sistema de banco de dados está em dar suporte à criação e à execução de aplicações, a qualidade das ferramentas disponíveis é, ou deve ser, um fator preponderante na “decisão sobre o banco de dados” (isto é, o processo de escolha do produto de banco de dados apropriado). Em outras palavras, o SGBD em si não é o único fator que precisa ser levado em consideração, nem mesmo é necessariamente o fator mais significativo. Encerramos esta seção com uma referência para o que se segue. Como o sistema por completo pode estar tão claramente dividido em duas partes, servidores e clientes, surge a possibilidade de executar os dois em

143

máquinas diferentes. Em outras palavras, existe o potencial para o processamento distribuído. O processamento distribuído significa que máquinas diferentes podem estar conectadas entre si para formar algum tipo de rede de comunicações, de tal modo que uma única tarefa de processamento de dados possa ser dividida entre várias máquinas na rede. Na verdade, essa possibilidade é tão atraente — por urna variedade de razões, principalmente de ordem econômica — que o termo “cliente/servidor” passou a se aplicar a quase exclusivamente ao caso em que o cliente e o servidor estão de fato localizados em máquinas diferentes. Examinaremos o processamento distribuído com mais detalhes na Seção 2.12. UTILITÁRIOS Utilitários são programas projetados para auxiliar o DBA com diversas tarefas administrativas. Como mencionamos na seção anterior, alguns programas utilitários operam no nível externo do sistema e, portanto, são na verdade apenas aplicações de uso especial; alguns podem nem mesmo ser fornecidos pelo fabricante do SGBD, mas sim por algum fabricante independente. Porém, outros utilitários operam diretamente no nível interno (em outras palavras, eles realmente fazem parte do servidor) e, desse modo, devem ser oferecidos pelo fornecedor do SGBD.

Aqui estão alguns exemplos dos tipos de utilitários que costumam ser necessários na prática: • Rotinas de carga, a fim de criar a versão inicial do banco de dados a partir de um ou mais arquivos do sistema operacional. • Rotinas de descarregamento/recarregamento, a fim de descarregar o banco de dados, ou partes dele, para o meio de armazenamento de backup e recarregar dados dessas cópias de backup (é claro que o “utilitário de recarregamento” é basicamente idêntico ao utilitário de carga que acabamos de mencionar). • Rotinas de reorganização, a fim de rearranjar os dados no banco de dados armazenado por várias razões (em geral, relacionadas com o desempenho) — por exemplo, para agrupar dados de algum modo particular no disco, ou para reaver o espaço ocupado por dados que se tornaram obsoletos. • Rotinas estatísticas, a fim de calcular diversas estatísticas de desempenho, tais como tamanhos de arquivos e distribuição de valores de dados ou contagens de E/S etc. • Rotinas de análise, a fim de analisar as estatísticas mencionadas antes. A lista precedente representa apenas uma pequena amostra das funções em geral oferecidas pelos utilitários; existe uma série de outras possibilidades. PROCESSAMENTO DISTRIBUÍDO Repetindo o que mencionamos na Seção 2.10, a expressão “processamento distribuído” significa que máquinas diferentes podem estar conectadas entre si em uma rede de comunicações como a Internet, de tal modo que uma única tarefa de processamento de dados possa se estender a várias máquinas na rede. (A expressão “processamento paralelo” também é utilizada algumas vezes com significado quase idêntico, exceto pelo fato de que as diferentes máquinas tendem a manter uma certa proximidade física em um sistema “paralelo” e não precisam estar tão próximas em um sistema “distribuído” — por exemplo, elas poderiam estar geograficamente dispersas no último caso.) A comunicação entre as várias máquinas é efetuada por algum tipo de software de gerenciamento de rede — possivelmente uma extensão do gerenciador DE discutido na Seção 2.9 ou, com maior probabilidade, um componente de software separado.

São possíveis muitos níveis ou variedades de processamento distribuído. Conforme mencionamos na Seção 2.10, um caso simples envolve a execução do back end do SGBD (o servidor) em uma das máquinas e dos front ends da aplicação (os clientes) em outra. Ver Figura 2.6.

Como vimos no final da Seção 2.10, o termo “cliente/servidor”, embora seja estritamente uma expressão relacionada com a arquitetura, passou a ser quase um sinônimo da disposição ilustrada na Figura 2.6, na qual o cliente e o servidor funcionam em máquinas diferentes. De fato, há muitos argumentos em favor de um esquema desse tipo: • O primeiro é basicamente o argumento usual sobre o processamento paralelo: especificamente, muitas unidades de processamento estão sendo agora aplicadas na tarefa global, enquanto o processamento do servidor (o banco de dados) e o processamento do cliente (a aplicação) estão sendo feitos em paralelo. O tempo de resposta e a vazão (throughput) devem assim ser melhorados. • Além disso, a máquina servidora pode ser uma máquina feita por encomenda para se ajustar à função de SGBD (uma “máquina de banco de dados”) e pode assim fornecer melhor desempenho de SGBD.

144

• Do mesmo modo, a máquina cliente poderia ser uma estação de trabalho pessoal, adaptada às necessidades do usuário final e, portanto, capaz de oferecer interfaces melhores, alta disponibilidade, respostas mais rápidas e, de modo geral, maior facilidade de utilização para o usuário.

• Várias máquinas clientes distintas poderiam ser capazes (na verdade, normalmente serão capazes) de obter acesso à mesma máquina servidora. Assim, um único banco de dados poderia ser compartilhado entre vários sistemas clientes distintos (ver Figura 2.7).

Além dos argumentos anteriores, existe também o fato de que a execução do(s) cliente(s) e do servidor em máquinas diferentes combina com a maneira como as empresas operam na realidade. E bastante comum que uma única empresa — um banco, por exemplo — opere muitos computadores, de tal modo que os dados correspondentes a uma parte da empresa sejam armazenados em um computador e os dados de outra parte sejam armazenados em outro computador. Prosseguindo com o exemplo do banco, é muito provável que os usuários de uma agência precisem ocasionalmente obter acesso a dados armazenados em outra agência. Portanto, observe que as máquinas clientes poderiam ter seus próprios dados armazenados, e a máquina servidora poderia ter suas próprias aplicações. Dessa forma, em geral caia maquina atuará como um servidor para alguns usuários e como cliente para outros (ver Figura 2.8); em outras palavras, cada máquina admitirá um sistema de banco de dados por inteiro, no sentido estudado em seções anteriores deste capítulo. O último ponto a mencionar é que uma única máquina cliente poderia ser capaz de obter acesso a várias máquinas servidoras diferentes (a recíproca do caso ilustrado na Figura 2.7). Esse recurso é desejável porque, como mencionamos antes, as empresas em geral operam de tal maneira que a totalidade de seus dados não fica armazenada em uma única máquina, mas se espalha por muitas máquinas diferentes, e as aplicações às vezes precisarão ter a capacidade de conseguir acesso a dados de mais de uma máquina. Basicamente, esse acesso pode ser fornecido de dois modos distintos: • Um dado cliente pode ser capaz de obter acesso a qualquer número de servidores, mas somente um de cada vez (ou seja, cada requisição individual ao banco de dados tem de ser direcionada para apenas um servidor). Em um sistema desse tipo não é possível, dentro de uma única requisição, combinar dados de dois ou mais servidores diferentes. Além disso, o usuário de tal sistema tem de saber qual máquina em particular contém cada um dos fragmentos de dados. • O cliente pode ser capaz de obter acesso a muitos servidores simultaneamente (isto é, uma única solicitação ao banco de dados poderia ter a possibilidade de combinar dados de vários servidores). Nesse caso, os servidores aparentam para o cliente — de um ponto de vista lógico — ser realmente um único servidor, e o usuário não precisa saber qual máquina contém cada um dos itens de dados.

145

Esse último caso constitui um exemplo daquilo que se costuma chamar sistema de banco de dados

distribuído, O tema de bancos de dados distribuídos é um grande tópico por si só; levado a sua conclusão lógica, o suporte completo a bancos de dados distribuídos implica que uma única aplicação deve ser capaz de operar “de modo transparente” sobre dados espalhados por uma variedade de bancos de dados diferentes, gerenciados por uma variedade de SGBDs diferentes, funcionando em uma variedade de máquinas distintas, com suporte de uma variedade de sistemas operacionais diferentes e conectados entre si por meio de uma variedade de redes de comunicações diferentes — onde “de modo transparente” significa que a aplicação opera, de um ponto de vista lógico, como se os dados fossem todos gerenciados por um único SGBD funcionando em uma única máquina. Esse recurso pode parecer algo muito difícil de conseguir, mas é altamente desejável de uma perspectiva prática, e os fabricantes estão trabalhando arduamente para tornar realidade esses sistemas. Discutiremos em detalhes os sistemas de bancos de dados distribuídos no Capítulo 20.

146

Normalização

O conceito de normalização foi introduzido por E. F. Codd em 1970 (primeira forma normal). Esta técnica

é um projeto matemático formal, que tem seus fundamentos na teoria dos conjuntos.

Através deste processo pode-se, gradativamente, substituir um conjunto de entidades e relacionamentos

por um outro, o qual se apresenta “purificado” em relação às anomalias de atualização (inclusão, alteração e

exclusão) as quais podem causar certos problemas, tais como: grupos repetitivos (atributos multivalorados) de

dados, dependências parciais em relação a uma chave concatenada, redundância de dados desnecessários,

perdas acidentais de informação, dificuldade na representação de fatos da realidade observada e dependências

transitivas entre atributos.

Os conceitos abordados podem ser aplicados às duas formas de utilização da normalização:

- sentido de cima para baixo (TOP-DOWN):

Após a definição de um modelo de dados, aplica-se a normalização para se obter uma síntese dos dados,

bem como uma decomposição das entidades e relacionamentos em elementos mais estáveis, tendo em vista sua

implementação física em um banco de dados;

- sentido de baixo para cima (BOTTON-UP):

Aplicar a normalização como ferramenta de projeto do modelo de dados, usando os relatórios,

formulários e documentos utilizados pela realidade em estudo, constituindo-se em uma ferramenta de

levantamento.

Anomalias de Atualização

Observando-se o formulário de PEDIDO apresentado na fig. 12.1, podemos considerar que uma entidade

formada com os dados presentes terá a seguinte apresentação:

• Atributos da entidade PEDIDO:

o número do pedido

o prazo de entrega

o cliente

o endereço

o cidade

o UF

o CGC

o inscrição estadual

o código do produto (*)

o unidade do produto (*)

o quantidade do produto (*)

o descrição do produto (*)

o valor unitário do produto (*)

o valor total do produto (*)

o valor total do pedido (*)

o código do vendedor

o nome do vendedor

(*) Atributos que se repetem no documento

147

Caso a entidade fosse implementada como uma tabela em um banco de dados, as seguintes anomalias

iriam aparecer:

• anomalia de inclusão: ao ser incluído um novo cliente, o mesmo tem que estar relacionado a uma venda;

• anomalia de exclusão: ao ser excluído um cliente, os dados referentes as suas compras serão perdidos;

• anomalia de alteração: caso algum fabricante de produto altere a faixa de preço de uma determinada

classe de produtos, será preciso percorrer toda a entidade para se realizar múltiplas alterações.

Primeira forma normal (1FN)

Em uma determinada realidade, às vezes encontramos algumas informações que se repetem (atributos

multivalorados), retratando ocorrências de um mesmo fato dentro de uma única linha e vinculadas a sua chave

primária.

Ao observarmos a entidade PEDIDO, apresentada acima, visualizamos um certo grupo de atributos

(produtos solicitados) se repete (número de ocorrências não definidas) ao longo do processo de entrada de dados

na entidade.

A 1FN diz que: cada ocorrência da chave primária deve corresponder a uma e somente uma informação

de cada atributo, ou seja, a entidade não deve conter grupos repetitivos (multivalorados).

148

Para se obter entidades na 1FN, é necessário decompor cada entidade não normalizada em tantas

entidades quanto for o número de conjuntos de atributos repetitivos. Nas novas entidades criadas, a chave

primária é a concatenação da chave primária da entidade original mais o(s) atributo(s) do grupo repetitivo

visualizado(s) como chave primária desse grupo.

Para a entidade PEDIDO, temos:

Entidade não normalizada:

Ao aplicarmos a 1FN sobre a entidade PEDIDO, obtemos mais uma entidade chamada de ITEM-DE-

PEDIDO, que herdará os atributos repetitivos e destacados da entidade PEDIDO.

149

Um PEDIDO possui no mínimo 1 e no máximo N elementos de ITEM-DE-PEDIDO e um ITEM-DE-PEDIDO

pertence a 1 e somente 1 PEDIDO, logo o relacionamento POSSUI é do tipo 1:N.

Variação Temporal e a Necessidade de Histórico

Observamos que normalmente, ao se definir um ambiente de armazenamento de dados, seja ele um

banco de dados ou não, geralmente se mantém a última informação cadastrada, que às vezes, por sua própria

natureza, possui um histórico de ocorrências. Mas como a atualização é sempre feita sobre esta última

informação, perdem-se totalmente os dados passados.

A não-observação deste fato leva a um problema na hora de uma auditoria de sistemas, que em vez de

utilizar uma pesquisa automatizada sobre os históricos, se vê obrigada a uma caçada manual cansativa sobre um

mar imenso de papeis e relatórios, e que na maioria das vezes se apresenta incompleta ou inconsistente devido a

valores perdidos (documentos extraviados) ou não documentados.

Com a não-utilização de históricos e a natural perda destas informações, a tomada de decisões por parte

da alta administração de uma empresa pode levar a resultados catastróficos para a corporação.

Toda vez que a decisão de armazenar o histórico de algum atributo for tomada, cria-se explicitamente um

relacionamento de um para muitos (1-N), entre a entidade que contém o atributo e a entidade criada para conter

o histórico deste atributo. Passa a existir então uma entidade dependente, contendo (no mínimo) toda data em

que houve alguma alteração do atributo bem como o respectivo valor do atributo para cada alteração. A chave

desta entidade de histórico será concatenada, e um de seus atributos será a data de referência.

150

Com base nesta necessidade de armazenamento de históricos, após a aplicação da 1FN devemos observar

para cada entidade definida, quais de seus atributos se transformarão com o tempo, se é preciso armazenar

dados históricos deste atributo e em caso afirmativo, observar o período de tempo que devemos conservar este

histórico, ou através de quantas alterações foram realizadas neste atributo.

Dependência Funcional

Para descrevermos as próximas formas normais, se faz necessária a introdução do conceito de

dependência funcional, sobre o qual a maior parte da teoria de normalização foi baseada.

Dada uma entidade qualquer, dizemos que um atributo ou conjunto de atributos A é dependente

funcional de um outro atributo B contido na mesma entidade, se a cada valor de B existir nas linhas da entidade

em que aparece, um único valor de A. Em outras palavras, A depende funcionalmente de B.

Ex.: Na entidade PEDIDO, o atributo PRAZO-DE-ENTREGA depende funcionalmente de NÚMERO-DO-

PEDIDO.

O exame das relações existentes entre os atributos de uma entidade deve ser feito a partir do

conhecimento (conceitual) que se tem sobre a realidade a ser modelada.

Dependência Funcional Total (Completa) e Parcial

Na ocorrência de uma chave primária concatenada, dizemos que um atributo ou conjunto de atributos

depende de forma completa ou total desta chave primária concatenada, se e somente se, a cada valor da chave (e

não parte dela), está associado um valor para cada atributo, ou seja, um atributo não (dependência parcial) se

apresenta com dependência completa ou total quando só dependente de parte da chave primária concatenada e

não dela como um todo.

Ex.: dependência total – na entidade ITEM-DO-PEDIDO, o atributo QUANTIDADE-DO-PRODUTO depende

de forma total ou completa da chave primária concatenada (NÚMERO-DO-PEDIDO+CODIGO-DO-PRODUTO).

A dependência total ou completa só ocorre quando a chave primária for composta por vários

(concatenados) atributos, ou seja, em uma entidade chave primária composta de um único atributo não ocorre

este tipo de dependência.

Dependência Funcional Transitiva

Quando um atributo ou conjunto de atributos A depende de outro atributo B que não pertence à chave

primária, mas é dependente funcional desta, dizemos que A é dependente transitivo de B.

Ex.: dependência transitiva – na entidade PEDIDO, os atributos ENDEREÇO, CIDADE, UF, CGC e INSCRIÇÃO-

ESTATUAL são dependentes transitivos do atributo CLIENTE. Nesta mesma entidade, o atributo NOME-DO-

VENDEDOR é dependente transitivo do atributo CODIGO-DO-VENDEDOR.

Com base na teoria sobre as dependências funcionais entre atributos de uma entidade, podemos

continuar com a apresentação das outras formas normais.

Segunda Forma Normal (2FN)

Devemos observar se alguma entidade possui chave primária concatenada, e para aqueles que

satisfizerem esta condição, analisar se existe algum atributo ou conjunto de atributos que dependem desta chave

parcial, ou seja, uma entidade para estar na 2FN não pode ter atributos com dependência parcial em relação à

chave primária.

Ex.: A entidade ITEM-DO-PEDIDO apresenta uma chave primária concatenada e por observação, notamos

que os atributos UNIDADE-DO-PRODUTO, DESCRIÇÃO-DO-PRODUTO e VALOR-UNITARIO depende de forma

parcial do atributo CODIGO-DO-PRODUTO, que faz parte da chave primária. Logo devemos aplicar a 2FN sobre

esta entidade. Quando aplicamos a 2FN sobre ITEM-DO-PEDIDO, será criada a entidade PRODUTO que herdará os

atributos UNIDADE-DO-PRODUTO, DESCRIÇÃO-DO-PRODUTO e VALOR-UNITÁRIO e terá como chave primária o

CODIGO-DO-PRODUTO.

151

Um PRODUTO participa de no mínimo 1 e no máximo N elementos de ITEM-DE-PEDIDO e um ITEM-DE-

PRODUTO só pode conter 1 e somente 1 PRODUTO. Logo, o novo relacionamento criado é do tipo N:1.

Terceira Forma Normal (3FN)

Uma entidade está na 3FN se nenhum de seus atributos possui dependência transitiva em relação a outro

atributo da entidade que não participe da chave primária, ou seja, não exista nenhum atributo intermediário

entre a chave primária e o próprio atributo observado.

Terceira Forma Normal (3FN)

Uma entidade está na 3FN se nenhum de seus atributos possui dependência transitiva em relação a outro

atributo da entidade que não participe da chave primária, ou seja, não exista nenhum atributo intermediário

entre a chave primária e o próprio atributo observado.

152

Ao retirarmos a dependência transitiva, devemos criar uma nova entidade que contenha os atributos que

contenha os atributos que dependem transitivamente de outro e sua chave primária é o atributo que causou esta

dependência.

Além de não conter atributos com dependência transitiva, entidades na 3FN não devem conter atributos

que sejam o resultado de algum cálculo sobre outro atributo, que de certa forma pode ser encarada como uma

dependência funcional.

Ex.: Na entidade PEDIDO, podemos observar que o atributo NOME-DO-VENDEDOR depende

transitivamente do atributo CODIGO-DO-VENDEDOR que não pertence à chave primária. Para eliminarmos esta

anomalia devemos criar a entidade VENDEDOR, com o atributo NOME-DO-VENDEDOR e tendo como chave

primária o atributo CODIGO-DO-VENDEDOR.

Encontramos ainda o conjunto de atributos formados por ENDEREÇO, CIDADE, UF, CGC e INSCRIÇÃO-

ESTADUAL que dependem transitivamente do atributo CLIENTE. Neste caso, devemos criar a entidade

VENDEDOR, com o atributo NOME-DO-VENDEDOR e tendo como chave primária o atributo CODIGO-DO-

VENDEDOR.

Encontramos ainda o conjunto de atributos formados por ENDEREÇO, CIDADE, UF, CGC e INSCRIÇÃO-

ESTADUAL que dependem transitivamente do atributo CLIENTE. Neste caso, devemos criar a entidade CLIENTE

que conterá os atributos ENDEREÇO, CIDADE, UF, CGC e INSCRIÇÃO-ESTADUAL. Para chave primária desta

entidade vamos criar um atributo chamado CODIGO-DO-CLIENTE que funcionará melhor como chave primária do

que NOME-DO-CLIENTE, deixa este último como simples atributo da entidade CLIENTE.

153

Um PEDIDO só é feito por um e somente um CLIENTE e um CLIENTE pode fazer de zero (clientes que

devem ser contatados mais frequentemente pelos vendedores) até N elementos de PEDIDO. Um PEDIDO só é

tirado por um e somente um VENDEDOR e um VENDEDOR pode tirar de zero (vendedores que devem ser

reciclados em termos de treinamento, para aumentar o poder de venda) a N elementos de PEDIDO.

Forma Normal de BOYCE/CODD (FNBC)

As definições da 2FN e 3FN, desenvolvidas por Codd, não cobriam certos casos. Esta inadequação foi

apontada por Raymond Boyce em 1974. Os casos não cobertos pelas definições de Codd somente ocorrem

quando três condições aparecem juntas:

• a entidade tenha várias chaves candidatas;

• estas chaves candidatas sejam concatenadas (mais de um atributo);

• as chaves concatenadas compartilham pelo menos um atributo comum.

Na verdade, a FNBC é uma extensão da 3FN, que não resolvia certas anomalias presentes na informação contida

em uma entidade. O problema foi observado porque a 2FN e a 3FN só tratavam dos casos de dependência parcial

e transitiva de atributos fora de qualquer chave, porém quando o atributo observado estiver contido em uma

chave (primária ou candidata), ele não é captado pelo verificação da 2FN e 3FN.

A definição da FNBC é a seguinte: uma entidade está na FNBC se e somente se todos os determinantes

forem chaves candidatas. Notem que esta definição é em termos de chaves candidatas e não sobre chaves

primárias.

Considere a seguinte entidade FILHO:

154

Por hipótese, vamos assumir que um professor possa estar associado a mais de uma escola e uma sala.

Sob esta suposição, tanto a chave (candidata) concatenada NOME-DA-ESCOLA+SALA-DA-ESCOLA bem como

NOME-DA-ESCOLA+NOME-DO-PROFESSOR podem ser determinantes. Logo esta entidade atende às três

condições relacionadas anteriormente:

• as chaves candidatas para a entidade FILHO são: NOME-DO-FILHO+ENDEREÇO-DO-FILHO+NÚMERO-DA-

SALA e NOME-DO-FILHO+NOME-DO-PROFESSOR;

• todas as três chaves apresentam mais de um atributo (concatenados);

• todas as três chaves compartilham um mesmo atributo: NOME-DO-FILHO.

Neste exemplo, NOME-DO-PROFESSOR não é completamente dependente funcional do NÚMERO-DA-

SALA, nem NÚMERO-DA-SALA é completamente dependente funcional do NOME-DO-PROFESSOR. Neste caso,

NOME-DO-PROFESSOR é realmente completamente dependente funcional da chave candidata concatenada

NOME-DO-FILHO+NOME-DO-PROFESSOR.

Ao se aplicar FNBC, a entidade FILHO deve ser dividida em duas entidades, uma que contém todos os

atributos que descrevem o FILHO, e uma segunda que contém os atributos que designam um professor em uma

particular escola e número de sala.

Quarta Forma Normal (4FN)

Na grande maioria dos casos, as entidades normalizadas até a 3FN são fáceis de entender, atualizar e de

se recuperar dados. Mas às vezes podem surgir problemas com relação a algum atributo não chave, que recebe

valores múltiplos para um mesmo valor de chave. Esta nova dependência recebe o nome de dependência

multivalorada que existe somente se a entidade contiver no mínimo três atributos.

Uma entidade que esteja na 3FN também está na 4FN, se ela não contiver mais do que um fato

multivalorado a respeito da entidade descrita. Esta dependência não é o mesmo que uma associação M:N entre

atributos, geralmente descrita desta forma em algumas literaturas.

155

Ex.: Dada a entidade hipotética a seguir:

Como podemos observar, esta entidade tenta conter dois fatos multivalorados: as diversas peças

compradas e os diversos compradores. Com isso apresenta uma dependência multivalorada entre CODIGO-

FORNECEDOR e o CODIGO-PEÇA e entre CÓDIGO-FORNECEDOR e o CODIGO-COMPRADOR. Embora esteja na 3FN,

ao conter mais de um fato multivalor, torna sua atualização muito difícil, bem como a possibilidade de problemas

relativos ao espaço físico de armazenamento poderem ocorrer, causados pela ocupação desnecessária de área de

memória (primária ou secundária), podendo acarretar situações críticas em termos de necessidade de mais

espaço para outras aplicações.

Para passarmos a entidade acima para a 4FN, é necessária a realização de uma divisão da entidade

original, em duas outras, ambas herdando a chave CODIGO-FORNECEDOR e concatenado, em cada nova entidade,

com os atributos CODIGO-PEÇA e CODIGO-COMPRADOR.

Quinta Forma Normal (5FN)

Esta última forma normal trata do conceito de dependência de junção, quando a noção de normalização é

aplicada à decomposição, devido a uma operação de projeção, e aplicada na reconstrução devido a uma junção.

A 5FN trata de casos bastantes particulares, que ocorrem na modelagem de dados, que são os

relacionamentos múltiplos (ternários, quaternários, ..., n-ários). Ela fala que um registro está na sua 5FN, quando

o conteúdo deste mesmo registro não puder ser reconstruído (junção) a partir de outros registros menores,

extraídos deste registro principal. Ou seja, se ao particionar um registro, e sua junção posterior não conseguir

recuperar as informações contidas no registro original, então este registro está na 5FN.

Vamos ilustrar o uso da 5FN utilizando um exemplo de relacionamento ternário:

Ex.: Uma empresa constrói equipamentos complexos. A partir de desenhos de projeto desses

equipamentos, são feitos documentos de requisições de materiais, necessários para a construção do

equipamento; toda a requisição de um material dá origem a um ou mais pedidos de compra. A modelagem deste

exemplo, irá mostrar quais materiais de que requisições geraram quais pedidos. Na fig. 12.2 é apresentado este

relacionamento ternário.

156

A tabela 2, representante do relacionamento ternário M-R-P, poderia conter os seguintes dados:

Utilizando uma soma de visualização da dependência de junção, obtemos o seguinte gráfico de

dependência de junção, mostrado na fig. 12.3.

Uma pergunta surge sobre este problema: é possível substituir o relacionamento ternário por

relacionamentos binários, como os apresentados na fig. 12.4.

Como resposta, podemos dizer que geralmente não é possível criar esta decomposição sem perda de

informação, armazenada no relacionamento ternário.

Realizando uma projeção na tabela anterior, chegamos às entidades apresentadas na fig. 12.5.

157

Se realizarmos, agora, um processo de junção destas três entidades, teremos:

• Inicialmente, vamos juntar a entidade 1 com a entidade 2, através do campo pedido de compra. Obtemos

então a entidade 4, mostrada na fig. 12.6:

• Podemos observar que o registro apontado pela seta não existia na tabela original, ou seja, foi criado pela

junção das tabelas parciais. Devemos juntar a entidade 4, resultante da primeira junção, com a entidade

3, através dos campos material e requisição. Após esta última operação de junção, obtemos a entidade 5,

mostrada na fig. 12.7.

158

Como se pode notar, ao se juntar as três entidades, fruto da decomposição da entidade original, as

informações destas foram preservadas. Isto significa que o relacionamento M-R-P não está na 5FN, sendo

necessário decompô-lo em relacionamento binários, os quais estarão na 5FN.

A definição da 5FN diz que: uma relação de 4FN estará em 5FN, quando seu conteúdo não puder ser

reconstruído (existir perda de informação) a partir das diversas relações menores que não possuam a mesma

chave primária. Esta forma normal trata especificamente dos casos de perda de informação, quando da

decomposição de relacionamentos múltiplos.

Com a 5FN, algumas redundâncias podem ser retiradas, como a informação de que o “ROTOR 1BW” está

presente na requisição “R3192”, será armazenada uma única vez, a qual na forma não normalizada pode ser

repetida inúmeras vezes.

Roteiro de Aplicação da Normalização

Entidade ou documento não normalizado, apresentando grupos repetitivo e certas anomalias de

atualização.

• Aplicação da 1FN

- Decompor a entidade em uma ou mais entidades, sem grupos repetitivos;

- Destacar um ou mais atributos como chave primária da(s) nova(s) entidade(s), e este será concatenado com a

chave primária da entidade original;

- Estabelecer o relacionamento e a cardinalidade entre a(s) nova(s) entidade(s) gerada(s) e a entidade geradora;

- Verificar a questão da variação temporal de certos atributos e criar relacionamentos 1:N entre a entidade

original e a entidade criada por questões de histórico.

Entidades na 1FN

• Aplicação da 2FN

- Para entidades que contenham chaves primárias concatenadas, destacar os atributos que tenham dependência

parcial em relação à chave primária concatenada;

- Criar uma nova entidade que conterá estes atributos, e que terá como chave primária o(s) atributo(s) do(s)

qual(quais) se tenha dependência parcial;

- Serão criadas tantas entidades quanto forem os atributos da chave primária concatenada, que gerem

dependência parcial;

- Estabelecer o relacionamento e a cardinalidade entre a(s) nova(s) entidade(s) gerada(s) e a entidade geradora.

• Entidades na 2FN

- Verificar se existem atributos que sejam dependentes transitivos de outros que não pertencem à chave

primária, sendo ela concatenada ou não, bem como atributos que sejam dependentes de cálculo realizado a

partir de outros atributos;

- Destacar os atributos com dependência transitiva, gerando uma nova entidade com este atributo e cuja chave

primária é o atributo que originou a dependência;

159

- Eliminar os atributos obtidos através de cálculos realizados a partir de outros atributos.

• Entidades na 3FN

• aplicação da FNBC

- Só aplicável em entidades que possuam chaves primárias e candidatas concatenadas;

- Verificar se alguma chave candidata concatenada é um determinante, e em caso afirmativo, criar uma entidade

com os que dependam funcionalmente deste determinante e cuja chave primária é o própria determinante.

• Entidades na FNBC

- Aplicação da 4FN

- Para se normalizar em 4FN, a entidade tem que estar (obrigatoriamente) na 3FN;

- Verificar se a entidade possui atributos que não sejam participantes da chave primária e que sejam

multivalorados e independentes em relação a um mesmo valor da chave primária;

- Retirar estes atributos não chaves e multivalorados, criando novas entidades individuais para cada um deles,

herdando a chave primária da entidade desmembrada.

• Entidades na 4FN

- aplicação da 5FN

- Aplicada em elemento que estejam na 4FN;

- A ocorrência deste tipo de forma normal está vinculada aos relacionamentos múltiplos (ternários, etc.) ou

entidades que possuam chave primária concatenada com três ou mais atributos;

- Verificar se é possível reconstruir o conteúdo do elemento original a partir de elementos decompostos desta;

- Se não for possível, o elemento observado não está na 5FN, caso contrário os elementos decompostos

representam um elemento na 5FN.

• Entidades na forma normal final

O processo de normalização leva ao refinamento das entidades, retirando delas grande parte das redundâncias e

inconsistências. Naturalmente, para que haja uma associação entre entidades é preciso que ocorram

redundâncias mínimas de atributos que evidenciam estes relacionamentos entre entidades.

Desnormalização

Parece um tanto quanto incoerente apresentar um item falando sobre desnormalização. Porém os

processos de síntese de entidades vistos até aqui levam à criação de novas entidades e relacionamentos.

Os novos elementos criados podem trazer prejuízos na hora de serem implementados em um SGBD.

Devido as características de construção física de certos banco de dados, algumas entidades e relacionamentos

devem ser desnormalizados para que o SGBD tenha um melhor desempenho.

Hoje em dia, existe um grande debate sobre as chamadas semânticas da normalização, sua utilidade e

facilidade em relação à implementação física em um sistema operacional.

Vários estudos e algumas considerações estão sendo realizados, e se chega até a utilização de banco de

dados relacionais não normalizados, por apresentarem maior ligação com a realidade e por terem vínculos

matemáticos mais amenizados.

Outro aspecto da normalização é que todas as definições sobre as formas normais após a 1FN, ainda não

foram exaustivamente examinadas, propiciando assim grandes controvérsias. A redução das anomalias de

atualização devido às formas normais de alta ordem sofre os ataques óbvios dos grandes problemas (físicos) de

atualização, pois as relações estão excessivamente normalizadas e com isso uma simples alteração pode encadear

um efeito cascata bastante profundo no banco de dados, ocasionando um aumento bastante significativo no

tempo.

160

Infelizmente, os argumentos que podem viabilizar o processo de desmoralização sofrem de uma

deficiência e aderência matemática.

Pesquisas recentes indicam que as estruturas desnormalizadas (apelidadas de forma normal não primeira)

têm um atrativo matemático similar ao que foi o da normalização. Estas pesquisas estão provendo recentes

definições sobre álgebra relacional desnormalizada e extensões à linguagem SQL para a manipulação de relações

desnormalizadas.

Ao se optar pela desnormalização, deve-se levar em conta o custo da redundância de dados e as

anomalias de atualização decorrentes.

Chegou-se à conclusão também que o espírito da normalização contradiz vários princípios importantes,

relativos à modelagem semântica e à construção de bases de dados em SGBD orientados por objeto.

Considerações Finais sobre Normalização

Antes de qualquer conclusão, podemos observar que as formas normais nada mais são do que restrições

de integridade, e à medida que se alimenta este grau de normalização, torna-se cada vez mais restritivas.

Dependendo do SGBD relacional utilizado, essas restrições podem se tornar benéficas ou não.

A forma de atuação da normalização no ciclo de vida de um projeto de banco de dados pode ser mais

satisfatória no desenvolvimento (botton-up) de modelos preliminares, a partir da normalização da documentação

existente no ambiente analisado, bem como de arquivos utilizados em alguns processos automatizados neste

ambiente.

No caso do desenvolvimento top-down, no qual um modelo de dados é criado a partir da visualização da

realidade, a normalização serve para realizar um aprimoramento deste modelo, tornando-o menos redundante e

inconsistente. No caso desta visão, a normalização torna-se um poderoso aliado da implementação física do

modelo.

Por experiência, podemos afirmar que a construção de um modelo de dados já leva naturalmente ao

desenvolvimento de entidades e relacionamentos na 3FN, ficando as demais (FNBC, 4FN, e 5FN) para melhorias e

otimizações.

A criação de modelos de dados, partindo-se da normalização de documentos e arquivos pura e

simplesmente, não é o mais indicado, pois na verdade estaremos observando o problema e não dando uma

solução para ele. Neste caso, estaremos projetando estruturas de dados que se baseiam na situação atual (muitas

vezes caótica) e que certamente não vão atender às necessidades reais do ambiente em análise. Ao passo que, se

partimos para a criação do modelo de dados com entidades e relacionamentos aderentes à realidade em estudo

(mundo real), vamos naturalmente desenvolver uma base de dados ligada à visão da realidade e como

consequência iremos solucionar o problema de informação.

A aplicação da modelagem de dados, ao longo da nossa vida profissional, tem sido bastante gratificante,

mostrando principalmente, que a técnica de normalização é uma ferramenta muito útil como apoio ao

desenvolvimento do modelo de dados. Seja ela aplicada como levantamento inicial (documentos e arquivos) bem

como otimizador do modelo de dados, tendo em vista certas restrições quanto à implementação física nos banco

de dados conhecidos.

Todas as ideias sobre eficiência da normalização passam necessariamente sobre tempo e espaço físico,

em função, principalmente, das consultas efetuadas pelos usuários bem como a quantidade de bytes necessários

para guardar as informações.

Nota-se, através da observação, que o projeto do modelo conceitual nem sempre pode ser derivado para

o modelo físico final. Com isso, é de grande importância que o responsável pela modelagem (analista, AD, etc.)

não conheça só a teoria iniciada por Peter Chen, mas também tenha bons conhecimentos a respeito do ambiente

do banco de dados utilizado pelo local em análise.

161

Regras de Integridade

As regras de integridade fornecem a garantia de que mudanças feitas no banco de dados por usuários

autorizados não resultem em perda de consistência de dados. Assim, as regras de integridade protegem o banco

de dados de danos acidentais.

Já vimos regras de integridade para o modelo E-R. Essas regras possuem a seguinte forma:

• Declaração de variáveis – a determinação de certos atributos, como chave candidata, para um dado

conjunto de entidades. O conjunto de inserções e atualizações válidas é restrito àquelas que não criem

duas entidades com o mesmo valor de chave candidata.

• Forma de um relacionamento – muitos para muitos, um para muitos, um para um. O conjunto de

relacionamentos um para um ou um para muitos restringe o conjunto de relacionamentos válidos entre

os diversos conjuntos de entidades.

Em geral, uma regra de integridade constitui um predicado arbitrário pertencente ao banco de dados. Entretanto,

predicados arbitrários podem representar altos custos para serem testados. Assim, normalmente, as regras de

integridade são limitadas às que podem ser verificadas com o mínimo tempo de processamento.

Restrições de Domínios

Vimos que um domínio de valores válidos pode ser associado a qualquer atributo. Vimos tais restrições

são especificadas em SQL DDL. Restrições de domínio são as mais elementares formas de restrições de

integridade. Elas são facilmente verificadas pelo sistema sempre que um novo item de dado é incorporado ao

banco de dados.

É possível que diversos atributos tenham um mesmo domínio. Por exemplo, os atributos nome_cliente e

nome_empregado podem ter o mesmo domínio: o conjunto de todos os nomes de pessoas. Entretanto, os

domínios de saldo e nome_agência certamente serão distintos. Será, talvez, menos claro o caso nome_cliente e

nome_agência que podem ter o mesmo domínio. No nível de implementação, os nomes de agência e clientes são

strings de caracteres. No entanto, essas linguagens inibem os “quebra-galhos”, que são frequentemente

necessários na programação de sistemas. Uma vez que os sistemas de banco de dados são projetados para

atender a diversos usuários que não são especialistas em banco de dados, os benefícios de tipos fortemente

definidos geralmente apresentam mais desvantagens do que vantagens. Apesar disso, muito dos sistemas

existentes permitem apenas um reduzido número de domínios de tipos. Poucos sistemas, particularmente os

sistemas de banco de dados orientado a objeto, oferecem um conjunto rico de tipos de domínios que podem ser

facilmente ampliados.

A cláusula check da SQL-92 permite modos poderosos de restrições de domínios que a maioria dos

sistemas de tipos das linguagens de programação não permite. Especificamente, a cláusula check permite ao

projeto do esquema determinar um predicado que deva ser satisfeito por qualquer valor designado a uma

variável cujo tipo seja o domínio. Por exemplo, uma cláusula check pode garantir que o domínio relativo ao turno

de trabalho de um operário contenha somente valores maiores que um dado valor (turno mínimo), como

ilustrado aqui:

create domain turno_trabalho numeric (5,2)

o domínio de turno_trabalho é declarado como um número decimal com um total de cinco dígitos, dois

dos quais colocados após o ponto decimal, e o domínio possui uma restrição que assegura que o turno de

trabalho não seja inferior a 4,00. A cláusula constraint valor_teste_turno é opcional e é usada para dar o nome

valor_teste_turno à restrição. O nome é usado para indicar quais restrições foram violadas em determinada

atualização.

A cláusula check pode também ser usada para restringir os valores nulos em um domínio, como ilustrado

a seguir:

162

Como outro exemplo, o domínio pode ser restrito a determinado conjunto de valores por meio do uso da

cláusula in:

Integridade Referencial

Frequentemente, desejamos garantir que um valor que aparece em uma relação para um dado conjunto

de atributos também apareça para um certo conjunto de atributos de outra relação. Essa condição é chamada

integridade referencial

Conceitos Básicos

Considere um par de relações r(R) e s(S) e a junção natural . Pode existir uma tupla tr em r que não

possa ser combinada a nenhuma tupla em s. Isto é, não existe nenhum tS em s tal que . Tais

tuplas são chamadas de tuplas pendentes. Dependendo do conjunto de entidades ou relacionamentos que está

sendo modelado, tuplas pendentes podem ou não ser aceitáveis.

Sabemos que existe um tipo diferente de junção – a junção externa – para operar relações contendo

tuplas pendentes. Não estamos abordando consultas aqui, mas o modo de tratar a existência, quando desejada,

de tuplas pendentes no banco de dados.

Suponha que exista a tupla t1 na relação conta, com t1[nome_agência] = “Lunartown”, mas que não haja

nenhuma tupla na relação agência para Lunartown. Essa situação pode ser indesejável.

Esperamos que a relação agência possua todas as agências do banco. Portanto, a tupla t1 faz referência a

uma conta de uma agência inexistente. Obviamente, gostaríamos de implementar uma regra de integridade que

proíba tuplas pendentes desse tipo.

No entanto, nem todos os tipos de tuplas pendentes são indesejáveis. Suponha que exista uma tupla t2 na

relação agência, com t2[nome_agência]= “Mokan”, mas não há nenhuma tupla da relação conta com referência à

agência Mokan. Nesse caso, existe uma agência que não possui nenhum conta.

Embora essa seja uma situação incomum, pode ocorrer quando uma agência está sendo aberta ou

fechada. Assim, não devemos proibir esse tipo de situação.

A diferença entre esses dois exemplos tem origem em dois fatos:

• O atributo nome_agência do Esquema_conta é uma chave estrangeira (foreign key) cuja referência é a

chave primária do Esquema_agência.

• O atributo nome_agência do Esquema_agência não é uma chave estrangeira.

No exemplo de Lunartown, a tupla t1 de conta tem um valor para a chave estrangeira nome_agência que

não aparece em agência. No exemplo da agência Mokan, a tupla t2 de agência tem um valor nome_agência que

não aparece em conta, mas nome_agência não é uma chave estrangeira. Assim, a diferença entre nossos dois

exemplos de tuplas pendentes é a existência de uma chave estrangeira.

Seja r1(R1) e r2(R2) relações com chaves primárias K1 e K2, respectivamente. Dizemos que um subconjunto

α de R2 é uma chave estrangeira associada a K1 em relação a r1 se é garantido que, para todo t2 em r2, existe uma

tupla t1 em r1, tal que t1[K1] = t2[α]. Exigências desse tipo são chamadas de regras de integridade referencial ou

subconjunto dependente. O último termo advém do fato de ser possível escrever a regra de integridade

referencial anterior como (r1). Note que, para a regra de integridade referencial ter sentido, cada α

deve ser igual a K1.

Integridade Referencial no Modelo E-R

Regras de integridade referencial aparecem com frequência. Se derivarmos nosso esquema de bancos de

dados relacional em tabelas originadas de diagramas E-R, então toda relação resultante de um conjunto de

relacionamentos possui regras de integridade referencial. A fig. 6.1 mostra um conjunto de relacionamentos R de

163

n elementos, relacionando os conjuntos de entidades E1, E2, ..., En. Seja Ki a chave primária de Ei. Os atributos para

o esquema da relação referente ao conjunto de relacionamentos R incluem K1ǓK2Ǔ... ǓKn. Cada Ki do esquema de

R é uma chave estrangeira que leva a uma regra de integridade referencial.

Outra fonte de regras de integridade referencial são os conjuntos de entidades fracas. Vimos que o

esquema da relação para um conjunto de entidades fracas precisa incluir a chave primária do conjunto de

entidades do qual ele é dependente. Assim, o esquema da relação para cada conjunto de entidades fracas inclui

uma chave estrangeira que leva a uma regra de integridade referencial.

Modificações no Banco de Dados

As modificações no banco de dados podem originar violação das regras de integridade referencial.

Colocamos aqui a verificação necessária a cada tipo de modificação no banco de dados para que se possa

preservar a seguinte regra de integridade referencial:

• Inserção. Se uma tupla t2 é inserida em r2, o sistema deve garantir que exista uma tupla t1 em r1 tal que

t1[K]=t2[α]. Isto é:

• Remoção. Se uma tupla t1 é removida de r1, o sistema deve tratar também no conjunto de tuplas em r2

que são referidos por t1: .

Se esse conjunto é vazio, o comando de remoção foi rejeitado devido a algum erro ou a tupla que faz

referência a t1 deve ser removida. Esta última alternativa pode levar a uma remoção em cascata se

algumas tuplas fizerem referência a tuplas que se referem a t1 e assim por diante.

• Atualização. Precisamos considerar duas situações de atualizações: as que se referem à relação (r2) e as

referidas pela relação (r1).

o Se uma tupla t2 da relação r2 é atualizada e a atualização modifica os valores da chave estrangeira

α, então é feita uma verificação similar à inserção. Seja t2’ o novo valor da tupla t2. O sistema

deverá assegurar que:

o Se uma tupla t1 da relação r1 é atualizada e a atualização modifica os valores de uma chave

primária (K), então deverá ser feito um teste similar ao realizado para a remoção. O sistema

deverá computar usando o valor antigo de t1 (o valor existente antes da aplicação

da atualização). Se esse conjunto não for vazio, a atualização será rejeitada como uma ocorrência

de erro ou as atualizações serão realizadas em cascata, de modo similar ao que ocorre na

remoção.

Integridade Referencial em SQL

É possível definir chaves primárias, secundárias e estrangeiras como parte do comando create table da

SQL:

164

• A cláusula primary key do comando create table inclui a lista dos atributos que constituem a chave

primária.

• A cláusula unique do comando create table inclui a lista dos atributos que constituem uma chave

candidata.

• A cláusula foreign key do comando create table inclui a lista dos atributos que constituem a chave

estrangeira quanto o nome da relação à qual a chave estrangeira faz referência.

Ilustramos as declarações de chaves estrangeiras e primárias usando definições de parte de nosso banco em SQL

DDL, mostrado na fig. 6.2. Note que não nos detivemos em modelar de modo preciso o mundo real no exemplo

do banco de dados de uma empresa da área bancária. No mundo real, diversas pessoas podem ter o mesmo

nome, assim nome_cliente não pode ser chave primária para cliente. No mundo real, alguns outros atributos,

como o número do seguro social, ou uma combinação de atributos como nome e endereço, poderiam ser usados

como chave primária. Usamos nome_cliente como chave primária para conferir simplicidade e concisão a nosso

esquema de banco de dados.

Podemos usar a forma simplificada para declarar que uma única coluna é uma chave estrangeira:

A SQL também dá suporte a uma outra maneira de formular a cláusula chave estrangeira, em que uma

lista de atributos da relação por ela referida pode ser explicitamente especificada, e esses atributos são usados no

lugar da chave primária; essa lista de atributos precisa ser declarada como chave candidata de uma relação

referida.

Quando uma regra de integridade referencial é violada, o procedimento normal é rejeitar a ação que

ocasionou essa violação. Entretanto, a cláusula relativa a foreign key em SQL-92 pode especificar que, se uma

remoção ou atualização na relação a que ela faz referência violar uma regra de integridade, então, em vez de

rejeitar a ação, executam-se passos para modificação da tupla na relação que contém a referência, de modo a

garantir a regra de integridade. Considere a seguinte definição de regra de integridade para a relação conta:

165

Devido à cláusula on delete cascade associada à declaração da chave estrangeira, se a remoção de uma

tupla de agência resultar na violação da regra de integridade anterior, a remoção não será rejeitada. Ao contrário,

a remoção é feita em “cascata” na relação conta, de modo que as tuplas que se referirem a uma agência

removida sejam também removidas. De modo similar, a atualização de um campo referido por uma regra de

integridade não será rejeitada se ela violar uma regra de integridade; pelo contrário, o campo nome_agência das

tuplas da relação conta será também atualizado. A SQL-92 permite, também, que a cláusula foreign key

especifique outros tipos de ações além de “cascata”, como alterar o campo em questão (no caso, nome_agência)

com nulos, ou um valor-padrão, caso a regra seja violada.

Se houver uma cadeia de dependências entre chaves estrangeiras de diversas relações, uma remoção ou

uma atualização em uma das extremidades poderá propagar-se ao longo de toda a cadeia. Se uma atualização ou

remoção em cascata provoca a violação de uma regra de integridade que não pode ser tratada por uma operação

de cascata seguinte, o sistema aborta a transação. Como resultado, todas as mudanças causadas pela transação,

assim como as ações em cascata decorrentes, serão desfeitas.

A semântica de chaves em SQL torna-se mais complexa pelo fato da SQL permitir valores nulos. As

seguintes regras, algumas das quais arbitrárias, são usadas para tratar esses valores nulos.

• Todos os atributos de uma chave primária são declarados implicitamente como not null.

• Atributos de uma declaração unique (isto é, atributos de uma chave candidata) podem ser nulos,

contanto que não sejam declarados não-nulos de outro modo. A restrição para garantia da unicidade em

uma relação é violada somente se duas tuplas da relação têm o mesmo valor para todos os atributos da

regra unique e todos os valores forem não-nulos. Assim, qualquer número de tuplas pode ser igual em

todas as colunas declaradas como únicas, sem que haja violação da regra de integridade, contanto que ao

menos uma das colunas tenha um valor nulo.

• Atributos nulos em chaves estrangeiras são permitidos, contanto que não tenha sido declarados como

não-nulos de outro modelo. Se todas as colunas de uma chave estrangeira são não-nulas em uma

determinada tupla, a definição usual de regra de integridade em chave estrangeira será empregada

naquela tupla. Se qualquer uma das colunas da chave é nula, a tupla é automaticamente aprovada na

regra de integridade. Essa definição é arbitrária e nem sempre uma boa opção, assim a SQL fornece,

também, construtores que possibilitam mudança de comportamento em relação aos valores nulos.

Dada a complexidade e arbitrariedade natural das formas e comportamentos de restrições (ou regras) de

integridade SQL em relação a valores nulos, é melhor assegurar que todas as colunas especificadas em unique e

foreign key sejam declaradas, não permitindo nulos.

Asserções

Uma asserção é um predicado que expressa uma condição que desejamos que seja sempre satisfeita no

banco de dados. Restrições de domínio e regras de integridade referencial são formas especiais de asserções.

Dispensamos substancial atenção a essas formas de asserções, porque são facilmente verificadas e aplicadas em

grande parte das aplicações em banco de dados. Entretanto, existem muitas restrições que não podem ser

expressas usando somente essas formas especiais. Exemplos dessas restrições compreendem:

• A soma de todos os totais em conta empréstimo de cada uma das agências deve ser menor que a soma

de todos os saldos da contas dessa agência.

166

• Todo empréstimo deve ter ao menos um cliente que mantenha uma conta com saldo mínimo de mil

dólares.

Uma asserção em SQL-92 toma a seguinte forma:

As duas regras mencionadas podem ser escritas como mostrado a seguir. Já que a SQL não oferece um

construtor “para todo X, P(X)” (em que P é um predicado), somos forçados a implementar o construtor usando o

construtor “não existe X tal que nenhum P(X)”, pode ser escrito em SQL.

Quando uma asserção é criada, o sistema verifica sua validade. Se as asserções são válidas então qualquer

modificação posterior no banco de dados será permitida somente quando a asserção não for violada. Quando as

asserções são complexas, a verificação pode gerar um aumento significativo em tempo de processamento. Por

isso, as asserções são usadas com muito cuidado.

Esse grande overhead para teste e manutenção de asserções tem levado alguns profissionais ligados ao

desenvolvimento de sistemas de banco de dados a excluir as asserções gerais ou a fornecer apenas formas

especiais de asserções, que possam ser verificadas facilmente.

Gatilhos (Triggers)

Um gatilho é um comando que é executado pelo sistema automaticamente, em consequência de uma

modificação no banco de dados. Duas exigências devem ser satisfeitas para a projeção de um mecanismo de

gatilho:

1. Especificar as condições sob as quais o gatilho deve ser executado.

2. Especificar as ações que serão tomadas quando um gatilho for disparado.

Os gatilhos são mecanismos úteis para avisos a usuários ou para executar automaticamente determinadas

tarefas quando as condições para isso são criadas. Como ilustração, suponha que, em vez de permitir saldos

negativos em conta, o banco crie condições para que a conta corrente seja zerada e o saldo negativo seja

transferido para uma conta empréstimo. Essa conta empréstimo terá o mesmo número da conta corrente em

questão. Para esse exemplo, a condição para o disparo de um gatilho é uma atualização na relação conta que

resulta em um valor negativo para saldo.

Suponha que Jones tenha feito um resgate em uma conta que gerou um saldo negativo. Suponha que t

denote a tupla de conta com um valor negativo para saldo. As ações a tomar são as seguintes:

• Inserir uma nova tupla s na relação empréstimo com:

167

(Note que, se t[saldo] for negativo, impedimos que t[saldo] receba um valor positivo em um total de

empréstimo.)

• Inserir uma nova tupla u na relação devedor com:

• Tornar t[saldo] igual a 0.

O padrão SQL-92 não dispõe de gatilhos, embora a proposta original da SQL do Sistema R proponha

alguns recursos para gatilhos. Alguns sistemas existentes possuem seus próprios recursos de gatilho não

padronizados. Ilustraremos, aqui, como um gatilho para saldo negativo poderia ser escrito na versão original de

SQL.

168

A palavra-chave new usada antes de T.saldo indica que o valor de T.saldo após a atualização deverá ser

usado; se ele for omitido, o valor existente antes da atualização será, então, usado.

Os gatilhos são chamados, algumas vezes, de regras (rules), ou regras ativas (active rules), mas não devem

ser confundidos com as regras da Datalog, que tratam realmente de definições de visões.

Dependência Funcional

A noção de dependência funcional é uma generalização da noção de chave. Dependências funcionais

representam um papel importante no projeto de banco de dados.

Conceitos Básicos

Dependências funcionais são restrições ao conjunto de relações válidas. Elas permitem expressar

determinados fatos em nosso banco de dados relativos à empresa que desejamos modelar.

Eis o conceito de superchave como segue. Seja R o esquema de uma relação. Um subconjunto K de R é

uma superchave de R em qualquer relação válida r(R) para todos os pares de t1 e t2 de tuplas em r tal que t1≠t2,

então t1[K]≠t2[K]. isto é, nenhum par de tuplas em qualquer relação validade r(R) deve ter o mesmo valor no

conjunto de atributos K.

A noção de dependência funcional generaliza a noção de superchave. Seja α R e β R. A dependência

funcional: α � β realiza-se em R se, em qualquer relação validade r(R), para todos os pares de tuplas t1 e t2 em r,

tal que t1[α] = t2[α], t1[β] = t2[β] também será verdade.

Usando a notação de dependência funcional, dizemos que K é uma superchave de R se K�R. isto é, K é

uma superchave se, para todo t1[K]=t2[K], t1[R]=t2[R] (isto é, t1=t2).

A dependência funcional permite-nos expressar restrições que as superchaves não expressam. Considere

o esquema:

esquema_info_empréstimo=(nome_agência, número_empréstimo, nome_cliente, total)

O conjunto de dependências funcionais que queremos garantir para esse esquema relação é:

Entretanto, não esperamos realizar dependência funcional para:

já que, em geral, um empréstimo pode ser contraído por mais de um cliente (por exemplo, para ambos os

membros de um casal, marido-mulher).

Podemos usar dependência funcional de dois modos:

1. Usando-as para o estabelecimento de restrições sobre um conjunto de relações válidas. Devemos, assim,

concentrá-las somente àquelas relações que devem satisfazer um dado conjunto de dependências

funcionais. Se desejarmos restringi-las a relações do esquema R que satisfaçam um conjunto F de

dependências funcionais, dizemos que F realiza-se em R.

2. Usando-as para verificação de relações, de modo a saber se as últimas são válidas sob um dado conjunto

de dependências funcionais. Se desejarmos restringi-las a relações do esquema R que satisfaçam um

conjunto F de dependências funcionais, dizemos que r satisfaz F.

Consideremos a relação r da fig. 6.3 para verificar quais dependências funcionais são satisfeitas. Observe que

A�C é satisfeita. Duas tuplas têm valor a1 em A. essas tuplas têm um mesmo valor de C – denominado cI. de

modo similar, duas tuplas com valor a2 em A têm mesmo valor c2 em C, mas eles possuem valor diferentes em A,

a2 e a3, respectivamente. Assim, encontramos um par de tuplas t1 e t2 tal que t1[C]=t2[C], mas t1[A]≠t2[A].

169

Diversas outras dependências funcionais são satisfeitas por r, incluindo, por exemplo, a dependência

funcional AB � D. Note que usamos AB como notação simplificada para [A,B], reduzindo-se a um padrão mais

prático. Observe que não existe nenhum par de tuplas distintas t1 e t2 tal que t1[AB]=t2[AB]. Portanto, se

t1[AB]=t2[AB], entoa é necessário que t1=t2 e, assim, t1[D]=t2[D]. Logo, r satisfaz AB � D.

Algumas dependências funcionais são consideradas triviais porque são satisfeitas por todas as relações.

Por exemplo, A�A é satisfeita por todas as relações que contêm o atributo A. Na leitura literal da definição de

dependência funcional, podemos notar que, para todos os atributos t1 e t2 tal que t1[A]=t2[A] será também

verdade. De modo similar, AB � A é satisfeita para todas as relações envolvendo o atributo A. Em geral, uma

dependência funcional da forma α�β é trivial se β α.

Para distinguir os conceitos de uma relação que satisfaz uma dependência e de uma dependência

realizando-se em um esquema, retornaremos ao exemplo do banco. Se considerarmos a relação cliente (com o

esquema_cliente), como mostrado na fig. 6.4, notamos que rua_cliente � cidade_cliente é satisfeita. Entretanto,

acreditamos que, no mundo real, duas cidades podem possuir duas ou mais ruas com o mesmo nome. Assim, é

possível, em algum tempo, haver uma instância da relação cliente na qual rua_cliente � cidade_cliente não é

satisfeita. Logo, não incluiremos rua_cliente � cidade_cliente no conjunto das dependências funcionais que são

realizadas no Esquema_cliente.

Na relação empréstimo (do esquema_empréstimo) na fig. 6.5, vemos que número_empréstimo � total é

satisfeita. Ao contrário do que ocorre em cidade_cliente e rua_cliente no esquema_cliente, acreditamos que, em

empresas reais, eles são modelados de modo a garantir que cada conta tenha somente um total. Portanto,

queremos que a condição número_empréstimo� total seja sempre satisfeita para a relação empréstimo. Em

outras palavras, necessitamos da restrição número_empréstimo�total para o esquema_empréstimo.

170

Na relação agência da fig. 6.6, vemos que nome_agência�fundos é satisfeita, assim como

fundos�nome_agência. Gostaríamos de garantir que nome_agência�fundos fosse realizada em

esquema_agência. Entretanto, não queremos que fundos�nome_agência seja realiza, uma vez que é possível

haver diversas agências com o mesmo valor de fundos.

Embora a SQL não forneça um modo simples para especificação de dependência funcional podemos

escrever consultas para verificação de dependências funcionais, assim como criar asserções para garantia de

dependências funcionais.

Conforme segue, consideramos que, quando projetamos um banco de dados relacional, primeiro

relacionamos as dependências funcionais que sempre precisam ser realizadas. No exemplo do banco, nossa

relação de dependências funcionais engloba as seguintes:

• No esquema_agência:

nome_agência � cidade_agência

nome_agência � fundos

• No esquema_cliente:

nome_cliente � cidade_cliente

nome_cliente � rua_cliente

• No esquema_empréstimo:

número_empréstimo � total

número_empréstimo � nome_agência

• No esquema_devedor:

nenhuma dependência funcional

171

• No esquema_conta:

número_conta � nome_agência

número_conta � saldo

• No esquema_depositante:

nenhuma dependência funcional

Clausura de um Conjunto de Dependências Funcionais

Não basta considerar um dado conjunto de dependências funcionais. É preciso considerar todos os

conjuntos de dependências funcionais que são realizados. Podemos mostrar que, dado um conjunto F de

dependências funcionais, prova-se que outras dependências funcionais realizam-se. Dizemos que esse tipo de

dependência funcional é logicamente implícito em F.

Suponha um dado esquema de relação R = (A, B, C, G, H, I) é o conjunto de dependências funcionais:

A � B

A � C

CG �H

CG � I

B � H

A dependência funcional:

A � H

É logicamente implícita. Isto é, podemos mostrar que, sempre que um dado conjunto de dependências

funcionais se realiza, A�H também se realiza. Suponha que t1 e t2 são duas tuplas, tais que:

t1[A] = t2[A]

Já que é dado que A�B, dessa definição de dependência funcional decorre que:

t1[B] = t2[B]

Então, desde que B�H seja dada, decorre desta definição de dependência funcional que:

t1[H]=t2[H]

Portanto, mostramos que, sempre que t1 e t2 forem tuplas, tais que t1[A]=t2[A], necessariamente t1[H] =

t2[H]. Mas essa é exatamente a definição de A�H.

Seja F um conjunto de dependências funcionais. A clausura de F é o conjunto de todas as dependências

funcionais logicamente implícitas em F. Denotamos a clausura de F por F+. Dado F, podemos computar F+

diretamente da definição formal de dependência funcional. Se F for grande, esse processo pode tornar-se lento e

difícil. Tal qual a computação de F+, exige argumentos do tipo dado anteriormente para mostrar que A�H está

em clausura em nosso conjunto de exemplo de dependências. Existem técnicas mais simples para o raciocínio da

dependência funcional.

A primeira técnica tem por base três axiomas ou regras para inferência da dependência funcional. Para

aplicação dessas regras, precisamos encontrar todos os F+ de um dado F. Nas regras a seguir, adotamos a

convenção do uso de letras gregas para conjuntos de atributos e de letras maiúsculas do alfabeto romano para

atributos individuais. Usamos αβ para denotar .

• Regra de reflexividade. Se α é um conjunto de atributos e , então α�β realiza-se.

• Regra de incremento. Se α�β realiza-se e γ é um conjunto de atributos, então γα�γβ também realiza-

se.

• Regra de transitividade. Se α�β realiza-se e β�γ realiza-se, então α�γ realiza-se.

172

Essas regras são sólidas, porque elas não geram nenhuma dependência funcional incorreta. As regras são

completas, porque, para um dado conjunto F de dependências funcionais, elas nos permitem criar todo F+. Para

simplificar, relacionamentos regras adicionais:

• Regra de união. Se α�β e α�γ, então α�βγ também realiza-se.

• Regra de decomposição. Se α�βγ realiza-se, então α�β e α�γ também realizam-se.

• Regra pseudotransitiva. Se α�β e γβ�δ, então αγ�δ também realiza-se.

Apliquemos nossas regras ao esquema do exemplo apresentado anteriormente R=(A, B, C, G, H, I) e ao conjunto F

de dependências funcionais {A�B, A�C, CG�H, CG�I, B�H}. Relacionamos diversos membros de F+ aqui:

• A�H. Desde que A�B e B�H realizam-se, aplicamos a regra da transitividade. Observe que foi mais fácil

aplicar os axiomas de Armstrong para mostrar que A�H se realiza do que raciocinando diretamente

sobre as definições, como fizemos anteriormente.

• CG�HI. Já que CG�H e CG�I, a regra da pseudotransitividade implica a realização de AG�I.

Clausura de Conjunto de Atributos

Para verificar se um conjunto α é uma superchave, precisamos conceber um algoritmo para computar o

conjunto de atributos determinados funcionalmente por α. Podemos deduzir que esse algoritmo também é útil

como parte do processamento da clausura de um conjunto de dependências funcionais F.

Seja α um conjunto de atributos. Chamamos o conjunto dos atributos funcionalmente determinados por

α, sob um conjunto de dependências funcionais F, de clausura de α sob F, denotada por α+. A fig. 6.7 mostra um

algoritmo, escrito em pseudo-Pascal, para computar α+. O conjunto de dependências funcionais F e o conjunto α

de atributos funcionam como entrada. A saída é armazenada na variável resultado.

Para ilustração de como o algoritmo da fig. 6.7 funciona, iremos usá-lo para processar (AG)+ com as

dependências funcionais definidas anteriormente. Começaremos com resultado = AG. A primeira execução do

laço while para verificação da dependência funcional irá chegar a:

• A�B obriga-nos a incluir B no resultado. Para perceber esse fato, observemos que A�B está em F, A

resultado (o que é AG), assim, resultado := resultado B.

• A�C obriga que resultado se torne ABCG.

• CG�H obriga que resultado se torne ABCGH.

• CG�I obriga que resultado se torne ABCGHI.

Na segunda vez em que executamos o laço while, não será adicionado nenhum outro atributo a

resultado, e o algoritmo terminará.

Vejamos por que o algoritmo da fig. 6.7 está correto. Já que α�α sempre se realiza (pela regra da

reflexidade), o primeiro passo é correto. Exigimos que, para qualquer subconjunto β de resultado, α�β. Já que

começamos o laço while com α�resultado sendo verdadeiro, podemos adicionar γ ao resultado somente se β

resultado e β�γ. Mas, então, pela regra reflexiva, resultado�β e, pela transitividade, α�β. Outra aplicação

da transitividade mostra que α�γ (usando α�β e β�γ). A regra da união implica que α�resultado γ, assim

α determina, funcionalmente, qualquer resultado novo gerado pelo laço while. Dessa forma, qualquer atributo

obtido pelo algoritmo está em α+.

173

É fácil perceber que o algoritmo encontra todos os atributos de α+. Se há um atributo de α+ que não esteja

ainda em resultado, então é preciso que haja uma dependência funcional β�, para a qual β resultado, e que

ao menos um atributo em γ não esteja em resultado.

Por outro lado, no pior caso, esse algoritmo pode tomar tempo proporcional ao quadrado do tamanho de

F. Há um algoritmo mais rápido (embora ligeiramente mais complexo) que consome tempo proporcionalmente

linear ao tamanho de F.

Cobertura Canônica

Suponha que tenhamos um conjunto de dependências funcionais F sobre o esquema de uma relação.

Sempre que uma atualização é realizada na relação, o sistema de banco de dados deve garantir que todas as

dependências funcionais em F sejam satisfeitas no novo estado do banco de dados, ou deverá reverter as

alterações caso não o sejam.

Podemos reduzir os esforços exigidos para o teste por meio da simplificação de um dado conjunto de

dependências funcionais, com ou sem a alteração daquele conjunto de clausura. Qualquer banco de dados que

satisfaça um conjunto simplificado de dependências funcionais deve também satisfazer o conjunto original e vice-

versa, uma vez que os dois conjuntos têm a mesma clausura. Entretanto, o conjunto simplificado é mais

facilmente verificado. O conjunto simplificado pode ser construído com descrevemos a seguir. Primeiro,

precisamos de algumas definições.

Um atributo de uma dependência funcional é extrínseco se podemos removê-lo sem alterar a clausura do

conjunto de dependências funcionais. Formalmente, atributos extrínsecos são definidos conforme segue.

Considere um conjunto de dependências funcionais F e a dependência funcional α�β em F.

• O atributo A é extrínseco a α se A α, e F implica logicamente (F – {α�β}) {(α – A)�β}.

• O atributo A é extrínseco a β se A β, e o conjunto de dependências funcionais (F – {α�β}) {(β – A)}

implica logicamente F.

Uma cobertura canônica Fc para F é o conjunto de dependências tal que F implique logicamente todas as

dependências de FC e FC implique logicamente todas as dependências em F. Além disso, FC deve apresentar as

seguintes propriedades:

• Nenhuma dependência funcional em FC contém um atributo extrínseco.

• Cada lado esquerdo da dependência funcional em FC é único. Isto é, não há duas dependências α1� β1 e

α2� β2 em Fc tal que α1= α2.

Uma cobertura canônica para um conjunto de dependências funcionais F pode ser computada como:

Pode-se mostrar que a cobertura canônica de F, FC, possui a mesma clausura de F; então, verificar se F é

satisfeita. Entretanto, Fc é mínima em certo sentido – ela não contém atributos extrínsecos e as dependências

funcionais, com mesmo lado esquerdo, foram combinadas. É mais econômico verificar FC que testar o próprio F.

Considere o seguinte conjunto de dependências funcionais F do esquema (A, B, C):

A�BC

B�C

A�B

AB�C

Vejamos como computar a cobertura canônica para F.

174

• Há duas dependências funcionais com o mesmo conjunto de atributos do lado esquerdo da seta:

A�BC

A�B

Combinamos essas dependências funcionais em A�BC.

• A é extrínseco em AB�C porque F implica logicamente (F – {AB�C}) {B�C}. Essa asserção é verdadeira

porque B�C já está em nosso conjunto de dependências funcionais.

• C é extrínseco em A�BC, uma vez que A�BC está implícita logicamente por A�B e B�C.

Assim, nossa cobertura canônica é:

A�B

B�C

Dado um conjunto F de dependências funcionais, pode ser que uma dependência funcional total no

conjunto seja extrínseca, no sentido de que, tirando-as, não há mudança na clausura de F. podemos mostrar que

uma cobertura canônica FC de F não contém nenhuma dependência funcional extrínseca. Suponha que, de modo

contrário, houvesse tal dependência funcional extrínseca em FC. O lado direito dos atributos da dependência

poderia ser extrínseco, o que não é possível na definição da cobertura canônica.

175

Projeto e implementação de um banco de dados relacional

Genericamente, o objetivo do projeto de um banco de dados relacional é gerar um conjunto de esquemas

de relações que nos permita armazenar informações sem redundância desnecessária e, ainda, nos permita

recuperar informações facilmente. Uma das abordagens possíveis seria projetar esquemas na forma normal

apropriada. Para determinar se um esquema de relação atende a uma das formas normais, precisamos de

informações adicionais sobre a empresa real cujo banco de dados estamos modelando. Já vimos como podemos

usar dependências funcionais para expressar fatos acerca dos dados.

Armadilhas no Projeto de banco de dados Relacional

Antes de prosseguir com nossa discussão sobre formas normais e dependências de dados, vejamos o que

determina a qualidade de um projeto de banco de dados. Entre as propriedades indesejáveis em um bom projeto

de banco de dados de banco de dados temos:

• Informações repetidas.

• Inabilidade para representação de certas informações.

Podemos discutir esses problemas usando uma modificação no projeto de banco de dados. Entre as

propriedades indesejáveis em um bom projeto de banco de dados temos:

• Informações repetidas.

• Inabilidade para representação de certas informações.

Podemos discutir esses problemas usando uma modificação no projeto de banco de dados no exemplo que temos

usado de uma empresa de área bancária; diferente do usado anterior, a informação acerca de empréstimos será

agora representada em uma única relação, linha_de_crédito, que será definido pelo esquema de relação:

esquema_linha_de_crédito=(nome_agência, cidade_agência, fundos, nome_cliente, número_empréstimo, total)

A fig. 7.1 mostra uma instância da relação linha_de_crédito (esquema_linha_de_crédito). Uma tupla t da

relação linha_de_crédito tem o seguinte significado intuitivo:

• t[fundos] são os fundos totais de uma determinada agência cujo nome é t[nome_agência].

• t[cidade_agência] é a cidade onde determinada agência de nome t[nome_agência] está localizada.

• t[número_empréstimo] é o número atribuído ao empréstimo feito na agência denominada

t[nome_agência] para o cliente de nome t[nome_cliente].

• t[total] é o montante da dívida do empréstimo cujo número é t[número_empréstimo].

Suponha que desejamos adicionar um novo empréstimo ao nosso banco de dados. Digamos que o

176

empréstimo, no valor de 1,5 mil dólares, foi contraído na agência Perryridge para o cliente Adams. O

número_empréstimo será L-31. Em nosso projeto, precisamos de uma tupla com valores em todos os atributos do

esquema_linha_de_crédito. Assim, é necessário repetir os dados sobre fundos e cidade referentes à agência

Perryridge na adição da tupla

(Perryridge, Horseneck, 1700000, Adams, L-31, 1500)

na relação linha_de_crédito. De modo geral, dados sobre fundos e localização da agência devem aparecer toda

vez que um empréstimo é contraído naquela agência.

A necessidade de repetição de informações imposta pelo nosso projeto alternativo é indesejável.

Repetições desperdiçam espaço. Além disso, dificulta atualizações no banco de dados. Suponha, por exemplo,

que a agência Perryridge se mude de Horseneck para Newtown. Em nosso projeto original, uma tupla da relação

agência será alterada. Nesse projeto alternativo, diversas tuplas da relação linha_de_crédito deverão sofrer

alterações. Assim, as atualizações serão mais custosas no novo projeto que no original. Quando realizarmos a

alteração no projeto alternativo, precisamos ter certeza de que todas as tuplas pertencentes à agência Perryridge

serão alteradas, senão nosso banco de dados irá apresentar duas cidades para a agência Perryridge.

Essa observação é fundamental para entender por que o projeto é considerado ruim. Sabemos que uma

agência bancária está localizada em apenas uma cidade, naturalmente. Por outro lado, sabemos que uma agência

pode conceder diversos empréstimos. Em outras palavras, a dependência funcional

nome_agência�cidade_agência se realiza no esquema_linha_de_crédito, mas não esperamos que a dependência

funcional pode ser usada para especificação formal quando o projeto de banco de dados é bom.

Outro problema com o projeto esquema_linha_de_crédito é que não podemos representar diretamente a

informação relativa a uma agência (nome_agência, cidade_agência, fundos), salvo se houver ao menos um

empréstimo concedido pela agência. O problema é que as tuplas da relação linha_de_crédito exigem valores para

número_empréstimo, total e nome_cliente.

Uma solução para esse problema é introduzir valores nulos para tratar as atualizações por meio de visões.

Recorde-se, entretanto, de que é difícil trabalhar com valores nulos. Se não estamos dispostos a tratar com

valores nulos, então só poderemos criar informações sobre as agências quando o primeiro empréstimo na

agência for realizado. Pior, poderemos perder essa informação quando todos os empréstimos da agência forem

pagos. Logicamente, essa situação é impensável, já que, em nosso projeto original, as informações a respeito das

agências estão disponíveis independente de existirem ou não empréstimos mantidos pela agência, e sem a

necessidade de usar valores nulos.

Decomposição

O exemplo de um maus projeto sugere que poderíamos decompor um esquema de relação com diversos

atributos em vários esquemas com diversos atributos em vários esquemas com menor número de atributos.

Decomposições descuidadas, entretanto, podem gerar outro tipo de projeto de má qualidade.

Considere uma alternativa de projeto, em que esquema_linha_de_crédito é decomposto nos dois

esquemas que se seguem:

esquema_agência_cliente = (nome_agência, cidade_agência, fundos, nome_cliente)

esquema_cliente_empréstimo = (nome_cliente, número_empréstimo, total)

Usando a relação linha_de_crédito da fig. 7.1, construímos nossas novas relações cliente_agência

(esquema_agência_cliente) e cliente_empréstimo (esquema_cliente_empréstimo), como se segue:

Mostramos as relações resultados agência_cliente e nome_cliente nas fig. 7.2 e 7.3, respectivamente.

Naturalmente, há casos em que precisamos reconstruir a relação empréstimo. Por exemplo, suponha que

desejamos encontrar todas as agências que tenham empréstimos cujos totais sejam inferiores a mil dólares.

177

Nenhuma relação em nosso banco de dados alternativo contém esses dados. Precisamos reconstruir a relação

linha_de_crédito. Parece que podemos fazê-lo escrevendo:

A fig. 7.4 mostra o resultado do processamento de agência_cliente cliente_empréstimo. Quando

comparamos essa relação e a primeira relação linha_de_crédito (fig. 7.1), notamos algumas diferenças. Embora

todas as tuplas que aparecem em linha_de_crédito apareçam em agência_cliente cliente_empréstimo,

há tuplas em agência_cliente cliente_empréstimo que não estão em linha_de_crédito. Em nosso

exemplo, agência_cliente cliente_empréstimo tem as seguintes tuplas adicionais:

178

Considere a consulta “encontre todas as agências que tenham feito um empréstimo com totais menores

que mil dólares”. Se olharmos a fig. 7.1, veremos que somente as agências Mianus e Round Hill possuem totais de

empréstimo menores que mil dólares. Entretanto, quando aplicamos a expressão

obtemos três nomes de agências: Mianus, Round Hill e Downtown.

Examinaremos esse exemplo mais de perto. Se acontecer de um cliente contrariar vários empréstimo em

diferentes agências, não poderemos identificar a qual agência pertence qual empréstimo. Assim, da junção de

agência_cliente e cliente_empréstimo, não obtemos somente as tuplas que tínhamos originalmente em

linha_de_crédito, mas também diversas tuplas adicionais. Embora haja mais tuplas em agência_cliente

cliente_empréstimo, temos, na verdade, menos informação. De qualquer modo, não poderemos representar nas

informações do banco de dados quais clientes são devedores de quais agências. Devido a essa perda de

informação, chamamos a decomposição do esquema_linha_de_crédito em esquema_agência_cliente e

esquema_cliente_empréstimo de decomposição com perda (lossy decomposition), ou uma decomposição com

perda na junção (lossy-join decomposition). Quando a decomposição não implica perda de informação, ela é

chamada decomposição sem perda na junção (lossless-join decomposition). Deve estar claro diante de nosso

exemplo que uma decomposição com perda na junção é, em geral, uma má opção de projeto de banco de dados.

Examinaremos essa decomposição mais atentamente para descobrir por que ela representa perda. Há um

atributo comum entre esquema_agência_cliente e esquema_cliente_empréstimo:

O único modo de representarmos um relacionamento entre, por exemplo, número_empréstimo e

nome_agência é por meio de nome_cliente. Essa representação não é adequada, porque um cliente pode

contrair diversos empréstimos e, ainda, esses empréstimos não são necessariamente obtidos na mesma agência.

Consideremos outra alternativa de projeto, na qual esquema_linha_de_crédito é decomposta nos dois

esquemas seguintes:

Há um atributo em comum entre os dois esquemas:

179

Assim, o único modo de representar um relacionamento entre, por exemplo, nome_cliente e fundos é por

meio de nome_agência. A diferença entre esse exemplo e o precedente é que o valor dos fundos de uma agência

é o mesmo, qualquer que seja o cliente em questão, enquanto a linha de crédito oferecida pela agência,

especialmente no montante envolvido, depende do cliente em questão. Para um dado nome_agência, há

exatamente um valor de fundos e exatamente uma cidade_agência; já um tratamento similar não pode ser

oferecido para nome_cliente. Isto é, a dependência funcional:

nome_agência � fundos cidade_agência

realiza-se, mas nome_cliente não determina funcionalmente número_empréstimo.

A noção de junção sem perda é fundamental para a maioria dos projetos de banco de dados. Portanto,

refaremos o exemplo anterior de modo mais precisa e mais formal. Seja R um esquema de relação. Um conjunto

de esquemas de relações {R1, R2, ..., Rn} é uma decomposição de R se:

Isto é, {R1, R2, ..., Rn} é uma decomposição de R se, para i = 1, 2, ..., n, cada Ri é um subconjunto de R e

todo atributo de R aparece ao menos uma vez em Ri.

Seja r uma relação do esquema R, e seja ri = para i=1,2,..., n. Isto é, {r1, r2, ..., rn} é o banco de dados

que resulta da decomposição de R em {R1, R2, ..., Rn}. Neste caso, é sempre válido:

Para verificar se essa declaração é verdadeira, considere uma tupla t da relação r. quando computamos as

relações r1, r2, ..., rn, a tupla t origina uma tupla ti em cada ri, i=1, 2, ..., n. Essas n tuplas podem ser combinadas

para reconstruir t quando computamos r1 r2 ... rn. Portanto, toda tupla em r aparece em .

De modo geral, r≠ . Como ilustração, considere nosso exemplo anterior, no qual:

• n = 2

• R = esquema_linha_de_crédito

• R1 = esquema_agência_cliente

• R2 = esquema_cliente_empréstimo

• r = a relação mostrada na fig. 7.1.

• r1 = a relação mostrada na fig. 7.2.

• r2 = a relação mostrada na fig. 7.3.

• r1 r2 = a relação mostrada na fig. 7.4.

Note que as relações das fig. 7.1 a 7.4 não são as mesmas.

Para decomposições sem perda, precisamos impor restrições ao conjunto das relações possíveis.

Concluímos que a decomposição do esquema_linha_de_crédito em esquema_agência e

esquema_info_empréstimos ocorre sem perdas porque a dependência funcional

nome_agência � cidade_agência fundos

realiza-se em esquema_agência.

Seja C a representação de um conjunto de restrições do banco de dados. Uma decomposição {R1, R2, ...,

Rn} do esquema de relação R é uma decomposição sem perda na junção de R se para todas as relações r no

esquema R válido sob C:

Normalização usando dependências funcionais

Podemos usar um dado conjunto de dependências funcionais para projetar um banco de dados relacional,

evitando a maior das propriedades não desejadas, já discutidas. Quando projetamos tais sistemas, pode tornar-se

desnecessário decompor uma relação em diversas relações menores. Usando a dependência funcional, podemos

definir algumas formas normais que representam “bons” projetos de banco de dados.

180

Propriedades Desejáveis da Decomposição

Podemos ilustrar nossos conceitos por meio do esquema_linha_de_crédito:

O conjunto de dependências funcionais F que desejamos que se realizem para o

esquema_linha_de_crédito são:

nome_agência � fundos cidade_agência

número_empréstimo � total nome_agência

O esquema_linha_de_crédito é um exemplo de projeto ruim de banco de dados. Suponha que tenhamos

decomposto esse esquema nas três relações a seguir:

esquema_agência= (nome_agência, fundos, cidade_agência)

esquema_empréstimo= (nome_agência, número_empréstimo, total)

esquema_devedor= (nome_cliente, número_empréstimo)

Afirmamos que essa decomposição apresenta diversas propriedades desejáveis.

Decomposição sem Perda na Junção

Já sustentamos que, quando decompomos uma relação em outras relações menores, é crucial que a

decomposição não resulte em perda de informação. Afirmamos que a decomposição é crucial que a

decomposição não resulte em perda de informação.

Seja R um esquema de relação e F um conjunto de dependências funcionais sobre R. Sejam R1 e R2 formas

de decomposição de R. Essa decomposição é uma decomposição sem perda na junção de R se ao menos uma das

seguintes dependências funcionais está em F+:

Mostraremos agora que nossa decomposição para o esquema_linha_de_crédito é uma decomposição

sem perda na junção mostrando uma sequência de passos que geraram essa decomposição. Comecemos pela

decomposição do esquema_linha_de_crédito em dois esquemas:

esquema_agência = (nome_agência, cidade_agência, fundos)

esquema_info_empréstimo = (nome_agência, nome_cliente, número_empréstimo, total)

Uma vez que nome_agência�cidade_agência fundos, a regra incremental (augmentation) para a

dependência funcional implica:

nome_agência � nome_agência cidade_agência fundos

Já que esquema_agência esquema_info_empréstimo = {nome_agência}, então nossa decomposição

inicial é uma decomposição sem perda na junção.

A seguir, decomporemos esquema_info_empréstimo em:

Esse passo resulta em uma decomposição sem perda na junção, desde que número_empréstimo seja um

atributo comum e número_empréstimo�total nome_agência.

Preservação da dependência

Outra meta do projeto de um banco de dados relacional é a preservação da dependência. Quando ocorre

uma atualização no banco de dados, o sistema deve checar se ele criará uma relação ilegal – isto é, uma relação

que não que não satisfaça todas as dependências funcionais. Se checarmos de modo eficiente essas atualizações,

181

poderemos projetar esquemas de banco de dados relacionais capazes de validar atualizações sem necessidade de

junções.

Para decidir se uma junção é ou não necessária, precisamos determinar quais dependências funcionais

devem ser testadas, verificando cada uma das relações individualmente. Seja F um conjunto de dependências

funcionais de um esquema R e seja R1, R2, ..., Rn uma decomposição de R. A restrição de F para Ri é o conjunto Fi

de todas as dependências funcionais em F+ que contenha somente os atributos de Ri. Já que todas as

dependências funcionais em uma restrição contêm atributos de apenas um esquema de relação, é possível testar

se tais dependências são satisfeitas checando somente uma relação.

O conjunto de restrições F1, F2, ..., Fn é o conjunto das dependências que podem ser checadas

eficientemente. Agora precisamos nos certificar de que é suficiente testar somente as restrições. Seja F’ =

. F’ é um conjunto de dependências funcionais do esquema R, mas, em geral, F’≠F. Entretanto,

mesmo que F’≠F, pode ser que F’+=F+. Se o último é verdadeiro, então toda a dependência de F está

compreendida logicamente em F’, e, se verificarmos que F’ é satisfeita, podemos verificar que F também o é.

Dizemos que uma decomposição é decomposição com preservação da dependência se possui a propriedade

F’+=F+. A fig. 7.5 mostra um algoritmo para o teste da preservação da dependência. A entrada é o conjunto dos

esquemas das relações decompostas D = {R1, R2, ..., Rn} e um conjunto de dependências funcionais F.

Podemos agora mostrar que nossa decomposição do esquema_linha_de_crédito é uma decomposição

com preservação de dependência. Consideramos cada membro do conjunto F das dependências funcionais que

desejamos impostas sobre esquema_linha_de_crédito e mostraremos que cada uma pode ser testada em ao

menos uma relação da decomposição.

• Podemos testar a dependência funcional: nome_agência�cidade_agência fundos usando

esquema_agência= (nome_agência, cidade_agência, fundos).

• Podemos testar a dependência funcional: número_empréstimo�total nome_agência usando

esquema_empréstimo= (nome_agência, número_empréstimo, total).

Como no exemplo mostrado anteriormente, frequentemente é mais fácil não aplicar o algoritmo da fig.

7.5 para o teste da preservação da dependência, já que o primeiro passo, o processamento de F+, toma tempo

exponencial.

Redundância de Informações

A decomposição do esquema_linha_de_crédito não sofre o problema da repetição da informação que foi

discutido anteriormente. No esquema_linha_de_crédito é necessário repetir, em cada empréstimo, a cidade e os

fundos relativos à agência.

182

A decomposição separa os dados a respeito da agência e do empréstimo em relações distintas

eliminando, desse modo, a redundância. Similarmente, observe que, se um único empréstimo é feito para

diversos clientes, repetiremos o total do empréstimo para cada um dos clientes (assim como a cidade e os fundos

da agência). Na decomposição, a relação do esquema esquema_devedor contém o relacionamento

número_do_empréstimo e nome_do_cliente que nenhum outro esquema contém.

Temos, portanto, somente na relação do esquema esquema_devedor contém o relacionamento

número_do_empréstimo e nome_do_cliente que nenhum outro esquema contém.

Temos, portanto, somente na relação do esquema esquema_devedor, uma tupla para cada cliente com

um empréstimo. Nas outras relações que contêm o atributo número_empréstimo (as dos esquemas

esquema_empréstimo, e esquema_devedor) aparece somente uma tupla por empréstimo.

Evidentemente, é desejável a ausência de redundância mostrada nessa decomposição. O grau alcançado

por essa ausência de redundância é representado pelas diversas formas normais.

Forma Normal de Boyce-Codd

Uma das mais procuradas formas normais é a forma normal de Boyce-Codd (FNBC). Uma relação do

esquema R está na FNBC com respeito a um conjunto F de dependências funcionais se para todas as

dependências funcionais em F+ da forma α�β, em que α R e β R, ao menos uma das seguintes realiza-se:

• α�β é uma dependência funcional trivial (isto é, β α).

• α é uma superchave para o esquema R.

Um projeto de banco de dados está na FNBC se cada membro do conjunto de relações dos esquemas que

constituem o projeto está na FNBC.

Como exemplo, consideremos os seguintes esquemas de relações e suas respectivas dependências

funcionais:

Afirmamos que o esquema_cliente está na FNBC. Notamos que uma chave candidata para o esquema é

nome_cliente. A única dependência funcional não trivial no que se realiza no esquema_cliente tem nome_cliente

é chave candidata, dependências funcionais com nome_cliente do lado esquerdo da seta não violam a definição

da FNBC. Analogamente, pode-se mostrar facilmente que a relação do esquema esquema_agência está na FNBC.

O esquema esquema_info_empréstimo, entretanto, não está na FNBC. Primeiro, note que

número_empréstimo não é superchave do esquema_info_empréstimo, já que poderia haver um par de tuplas

representando um único empréstimo adquirido por duas pessoas – por exemplo:

Como não fizemos a lista das dependências funcionais que não são admitidas no caso precedente,

número_empréstimo não é uma chave candidata. Entretanto, a dependência funcional

número_empréstimo�total não é trivial. Portanto, esquema_info_esquema não satisfaz a definição FNBC.

Afirmamos que o esquema_info_empréstimo não é conveniente, uma vez que está sujeito ao problema

de informações redundantes já descrito. Observemos que, se existir diversos nomes de clientes associados a um

empréstimo, em uma relação do esquema_info_empréstimo, então somos forçadas a repetir o nome da agência

e o total do empréstimo a cada cliente.

183

Podemos eliminar essa redundância redesenhando nosso banco de dados de modo que todos os

esquemas estejam em FNBC. Uma abordagem para esse problema é tomar um projeto que não está na FNBC

como ponto de partida e decompor os esquemas necessários. Considere a decomposição do

esquema_info_empréstimo em dois outros esquemas:

Essa decomposição é uma decomposição sem perda na junção.

Para determinar se esses esquemas estão na FNBC, precisamos determinar quais dependências funcionais

se aplicam a elas. Nesse exemplo, é fácil perceber que:

número_empréstimo� total nome_agência

aplica-se a esquema_empréstimo e que somente dependências funcionais aplicam-se a esquema_devedor.

Embora número_empréstimo não seja uma superchave para esquema_info_empréstimo, ele é chave candidata

para esquema_empréstimo. Assim, ambos os esquemas de nossa decomposição estão em FNBC.

Assim, é possível evitar redundância quando há diversos clientes associados a um único empréstimo. Há

exatamente uma tupla para cada empréstimo na relação do esquema_empréstimo e uma tupla para cada

empréstimo de cada cliente na relação do esquema_devedor. Logo, não precisamos repetir o nome da agência e

o total para cada cliente associado a um empréstimo.

Podemos agora determinar um método geral para criar uma coleção de esquemas na FNBC. Se R não está

na FNBC, podemos decompor R em um grupo de esquemas R1, R2, ..., Rn na FNBC usando o algoritmo da fig. 7.6,

que não só gera decomposições na FNBC, como todas as decomposições sem perda na junção. Para perceber por

que nosso algoritmo gera somente decomposições sem perda na junção, note que, quando substituímos um

esquema Ri por (Ri – β) e (α,β), a dependência α�β realiza-se e .

Vejamos a aplicação do algoritmo de decomposição FNBC no esquema_linha_de_crédito que usamos

anteriormente como exemplo de um projeto deficiente de banco de dados:

esquema_linha_de_crédito = (nome_agência, cidade_agência, fundos, nome_cliente, número_empréstimo, total)

O conjunto de dependências funcionais que exigimos que se realizem são:

Uma chave candidata para esse esquema é {número_empréstimo, nome_cliente}.

Aplicamos o algoritmo da fig. 7.6 para o exemplo com esquema_linha_de_crédito, conforme segue:

• A dependência funcional nome_agência � fundos cidade_agência

realiza-se no esquema_linha_de_crédito, mas nome_agência não é uma superchave.

Assim, esquema_linha_de_crédito não está na FNBC. Substituímos o esquema_linha_de_crédito por:

184

• A única dependência funcional não trivial que se realiza no esquema_agência contém nome_agência no

lado esquerdo da seta. Uma vez que nome_agência é uma chave para o esquema_agência, a relação

esquema_agência está na FNBC.

• A dependência funcional

número_empréstimo � total nome_agência

realiza-se no esquema_info_empréstimo, mas número_empréstimo não é chave para o

esquema_info_empréstimo. Substituímos esquema_info_empréstimo por:

• esquema_empréstimo e esquema_devedor estão na FNBC.

Assim, a decomposição do esquema_linha_de_crédito resulta nos três esquemas de relações

esquema_agência, esquema_empréstimo, e esquema_devedor, cada um dos quais na FNBC. Esses esquemas de

relação são os mesmos já usados anteriormente.

Nem toda decomposição na forma FNBC tem dependência preservada. Considere a seguinte relação:

esquema_bancário= (agência_nome, nome_cliente, nome_bancário)

que informa que um cliente possui atendimento personalizado, de responsabilidade de um bancário

determinado, em uma dada agência. O conjunto F de dependências funcionais necessárias ao esquema_bancário

são:

Naturalmente, esquema_bancário não está na FNBC, uma vez que nome_bancário não é uma superchave.

Se aplicarmos o algoritmo da fig. 7.6, obtemos a seguinte decomposição em FNBC:

A decomposição dos esquemas preserva somente nome_bancário�nome_agência (e as dependências

triviais), mas a clausura {nome_bancário�nome_agência} não engloba nome_cliente nome_agência �

nome_bancário. Uma violação dessa dependência pode ser detectada somente por meio de uma junção.

Para verificar se a decomposição de esquema_bancário nos esquemas esquema_bancário_agência e

esquema_cliente_bancário não acontece com preservação da dependência, aplicamos o algoritmo da fig. 7.5.

Consideremos que as restrições F1 e F2 de F para cada um dos esquemas são:

Assim, uma cobertura canônica para o conjunto F’ é F1.

É fácil perceber que a dependência funcional nome_cliente nome_agência � nome_bancário não está

em F’+ mesmo que esteja em F+. Portanto, F’+≠F+ e a decomposição não preserva a dependência.

O exemplo precedente demostra que nem toda decomposição FNBC preserva a dependência. Além disso,

ela demonstra que nem sempre as três metas de projeto podem ser satisfeitas:

1. FNBC

2. Sem perda na junção

3. Preservação da dependência

Não podemos realiza-las neste exemplo porque toda decomposição FNBC do esquema_bancário falha na

preservação de nome_cliente nome_agência � nome_bancário.

185

Terceira Forma Normal

Nos caos em que não conseguimos alcançar todos os três critérios de projeto, abandonamos FNBC e

aceitamos uma forma normal mais fraca chamada terceira forma normal (3FN). Vemos que sempre é possível

alcançar decomposição sem perda na junção, decomposição com preservação da dependência que está na 3FN.

A FNBC exige que todas as dependências não triviais sejam da forma α�β, em que α é uma superchave. A

3FN suaviza essa restrição permitindo dependências funcionais não-triviais cujo lado esquerdo da seta não seja

superchave.

Um esquema de relação R está na 3FN com respeito a um conjunto de dependências funcionais F se, para

todas as dependências funcionais F+ da forma α�β, em que α R e β R, ao menos uma das seguintes

condições for realizada:

• α�β é uma dependência funcional trivial.

• α é uma superchave de R.

• Cada atributo de A em β – α está contido em uma chave candidata de R.

A definição da 3FN permite algumas dependências funcionais que não são permitidas na FNBC. A

dependência α�β, que satisfaz apenas a terceira condição da definição da 3FN, não é permitida na FNBC, mas é

permitida na 3FN. Essas dependências são um exemplo de dependências transitivas.

Observe que, se um esquema de relação está na FNBC, então todas as dependências funcionais são da

forma “superchave determina um conjunto de atributos”, ou a dependência é trivial. Assim, um esquema FNBC

não pode conter nenhuma dependência transitiva. Como resultado, todo esquema FNBC é também da 3FN, e a

FNBC, portanto, mantém regras mais restritivas que a 3FN.

Retornemos ao nosso exemplo do esquema_bancário. Mostramos que esse esquema de relação não tem

a dependência preservada, com decomposição sem perda da junção em FNBC. Esse esquema, entretanto, sai da

3FN. Para essa verificação, notamos que {nome_cliente, nome_agência} é uma chave candidata para

esquema_bancário, assim o único atributo não contido em chave candidata de esquema_bancário é

nome_bancário. As únicas dependências funcionais não-triviais da forma α�nome_bancário incluem

{nome_cliente, nome_agência} como parte de α. Já que {nome_cliente, nome_agência} é chave candidata, essas

dependências não violam a definição da 3FN.

A fig. 7.7 mostra um algoritmo para chegar à preservação de dependência, com decomposição sem perda

na junção em 3FN. O fato de cada esquema de relação Ri estar na 3FN decorre diretamente de nossa exigência de

que o conjunto de dependências funcionais F seja da forma canônica. O algoritmo assegura a preservação das

dependências explicitando um esquema para cada dependência. Ele assegura que a decomposição é sem perda

na junção por meio da garantia de que ao menos um esquema contém uma chave candidata para o esquema que

está sendo decomposto.

Para ilustrar o algoritmo da fig. 7.7 consideramos a seguinte extensão do esquema_bancário:

esquema_info_bancário= (nome_agência, nome_cliente, nome_bancário, número_seção)

A principal diferença aqui é que incluímos o número da seção do bancário como parte da informação. As

dependências funcionais para esse esquema de relação são:

Uma vez que esquema_bancário contém uma chave candidata para o esquema_info_bancário,

terminamos o processo de decomposição.

186

Comparação entre FNBC e 3FN

Vimos duas formas normais de esquemas de banco de dados relacionais: 3FN e FNBC. Uma vantagem de

um projeto na 3FN é saber que sempre é possível obtê-la sem sacrificar uma decomposição sem perda na junção

ou preservação da dependência. Apesar disso, há uma desvantagem na 3FN. Se não a eliminarmos todas as

dependências transitivas, teremos de usar valores nulos para representação de alguns dos possíveis

relacionamentos significativos entre itens de dados, e há ainda o problema da repetição da informação.

Como ilustração, considere novamente o esquema_bancário e suas dependências funcionais associadas.

Dado nome_bancário�nome_agência, podemos desejar representar em nosso banco de dados relacionamentos

entre valores de nome_bancário e valores de nome_agência. No entanto, se fizermos isso, será preciso ter um

valor correspondente para nome_cliente ou usar valores nulos para o atributo nome_cliente.

Outra dificuldade com esquema_bancário é a repetição da informação. Como ilustração, considere uma

instância de esquema_bancário mostrada na fig. 7.8. Note que a informação de que Johnson está trabalhando na

agência Perryridge é repetida.

Se formos forçados a escolher entre FNBC e preservação da dependência com 3FN, geralmente é

preferível optar pela 3FN. Se não pudermos verificar eficientemente a preservação da dependência, termos de

pagar alto custo em desempenho do sistema ou corremos riscos em relação à integridade de dados de nosso

banco de dados. Nenhuma dessas opções é atraente. Com tais alternativas, o limite à redundância imposto pelas

dependências transitivas sob a 3FN é o menos pior. Assim, normalmente optamos por manter a preservação da

dependência e sacrificar a FNBC.

Em resumo, repetimos que as três metas de projeto para um banco de dados relacional são:

1. FNBC

187

2. Junção sem perda

3. Preservação da dependência

Se não pudermos alcançar as três, aceitamos:

1. 3FN

2. Junção sem perda

3. Preservação da dependência

Normalização usando Dependências Multivaloradas

Há esquemas de relação na FNBC que não estão normalizados o bastante, no sentido de que eles ainda

sofrem de problemas de repetição de informação. Considere novamente nosso exemplo relativo ao banco.

Assuma que, como alternativa de projeto para o esquema de um banco de dados de uma empresa na área

bancária, tenhamos o esquema:

esquema_BC = (número_empréstimo, nome_cliente, rua_cliente, cidade_cliente)

Podemos perceber que esse esquema não está na FNBC por causa da dependência funcional

nome_cliente � rua_cliente cidade_cliente

que afirmamos anteriormente, e porque nome_cliente não é uma chave do esquema_BC. Entretanto,

consideremos que nosso banco está atraindo clientes ricos que possuem diversos endereços (digamos, casa de

inverno e casa de verão). Então, não desejaremos mais a dependência funcional nome_cliente�rua_cliente

cidade_cliente. Se retirarmos essa dependência funcional, concluiremos que o esquema_BC está na FNBC com

respeito ao nosso conjunto de dependências funcionais modificadas. Ainda, mesmo que o esquema_BC esteja na

FNBC com respeito ao nosso conjunto de dependências funcionais modificadas. Ainda, mesmo que o

esquema_BC esteja na FNBC, nós teremos o problema da repetição de informações que tínhamos anteriormente.

Para tratar esse problema, precisamos definir uma nova forma de restrição, chamada dependência

multivalorada. Como fizemos com as dependências funcionais, usaremos as dependências funcionais

multivaloradas para definir uma forma normal para os esquemas das relações. Essa forma normal, chamada

quarta forma normal (4FN), é mais restritiva que a FNBC. Podemos ver que todo esquema na 4FN está também na

FNBC, mas há esquemas na FNBC que não estão na 4FN.

Dependências Multivaloradas

As dependências funcionais rejeitam certas tuplas como participantes de uma relação. Se A�B, então

não podemos ter duas tuplas com os mesmos valores de A, mas diferentes valores de B. Dependências

multivaloradas não rejeitam a existência de certas tuplas. Pelo contrário, elas exigem que outras tuplas, de uma

certa forma, estejam presentes na relação. Por essa razão, por vezes, as dependências funcionais são chamadas

dependências geradas por igualdade (equality-generating dependencies) e dependências multivaloradas são

referidas como dependências geradas por tuplas (tuple-generating dependencies).

Seja R um esquema de relação e seja α R e β R. A dependência multivalorada α��β realiza-se em R

se, para qualquer relação r(R), para todos os pares de tuplas t1 e t2 de r, tal que t1[α] =t2 [α], existem tuplas t3 e t4

em r tal que

Essa definição é menos complexa do que parece. Na fig. 7.9, damos uma apresentação tabular para t1, t2,

t3 e t4. Intuitivamente, a dependência multivalorada α��β diz que um relacionamento entre α e β é

independente do relacionamento entre α e R – β. Se uma dependência multivalorada α��β é satisfeita para

todas as relações do esquema R, então α��β é uma dependência multivalorada trivial do esquema R. Assim,

α��β é trivial se β α β α=R.

188

Para ilustrar as diferenças entre dependência funcional e multivalorada, consideremos novamente o

esquema_BC e a relação bc (esquema_BC) da fig. 7.10. Precisamos repetir o número do empréstimo de um

mesmo cliente. Essa repetição é desnecessária, já que o relacionamento entre o cliente e seu endereço é

independente do relacionamento entre o cliente e um empréstimo. Se um cliente (digamos, Smith) tem um

empréstimo (digamos, o de número L-23), queremos que o empréstimo seja associado a todos os endereços de

Smith. Assim, a relação da fig. 7.11 não é validade. Para tornar essa relação válida, precisamos adicionar as tuplas

(L-23, Smith, Main, Manchester) e (L-27, Smith, North, Rye) à relação bc da fig. 7.11.

Comparando o exemplo anterior com nossa definição de dependência multivalorada, percebemos que

desejamos que a dependência multivalorada nome_cliente �� rua_cliente cidade_cliente se realize (a

dependência multivalorada nome_cliente ��número_empréstimo faz a mesma coisa. Assim, verificamos que

são equivalentes).

Como fizemos para as dependências funcionais, podemos usar a dependência multivalorada de dois

modos:

1. Para testar relações para determinar se elas são validades sob um dado conjunto de dependências

funcionais e multivaloradas.

2. Para especificar restrições sobre um conjunto de relações válidas; devemos, assim, nos restringir somente

àquelas relações que satisfazem um dado conjunto de dependências funcionais e multivaloradas.

Note que, se uma relação r não satisfaz uma dada dependência multivalorada, podemos criar uma relação r’ que

satisfaça a dependência multivalorada adicionando tuplas a r.

Teoria das Dependências Multivaloradas

Como foi feito para a dependência funcional, para 3FN e para FNBC, precisamos determinar todas as

dependências multivaloradas que estão logicamente implícitas em um dado conjunto de dependências

multivaloradas.

Adotamos o mesmo enfoque tomado anteriormente para as dependências funcionais. Seja D um

conjunto de dependências funcionais e multivaloradas. A clausura D+ de D é o conjunto de todas as dependências

189

funcionais e multivaloradas logicamente implícitas em D. Como fizemos para as dependências funcionais,

podemos computar D+ por meio de D, usando a definição formal de dependências funcionais e dependências

multivaloradas. Entretanto, é normalmente mais fácil ponderar acerca de conjuntos de dependências

multivaloradas. Entretanto, é normalmente mais fácil ponderar acerca de conjuntos de dependências usando um

sistema de regras de inferências.

A seguinte lista de regras de inferências para dependências funcionais e dependências multivaloradas é

sólida e completa. Recorde que as regras para solidez não criam qualquer dependência que não esteja

logicamente implícita em D e regras completas permite-nos criar todas as dependências em D+.

Seja R= (A, B, C, G, H, I) um esquema de relação. Suponha que A��BC realiza-se. A definição de

dependência multivalorada implica que, se t1[A] = t2[A], então existem as tuplas t3 e t4 tal que:

A regra de complementação coloca que, se A��BC, então A��GHI. Observe que t3 e t4 satisfazem a

definição de que A��GHI simplesmente mudando os subescritos.

Podemos proporcionar uma justificativa similar para as regras 5 e 6 usando a definição de dependência

multivalorada.

Regra 7, a regra da replicação, envolve dependências funcionais e multivaloradas. Suponha que A�BC

realiza-se em R. Se t1[A] =t2[A] e t1[BC] =t2[BC], então as próprias t1 e t2 podem ser as tuplas t3 e t4 exigidas na

definição da dependência multivalorada A��BC.

Regra 8, a regra da coalescência, é a mais difícil de se verificar entre as oito regras.

Podemos simplificar a computação da clausura de D usando as seguintes regras, as quais podemos provar

usando as regras 1 a 8.

• Regra da união multivalorada. Se α��β e α��γ realizam-se, então α�βγ também realiza-se.

• Regra da interseção. Se α��β e α��γ realizam-se, então α��β γ realiza-se.

• Regra da diferença. Se α��β e α��γ realizam-se, então α��β – γ realiza-se e α��γ – β realiza-se.

Apliquemos nossas regras no seguinte exemplo. Seja R = (A, B, C, G, H, I) com o seguinte conjunto de

dependências D dado:

Relacionamos alguns membros de D+ aqui:

190

• A��CGHI: desde que A��B, a regra da complementação (regra 4) implica que A��R – B – A, R – B –

A = CGHI, assim A��CGHI.

• A��HI: desde que A��B e B��HI, a regra da transitividade multivalorada (regra 6) implica que

A��HI – B. Desde que HI – B = HI, A��HI.

• B�H: para mostrar isso, precisamos aplicar a regra da transitividade multivalorada (regra 6) implica que

A��HI – B. Desde que HI – B = HI, A��HI.

• B�H: para mostrar isso, precisamos aplicar a regra da coalescência (regra 8). B�HI realiza-se. Desde que

H HI e CG�H e CG HI = , satisfazemos a regra da coalescência, com α estando em B, β estando em

HI, δ estando em CG e γ estando em H. Concluímos que B�H.

• A��CG: também sabemos que A��CGHI e A��HI. Pela regra da diferença, A��CGHI. Desde que

CGHI – HI = CG. A�CG.

Quarta Forma Normal

Retornemos a nosso exemplo esquema_BC no qual a dependência multivalorada nome_cliente ��

rua_cliente cidade_cliente realiza-se, mas nenhuma dependência funcional não-trivial realiza-se. Vimos

anteriormente que, embora esquema_BC esteja na FNBC, o projeto não é adequado, uma vez que precisamos

repetir as informações acerca do endereço do cliente a cada empréstimo. Podemos ver que é possível usar a

dependência multivalorada dada para melhorar o projeto do banco de dados, por meio da decomposição

esquema_BC em uma decomposição na quarta forma normal (4FN).

Um esquema de relação R está na 4FN com respeito a um conjunto D de dependências funcionais e

multivaloradas se, para todas as dependências multivaloradas em D+ da forma α��β, em que α R e β R, ao

menos uma das seguintes condições se realize:

• α��β é uma dependência multivalorada trivial.

• α é uma superchave para o esquema R.

Um projeto de banco de dados está na 4FN se cada membro do conjunto dos esquemas de relações que

constituem o projeto estiver na 4FN.

Note que a definição da 4FN difere da definição da FNBC somente no uso de dependências multivaloradas

em vez de dependências funcionais. Todo esquema na 4FN está na FNBC.

Para constatar esse fato, notamos que, se um esquema R não está na FNBC, então há uma dependência

funcional não-trivial α�β realizando-se em R, na qual α não é superchave. Desde que α�β implica que α��β

(pela regra da replicação), R não poderá estar na 4FN.

A analogia entre 4FN e FNBC aplica-se ao algoritmo para a decomposição de um esquema para a 4FN. A

fig. 7.12 mostra o algoritmo para a decomposição na 4FN. Ele é idêntico ao algoritmo de decomposição para FNBC

mostrado na fig. 7.6, exceto pelo fato de usar dependência multivalorada em vez da dependência funcional.

Se aplicarmos o algoritmo da fig. 7.12 para o esquema_BC, concluímos que nome_cliente � �

número_empréstimo é uma dependência multivalorada não-trivial e nome_cliente não é uma superchave para o

esquema_BC pelos dois esquemas:

esquema_devedor = (nome_cliente, número_empréstimo)

esquema_cliente = (nome_cliente, rua_cliente, cidade_cliente)

Esse par de esquemas, que estão na 4FN, eliminam o problema encontrado anteriormente em relação à

redundância do esquema_BC.

Como no caso em que tratamos somente das dependências funcionais, estamos interessados em

decomposições sem perda na junção e com preservação das dependências. Os fatos seguintes sobre

dependências multivaloradas e sem perda na junção mostram que o algoritmo da fig. 7.12 cria somente

decomposições sem perda na junção:

191

• Seja o esquema de relação R e seja D um conjunto de dependências funcionais e multivaloradas de R. Seja

R1 e R2 decomposições de R. Esta decomposição é sem perda na junção de R se e somente se ao menos

uma das seguintes dependências multivaloradas estiver em D+:

Lembre-se de que estabelecemos anteriormente que, se R1 R2�R1 ou R1 R2�R2, então R1 e R2 são

decomposições sem perda na junção de R. O fato precedente ressalta que as dependências multivaloradas

constituem uma forma mais genérica de junção sem perda. Ele indica que, para toda decomposição sem perda na

junção de R em dois esquemas R1 e R2, uma das duas dependência R1 R2 ��R1 ou R1 R2�R2 deve se realizar.

A questão da preservação da dependência quando temos dependência multivalorada não é tão simples

quanto quando temos somente dependências funcionais. Seja R um esquema de relação e sejam R1, R2, ..., Rn

decomposições de R. Lembre-se de que, para um conjunto de dependências funcionais F, a restrição F1 de F para

Ri são todas as dependências funcionais em F+ que incluem somente atributos de Ri. Agora, consideremos um

conjunto D, tanto de dependências funcionais quanto de dependências multivalorados. A restrição de D para R1 é

o conjunto Di, consistindo:

• Todas as dependências funcionais em D+ que incluem somente atributos de Ri.

• Todas as dependências multivaloradas da forma.

α��β Ri em que α Ri e α��β está em D+.

Uma decomposição do esquema R nos esquemas R1, R2, ..., Rn é uma decomposição com preservação da

dependência com respeito ao conjunto D de dependências funcionais e multivaloradas se, para todo conjunto de

relações r1(R1), r2(R2), ..., rn (Rn), tal que, para todo i, ri satisfaça Di, lá houver uma relação r(R) que satisfaça D e

para qual ri= Ri(r) para todo i.

Apliquemos o algoritmo para decomposição na 4FN da fig. 7.12 em nosso exemplo de R = (A, B, C, G, H, I)

com D = {A��B, B��HI, CG� H}. Podemos, então, testar a decomposição resultante em relação à preservação

da dependência.

R não está na 4FN. Observe que A��B não é trivial, ainda A não é uma superchave. Usando A��B na

primeira iteração do while, substituímos R por dois esquemas, (A, B) e (A, C, G, H, I). É fácil perceber que (A,B)

está na 4FN desde que todas as dependências multivaloradas que valem em (A, B) sejam triviais. Entretanto, o

esquema (A, C, G, H, I) não está na 4FN. Aplicando a dependência multivalorada CG��H (que decorre na

dependência funcional dada CG��H pela regra da replicação), substituímos (A, C, G, H, I) pelos dois esquemas,

(C, G, H) e (A, C, G, I). O esquema (C, G, H) está na 4FN, mas o esquema (A, C, G, I) não está. Para perceber que (A,

C, G, I) não está na 4FN, lembremos que foi mostrado anteriormente que A��HI está em D+. Portanto, A��I

está na restrição de D para (A, C, G, I). Assim, na terceira iteração do laço while, substituímos (A, C, G, I) pelos dois

esquemas (A, I) e (A, C, G). O algoritmo então termina e a decomposição 4FN é {(A, B), (C, G, H), (A, I), (A, C, G)}.

192

Essa decomposição para a 4FN não preserva a dependência, uma vez que ela falha na preservação da

dependência multivalorada B��HI. Considere a fig. 7.13m que mostra as quatro relações que poderiam resultar

da projeção de uma relação em (A, B, C, G, H, I) nos quatro esquemas de nossa decomposição. A restrição de D

para (A, B) é A��B e algumas dependências triviais. É fácil perceber que r1 satisfaz A��B porque não há

nenhum par de tuplas com o mesmo valor de A. Observe que r2 tenha os mesmos valores em qualquer atributo.

Um comando similar pode ser feito para r3 e r4. Portanto, a versão decomposta de nosso banco de dados satisfaz

a todas as dependências da restrição de D. Entretanto, não há nenhuma relação r em (A, B, C, G, H, I) que

satisfaça D e possa ser decomposta em r1, r2, r3 e r4. A fig. 7.14 mostra a relação r = . A relação

r não satisfaz B��HI. Qualquer relação s contendo r e satisfazendo B��HI deve incluir a tupla (a2, b1, c2, g2, h1,

i1). Entretanto, CGH(s) inclui uma tupla (c2, g2, h1) que não está em r2. Assim, nossa decomposição falha da

detecção da violação de B��HI.

Vimos que, se estamos definindo um conjunto de dependências funcionais e multivaloradas, é vantajoso

chegar a um projeto de banco de dados que tenha como critérios:

1. 4FN

2. Preservação de dependência

3. Junção sem perda

Se tudo o que tivermos forem dependências funcionais, então o primeiro critérios será apenas a FNBC.

Vimos também que nem sempre é possível alcançar todas as três condições. Obtivemos sucesso na

decomposição do exemplo do banco, mas falhamos no exemplo do esquema R= (A, B, C, G, H, I).

Quando não conseguimos alcançar as três metas, abrimos mão da 4FN aceitando FNBC ou mesmo 3FN, se

necessário, assegurando a preservação da dependência.

Normalização usando dependências na Junção

193

Vimos que a propriedade sem perda na junção é uma das diversas propriedades para o projeto de um

banco de dados. De fato, essa propriedade é essencial: sem ela, há perda de informação. Quando restringimos o

conjunto das relações válidas entre as que satisfazem um conjunto de dependências funcionais e multivaloradas,

podemos usar essas dependências para mostrar que certas decomposições são decomposições sem perda na

junção.

Por causa da importância desse conceito de “sem perda na junção”, é útil conseguir restringir um

conjunto de relações válidas sobre uma esquema R para aquelas relações para as quais uma dada decomposição

é uma decomposição sem perda na junção. Vamos agora definir o que é dependência de junção. Apenas como

tipos de dependências conduzidas por outras formas normais, dependências de junção serão direcionadas a uma

forma normal chamada forma normal de projeção de junção – Project-join normal form (FNPJ).

Dependências de junção (Join Dependencies)

Seja R um esquema de relação e R1, R2, ..., Rn seja uma decomposição de R. A dependência de junção *

(R1, R2, ..., Rn) é usada para restringir o conjunto de relações legais para aquelas para as quais R1, R2, ..., Rn é uma

decomposição sem perda na junção de R. Formalmente, se R= , dizemos que uma relação r(R)

satisfaz a dependência de junção * (R1, R2,..., Rn) se:

Uma dependência de junção é trivial se um dos Ri for o próprio R.

Considere a dependência de junção *(R1, R2) do esquema R. Essa dependência exige que, para toda

r(R)válida:

Seja r contendo duas tuplas t1 e t2, conforme segue:

Assim, t1[R1 R2]=t2[R1 R2], mas t1 e t2 têm diferentes valores em todos os outros atributos.

Computemos . A fig. 7.15 mostra e . Quando computamos a junção, temos duas

tuplas, além de t1 e t2, exigidas na fig. 7.16 por t3 e t4.

Se *(R1, R2) vale, então, sempre que tivermos as tuplas t1 e t2, devemos também ter t3 e t4. Assim, a fig.

7.16 mostra uma representação tabular da dependência de junção *(R1, R2). Compare a fig. 7.16 com a fig. 7.9, na

qual temos a representação tabular de α��β. Se tivermos e β=R1, podemos ver que as duas

representações tabulares nessas figuras são as mesmas. De fato, *(R1, R2) é apenas um outra forma de determinar

. Usando as regras da complementação e do incremento para dependências multivaloradas,

podemos mostrar que ��R1 implica ��R2. Assim, *(R1, R2) é equivalente a ��R2. Essa

observação não causa surpresa tendo em vista que, conforme observamos anteriormente, R1 e R2 formam uma

decomposição de R sem perda na junção se, e somente se, ��R2 ou ��R1.

194

Toda dependência de junção da forma *(R1, R2) é, portanto, equivalente a uma dependência

multivalorada. No entanto, há dependências de junção que não são equivalentes a nenhuma dependência

multivalorada. O exemplo mais simples desse tipo de dependência é o esquema R=(A, B, C). A dependência de

junção: *((A, B), (B, C), (A, C))

não é equivalente a nenhuma coleção de dependências multivaloradas. A fig. 7.17 mostra uma representação

tabular dessa dependência de junção. Para notar que nenhum conjunto de dependências multivaloradas

implicam logicamente *((A, B), (B, C), (A, C)), consideramos a fig. 7.17 como uma relação r(A, B, C), como mostra a

fig. 7.18.

A relação r satisfaz a dependência de junção *((A, B), (B, C), (A, C)), como podemos verificar computando:

e mostrando que o resultado é exatamente r. Entretanto, r não satisfaz qualquer dependência multivalorada não-

trivial. Para percebemos isso, verificamos que r não satisfaz nenhuma das A��B, A��C, B��A, B��C,

C��A, ou C��B.

195

Da mesma forma que a dependência multivalorada é um modo de estabelecer a independência de um

par de relacionamentos, uma dependência de junção é um modo de estabelecer que os membros de um conjunto

de relacionamentos são todos independentes. Essa noção de independência de relacionamentos é consequência

natural do modo pelo qual geralmente definimos uma relação. Considere:

esquema_info_empréstimo=(nome_agência, nome_cliente, número_empréstimo, total)

de nosso exemplo bancário. Podemos definir uma relação info_empréstimo (esquema_info_empréstimo) com o

conjunto de todas as tuplas do esquema_info_empréstimo) com o conjunto de todas as tuplas do

esquema_info_empréstimo, tal que:

• O empréstimo representado por número_empréstimo é feito pela agência de nome nome_agência.

• O empréstimo representado por número_empréstimo é feito pelo cliente chamado nome_cliente.

• O empréstimo representado por número_empréstimo está no total de nome total.

A definição anterior da relação info_empréstimo é uma conjugação de três predicados: um em

número_agência e nome_agência, um em número_empréstimo e nome_cliente e um em número_empréstimo e

total.

Supreendentemente, pode ser mostrado que a definição intuitiva anterior de info_empréstimo implica

logicamente a dependência de junção *((número_empréstimo, nome_agência), (número_empréstimo,

nome_cliente), (número_empréstimo, total)).

Assim, as dependências de junção têm um aspecto intuitivo e correspondem a um dos três critérios

apresentados para um bom projeto de banco de dados.

Para dependências funcionais e multivaloradas, temos de fornecer um sistema de regras de inferências

que devem ser válidas e completas. Infelizmente, nenhum conjunto de regras é conhecido para dependências de

junções. Esse fato nos leva a considerar classes mais genéricas de dependências que as dependências de junções

para construir um conjunto de regras de inferências sólido e completo.

Forma Normal de Projeção de Junção

Forma normal de projeção de junção (FNPJ) é definida de maneira similar a FNBC e a 4FN, exceto pelo

fato das dependências de junções serem usadas. Um esquema de relação r está em FNPJ com respeito a um

conjunto D de dependências funcionais, multivaloradas e de junção se, para todas as dependências de junções

em D+ na forma *(R1, R2, ..., Rn), em que cada Ri R e R = , ao menos uma das seguintes for

validade:

• *(R1, R2, ..., Rn) é uma dependência de junção trivial.

• Todo Ri é superchave para R.

Um projeto de banco de dados está na FNPJ se cada membro de um conjunto de esquemas de relações

que constituem o projeto estiver na FNPJ. A FNPJ é chamada quinta forma normal (5FN) em parte da literatura

sobre normalização de banco de dados.

Retornaremos ao nosso exemplo bancário. Dada a dependência de junção *((número_empréstimo,

nome_agência), (número_empréstimo, nome_cliente), (número_empréstimo, total)), esquema_info_empréstimo

não está na FNPJ. Para colocar esquema_info_empréstimo na FNPJ, precisamos decompô-la em três esquemas

específicos por meio da dependência de junção: (número_empréstimo, nome_agência), (número_empréstimo,

nome_cliente) e (número_empréstimo, total).

Como todas as dependências multivaloradas são também dependências de junção, é fácil perceber que

todo esquema na FNPJ está também na 4FN. Assim, em geral, não precisamos chegar à decomposição com

preservação de dependência na FNPJ para um dado esquema.

Forma Normal Domínio-chave

196

A abordagem que utilizamos para a normalização toma por base a definição de restrições (funcional,

multivalorada ou dependência de junção) e então usa essa restrição para definir a forma normal. A forma normal

domínio-chave (FNDC) baseia-se em três noções:

1. Declaração de domínio. Seja A um atributo e dom um conjunto de valores. A declaração de domínio A

dom exige que o valor de A em todas as tuplas seja um valor em dom.

2. Declaração de chaves. Seja R um esquema de relação com K R. A declaração da chave key(K)

necessita que K seja uma superchave do esquema R – isto é, K�R. Note que todas as declarações de

chaves são dependências funcionais, mas nem todas as dependências funcionais são declarações de

chaves.

3. Restrições gerais. Uma restrição geral é um predicado do conjunto de todas as relações de um dado

esquema. As dependências que estudamos nesse capítulo são exemplos de restrições gerais.

Normalmente, uma restrição geral é um predicado expresso em uma forma agregada, como a lógica de

primeira ordem.

Damos agora um exemplo de uma restrição geral que não é funcional, multivalorada ou dependente de

junção. Suponha que todas as contas que comecem com o dígito 9 sejam contas especiais com altas taxas de

juros, cujo saldo mínimo é 2500 dólares. Então incluímos como restrição geral: “se o primeiro dígito de

t[número_conta] for 9, então t[saldo]≥2500”.

Declarações de domínio e declarações de chave são fáceis de testar em um sistema de banco de dados

prático. Restrições gerais, entretanto, podem ser extremamente custosas (em termos de tempo de

processamento e espaço) para testar. O proposito de um projeto de banco de dados na FNPJ é permitir-nos o

teste de restrições gerais usando somente restrições de domínio e chaves.

Formalmente, seja D um conjunto de restrições de domínio e K, um conjunto de restrições por chaves

para o esquema de relação R. Suponha que G represente as restrições gerais de R. O esquema R está na FNDC se

D K implica logicamente G.

Retornemos às restrições gerais que demos para as contas. As restrições implicam que nosso projeto de

banco de dados não está na FNDC. Para criar um projeto na FNDC, precisamos de dois esquemas no lugar do

esquema_conta:

esquema_conta_regular=(nome_agência, número_conta, saldo)

esquema_conta_especial=(nome_agência, número_conta, saldo)

Conservamos, como restrições gerais, todas as dependências que tínhamos no esquema_conta. As

restrições de domínio para esquema_conta_especial exigem que, para cada conta:

• O número da conta comece por 9.

• O saldo da conta seja maior que 2500.

As restrições de domínio para esquema_conta_regular exigem que o número da conta não comece por 9.

O projeto resultante está na FNDC.

Comparemos a FNDC com outras formas normais estudadas. Nas outras formas normais, não levamos em

consideração restrições de domínio. Assumimos (de modo implícito) que o domínio de cada atributo possui

domínio infinito, como o conjunto de todos os inteiros ou conjunto de todas as cadeias de caracteres. Permitimos

restrições por chaves (de fato, permitimos dependências funcionais).

Para cada forma normal, permitimos uma forma específica de restrição geral (um conjunto de

dependências funcionais, multivaloradas ou de junção). Assim, podemos reescrever as definições de FNPJ, 4FN,

FNBC e 3FN de modo a mostra-las como casos especiais de FNDC.

A seguir reescreveremos nossa definição de FNPJ inspirada na FNDC. Seja o esquema da relação R = (A1,

A2, ..., An). Seja dom(Ai) o domínio do atributo Ai e sejam infinitos esses domínios. Então, todas as restrições de

domínio D são da forma Ai dom(Ai). Sejam as restrições gerais um conjunto G de dependências funcionais

197

multivaloradas ou de junção. Se F é um conjunto de dependências funcionais em G, seja o conjunto K de

restrições por chave aquelas dependências funcionais não triviais em F+ da forma α�R. O esquema R está na

FNPJ se e somente se ele estiver na FNDC com respeito a D, K e G.

Uma consequência da FNDC é que toda inserção e remoção anômala é eliminada.

A FNDC constitui a última forma normal porque permite restrições arbitrárias, em vez de dependências, e

permite ainda testes eficientes para essas restrições. Naturalmente, se um esquema não está na FNDC, podemos

alcançá-la por meio de decomposições, mas tais decomposições, como já foi visto, não são sempre

decomposições com preservação da dependência. Assim, embora a FNDC seja a meta de um projetista de banco

de dados, poderá ser sacrificada em projetos reais.

Abordagens Alternativas para Projeto de Banco de Dados

Vamos reexaminar a normalização de esquemas de relação com ênfase nos efeitos dessa normalização no

projeto de um banco de dados real.

Adotamos como abordagem começar com um único esquema de relação e, então, decompô-lo. Uma de

nossas metas é escolher uma decomposição que resulte em decomposição sem perda na junção. Para considerar

essa ausência de perda na junção, assumimos ser válido falar em junção de todas as relações de um banco de

dados decomposto.

Considere o banco de dados da fig. 7.19, representamos a relação info_empréstimo decomposta na FNPJ.

Na fig. 7.19, representamos uma situação na qual ainda não determinamos o total do empréstimo L-58, mas

desejamos registrar a existência do dado no empréstimo. Se computarmos uma junção natural dessas relações,

notaremos que todas as tuplas referentes ao empréstimo L-58 desaparecerão. Em outras palavras, não há

nenhuma relação info_empréstimo correspondente às relações da fig. 7.19. Tuplas que desaparecem quando

computamos a junção são tuplas pendentes. Formalmente, seja r1(R1), r2(R2), ..., rn(Rn) um conjunto de relações.

Uma tupla t de uma relação ri é uma tupla pendente se t não está na relação:

As tuplas pendentes podem ocorrer em aplicações de banco de dados reais. Representam informações

incompletas, como ocorreu em nosso exemplo quando desejamos armazenar dados sobre um empréstimo que

estava ainda em processo de negociação. A relação r1 r2 ... rn é chamada relação universal, uma vez que

envolve todos os atributos do universo definido por R1 R2 ... Rn.

O único modo por meio do qual podemos escrever uma relação universal para o exemplo da fig. 7.19 é

incluindo valores nulos na relação universal. Vimos que valores nulos originam sérias dificuldades. Pesquisas a

respeito de valores nulos e relações são discutidos em notas bibliográficas. Devido à dificuldade de manuseio de

valores nulos, pode ser mais adequado tratar as relações de um projeto decomposto como a representação do

banco de dados, em vez das relações universais cujos esquemas foram decompostos durante o processo de

normalização.

198

Note que não devemos introduzir informações incompletas no banco de dados da fig. 7.19 sem recorrer

ao uso de valores nulos. Por exemplo, não podemos introduzir um número de empréstimo se não conhecermos

ao menos uma das seguintes informações:

• O nome do cliente.

• O nome da agência.

• O total do empréstimo.

Assim, uma decomposição em particular define uma forma restritiva para uma informação incompleta

que é aceitável em nosso banco de dados.

A forma normal que definimos gera bons projetos de banco de dados do ponto de vista da representação

de informações incompletas. Retornando novamente ao exemplo da fig. 7.19, poderíamos desejar não permitir o

armazenamento do seguinte fato: “há empréstimos (cujo número é desconhecido) para Jones cujo montante é

cem dólares”. Uma vez que

número_empréstimo � nome_cliente total,

o único modo de podermos relacionar nome_cliente e total é por meio de número_empréstimo. Se desejarmos

saber o número do empréstimo, não poderemos diferenciar esse empréstimo de outros cujos números são

desconhecidos.

Em outras palavras, não conseguimos armazenar dados cujos atributos-chave sejam desconhecidos.

Observe que as formas normais que definimos não nos permitem armazenar esse tipo de informação sem a

utilização de valores nulos. Assim, nossas formas normais permitem a representação de informações incompletas

não desejáveis.

Se permitimos tuplas suspensas em nosso banco de dados, podemos preferir uma visão alternativa do

processo de projeto de banco de dados. Em vez de decompor uma relação universal, podemos sintetizar uma

coleção de esquemas na forma normal de um determinado conjunto de atributos. Estamos interessados nas

mesmas formas normais, independente de usar decomposição ou síntese. A abordagem da decomposição é

melhor entendida e mais largamente utilizada.

Outra consequência da abordagem usada no projeto de um banco de dados é que os nomes dos atributos

devem ser únicos nas relações universais. Não podemos usar nome para referência tanto para nome_cliente

quanto para nome_agência. Geralmente, é preferível usar nomes únicos, como vínhamos fazendo. Ainda assim,

se definíssemos nossos esquemas de relações diretamente, em vez de usarmos relações universais, obteríamos

relações com esquema tais como os que se seguem para nosso exemplo bancário:

agência_empréstimo (nome, número)

cliente_empréstimo (número, nome)

tot (número, total)

Observe que, com as relações anteriores, expressões como agência_empréstimo cliente_empréstimo

não têm sentido. Na verdade, a expressão, agência_empréstimo cliente_empréstimo aponta os empréstimos

feitos para clientes cujos nomes sejam os mesmos do nome da agência.

Em linguagens como SQL, entretanto, há operações de junção não-naturais, então, em uma consulta

envolvendo agência_empréstimo e cliente_empréstimo, precisamos de referências não ambíguas para nome

usando, para isso, o nome da relação como prefixo. Nesses ambientes, os diversos papeis de nome (como nome

da agência e nome do cliente), são menos problemáticos e provavelmente de utilização mais simples.

Acreditamos que usar o critério do papel único – cada nome de atributo tem um único significado no

banco de dados – é geralmente preferível que a utilização de um mesmo nome em diversos papeis. Quando o

critério do papel único não é adotado, o projetista do banco de dados deve ser cuidadoso durante a construção

de um projeto de banco de dados relacional normalizado.

199

SQL 2003

Durante o desenvolvimento do sistema R, a IBM desenvolveu a linguagem SEQUEL, primeira linguagem de

acesso aos SGBD´s relacionais. Com o desenvolvimento de um número cada vez maior de SGBD´s, fez-se

necessário especificar um padrão da linguagem, chamada SQL, em 1986. Esse lançamento foi um esforço

particular da ANSI em conjunto com a ISO. A padronização de uma linguagem de consulta foi responsável pelo

sucesso dos SGBD´s relacionais.

Em linhas gerais, os comandos da linguagem SQL podem ser divididos em sublinguagens, tais como a

Linguagem de Manipulação de Dados (DML) e a Linguagem de Definição de Dados (DDL). A DML trata dos

comandos ligados à manipulação de Dados, definindo comandos para a seleção, inclusão, alteração e exclusão

dos dados das tabelas. Já a DDL, reúne os comandos para a criação e manutenção de estruturas e objetos do

banco de dados, tais como tabelas, visões e índices.

Os fornecedores de Gerenciadores de Banco de Dados não hesitaram em adotar a SQL. Entretanto,

criaram variações próprias da linguagem, adicionando funções ou comandos que, muitas vezes, em virtude de seu

sucesso, acabaram se incorporando em versões posteriores do padrão. Contudo, a maior parte do padrão é

implementado de forma idêntica nos principais SGBD´s, possibilitando a portabilidade de aplicações e maior

facilidade para os conhecedores da linguagem.

A linguagem passou por aperfeiçoamento em 1989 e, em 1992, foi lançada a SQL-92, ou SQL2. Embora

muitos dos conceitos especificados na SQL-92 somente tenham sido implementados por SGBD´s relacionais

cresceu rapidamente.

A SQL:2003 é a mais nova versão do padrão SQL. Nesta versão foi feita uma grande revisão do padrão

SQL3 e adicionada uma nova parte, ligada ao tratamento de XML.

Tipos de Dados

Em banco de dados relacionais e relacionais estendidos, as informações ficam armazenadas em tabelas.

As tabelas são as materializações das relações do modelo relacional. Elas têm linhas e colunas, onde cada linha

representa a instância de um item armazenado e cada coluna uma informação relativa ao item em questão. A

cada coluna de uma tabela está associado um tipo de dados, fazendo com que somente dados do tipo adequado

sejam armazenados na coluna.

A SQL define alguns tipos de dados que são implementados em alguns SGBD´s. Ele define, também, os

comandos para criação, alteração e exclusão de tabelas.

Tipos de dados básicos

Em banco de dados relacionais e relacionais-estendidos, as informações são armazenadas em tabelas.

Cada tabela poderá conter várias colunas, as quais estão armazenados dados. A cada coluna existirá um tipo de

dados associado.

O tipo de dados de cada coluna é definido durante a criação da tabela. O padrão de SQL define vários

tipos de dados simples. Permite, também, que o usuário defina tipos de dados próprios, a partir da composição

de tipos definidos pela linguagem.

A tabela 2.1 apresenta os principais tipos de dados definidos no padrão da linguagem SQL. A partir desta,

podemos dividir os tipos de dados em cinco grupos: (i) relativos a cadeias de caracteres; (ii) relativos a dados

numéricos; (iii) para armazenamento de “objetos grandes”; (iv) para armazenamento de informação booleana e

(v) tipos de dados relativos a datas e horas.

200

Os vários fornecedores de SGBD´s utilizam variações próprias de dados definidos no SQL:2003.

Criando Tabelas

Uma tabela é a materialização de um local para armazenamento de dados. Tais dados são agrupados em

linhas. Cada linha contém um conjunto de uma ou mais colunas. Todas as linhas têm o mesmo número de

colunas. Cada coluna possui seu tipo de dados próprio, que é o mesmo para todas as linhas.

Ao criarmos uma tabela, devemos especificar o seu nome, quais colunas que a compõe e quais os tipos de

dados das colunas em questão. Além disso, podem ser definidas restrições de integridade e regras de domínio,

entre outros. Assim, podemos especificar, durante a criação de uma tabela, por exemplo, que uma coluna refere-

se à chave primária da tabela ou que uma dada coluna somente poderá conter determinados valores.

Quando uma tabela é criada, ela não contém dados, ou seja, linhas. Somente depois os dados são

inseridos. Entretanto, algumas colunas da tabela podem não ter o seu preenchimento obrigatório. Para estas,

quando nenhum dado for fornecido durante a inclusão, é incluído o valor NULL (nulo).

A princípio, todas as colunas, independente de seus tipos de dados, podem apresentar o valor NULL no

seu conteúdo, em uma ou mais linhas. Ou seja, uma coluna definida como de tipo INTEGER pode suportar

números inteiros ou o valor NULL. Da mesma forma, uma coluna CHAR suporta cadeia de caracteres ou o valor

NULL. Notamos que NULL indica ausência de valor definido. É diferente de zero ou de uma cadeia de caracteres

de comprimento zero. Ao criarmos uma tabela, podemos especificar que uma ou mais colunas não podem conter

o valor NULL. Ou seja, tais colunas têm o seu preenchimento obrigatório. Quando nada é informado para uma

coluna, ela aceita NULL no seu conteúdo.

Utilizamos o comando CREATE TABLE para criação de tabelas. A sintaxe básica do comando é a seguinte:

CREATE TABLE NOME_TABELA(

COL1 TIPO_COL1 [NOT NULL],

COL2 TIPO_COL2 [NOT NULL],

COLN TIPO_COLN [NOT NULL]

201

)

Exemplo de criação de tabela no Oracle 10g:

CREATE TABLE EDITORA (

CODIGO NUMBER(2) NOT NULL,

NOME VARCHAR2(80) NOT NULL

)

Podemos, na criação de tabelas, especificar vários tipos de restrições. Para uma dada coluna, a SQL:2003

nos permite que restrições sejam especificadas após o nome do tipo de dados da coluna em questão. As

principais restrições são:

• Chave primária: devemos posicionar a expressão PRIMARY KEY ao lado da definição do tipo de dados da coluna em questão.

• Chave estrangeira: posicionamos a expressão FOREIGN KEY REFERENCES NOME_TABELA ao lado da definição do tipo da coluna. FOREIGN KEY é opcional. NOME_TABELA deve ser substituído pelo nome da tabela que é referenciada pela chave estrangeira. A tabela referenciada pela chave estrangeira deve ser criada antes da criação da chave estrangeira.

• Chave alternada: a expressão UNIQUE deve ser usada ao lado da definição do tipo de dados da coluna. UNIQUE faz com que não seja possível inserir valores repetidos na coluna.

• Restrição de domínio: para verificar que o valor de uma coluna deve estar contido em uma lista ou faixa de valores, utilizamos a palavra CHECK, seguida de uma expressão booleana delimitada por parênteses.

CONSTRAINT NOME_RESTRICAO TIPO_RESTRICAO

Onde:

• Constraint – indica a definição de uma restrição de integridade.

• Nome_Restricao – nome dada a restrição (CONSTRAINT) que está sendo criada.

• Tipo_Restricao – tipo da restrição que está sendo criada: PRIMARY KEY, FOREIGN KEY OU UNIQUE, por exemplo. No caso de uma chave estrangeira, FOREIGN KEY é opcional e o comando deve ser complementado com a cláusula REFERENCES.

A designação de um nome para cada restrição é bastante útil para o controle de erros que porventura

ocorram. Suponha que, por exemplo, durante a execução de um programa que acessa um banco de dados, tente-

se incluir um valor repetido em linhas de uma coluna marcada como chave primária de uma tabela. Neste caso, o

SGBD não permite operação e um erro ocorre. O SGBD informa, na mensagem de erro, o nome da restrição que

foi violada. Isto permite que a aplicação trate o erro corretamente ou, ainda que seja mais facilmente detectado o

ponto onde a aplicação deve ser alterada de forma a não permitir que tal situação ocorra. Por isso, é interessante

que o nome da CONSTRAINT indique o tipo de restrição, a tabela a que se refere e, quando possível, a coluna que

sofre a restrição.

Consideremos a coluna CPF da tabela AUTOR. Esta coluna não deve aceitar valores repetidos. Para isso,

utilizamos a seguinte definição para a coluna:

CPF CHAR(11) NOT NULL UNIQUE

Por exemplo, para criar a tabela EDITORA no banco de dados Oracle 10g:

CREATE TABLE EDITORA (

CODIGO NUMBER(2) NOT NULL PRIMARY KEY,

NOME VARCHAR2 (80) NOT NULL )

Para criar a tabela ASSUNTO, definindo PK_ASSUNTO como nome para a CONSTRAINT chave primária, utilizamos,

no Oracle 10g:

CREATE TABLE ASSUNTO(

SIGLA CHAR(1) NOT NULL

CONSTRAINT PK_ASSUNTO PRIMARY KEY,

DESCRICAO VARCHAR2(50)

202

)

Vejamos agora, um exemplo da definição de chave estrangeira, a partir da criação da tabela LIVRO.

Atribuiremos o nome PK_LIVRO para a restrição de chave primária, FK_LIVRO_ASSUNTO para a restrição de chave

estrangeira da coluna que referencia a tabela ASSUNTO e FK_LIVRO_EDITORA para a restrição de chave

estrangeira da coluna que referencia a tabela EDITORA.

CREATE TABLE LIVRO(

CODIGO NUMBER(3) NOT NULL

CONSTRAINT PK_LIVRO PRIMARY KEY,

TITULO VARCHAR2(80) NOT NULL,

PRECO NUMBER(10,2),

LANCAMENTO DATE,

ASSUNTO CHAR(1)

CONSTRAINT FK_LIVRO_ASSUNTO

REFERENCES ASSUNTO,

EDITORA NUMBER(2)

CONSTRAINT FK_LIVRO_EDITORA

REFERENCES EDITORA

)

Restrições de domínio podem ser implementadas através da cláusula CHECK, seguida da expressão

booleana delimitada por parênteses. Para a montagem da expressão booleana podemos utilizar operadores de

comparação (<,>,>=,<=, =, <>) e predicados como LIKE e IN.

Exemplos:

• Para definirmos a coluna SIGLA, que não admite valores nulos, somente possa aceitar os valores “R”, “B” e “F”:

SIGLA CHAR(1) NOT NULL CHECK (SIGLA IN(‘R’, ‘B’, ‘F’))

• Para definirmos que a coluna MATRICULA somente aceitará valores maiores que 1000: MATRICULA INTEGER CHECK (MATRICULA > 1000)

Podemos, também, atribuir um valor padrão para as colunas de uma tabela. O valor padrão será

automaticamente atribuído à coluna durante a criação de uma linha, caso nenhum valor tenha sido fornecido.

Sua definição dá-se no momento da criação da tabela através da cláusula DEFAULT, conforme apresentado a

seguir:

COL1 TIPO_COLUNA DEFAULT VALOR_PADRAO

Por exemplo: SEXO CHAR(1) DEFAULT ‘M’

A utilização de restrições de integridade no banco de dados pode ser bastante útil para a manutenção da

correção das informações. A definição de chaves primárias e restrições UNIQUE impedem que sejam incluídos

valores repetidos em uma dada coluna. A definição de chaves estrangeiras faz com que somente sejam

permitidos valores que existam na tabela referenciada.

A tabela 2.6 apresenta um conteúdo da tabela EDITORA:

203

Na tabela LIVRO que criamos anteriormente, especificamos a coluna EDITORA como chave estrangeira

para a tabela EDITORA. Dessa forma, se considerarmos o conteúdo da tabela EDITORA apresentado na tabela

acima, então a coluna EDITORA da tabela LVIRO somente poderá conter os valores NULL, 1, 2, 3 e 4. Se tentarmos

incluir quaisquer outros valores, o SGBD gerará um erro e não permitirá a inclusão das informações.

Considere, agora, que existe uma linha na tabela LIVRO onde a coluna EDITORA possua o valor 2.

Consideremos que um usuário tente excluir, da tabela EDITORA, a linha de código 2. Se isto for possível, teremos

uma inconsistência nos dados, pois existirá um valor na chave estrangeira da tabela LIVRO que não existe na

chave primária da tabela EDITORA. De acordo com o padrão SQL é possível especificar, durante a definição da

restrição, três diferentes ações a serem tomadas pelo SGBD, quando tal situação ocorrer:

• Impedir a exclusão da linha da tabela pai (EDITORA) caso existam outras tabelas que referenciem o valor a ser excluído. A linha não é excluída e um erro é gerado. Esta é a situação padrão, a qual ocorrerá sempre que uma chave estrangeira for definida, a menos que declaremos o contrário. É especificada atraves da opção ON DELETE RESTRICT; ASSUNTO CHAR(1)

CONSTRAINT FK_LIVRO_ASSUNTO

REFERENCES ASSUNTO

ON DELETE RESTRICT

• Alterar o valor da coluna da chave estrangeira na tabela filho (LIVRO), tornando-o NULL para as linhas que possuam o valor que está sendo apagado na tabela pai. A linha da tabela pai também é apagada. Esta ação é especificada atraves da cláusula ON DELETE SET NULL; ASSUNTO CHAR(1)

CONSTRAINT FK_LIVRO_ASSUNTO

REFERENCES ASSUNTO

ON DELETE SET NULL

• Apagar as linhas da tabela filho onde existir, na coluna de chave estrangeira, o valor que está sendo apagado na tabela pai. A linha da tabela pai também é apagada. Esta ação é definida atraves da cláusula ON DELETE CASCADE. ASSUNTO CHAR(1)

CONSTRAINT FK_LIVRO_ASSUNTO

REFERENCES ASSUNTO

ON DELETE CASCADE

Existe, ainda, um outro formato para a especificação de restrições em tabelas. Podemos especificar

restrições após a declaração de todas as colunas. Neste caso, deveremos seguir o formato:

CONSTRAINT NOME_RESTRICAO TIPO_RESTRICAO

(COLUNA1_RESTRICAO, COLUNA2_RESTRICAO,...)

204

Eis a sintaxe do Oracle 10g:

CREATE TABLE LIVRO (

CODIGO NUMBER(3),

TITULO VARCHAR2(80) NOT NULL,

PRECO NUMBER(10,2),

LANCAMENTO DATE,

ASSUNTO CHAR(1),

EDITORA NUMBER(2)

CONSTRAINT PK_LIVRO PRIMARY KEY (CODIGO),

CONSTRAINT FK_LIVRO_ASSUNTO FOREIGN KEY (ASSUNTO)

REFERENCES ASSUNTO,

CONSTRAINT FK_LIVRO_EDITORA

FOREIGN KEY (EDITORA)

REFERENCES EDITORA

)

Alterando tabelas

A SQL:2003 nos dá a opção de alterar tabelas já existentes no banco de dados. Para isso, utilizamos o

comando ALTER TABLE. Este comando pode ser utilizado em conjunto com as cláusulas adicionais para executar,

entre outras, uma das seguintes opções:

• Incluir novas colunas em uma tabela existente;

• Excluir colunas existentes em uma tabela;

• Adicionar a definição de uma restrição a uma tabela existente;

• Excluir a definição de uma restrição existente em uma tabela.

Incluindo novas colunas em uma tabela já existente

Para incluir novas colunas em uma tabela, utilizamos o comando ALTER TABLE, conforme a seguir:

ALTER TABLE NOME_TABELA

ADD [COLUMN] NOME_COLUNA TIPO_COLUNA RESTRICOES

Por exemplo, vamos adicionar a coluna IDENTIDADE à tabela AUTOR, já criada no banco de dados.

ALTER TABLE AUTOR

ADD IDENTIDADE CHAR(10)

Excluindo colunas existentes em uma tabela

Para excluir colunas existentes em uma tabela, utilizamos o comando ALTER TABLE no formato

apresentado a seguir:

ALTER TABLE NOME_TABELA

DROP [COLUMN] NOME_COLUNA

Por exemplo, vamos remover a coluna IDENTIDADE da tabela AUTOR, já criada no banco de dados:

ALTER TABLE AUTOR

DROP COLUMN IDENTIDADE

Adicionando a definição de uma restrição a uma tabela existente

ALTER TABLE NOME_TABELA

ADD CONSTRAINT NOME_RESTRICAO TIPO_RESTRICAO

(COLUNA1_RESTRICAO, COLUNA2_RESTRICAO, ...)

205

Por exemplo, suponha que tenhamos criado a tabela LIVRO sem especificar nenhuma restrição. Vamos

agora adicionar essas restrições atraves de três comandos:

Adicionando a chave primária:

ALTER TABLE LIVRO

ADD CONSTRAINT PK_LIVRO PRIMARY KEY (CODIGO)

Adicionando a chave estrangeira a tabela ASSUNTO:

ALTER TABLE LIVRO

ADD CONSTRAINT FK_LIVRO_ASSUNTO FOREIGN KEY (ASSUNTO)

REFERENCES ASSUNTO

Adicionando a chave estrangeira para a tabela EDITORA:

ALTER TABLE LIVRO

ADD CONSTRAINT FK_LIVRO_EDITORA

FOREIGN KEY (EDITORA)

REFERENCES EDITORA

Excluindo a definição de uma restrição existente em uma tabela

Alterando tabelas

Podemos também alterar tabelas já definidas no banco de dados. Para isso utilizamos o comando ALTER

TABLE. Este comando pode ser utilizado em conjunto com as cláusulas adicionais para executar, entre outras,

uma das seguintes opções:

• Incluir novas colunas em uma tabela existente;

• Excluir colunas existentes em uma tabela;

• Adicionar a definição de uma restrição em uma tabela já existente;

• Excluir a definição de uma restrição existente em uma tabela.

Incluir novas colunas em uma tabela existente

Para incluir novas colunas em uma tabela existente, utilizamos o comando ALTER TABLE conforme a

seguir:

ALTER TABLE NOME_TABELA

ADD [COLUMN] NOME_COLUNA TIPO_COLUNA RESTRICOES

Por exemplo: vamos adicionar a coluna IDENTIDADE à tabela AUTOR, já criada no banco de dados.

ALTER TABLE AUTOR

ADD IDENTIDADE CHAR(10)

Excluindo colunas existentes em uma tabela, utilizamos o comando ALTER TABLE no formato apresentado a

seguir:

ALTER TABLE NOME_TABELA

DROP [COLUMN] NOME_COLUNA

Exemplo, vamos remover a coluna IDENTIDADE da tabela AUTOR, já criada no banco de dados:

ALTER TABLE AUTOR

DROP COLUMN IDENTIDADE

Adicionando a definição de uma restrição a uma tabela existente

Para incluir uma nova restrição a uma tabela existente, também utilizamos o comando ALTER TABLE. A

sintaxe básica utilizada é:

206

ALTER TABLE NOME_TABELA

ADD CONSTRAINT NOME_RESTRICAO TIPO_RESTRICAO

(COLUNA1_RESTRICAO, COLUNA2_RESTRICAO, ...)

Por exemplo: Suponha que tenhamos criado a tabela LIVRO sem especificar nenhuma restrição. Vamos,

agora, adicionar as restrições a essa tabela, através de três comandos.

Adicionando a chave primária:

ALTER TABLE LIVRO

ADD CONSTRAINT PK_LIVRO KEY(CODIGO)

Adicionando a chave estrangeira para a tabela ASSUNTO:

ALTER TABLE LIVRO

ADD CONSTRAINT FK_LIVRO_ASSUNTO FOREIGN KEY(ASSUNTO)

REFERENCES ASSUNTO

Adicionando a chave estrangeira para a tabela EDITORA:

ALTER TABLE EDITORA

ADD CONSTRAINT FK_LIVRO_EDITORA

FOREIGN KEY (EDITORA)

REFERENCES EDITORA

Excluindo a definição de uma restrição existente em uma tabela

O comando ALTER TABLE também pode ser utilizado para excluir uma restrição já existente em uma

tabela. Para isso, utilizamos o seguinte formato:

ALTER TABLE NOME_TABELA

DROP CONSTRAINT NOME_RESTRICAO

Por exemplo, desejamos excluir a chave primária da tabela LIVRO:

ALTER TABLE LIVRO

DROP CONSTRAINT PK_LIVRO

Desejamos destruir a chave estrangeira da tabela LIVRO que aponta para a tabela EDITORA.

ALTER TABLE LIVRO

DROP CONSTRAINT FK_LIVRO_EDITORA

Destruindo tabelas

Para destruir uma tabela utilizamos o comando DROP TABLE. Sua sintaxe básica é:

DROP NOME NOME_TABELA [CASCADE]

Exemplo: Para destruir a tabela LIVRO.

DROP TABLE LIVRO

207

Comandos Básicos

Inclusão de Dados

Para incluirmos dados em uma tabela, devemos utilizar o comando INSERT. Sua sintaxe básica, segundo a

qual inserimos uma linha em uma tabela é mostrada a seguir:

INSERT INTO NOME_TABELA (COL1, COL2, ..., COLN)

VALUES (VAL1, VAL2, ..., VALN)

Considere a tabela LIVRO já apresentada anteriormente. A tabela 3.1 apresenta um exemplo de instância

da tabela LIVRO apresentado. Notamos que existe um livro com valor nulo para o campo LANCAMENTO. Isso

significa que ele ainda não foi lançado.

A descrição completa dos assuntos está na tabela ASSUNTO. Nesta existem vários assuntos cadastrados,

independentemente se existe um livro do assunto em questão. Tomemos a tabela 3.2 como exemplo de instância

da tabela EDITORA.

Para concluirmos as três primeiras linhas na tabela LIVRO, devemos realizar os seguintes comandos

INSERT:

INSERT INTO LIVRO

(CODIGO, TITULO, PRECO, LANCAMENTO, ASSUNTO, EDITORA) VALUES

(1, ‘BANCO DE DADOS PARA WEB’, 31.2, ‘10/01/1999’, ‘B’, 1)

INSERT INTO LIVRO

(TITULO, CODIGO, LANCAMENTO, PRECO, ASSUNTO, EDITORA) VALUES

(‘PROGRAMANDO EM LINUGAGEM C’, 2, ‘01/10/1997’, 30, ‘P’, 1)

Notamos que a diferença dos dois primeiros comandos anteriores está na diferença da ordem. Quando

especificamos o nome não há uma limitação na ordenação das mesmas.

A especificação dos nomes das colunas permite, ainda, que não sejam inseridos dados para todas as

colunas de uma tabela. Considere o caso do livro de código ‘4’, que não possui data de lançamento. Ao inseri-lo,

podemos omitir a coluna LANCAMENTO do comando INSERT a ser utilizado:

INSERT INTO LIVRO (CODIGO, TITULO, PRECO, ASSUNTO, EDITORA) VALUES

(4, ‘BANCO DE DADOS PARA INFORMATICA’, 48, ‘B’, 2)

208

Neste caso, será inserido o valor NULL para LANCAMENTO. Note que, se essa coluna possuir uma

restrição para não aceitar valores nulos, e não estiver definido um valor padrão, o comando não poderá ser

executado.

Outra forma de executar o comando anterior, especificando todas as colunas da tabela, é através da

utilização da palavra NULL:

INSERT INTO LIVRO

(CODIGO, TITULO, PRECO, LANCAMENTO, ASSUNTO, EDITORA) VALUES

(4, ‘BANCO DE DADOS PARA BIOINFORMATICA’, 48, NULL, ‘B’, 21)

Caso estejamos inserindo valores para todas as colunas da tabela, podemos omitir seus nomes.

Entretanto, nesse caso, devemos especificar os valores a serem inseridos na mesma ordem em que as colunas da

tabela foram criadas:

Errado:

INSERT INTO LIVRO VALUES

(‘R’, 42, 5, ‘01/09/1996’, ‘REDES DE COMPUTADORES’, 2)

Correto:

INSERT INTO LIVRO

(CODIGO, TITULO, PRECO, LANCAMENTO, ASSUNTO, EDITORA) VALUES

(5, ‘REDES DE COMPUTADORES’, 42, ‘01/09/1996’, ‘R’, 2)

Quando incluímos dados em tabelas que possuem chaves estrangeiras referenciando outras tabelas, os

valores que estão sendo inseridos nas colunas da chave estrangeira já devem constar na chave primária da tabela

referenciada.

Consulta simples

A consulta simples a dados armazenados é, usualmente, a operação realizada com mais freqüência em

sistemas comerciais. À medida que a quantidade de linhas em tabelas cresce e que utilizamos varias tabelas em

uma mesma consulta, não só a complexidade do comando SQL aumenta, como também o tempo de resposta da

consulta pode ser muito alto, exigindo, assim, mais atenção na montagem do comando.

Para a realização de consulta ao banco de dados, utilizamos o comando SELECT. A sintaxe básica do

comando SELECT é:

SELECT COL1, COL2, ..., COLN

FROM NOME_TABELA

Considere a instância da tabela LIVRO apresentado anteriromente. Se quisermos recuperar as colunas

TITULO e CODIGO dessa tabela, devemos executar o comando:

SELECT CODIGO, TITULO

FROM LIVRO

Se quisermos recuperar todas as colunas da tabela LIVRO podemos especificá-la no comando SELECT ou

utilizar o caractere ‘*’.

As consultas:

SELECT CODIGO, TITULO, PRECO, LANCAMENTO, ASSUNTO, EDITORA

FROM LIVRO

e

SELECT * FROM LIVRO

209

Quando especificamos as colunas no comando SELECT, elas serão apresentadas no resultado, na ordem

especificada. Quando o caractere ‘*’ é utilizado, as colunas estarão ordenadas da mesma forma que o foram na

criação da tabela.

A cláusula WHERE

Os resultados de comandos como apresentado anteriormente possuem todas as linhas da tabela. No

entanto, na maioria das consultas queremos consultar apenas as informações referentes a algumas linhas. Essas

linhas devem atender a alguma condição.

A cláusula WHERE permite que sejam especificadas linhas sobre as quais será aplicada. A instrução pode

ser um comando SELECT ou comandos de atualização e exclusão de dados.

WHERE é sempre usada com expressões lógicas, a qual pode ser operadores de comparação (>,<,>=,<=, =,

<>), operadores lógicos (AND, OR e NOT) e predicados próprios de linguagem SQL, tais como IS(NOT) NULL, IS

(NOT) LIKE, IN e EXISTS. Os operadores lógicos AND e OR são usados para conectar comparações.

A expressão lógica terá um resultado que poderá assumir os valores verdadeiro ou falso. A instrução

especificada será executada para as linhas que tornarem o resultado da expressão lógica verdadeiro.

Por exemplo:

• Preço superior a R$ 50,00 WHERE PRECO > 50

• Preço igual a R$ 50,00 e de assunto ‘p’ WHERE PRECO > 50 AND ASSUNTO = ‘P’

• Preço inferior a R$ 50,00 ou de assunto ‘P’ WHERE PRECO < 50 OR ASSUNTO = ‘P’

• Lançamento é nulo WHERE LANCAMENTO IS NULL

Quando realizamos comparações com colunas de tipos cadeias de caracteres, frequentemente queremos

encontrar valores que possuem determinados caracteres ou sequencias de caracteres. Para tal, usamos o

predicado LIKE em conjunto com um ou mais caracteres coringa.

O caractere coringa é usado para substituir um ou mais caracteres que não conhecemos. Os caracteres

coringa são ‘%’ e ‘_’. O caractere % é utilizado para substituir uma cadeia ilimitada de caracteres onde ele é

posicionado, enquanto _ substitui zero ou apenas um caractere.

Por exemplo: WHERE TITULO IS LIKE ‘%BANCO_ DE DADOS%’

Atualização de informações

Os dados inseridos em tabelas do banco de dados podem ser modificados. Para tal, utiliza-se o comando

UPDATE. Sua sintaxe é:

UPDATE NOME_TABELA

SET COL1 = VAL1, COL2 = VAL2, ..., COLN = VALN

WHERE EXPRESSAO_LOGICA

Considerando a tabela LIVROS, temos os seguintes exemplos:

• Atualizar o preço de todos os livros fornecendo um aumento de 10%. UPDATE LIVRO

SET PRECO = PRECO * 1.1

• Atualizar o preço do livro Programando em Linguagem C para R$ 32,00. UPDATE LIVRO

SET PRECO = 32

WHERE TITULO = ‘PROGRAMANDO EM LINGUAGEM C’

210

• Alterar o titulo e o preço do livro de código 2 para Programação em Linguagem C e R$ 42,00, respectivamente.

UPDATE LIVRO

SET PRECO = 42,

TITULO = ‘PROGRAMACAO EM LINGUAGEM C’

WHERE CODIGO = 2

Quando atualizamos os dados de uma ou mais colunas marcadas como chaves estrangeiras, os novos

valores já devem constar na chave primária da tabela referenciada.

Exclusão de linhas

O comando DELETE é utilizado para excluir linhas de uma tabela. Sua sintaxe é a seguinte:

DELETE FROM NOME_TABELA

WHERE EXPRESSAO_LOGICA

Nos exemplos a seguir, utilizaremos novamente a tabela LIVROS:

• Excluir todos os livros da tabela. DELETE FROM LIVRO

• Excluir os livros que tenham preço superior a R$ 100,00 e que não tenham sido lançados. DELETE FROM LIVRO

WHERE LANCAMENTO IS NULL AND PRECO > 100

• Excluir os livros que tenham ‘R’ como assunto ou que ainda não tenham sido lançados. DELETE FROM LIVRO

WHERE ASSUNTO = ‘R’ OR LANCAMENTO IS NULL

Agrupamento Dados

Na linguagem SQL são definidas várias funções que operam sobre grupos de dados. Tais funções,

usualmente, realizam operações ou comparações sobre um conjunto de dados e retornam, como resultado, uma

relação de apenas uma linha ou uma coluna. São chamadas Funções Agregadas.

Contagem

Muitas vezes é necessário contar uma quantidade de linhas que satisfaz uma determinada condição. Para

isso, utilizamos a função COUNT. Assim como as outras funções que serão apresentadas mais adiante, a função

COUNT recebe um parâmetro e retorna um numero.

Como parâmetro para a função COUNT podemos utilizar o nome de uma coluna ou o caractere *. No caso

da utilização do caractere * o resultado obtido é a contagem do numero de linhas da tabela. No caso da utilização

de uma coluna como parâmetro, o reesultado obtido é o numero de ocorrências não nulas desta coluna na tabela

pesquisada.ç

Por exemplo:

• Contar a quantidade de linhas da tabela LIVRO: SELECT COUNT(*)

FROM LIVRO

• Contar a quantidade de linha da tabela LIVRO com a coluna de código preenchida: SELECT COUNT(CODIGO)

FROM LIVRO

A função COUNT retornará zero quando nenhuma linha atender ao critério utilizado.

Soma

211

Outra operação comumente utilizada é a soma. Para realizar a soma de valores de uma coluna para um

grupo de dados, utilizamos a função SUM.

Exemplo:

• Somatório dos preços da tabela de livros: SELECT SUM(PRECO)

FROM LIVRO

A função SUM retornará o somatório dos valores não-nulos da coluna utilizada como parâmetro.

Retornará NULL se todos os valores desta coluna forem nulos ou se nenhum valor atender ao critério de seleção.

Média

Para obter a média aritmética dos valores de uma coluna utilizada como parâmetro, utilizamos o

comando AVG, informando, como parâmetro para a mesma, o nome da coluna para a qual desejamos obter a

média.

A função AVG retornará a média considerando apenas os valores não-nulos da coluna especificada.

Exemplo:

SELECT AVG(PRECO)

FROM LIVRO

Valor Máximo

Para obter o valor máximo de uma coluna em um conjunto de dados, utilizamos a função MAX. Assim

como a função AVG, MAX recebe um parâmetro e retorna o valor NULL se em todas as linhas da tabela

consultada o valor da coluna em questão for nulo, ou se nenhuma linha atender ao valor do critério de seleção.

Exemplo:

SELECT MAX(PRECO)

FROM LIVRO

WHERE ASSUNTO = ‘P’

Valor mínimo

Em oposição à função MAX, temos a função MIN, que retorna o menor valor de uma coluna para a tabela

especificada.

• Menor preço da tabela de livros, para cujos livros o assunto seja ‘B’: SELECT MIN(PRECO)

FROM LIVRO

WHERE ASSUNTO = ‘B’

Outras funções

Podemos destacar:

• STDDEV_POP: desvio padrão da população;

• STDDEV_SAMP: desvio padrão da amostra;

• VAR_POP: variância calculada como o quadrado de STDDEV_POP;

• VAR_SAMP: variância calcula como o quadrado de STDDEV_SAMP.

Cláusula GROUP BY

Para utilizarmos uma função de agregação em conjunto com colunas da tabela na cláusula SELECT,

devemos utilizar a cláusula GROUP BY. A sintaxe de utilização da cláusula GROUP BY é a seguinte:

SELECT COL1, COL2, ..., COLN, FUNCAO1, …, FUNCAON

FROM NOME_TABELA

WHERE CONDICAO

212

GROUP BY COL1, COL2, ..., COLN

A utilização da cláusula GROUP BY faz com que os dados sejam sumarizados pelas colunas que são

especificadas na mesma. Assim, somente valores distintos destas colunas farao parte do resultado. Neste caso,

torna-se possível utilizar funções agregadas, as quais irão operar sobre as linhas que foram utilizadas para montar

cada grupo (sumarização) de dados. Vejamos alguns exemplos:

• Qual o preço médio dos livros de cada assunto? SELECT ASSUNTO, AVG(PRECO)

FROM LIVRO

GROUP BY ASSUNTO

• Quantos livros existem para cada assunto? SELECT ASSUNTO, COUNT(*)

FROM LIVRO

GROUP BY ASSUNTO

• Qual o preço do livro mais caro de cada assunto, dentre aqueles que já foram lançados? SELECT ASSUNTO, MAX(PRECO)

FROM LIVRO

WHERE LANCAMENTO IS NOT NULL

GROUP BY ASSUNTO

• Quantos livros já foram lançados para cada editora? SELECT EDITORA, COUNT(*)

FROM LIVRO

WHERE LANCAMENTO IS NOT NULL

GROUP BY EDITORA

Cláusula HAVING

A cláusula WHERE não nos permite realizar não nos permite realizar restrições com base nos resultados

das funções agregadas. Para isso, devemos utilizar a cláusula HAVING.

A cláusula HAVING será seguida de uma expressão lógica que poderá ser composta ou não, de forma

idêntica ao que foi apresentado na cláusula WHERE. Assim como a cláusula WHERE, a cláusula HAVING serve de

filtro para as linhas constantes do resultado do comando SQL.

A principal diferença entre essas cláusulas se dá no fato de que, no caso da cláusula WHERE, o filtro é

aplicado quando as linhas são recuperadas do banco de dados, fazendo com que estas nem cheguem a ser

consideradas quando da realização de agrupamentos ou na execução da função de agregação. Já as restrições

descritas na cláusula HAVING serão aplicadas somente após a recuperação das linhas no banco de dados, da

montagem dos grupos e da execução de funções agregadas. Por isso, é possível utilizar funções agregadas em

expressões lógicas da cláusula HAVING.

Sintaxe básica:

SELECT COL1, COL2, ..., COLN

FROM NOME_TABELA

WHERE EXPRESSAO_LOGICA_WHERE

GROUP BY COL1, COL2,..., COLN

HAVING EXPRESSAO_LOGICA_HAVING

Exemplo:

• Quais os assuntos cujo preço médio dos livros ultrapassa R$50,00?

213

SELECT ASSUNTO

FROM LIVRO

GROUP BY ASSUNTO

HAVING AVG(PRECO) > 50

• Quais os assuntos que possuem mais de dois livros? SELECT ASSUNTO, COUNT(*)

FROM LIVRO

GROUP BY ASSUNTO

HAVING COUNT(*) > 1

• Quais os assuntos que possuem mais de dois livros já lançados? SELECT ASSUNTO, COUNT(*)

FROM LIVRO

GROUP BY ASSUNTO

HAVING COUNT(*) > 1

• Quantos livros já foram lançados por assunto? SELECT ASSUNTO, COUNT(*)

FROM LIVRO

WHERE LANCAMENTO IS NULL

GROUP BY ASSUNTO

214

Operando, Ordenando e Formatando Resultados

Usando apelidos

Não é possível atribuir apelidos tanto a colunas quanto a tabelas, e referenciá-las através de seus apelidos.

Apelidos para colunas � atribuir um apelido a uma coluna resultante de uma consulta é extremamente fácil. Veja

a sintaxe:

SELECT COL1 AS MINHA_COLUNA

FROM NOME_TABELA

Quaisquer construções particionadas na cláusula SELECT, como as funções, podem receber apelidos. Por

exemplo:

• Considere a consulta que retorna o maior preço da tabela de livros para livros cujo assunto seja ‘P’: SELECT MAX(PRECO) AS PRECO_MAXIMO

FROM LIVRO

WHERE ASSUNTO = ‘P’

Apelidos para as tabelas � em nossas consultas ao banco de dados podemos fazer referência a uma coluna

explicitando a sua tabela de origem como uma construção no formato: NOME_TABELA.NOME_COLUNA. Na

verdade, há situações onde somos obrigados a utilizar esse tipo de construção.

Em alguns casos, as tabelas são representadas por nomes extensos e utilizar a construção anterior pode não

ser só cansativo quanto tornar o comando extenso e confuso. Nessas situações, possuir um apelido menor e

expressivo para a tabela é interessante. Na verdade, não se trata somente de questão de clareza de comando. Há

situações onde somos obrigados a utilizar apelidos para tabelas e relações. Vejamos, então, como fornecer

apelidos para tabelas:

SELECT COL1

FROM NOME_TABELA_ORIGINAL AS NOVO_NOME_TABELA

Abaixo, a tabela LIVRO é substituída pelo apelido L:

SELECT MAX(L.PRECO) AS PRECO_MAXIMO

FROM LIVRO L

WHERE L.ASSUNTO = ‘P’

Constantes e concatenação

A SQL permite definir constantes que serão repetidas em todas as linhas do resultado de uma consulta, o que

atende ao caso anterior. Para isso, basta especificar a constante desejada na cláusula SELECT do comando. Por

exemplo:

SELECT ‘LIVRO:’ AS TEXTO, TITULO

FROM LIVRO

Neste exemplo, a cadeia de caracteres ‘LIVRO:’ foi tratada como uma coluna independente. A linguagem SQL

define o operador de concatenação é ||, e deve ser utilizado entre as colunas ou textos que desejamos

concatenar.

Vejamos o exemplo da utilização de concatenação do exemplo anterior:

SELECT ‘LIVRO:’ || TITULO AS TEXTO

FROM LIVRO

Realizando operações aritméticas

215

Da mesma forma que utilizamos a cláusula SELECT, podemos realizar operações aritméticas sobre os

resultados de uma consulta. Os operadores +, -, *, e / podem ser utilizados em expressões matemáticas. Além

disso, parênteses também podem ser utilizados, para determinar prioridades na execução das operações. Por

exemplo:

• Listar os novos preços dos livros se os valores fossem reajustados em 10%. SELECT TITULO, PRECO*1.1 AS NOVO_PRECO

FROM LIVRO

Podemos reescrever a consulta para demonstrar a utilização de outros operadores mas obtendo o mesmo

resultado.

SELECT TITULO, PRECO + (PRECO/10) AS NOVO_PRECO

FROM LIVRO

Notamos que as operações realizadas modificam somente o resultado das consultas, não alterando os dados

das tabelas.

Aplicando Funções

Formatando os resultados de uma consulta através de funções do SGBD pode fornecer resultados mais

compreensíveis e, também, fazer com que a consulta retorne informações em formato adequado para exibição

ao usuário final. Além disso, muitas vezes, podemos utilizar funções que manipulam os resultados de consultas

SQL formatando-os ou alterando-os.

Cadeia de caracteres � o padrão SQL define várias funções que manipulam e formatam cadeias de caracteres.

Nesta seção são apresentadas as principais: UPPER, LOWER, TRIM, SUBSTRING e LENGHT.

As funções UPPER, LOWER, TRIM e LENGHT recebem uma cadeia de caracteres (ou uma coluna de dados do

tipo cadeia de caracteres) como parâmetro e retornam um resultado do mesmo tipo. Já a função SUBSTRING, que

também retorna uma cadeia de caracteres como resultado, recebe três parâmetros como entrada, sendo dois

numéricos e uma cadeia de caracteres.

A função UPPER retornam o seu parâmetro de entrada com todos os caracteres convertidos para maiúsculas.

Em oposição à função UPPER, a função LOWER retorna todos os caracteres convertidos para minúsculos. Vejamos

um exemplo:

SELECT UPPER(‘LIVRO:’) || LOWER(TITULO) AS TEXTO

FROM LIVRO

A função TRIM retira os caracteres ‘espaço’ contidos nas margens da cadeia de caracteres que recebe como

parâmetro. Exemplo:

Consulta:

SELECT TRIM(‘ LIVRO: ’) || TITULO AS TEXTO

FROM LIVRO

WHERE ASSUNTO = ‘P’

A função SUBSTRING retorna o trecho da cadeia de caracteres que ela recebe como parâmetro. O trecho a ser

retornado é definido por outros dois parâmetros de entrada: a posição de início do trecho e o seu comprimento.

Exemplo:

• Listar os dez primeiros caracteres dos títulos dos livros: SELECT SUBSTRING(TITULO, 1, 10) AS TRECHO

FROM LIVRO

216

LENGTH retorna o comprimento da cadeia de caracteres que recebe como parâmetro. Retornará nulo, caso

receba NULL como parâmetro. Exemplo:

SELECT LENGTH(TITULO) AS COMPRIMENTO

FROM LIVRO

WHERE ASSUNTO = ‘R’

Datas

A formatação de campos relacionados a datas e horários é uma das que apresenta o maior número de

variações entre as implementações dos SGBD´s padrões para SQL. Dentre as funções mais utilizadas, temos as

funções DAY, MONTH e YEAR. Estas funções recebem uma data como parâmetro e retornam o dia, o mês e o ano

da data, respectivamente. Exemplo:

• Selecionar o dia da publicação do livro de código 1 SELECT DAY(LANCAMENTO) AS DIAS

FROM LIVRO

WHERE CODIGO = 1

• Selecionar o mês e o ano da publicação dos livros cujo assunto é ‘R’: SELECT MONTH(LANCAMENTO) AS MÊS,

YEAR(LANCAMENTO) AS ANO

FROM LIVRO

WHERE ASSUNTO = ‘R’

Números

A linguagem SQL e os SGBD´s oferecem várias funções predefinidas para a manipulação de números.

Na tabela 5.1 são apresentadas as principais funções matemáticas definidas pela SQL. A maioria das funções

da tabela 5.1 recebe apenas um parâmetro de entrada numérico e retorna um número.

Também são comuns implementações de funções trigonométricas para a SQL. Dentre as principais funções

trigonométricas, temos as apresentadas na tabela 5.2:

217

A utilização das funções algébricas e trigonométricas apresentadas é semelhante à das funções de cadeias de

caracteres apresentadas anteriormente. Veja o exemplo para a função CEIL:

SELECT CEIL(PRECO)

FROM LIVRO

WHERE CODIGO = 3

Eliminando repetições

Quando utilizamos consultas sobre tabelas, podemos obter linhas repetidas. Para eliminar repetições, em

relações resultantes de consultas, foi definido o predicado DISTINCT. Este predicado poderá ser utilizado

isoladamente na cláusula SELECT ou em conjunto com outras funções SQL.

Para eliminar resultados distintos de uma consulta, basta posicionar o predicado DISTINCT após a cláusula

SELECT e antes da especificação das colunas a serem recuperados. Vejamos um exemplo:

• recuperar os assuntos distintos da tabela de livros: SELECT DISTINCT ASUNTO AS ASSUNTO

FROM LIVRO

O predicado DISTINCT pode ainda ser utilizado com a função COUNT, quando posicionado junto ao seu

parâmetro de entrada. Neste caso, é possível contar os valores distintos de uma coluna, por exemplo.

SELECT COUNT(DISTINCT ASSUNTO)

FROM LIVRO

Ordenando os resultados

Ao realizarmos consultas SQL não sabemos, a priori, quais e em que ordem as linhas do resultado serão

apresentadas. No entanto, muitas vezes, desejamos obter os resultados ordenados por uma ou mais colunas.

Para isso, devemos utilizar a cláusula ORDER BY.

A cláusula ORDER BY é sempre posicionada como a última de um comando SELECT. Vejamos um exemplo de

sintaxe:

SELECT COL1, COL2, ..., COLN

FROM NOME_TABELA

WHERE CONDICAO

GROUP BY COL1, COL2, ..., COLN

HAVING EXPRESSAO_LOGICA

ORDER BY COL1 [DESC,ASC], COL2 [DESC, ASC]

Consulta:

• Gerar a listagem dos livros contendo assunto, título, e preço. A listagem deverá estar ordenada em ordem crescente de assunto e decrescente de preço.

SELECT ASSUNTO, TITULO, PRECO

218

FROM LIVRO

ORDER BY ASSUNTO, PRECO DESC

• Gerar listagem dos livros contendo assunto, título e preço. A listagem deverá estar ordenada em ordem crescente de título e descente de preço.

SELECT ASSUNTO, TITULO, PRECO

FROM LIVRO

ORDER BY 2, PRECO DESC

A SQL:2003 nos permite, ainda, utilizar funções, como SUBSTRING, na cláusula ORDER BY.

219

Junções

Nos comandos apresentados anteriormente, somente uma tabela era acessada por vez. Entretanto, muitas

vezes precisamos acessar informações de mais de uma tabela em uma mesma consulta.

Para acessar mais de uma tabela em um mesmo comando SELECT, devemos realizar operações chamadas

junções. Existem vários tipos de juntos, como a interna, a externa e a natural. As diferenças entre as junções se

dão na forma como as tabelas da consulta são combinadas para a montagem dos resultados.

Para coletar informações de mais de uma tabela, realizamos junções. As junções são ligações entre tabelas,

realizadas através dos valores de uma ou mais colunas. Usualmente, essas ligações ocorrem entre a chave-

primária de uma tabela e a chave estrangeira de outra.

No caso de nosso exemplo, temos que a coluna ASSUNTO da tabela LIVRO é a chave estrangeira para a

coluna SIGLA, chave-primária da tabela ASSUNTO. Assim, neste caso, a ligação entre as informações de uma

tabela com a outra se dará através das colunas ASSUNTO e SIGLA.

A junção entre tabelas faz com que seja gerada uma relação resultante contendo todas as colunas das

tabelas originais. Para a junção entre as tabelas anteriores será gerada uma relação contendo as colunas CODIGO,

TITULO, PRECO, LANCAMENTO, ASSUNTO, EDITORA, SIGLA e DESCRICAO. Essa relação será gerada somente para

a execução da consulta e sobre ela poderão ser aplicadas as operações apresentadas anteriormente. As linhas

que participarão da relação resultante serão escolhidos com base no tipo de junção que está sendo realizada e

com o predicado de junção.

Junção Interna

A junção interna entre tabelas é a modalidade de junção que faz com que somente participarem da relação

resultante as linhas das tabelas de origem que atenderem à cláusula de junção. A sintaxe básica para a realização

da junção interna é:

SELECT COL1, COL2, ..., COLN, FUNCAO1, ..., FUNCAON

FROM NOME_TABELA

INNER JOIN NOME_TABELA2

ON NOME_TABELA.COL1 = NOME_TABELA2.COL1

WHERE CONDICAO

GROUP BY COL1, COL2, ..., COLN

HAVING EXPRESSAO_LOGICA

ORDER BY COL1, COL2, …, COLN

Por exemplo:

• Quais os títulos dos livros já lançados e a descrição dos seus assuntos? SELECT TITULO, DESCRICAO

FROM LIVRO

INNER JOIN ASSUNTO

ON SIGLA = ASSUNTO

WHERE LANCAMENTO IS NOT NULL

No exemplo anterior, nem todas as linhas da tabela ASSUNTO (cuja instância está representada na tabela 3.2)

fazem parte do resultado da consulta. Isto ocorre porque estamos utilizando uma junção interna, onde somente

participam do resultado as linhas nas quais os valores das colunas de junção possuem correspondente em ambas

as tabelas. As junções externas, apresentadas a seguir, permitirão que linhas onde não existam valores

correspondentes em ambas as tabelas participam.

Os primeiros SGBD´s possuíram a cláusula INNER JOIN. Mas as junções internas já eram utilizadas. Para tal, as

tabelas eram listadas na cláusula FROM, separadas por virgulas, e as condições de junção eram descritas na

220

cláusula WHERE. Nesta construção, as condições de junção são listadas juntamente com as condições de seleção.

Veja o exemplo da junção interna sem a cláusula INNER JOIN:

SELECT TITULO, DESCRICAO

FROM LIVRO, ASSUNTO

WHERE ASSUNTO = SIGLA

AND LANCAMENTO IS NOT NULL

Em consultas complexas formuladas dessa forma, é comum que alguma importante condição seja esquecida.

Embora bastante difundidas, é aconselhável que construções deste tipo sejam substituídas pela sintaxe contendo

a cláusula INNER JOIN.

Suponha que, agora, busquemos gerar uma listagem contendo o título do livro, o nome da editora que o

publicou e a descrição do assunto de que trata. Para isso, teremos que utilizar o acesso a três tabelas. Por

exemplo:

SELECT TITULO, NOME, DESCRICAO

FROM LIVRO

INNER JOIN EDITORA E

ON EDITORA = E.CONTEUDO

INNER JOIN ASSUNTO

ON ASSUNTO = SIGLA

Em consultas onde ocorrem junções, todas as cláusulas apresentadas anteriormente como (WHERE, GROUP

BY, HAVING e ORDER BY) continuam válidas. Por exemplo:

Montar a listagem das editoras e dos títulos dos livros que lançaram, ordenada pelo nome da editora e, em

seguida, pelo título do livro. Apresentar somente os livros já lançados.

Consulta:

SELECT NOME, TITULO

FROM EDITORA E

INNER JOIN LIVRO

ON EDITORA = E.CODIGO

WHERE LANCAMENTO IS NOT NULL

ORDER BY NOME, TITULO

Junções Externas

Nas consultas onde se realizam junções internas, somente participam dos resultados as linhas cujas colunas

de junção possuem os mesmos valores em ambas as tabelas participantes da junção.

Nesta seção, serão apresentadas três formas de executar a junção externa, modalidade de junção onde a não

inexistência de valores correspondentes não limita a participação de linhas no resultado de uma consulta.

Junção externa à esquerda

Suponha que desejamos obter uma listagem de todas as editoras cadastradas em nosso banco de dados e,

para aquelas que possuam livros publicados, o nome dos mesmos. Vamos ter que utilizar uma junção externa. Eis

a sintaxe da junção externa à esquerda:

SELECT COL1, COL2, ..., COLN, FUNCAO1, FUNCAO2

FROM NOME_TABELA

LEFT OUTER JOIN NOME_TABELA2

ON NOME_TABELA.COL1 = NOME_TABELA2. COL1

WHERE CONDICAO

GROUP BY COL1, COL2, ..., COLN

HAVING EXPRESSAO_LOGICA

221

ORDER BY COL1, COL2, …, COLN

A única diferença para sintaxe da junção interna é a substituição do termo INNER JOIN pelo termo LEFT

OUTER JOIN, indicador da junção externa à esquerda.

Em uma junção externa à esquerda, a junção ocorre de forma que todas as linhas pertencentes à tabela

posicionada à esquerda do termo LEFT OUTER JOIN no comando e que atendem aos critérios definidos na

cláusula WHERE farão parte do resultado, independente se existem valores correspondentes na coluna de junção

da tabela posicionada à direita do comando. Caso não existam valores correspondentes na tabela à direita, as

colunas selecionadas desta tabela, nas linhas onde não existe correspondência, terão valor NULL.

Montando a listagem de todas as editoras cadastradas em nossa base de dados e, para aquelas que possuam

livros publicados, relacionar, também, o título dos mesmos. Vamos ordenar os resultados pelo nome da editora e

pelo título do livro.

SELECT NOME, TITULO

FROM EDITORA E

LEFT OUTER JOIN LIVRO

ON EDITORA = E.CODIGO

ORDER BY NOME, TITULO

Outro exemplo: Mostrar a listagem completa de assuntos contendo, também, os títulos dos livros e seus

respectivos assuntos. Resultados ordenados pela descrição do assunto.

Consulta:

SELECT DESCRICAO, TITULO

FROM ASSUNTO

LEFT OUTER JOIN LIVRO

ON SIGLA = ASSUNTO

ORDER BY DESCRICAO

Junção externa à Direita

A junção externa à direita é extremamente parecida com a junção externa à esquerda. A única diferença

consta no fato de que a tabela da qual todas as linhas constarão do resultado está posicionada à direita do termo

RIGHT OUTER JOIN no comando. Sua sintaxe é:

SELECT COL1, COL2, ..., COLN, FUNCAO1, ..., FUNCAON

FROM NOME_TABELA

RIGHT OUTER JOIN NOME_TABELA2

ON NOME_TABELA.COL1 = NOME_TABELA2.COL1

WHERE CONDICAO

GROUP BY COL1, COL2, ..., COLN

HAVING EXPRESSAO_LOGICA

ORDER BY COL1, COL2, …, COLN

Onde, a única diferença em termos de sintaxe para a junção externa à esquerda é a substituição do termo

LEFT OUTER JOIN pelo termo RIGHT OUTER JOIN, indicador da junção externa à direita.

Se reescrevermos a consulta do exemplo anterior de forma a obtermos o mesmo resultado e com a utilização

da junção externa à direita, teremos a seguinte consulta:

SELECT DESCRICAO, TITULO

FROM LIVRO

RIGHT OUTER JOIN ASSUNTO

ON SIGLA = ASSUNTO

222

ORDER BY DESCRICAO

Note que para que as consultas sejam equivalentes, temos que inverter a ordem das tabelas na cláusula

FROM.

Junção Externa Completa

Podemos ainda querer montar as listagem de todas as linhas das tabelas participantes que atendam aos

critérios de seleção especificados na cláusula WHERE participem do resultado, independente da correspondência

de valores da cláusula de junção. A cláusula de junção atua de forma a montar a relação quando existir

correspondência entre valores. Quando não existir, as colunas da tabela onde inexiste o valor devem apresentar o

valor nulo. Esta é a junção externa completa.

A diferença da junção externa para as junções à direita e à esquerda se dá no fato de que, naquelas, uma das

tabelas somente apresentava os valores com correspondência à outra, a qual apresentava todos os seus valores.

Na junção externa completa, as duas tabelas poderão apresentar valores sem correspondentes. A sintaxe é:

SELECT COL1, COL2, ..., COLN, FUNCAO1, ..., FUNCAON

FROM NOME_TABELA

FULL OUTER JOIN NOME_TABELA2

ON NOME_TABELA.COL1 = NOME_TABELA2.COL1

WHERE CONDICAO

GROUP BY COL1, COL2, ..., COLN

HAVING EXPRESSAO_LOGICA

ORDER BY COL1, COL2, …, COLN

Onde a única diferença para a sintaxe da junção externa à esquerda é a substituição do termo FULL OUTER

JOIN, em detrimento de LEFT OUTER JOIN e RIGHT OUTER JOIN, respectivamente.

Consideremos agora, a tabela de editoras, cujo exemplo é apresentado na tabela 2.6, e a tabela de livros,

onde foram adicionados mais livros, ainda sem data prevista para o lançamento, sem editora definida e sem

preço. A nossa nova tabela de livros é apresentada na tabela 6.1. Vejamos, então um exemplo para a realização

da junção externa completa.

Listagem com a exibição de todos os títulos e de todas as editoras, relacionando a obra com a editora que a

publica, quando caso. A listagem deverá estar ordenada pelo título da obra.

Consulta:

SELECT TITULO, NOME

FROM LIVRO

FULL OUTER JOIN EDITORA

ON EDITORA.CODIGO = EDITORA

ORDER BY TITULO

Junção Cruzada

223

Algumas vezes queremos gerar todas as combinações possíveis entre elementos de duas tabelas. É uma

operação idêntica a um produto cartesiano dos elementos das tabelas. Para isso, podemos utilizar um tipo de

junção conhecida como CROSS JOIN. Sua sintaxe é:

SELECT COL1, COL2, ..., COLN, FUNCAO1, …, FUNCAON

FROM NOME_TABELA

CROSS JOIN NOME_TABELA2

WHERE CONDICAO

GROUP BY COL1, COL2, …, COLN

HAVING EXPRESSAO_LOGICA

ORDER BY COL1, COL2, …, COLN

Suponha um torneio onde seleções são divididas em dois grupos, A e B, e onde todos os membros do grupo A

jogam contra todos os membros do grupo B. A tabela 6.2 representa as seleções do Grupo A, euqunato a tabela

6.3, as do grupo B.

Consulta:

SELECT A.NOME AS TIME_A, B.NOME AS TIME_B

FROM GRUPO_A A CROSS JOIN GRUPO_B B

Resultado:

Junção Natural e Baseada em Nomes de Colunas

Anteriormente foram apresentadas as junções internas e externas. Agora vamos verificar a junção natural e a

junção baseada em nomes de colunas. Ambas são construções que podem ser aplicadas em conjunto com as

modalidades apresentadas anteriromente para substituir a utilização da cláusula ON.

224

Na verdade, como, em alguns casos, as tabelas sobre as quais queremos realizar a junção apresentam

colunas de mesmo nome e para que, nesses asos, não seja necessário explicitar o nome das colunas, é definida a

junção natural. Nesta modalidade de junção, todas as colunas de mesmo nome nas tabelas em questão

participam da condição de junção.

Para utilizar a junção natural, basta incluir a palavra reservada NATURAL antes das palavras INNER, LEFT,

OUTER ou FULL, de acordo com a situação. Então, não será necessário utilizar a cláusula ON.

Entretanto, realizar automaticamente a junção por todas as colunas de mesmo nome pode ser um problema.

Frequentemente encontramos tabelas com colunas de nomes intituladas “nome” e “descrição”. No entanto, não

é comum realizar junções por tais colunas.

Para solucionar essa questão, foi elaborada a junção baseada em nomes de colunas. Assim, como no caso da

junção natural, neste caso, as colunas de mesmo nome nas diferentes tabelas serão utilizadas para a junção.

Entretanto, agora, as colunas não serão utilizadas automaticamente. Será necessário especificar quais colunas

serão utilizadas.

A sintaxe da junção baseada em nomes de coluna é:

SELECT COL1, COL2, ..., COLN

FROM NOME_TABELA

[INNER, LEFT OUTER, RIGHT OUTER,

FULL OUTER] JOIN NAME_TABELA2

USING [NOME_COLUNA]

A principal diferença entre a junção natural e a baseada em nomes de colunas se dá no fato de que, na

junção natural, todas as colunas de mesmo nome nas tabelas serão utilizadas para realizar a junção, enquanto a

junção baseada em nomes de colunas, somente serão utilizadas as colunas que forem listadas.

Formatando a Saída e as junções externas

Quando utilizamos as funções externas, podemos obter vários valores nulos em uma ou mais coluna do

resultado. A função COALESCE nos permite substituir os valores nulos por outros que desejamos. Ela recebe uma

lista de parâmetros e retorna o primeiro que possuir um valor não-nulo. Frequentemente, é exigido que todos os

parâmetros sejam do mesmo tipo de dados.

Por exemplo:

Obter uma listagem com a descrição de todos os assuntos e os títulos dos livros de cada um. Quando não

existir um assunto associado, deve ser escrita a frase “SEM PUBLICAÇÕES”.

Consulta:

SELECT DESCRICAO, COALESCE(TITULO, ‘SEM PUBLICAÇÕES’) AS TITULO

FROM ASSUNTO

LEFT OUTER JOIN LIVRO

ON SIGLA = ASSUNTO

ORDER BY DESCRICAO

225

Combinando Comandos

Todos os commandos de consulta da linguagem SQL atuam sobre uma relação, que pode ou não estar

materializada em formato de uma tabela. O resultado dos comandos de seleção também é uma relação. Tanto as

relações de entrada quanto as relações de saída podem possuir diversos números de colunas e linhas.

Este tipo de construção permite que utilizemos consultas embutidas na cláusula FROM de uma consulta,

fazendo com que a saída de um comando SELECT seja utilizada como entrada para outro comando SELECT. Outras

formas de combinarmos os dois ou mais comandos SELECT para obter um único resultado final são subconsultas

da cláusula WHERE, correlacionadas ou não, e as operações baseadas nas operações de conjunto.

Subconsultas da cláusula WHERE

A utilização de subconsultas na cláusula WHERE é uma das formas de combinação de duas ou mais consultas

para um único resultado final. Nestas construções, o resultado da subconsulta não é apresentado ao usuário,

sendo construído de forma temporária pelo SGBD, o qual utiliza os resultados temporários em testes das

consultas mais externas. Existem dois tipos de subconsultas: correlacionadas e não-correlacionadas.

Subconsultas não-correlacionadas

Com a utilização do predicado IN era possível comparar o valor de uma coluna com uma lista de valores. Na

subconsulta não-correlacionada, substituímos a lista de valores do predicado IN por uma consulta.

A sintaxe básica do comando para utilização da subconsulta não-correlacionada é:

SELECT COL1, COL2, ..., COLN

FROM NOME_TABELA

WHERE COLM [NOT] IN (SELECT COLX FROM NOME_TABELA2)

Note que, à esquerda do predicado [NOT] IN continua posicionada uma coluna (a utilização do operador NOT

é opcional). A consulta interna ao predicado IN (aquela que substitui a lista de valores), não tem nenhuma ligação

com a consulta externa. Ambas as consultas poderão possuir todas as construções apresentadas anteriormente,

sem nenhuma restrição adicional devido à presença da subconsulta. A consulta interna deverá, no entanto,

retornar uma coluna apenas.

Exemplos:

Considerando a base de dados de publicações composta pelas tabelas ASSUNTO, LIVRO e EDITORA, conforme

anteriormente. Desejamos saber os nomes das editoras que possuem livros já lançados.

Consulta:

SELECT NOME

FROM EDITORA

WHERE CODIGO IN ( SELECT EDITORA

FROM LIVRO

WHERE LANCAMENTO IS NOT NULL)

Neste exemplo, a subconsulta gera uma relação temporária de uma única coluna (não exibida ao usuário em

nenhum momento) contendo os códigos de editoras que publicaram livros que já foram lançados. A consulta

externa procurará por nomes de editoras para as quais o código consta na relação produzida para subconsulta.

Desejamos saber quais assuntos não foram lançados livros.

SELECT DESCRICAO

FROM ASSUNTO

WHERE SIGLA NOT IN (SELECT ASSUNTO FROM LIVRO WHERE LANCAMENTO IS NOT NULL)

Neste exemplo, a subconsulta gera uma listagem de assuntos dos livros que já foram lançados. A consulta

externa procurará, na tabela de assuntos, quais assuntos não constam na listagem gerada pela subconsulta.

226

Também podemos utilizar subconsultas combinadas com operações de atualização e exclusão de dados. Por

exemplo:

Desejamos excluir as editoras que não publicaram os livros. O comando para tal operação é:

DELETE FROM EDITORA

WHERE CODIGO NOT IN (SELECT EDITORA FROM LIVRO)

Subconsultas Correlacionadas

É possivel utilizar o predicado IN em conjunto com um commando SQL em uma construção chamada de não-

correlacionada. Agora, veremos outro tipo de construção, chamado de subconsulta não-correlacionada. Agora,

veremos outro tipo de construção, chamada de subconsulta correlacionada, pois, neste caso, a subconsulta

possui dependência direta da consulta externa.

Na subconsulta correlacionada utilizaremos o predicado EXISTS. O predicado IN permitia testar se valores de

uma coluna constavam em uma listagem de valores. O predicado EXISTS testa se uma condição é verdadeira ou

falsa. Vejamos exemplo da sintaxe para sua utilização:

SELECT COL1, COL2, ..., COLN

FROM NOME_TABELA TAB_EXTERNA

WHERE [NOT] EXISTS

(SELECT COLX

FROM NOME_TABELA2 TAB_EXTERNA

WHERE TAB_EXTERNA.COLA = TAB_INTERNA.COLA)

Na subconsulta do exemplo anterior, existe uma comparação entre uma coluna da tabela externa com uma

coluna da tabela interna (em negrito). Este tipo de teste é possível em subconsultas, onde a consulta mais interna

poderá acessar uma coluna da coluna mais externa, usualmente utilizando o apelido (ou correlation name) da

tabela mencionada da coluna mais externa, usualmente utilizando o apelido da tabela mencionada da consulta

mais externa.

Esse comando começa a ser executado pela consulta mais externa. Então, para cada linha de NOME_TABELA,

a subconsulta será executada, substituindo-se o valor de TAB_EXTERNA.COLA por seu valor na linha em questão.

Se a subconsulta retornar algum valor a cláusula EXISTS será verdadeira e a linha recuperada na consulta mais

externa fará parte do resultado final. Em caso contrário, a consulta mais externa realiza o teste para a próxima

linha de TAB_EXTERNA.COLA.

Note que, na utilização do predicado EXISTS, não é posicionada nenhuma coluna à esquerda do mesmo, pois

ele não compara valores, e, sim, testa uma condição booleana. Assim, a coluna posicionada na cláusula SELECT da

subconsulta não influenciará no resultado do comando.

Devido à existência, na subconsulta, da utilização de uma coluna da consulta mais externa em uma

comparação, esta construção é chamada de consulta correlacionada.

Vejamos os exemplos anteriores reescritos para o formato de subconsultas correlacionadas:

• Desejamos saber os nomes das editoras que possuem livros lançados. SELECT NOME

FROM EDITORA ED

WHERE EXISTS (SELECT EDITORA

FROM LIVRO

WHERE LANCAMENTO IS NOT NULL

AND ED.CODIGO = EDITORA)

• Desejamos saber sobre quais assuntos nao foram lançados livros. SELECT DESCRICAO

FROM ASSUNTO ASS

WHERE NOT EXISTS (SELECT ASSUNTO

227

FROM LIVRO

WHERE LANCAMENTO IS NOT NULL

AND ASS.SIGLA = ASSUNTO)

Assim como no caso do predicado IN, poderemos utilizar EXISTS em commandos de atualização e exclusão de

dados. Vejamos a reescrita do comando para exclusão das editoras que não possuem livros associados, com a

utilização de EXISTS:

DELETE FROM EDITORA E

WHERE NOT EXISTS (

SELECT 1 FROM LIVRO WHERE EDITORA = E.CODIGO)

Subconsultas substituindo valores

Em uma consulta, para cada linha da relação resultante temos, em uma dada coluna, uma pequena relação

de uma linha e uma coluna que pode ser substituída por um comando SELECT que retorne apenas uma linha e

uma coluna. Tal comando SELECT pode ser formador tanto de uma consulta correlacionada quanto de uma

consulta não-correlacionada.

Considere que desejamos montar uma relação que contenha em uma coluna a descrição dos assuntos

existentes e, em outra, a quantidade de livros lançados de cada assunto.

Para obter a coluna ASSUNTOS basta realizar uma seleção sobre a coluna DESCRIÇÃO da tabela de assuntos e

utilizar o apelido ASSUNTOS para a coluna. Para obter a coluna LIVROS_LANCADOS devemos contar, para cada

assunto, quantos livros já lançados existem na tabela de livros do assunto em questão. O comando que monta o

resultado anterior é o seguinte:

SELECT DESCRICAO AS ASSUNTOS,

(SELECT COUNT(*)

FROM LIVRO L

WHERE L.ASSUNTO = A.SIGLA

AND LANCAMENTO IS NOT NULL

) AS LIVROS_LANCADOS

FROM ASSUNTO A

Note que, no comando anterior, foi utilizada uma subconsulta correlacionada substituindo um valor em um

comando SELECT e, para a qual, foi dado um apelido (LIVROS_LANCADOS). A subconsulta utilizada possui

somente uma coluna onde foi usada a função COUNT que retorna somente uma linha, de forma a atender ao

requisito apresentado anteriormente. Como a correlação desta com a tabela externa (L.ASSUNTO = A.SIGLA) faz

com que a contagem de livros seja feita para o assunto adequado.

Outro exemplo:

• Listar o nome das editoras e o preço médio das publicações de cada uma. SELECT NOME,

(SELECT AVG(PRECO)

FROM LIVRO V

WHERE V.EDITORA = E.CODIGO

AND LANCAMENTO IS NOT NULL) AS PRECO_MEDIO

FROM EDITORA E

ORDER BY NOME

Poderemos utilizar subconsultas que retornem uma relação de uma linha e uma coluna em vários comandos

e locais como, por exemplo, substituindo o valor no comando UPDATE TABELA SET COLUNA = VALOR.

228

Tabelas aninhadas

Uma tabela é a materialização de uma relação. Quando realizamos uma consulta e posicionamos uma tabela

na cláusula FROM, estamos fazendo uma consulta sobre uma relação. Logo, podemos substituir uma tabela por

uma subconsulta que retorne uma relação. A esta construção chamamos de tabelas aninhadas.

Para utilizarmos o resultado de uma consulta como uma tabela, devemos posicionar a consulta delimitada

por parênteses em local destinado a uma tabela. Para que possamos acessar as colunas do resultado da

subconsulta como se acessássemos as colunas de uma tabela, pode ser necessário atribuir um apelido para a

subconsulta. Vejamos um exemplo de sintaxe para a junção entre uma tabela aninhada e uma tabela:

SELECT COL1, COL2, COLN, COLX, COLY, COLZ

FROM

(SELECT COLX, COLY, COLZ FROM TAB_INTERNA) TAB_CONSULTA

INNER JOIN TAB_EXTERNA

ON TAB_CONSULTA.COLX = TAB_EXTERNA.COL1

Notamos que a expressão TAB_CONSULTA.COLX representa o acesso à coluna COLX do resultado da

subconsulta. Qualquer coluna da tabela aninhada poderá ser acessada como uma coluna de uma tabela.

• Listar o nome das editoras e as publicações das editoras que lançaram ao menos dois livros, ordenados por nome da editora e pelo título da publicação.

SELECT NOME, TITULO

FROM

(SELECT EDITORA, COUNT(*) AS QUANTIDADE

FROM LIVRO V

WHERE LANCAMENTO IS NOT NULL

GROUP BY EDITORA) EDITORA_QUANT

INNER JOIN LIVRO

ON EDITORA_QUANT.EDITORA = LIVRO.EDITORA

INNER JOIN EDITORA

ON EDITORA_QUANT.EDITORA = EDITORA.CODIGO

WHERE QUANTIDADE >= 2

ORDER BY NOME

Essa relação, que não é exibida ao usuário, é utilizada exatamente como uma tabela e referenciada pelo

nome EDITORA_QUANT.

• Listar os títulos dos livros dos assuntos para os quais o preço médio das publicações é superior a R$ 40,00, juntamente com os respectivos assuntos.

SELECT TITULO, DESCRICAO AS ASSUNTO

FROM ( SELECT ASSUNTO, AVG(PRECO) AS PRECO_MEDIO

FROM LIVRO V

GROUP BY SIGLA

HAVING AVG(PRECO) > 40) ASSUNTO_PRECO

INNER JOIN LIVRO

ON ASSUNTO_PRECO.ASSUNTO = LIVRO.ASSUNTO

INNNER JOIN ASSUNTO

ON ASSUNTO_PRECO.ASSUNTO = ASSUNTO.SIGLA

Operações de Conjunto

Podemos realizar as operações tradicionais sobre conjuntos: união, interseção, e diferença.

União

229

Quando realizamos junções entre tabelas, formamos uma relação resultante com as colunas que contêm as

colunas das tabelas originais. Por outro lado, podemos querer realizar um comando SQL que apresente, como

resultado, todas as linhas que foram recuperadas por outros dois comandos SQL realizados em separado. Para

unir as linhas resultantes de duas ou mais consultas, utilizamos o predicado UNION.

O predicado UNION é utilizado posicionado entre dois comandos de consulta, da seguinte forma:

SELECT COL1, COL2

FROM TABELA1

UNION [ALL]

SELECT COL3, COL4

FROM TABELA2

Observamos que os comandos poderão acessar tabelas diferentes e utilizar as mais diversas construções da

linguagem. Existem, somente, duas regras para utilização do UNION: (i) os comandos devem retornar o mesmo

número de colunas e (ii) as colunas correspondentes em cada comando devem possuir os mesmo tipos de dados.

O UNION irá atuar fazendo com que o resultado das consultas participante seja combinado com o resultado

final. Quando utilizamos o UNION isoladamente, no resultado final não constarão linhas repetidas. Se desejarmos

que linhas repetidas apareçam, devemos utilizar o predicado ALL logo após UNION, conforme mostrado antes.

* Listar os títulos dos livros que cujo assunto é “Banco de Dados” ou que foram lançados por editoras que

contenham “Mirandela” no nome.

SELECT TITULO

FROM LIVRO

INNER JOIN ASSUNTO

ON ASSUNTO = SIGLA

WHERE DESCRICAO = ‘BANCO DE DADOS’

UNION [ALL]

SELECT TITULO

FROM LIVRO

INNER JOIN EDITORA E

ON EDITORA = E.CODIGO

WHERE NOME LIKE “%MIRANDELA%”

Interseção

Para obtermos a interseção entre os resultados do comando SELECT, utilizamos o comando INTERSECT.

O INTERSECT é utilizado da mesma forma que o UNION, posicionado entre dois comandos SELECT, e atende

às mesmas regras: (i) os comandos devem retornar o mesmo número de colunas e (ii) as colunas correspondentes

em cada comando devem possuir os mesmos tipos de dados. Então, o INTERSECT retornará as linhas que estejam

presente nos resultados de todas as consultas participantes.

Da mesma forma que o UNION, o INTERSECT utilizado isoladamente eliminará as linhas repetidas. Para que

as linhas repetidas constem no resultado final, o predicado ALL deve ser utilizado.

• Listar os títulos dos livros cujo assunto é ‘Programação’ e que foram lançados por uma editora que contenha a palavra “Mirandela” no nome, sem repetições.

SELECT TITULO

FROM LIVRO

INNER JOIN ASSUNTO

ON ASSUNTO = SIGLA

WHERE DESCRICAO = ‘PROGRAMACAO’

INTERSECT

SELECT TITULO

FROM LIVRO

230

INNER JOIN EDITORA E

ON EDITORA = E.CODIGO

WHERE NOME LIKE “%MIRANDELA%”

Diferença

Também é possível realizar a diferença entre os resultados de comandos SELECT. Neste caso, o predicado

utilizado é o EXCEPT.

O EXCEPT será utilizado da mesma forma que o UNION e o INTERSECT (entre os comandos SELECT). Estará

sujeito às mesmas duas regras apresentadas nos casos anteriores sobre o número de colunas e os tipos de dados

das mesmas. Isoladamente, ele não permite linhas repetidas no resultado final. Da mesma forma que o UNION e

o INTERSECT, ele pode ser utilizado em conjunto com o predicado ALL.

Exemplo: Listar os títulos dos livros cujo assunto é “Banco de Dados” e que não foram lançados por uma editora

que contenha a palavra ‘Mirandela’ no nome.

SELECT TITULO

FROM LIVRO

INNER JOIN ASSUNTO

ON ASSUNTO = SIGLA

WHERE DESCRICAO = ‘BANCO DE DADOS’

EXCEPT

SELECT TITULO

FROM LIVRO

INNER JOIN EDITORA E

ON EDITORA = E.CODIGO

WHERE NOME LIKE ‘MIRANDELA’

Como o EXCEPT realiza a diferença entre conjuntos, a ordem de declaração das consultas com relação ao

predicado EXCEPT altera o resultado final, diferentemente do que acontece com os predicados UNION e

INTERSECT.

• Listar o título dos livros que foram lançados por editora que contenham a palavra ‘Mirandela’ em seu nome e cujo assunto não é ‘Banco de Dados’.

SELECT TITULO

FROM LIVRO

INNER JOIN EDITORA E

ON EDITORA = E.CODIGO

WHERE NOME LIKE ‘%MIRANDELA%’

EXCEPT

SELECT TITULO

FROM LIVRO

INNER JOIN ASSUNTO

ON ASSUNTO = SIGLA

WHERE DESCRICAO = ‘BANCO DE DADOS’

Comandos e Estruturas Avançadas

A seguir serão apresentadas construções da SQL:2003 que permitem a realização de consultas bastante

poderosas, como as consultas recursivas, ou de grande utilidade, como o predicado CASE e os comandos para

criação e exclusão de visões. Também é apresentado o comando MERGE, para inclusão e atualização de

informações em tabelas.

Visões e Visões Temporárias

231

Muitas vezes gostaríamos de utilizar os dados contidos em nosso banco de dados como se estivessem em um

formato diferente daquele em que realmente estão.

Consideremos uma situação em que queremos constantemente queremos consultar o título de um livro, seu

preço, o nome da editora que o publica e a descrição do assunto do livro. Estas informações estão contidas em

nosso banco de dados, mas espalhada em três tabelas. Para uni-las devemos realizar o comando SELECT com

junções entre as tabelas. Entretanto, como a consulta é realizada constantemente, gostaríamos de ter uma

diferente visão de nosso banco de dados: gostaríamos de ter uma visão onde as informações já aparecessem

unidas. A linguagem SQL nos permite isso através da criação de um objeto chamado visão.

Visões são tabelas artificiais cujo conteúdo provém de tabelas reais. Os dados que as compõem são definidos

através de comandos SELECT realizados sobre tabelas dos bancos de dados. Na verdade, os dados continuam

armazenados na tabela original. Cada vez que realizamos uma consulta sobre a visão, o SGBD se encarrega de

coletar os dados nas tabelas de origem, a partir do comando SELECT que definem a visão como se ela fosse uma

tabela.

Para criamos visões, utilizamos o comando CREATE VIEW, cuja estrutura básica é apresentada a seguir.

CREATE VIEW NOME_VISÃO

AS COMANDO DE CONSULTA

Vejamos, a seguir o comando para criação da visão que contém o título dos livros, seus preços, o nome da

editora que os publica e a descrição de seus assuntos.

CREATE VIEW LIVRO_EDITORA_ASSUNTO

AS

SELECT TITULO, PRECO, NOME AS EDITORA,

DESCRICAO AS ASSUNTO

FROM LIVRO

INNER JOIN EDITORA ED

ON EDITORA = ED.CODIGO

INNER JOIN ASSUNTO

ON ASSUNTO.SIGLA = LIVRO.ASSUNTO

Agora, poderemos consultar a visão como se consultássemos uma tabela de nosso banco de dados. Como

exemplo, vamos apresentar o comando para obtermos o título, o nome da editora, e a descrição do assunto dos

livros que possuem preço superior a R$ 45,00. Queremos a listagem ordenada por título do livro.

SELECT TITULO, EDITORA, ASSUNTO

FROM LIVRO_EDITORA_ASSUNTO

WHERE PRECO > 45

ORDER BY TITULO

A definição de uma visão permanece no banco de dados, de forma que a visão pode ser acessada a qualquer

momento. Para apagarmos uma visão, utilizamos o comando DROP VIEW, seguido do nome da visão. A seguir,

como exemplo, temos o comando para exclusão da visão

LIVRO_EDITORA_ASSUNTO

DROP VIEW LIVRO_EDITORA_ASSUNTO

Na exclusão da visão, somente sua definição é excluída. Os dados permanecem nas tabelas originais.

Por outro lado, podemos querer criar uma visão para ser utilizada em um comando somente, sem que sua

definição fique armazenada no banco de dados. Para isto, utilizamos o comando WITH. Uma estrutura simples

para o comando WITH é apresentada a seguir.

WITH NOME_VISÃO_TEMPORÁRIA [(NOME_COLUNAS_VISÃO)]

AS

(COMANDO_DEFINIÇÃO_VISÃO)

COMANDO_DE_CONSULTA

232

Vamos, agora, como exemplo, utilizar o mesmo comando que colocou a visão LIVRO_EDITORA_ASSUNTO na

definição de uma visão temporária. Usaremos, também, a mesma consulta sobre esta visão que utilizamos

anteriormente.

WITH CONSULTA_EDITORA_ASSUNTO AS (

SELECT TITULO, PRECO, NOME AS EDITORA, DESCRICAO AS ASSUNTO

FROM LIVRO

INNER JOIN EDITORA ED

ON EDITORA = ED.CODIGO

INNER JOIN ASSUNTO

ON ASSUNTO.SIGLA = LIVRO.ASSUNTO)

SELECT TITULO, EDITORA, ASSUNTO

FROM LIVRO_EDITORA_ASSUNTO

WHERE PRECO > 45

ORDER BY TITULO

Consultas recursivas

A inclusão de consultas na linguagem SQL permitiu a realização de comandos que antes deveriam ser

realizados somente através da utilização de linguagens de programação.

Vamos considerar um fórum de mensagens na Intenet. Neste fórum, o usuário pode enviar uma mensagem

ou responder a outra enviada anteriormente. O usuário pode, inclusive, responder a uma resposta anterior. Um

exemplo dessa estrutura é apresentado na figura 8.1. Notamos que essa estrutura pode ser visualizada em

formato de árvore.

Em termos de modelagem, consideremos que a mensagem seja representada por um auto-relacionamento,

conforme a figura 8.2. Todas as mensagens poderão estar contidas em uma única tabela do banco de dados, da

qual temos um exemplo na tabela 8.1. Nesta tabela, a coluna ID_MENSAGEM é chave primária, identificando

univocamente cada mensagem da tabela. A coluna ASSUNTO representa o assunto da mensagem. No caso de

uma mensagem estar respondendo à outra, na coluna ID_MENSAGEMPAI estará contido o identificador da

mensagem que está sendo respondida. Em caso contrário, esta coluna estará vazia.

233

Se desejarmos obter quais as respostas para a mensagem de número 1, basta realizarmos uma consulta

procurando as linhas onde a coluna ID_MENSAGEMPAI possui o valor 1. Entretanto, podemos querer obter todas

as mensagens originadas a partir da pergunta de mensagem de número 1, ou seja, todas aquelas que estão

diretamente respondendo à mensagem de número 1, aquelas que respondem a respostas da mensagem de

número 1 e assim por diante. Para obter todas essas mensagens, termos que utilizar uma consulta recursiva.

Consultas recursivas utilizam-se da cláusula WITH combina com os comandos SELECT e UNION ALL.

A construção a ser utilizada é semelhante à apresentada anteriormente. A principal diferença reside na

construção da consulta que define a visão. Esta deverá conter consultas, unidas via UNION ALL. A primeira, mais

básica, define a semente do comando, acessando a linha a partir da qual iremos querer disparar a recursão. A

segunda conterá uma segunda definição de tabela, ligada à primeira, montando a cláusula da recursão.

Assim, para o exemplo anterior, onde queremos obter todas as mensagens que foram originadas a partir da

mensagem de número 1, devemos utilizar o seguinte comando listado a seguir.

WITH JA_SELECIONADO

(ID_MENSAGEM, ASSUNTO, ID_MENSAGEM_PAI)

AS

(SELECT ID_MENSAGEM, ASSUNTO, ID_MENSAGEM_PAI

FROM MENSAGEM

WHERE ID_MENSAGEM = 1

UNION ALL

SELECT M.ID_MENSAGEM, M.ASSUNTO, M.ID_MENSAGEM_PAI

FROM JA_SELECIONADO J, MENSAGEM M

WHERE J.ID_MENSAGEM = M.ID_MENSAGEM_PAI)

SELECT * FROM JA_SELECIONADO

Note, no comando, que utilizamos a consulta

SELECT ID_MENSAGEM, ASSUNTO, ID_MENSAGEM_PAI

FROM MENSAGEM

WHERE ID_MENSAGEM = 1

234

Como semente da recursão, selecionando a primeira linha da tabela temporária JÁ_SELECIONADO. O

comando SELECT, que se une a este, acessa as tabelas MENSAGEM e JÁ_SELECIONADO, unindo-as pela chave

estrangeira e fazendo com que novas linhas sejam adicionadas à visão temporária.

Embora as consultas recursivas tenham sido um grande avanço, elas também criaram um novo problema a

ser tratado: o loop infinito. Assim, ao construirmos consultas recursivas, devemos estar atentos para montá-las de

forma a que tenham fim.

Predicado CASE

Algumas vezes queremos selecionar resultados diferentes em função de uma ou mais condições. O predicado

CASE, que pode ser utilizado em conjunto com comandos SELECT, permite que realizemos tal tarefa.

Existem duas diferentes construções para o predicado CASE. A construção mais simples possui o seguinte

formato:

CASE COLUNA

WHEN VALOR THEN RESULTADO

WHEN VALOR2 THEN RESULTADO2

...

[ELSE RESULTADO_ELSE]

END

• Para os livros cujo assunto é ‘B’, retornar o título concatenado com a expressão ‘-MUITO INTERESSANTE’. Para aqueles cujo assunto é ‘P’ retornar o título concatenado com a expressão ‘-INTERESSANTE MÉDIO’. Para os outros retornar a expressao ‘-SEM INTERESSE’ concatenada com seu título. Consideremos a Tabela 6.1 como a instância para a tabela LIVRO.

SELECT CASE ASSUNTO

WHEN ‘B’ THEN TITULO || ‘-MUITO INTERESSANTE’

WHEN ‘P’ THEN TITULO || ‘-INTERESSE MÉDIO’

ELSE TITULO || ‘-SEM INTERESSE’

END AS IMPORTANCIA

FROM LIVRO

O segundo formato da expressão CASE permite que testes envolvendo diferentes colunas, variáveis e

expressões sejam avaliados. Sua estrutura é a seguinte:

CASE

WHEN EXPRESSAO_BOOLEANA THEN RESULTADO

WHEN EXPRESSAO_BOOLEANA2 THEN RESULTADO2

...

[ELSE RESULTADO_ELSE]

END

• Consideremos, novamente, o exemplo de instância para a tabela LIVRO apresentado na tabela 6.1. Para os livros que possuem data de lançamento e o preço é superior a R$ 45,00 retornar o título concatenado com a expressão ‘-LIVRO CARO’. Para aqueles que não possuem data de lançamento, mas possuem editora, retornar o título concatenado com a expressão ‘- LANCAMENTO EM BREVE’. Para aqueles que não possuem editora, retornar o título concatenado com a expressao ‘-LIVRO BARATO’.

SELECT CASE

WHEN LANCAMENTO IS NOT NULL AND PRECO > 40

THEN TITULO || ‘ – LIVRO CARO’

WHEN LANCAMENTO IS NOT NULL AND EDITORA IS NOT NULL

THEN TITULO || ‘ – LANCAMENTO EM BREVE’

235

WHEN EDITORA IS NULL

THEN TITULO || ‘ – EM PREPARACAO’

ELSE TITULO || ‘ – LIVRO BARATO’

END AS IMPORTANCIA

FROM LIVRO

CASE pode ser utilizado em consultas SELECT que tenham complexidade que desejemos. Pode, inclusive, ser

utilizado na cláusula WHERE. Exemplo:

• As várias editoras estão estudando a aplicação de diferentes fatores a reajustes de preços. Queremos saber quais os títulos dos livros que custarão acima de R$50,00 caso a editora ‘VIA-NORTE’ aplique um reajuste de 10% nos preços de seus livros e a editora ‘ILHAS TIJUCAS’ aplique um reajuste de 25%.

SELECT TITULO

FROM LIVRO

INNER JOIN EDITORA

ON EDITORA = EDITORA.CODIGO

WHERE 50 < (CASE WHEN NOME= ‘EDITORA VIA-NORTE’

THEN PRECO *1.1

WHEN NOME = ‘EDITORA ILHAS TIJUCAS’

THEN PRECO * 1.25

ELSE PRECO

END)

Comando MERGE

Suponhamos que tenhamos uma tabela de clientes. A descrição da tabela de clientes será apresentada na

figura 8.3. Esta tabela possui várias linhas, sendo que, algumas delas são referentes a pessoas que também atuam

como atores e que também estão cadastradas na tabela de autores.

Foi decidido que todos os autores serão

cadastrados como clientes. Para os autores que já se encontram cadastrados como clientes, deveremos atualizar

as informações na tabela de clientes com base nos dados da tabela de autores.

Para realizar tais operações utilizando os comandos INSERT e UPDATE, teríamos que percorrer todas as linhas

da tabela de autores, verificando cada uma se o autor já está cadastrado como cliente. Caso não esteja, teríamos

que realizar um comando INSERT para cadastrá-lo. Caso o autor já estivesse cadastrado, teríamos que fazer um

comando UPDATE, para atualizar essas informações na tabela de clientes. Para simplificar situações como essa,

foi criado o comando MERGE (algumas vezes conhecidos como UPSERT, devido às suas circunstâncias).

MERGE faz, automaticamente, comparações entre informações de tabelas, inserindo as linhas que não existem e

atualizando as outras. Uma sintaxe simplificada para o comando MERGE é apresentado a seguir:

MERGE INTO TABELA_DESTINO [AS APELIDO_DESTINO]

USING TABELA_ORIGEM [AS APELIDO_ORIGEM]

ON (EXPRESSAO_BOOLEANA)

WHEN MATCHED THEN OPERAÇÃO_VERDADEIRO

WHEN NOT MATCHED THEN OPERAÇÃO_FALSO

Vejamos, a seguir, como exemplo, a realização da inclusão e atualização dos dados de autores como clientes,

conforme descrito anteriormente, através do comando MERGE.

MERGE INTO CLIENTE CLI

236

USING AUTOR AU

ON (AU.CPF = CLI.CPF)

WHEN MATCHED THEN

UPDATE SET CLI.NOME = AU.NOME

CLI.ENDERECO = AU.NOME,

CLI.DATA_NASCIMENTO = AU.DATA_NASCIMENTO

WHEN NOT MATCHED THEN

INSERT(CODIGO, NOME, CPF, ENDERECO, DATA_NASCIMENTO)

VALUES (AU.MATRICULA, AU.NOME, AU.CPF, AU.ENDERECO, AU.DATA_NASCIMENTO)

Transações e Concorrência

Uma transação é formada por um conjunto de comandos SQL que são atômicos no que diz respeito à sua

execução e à recuperação do banco de dados. Cada comando executado em um banco de dados faz parte de uma

transação, mesmo que unitária. As propriedades ACID (Atomicidade, Consistência, Isolamento e Durabilidade) são

importantes propriedades em transações. Para implementá-las, os SGBD´s utilizam mecanismos que podem

reduzir o nível de concorrência do sistema como um todo.

SGBD´s são sistemas que, em situações reais podem ser acessados por milhares de usuários

concorrentemente. São comuns situações onde vários usuários realizam consultas e atualizações nos mesmos

dados. Na situação ideal, cada usuário de um banco de dados deveria executar os seus comandos como se ele

fosse o único usuário. Esse é o maior nível de isolamento a ser obtido. Entretanto, é muito baixo o nível de

concorrência correspondente a tal nível de isolamento. Neste caso, usuários poderão ficar aguardando por

informações durante tempos relativamente longos, à espera do término de outras transações.

De forma a aumentar os níveis de concorrência em detrimento do isolamento, a SQL define quatro níveis de

isolamento: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ e SERIAZABLE.

Iniciando, Alterando e Concluindo Transações

Se uma transação for concluída com sucesso, as alterações por ele realizadas não poderão ser desfeitas.

Mesmo que ocorram falhas no sistema, o SGBD deve prover mecanismos para recuperação de informações de

forma a manter o estado que as mesmas possuíam quando da confirmação do término de uma transação.

De forma análoga, se uma transação é concluída com fracasso, o SGBD deve desfazer todas as operações de

atualização dos dados contidas na transação em questão, retornando o banco de dados para o estado em que

estava ao início da transação.

Iniciando uma Transação

O comando a seguir é uma simplificação do comando de início de uma transação no padrão SQL:2003:

START TRANSACTION [NIVEL_DE_ISOLAMENTO]

Concluindo uma Transação

Uma transação pode ser concluída com sucesso ou com fracasso. Para concluirmos uma transação com

sucesso, utilizamos o comando:

COMMIT [WORK]

Para terminar uma transação com fracasso, ou seja, para desfazer todas as ações ocorridas durante a

transação, retornando ao seu estado inicial, utilizamos o comando:

ROLLBACK [WORK]

Por exemplo: Considere a instância da tabela ASSUNTO representa na tabela 3.2. Considere que uma transação

foi iniciada e que os dois comandos a seguir foram executados:

237

INSERT INTO ASSUNTO (SIGLA, DESCRICAO)

VALUES (‘X’, ‘XML’)

UPDATE ASSUNTO SET DESCRICAO = ‘BIOINFORMATICA’ WHERE SIGLA = ‘B’

A nova instância da tabela é representada na tabela 9.1. No entanto, essa instância está sendo válida para a

transação em questão. Então, consideremos que os comando a seguir foi realizado.

ROLLBACK

Neste caso, todas as alterações foram desfeitas, ou seja, a tabela ASSUNTO voltou a ter o conteúdo da tabela

3.2.

Se, ao invés de desfazermos as alterações, quiséssemos mantê-las, tomando a instância representada pela

tabela 9.1 definitiva, bastaria realizar o comando COMMIT ao invés do comando ROLLBACK.

Inserindo figura da página 8.1

Marcando pontos de retorno

A SQL permitem que sejam definidos marcadores durante uma transação chamados de pontos de

salvamento (SAVEPOINT). A transação pode, então, ser desfeita até um ponto de salvamento, tornando sem

efeito os comandos que foram executados após o mesmo.

Os pontos de salvamento somente são validos dentro de transações que foram definidos. Não é possível

retornar um ponto de salvamento após a conclusão da transação, quer seja com sucesso ou com fracasso.

Um ponto de salvamento pode ser definido através do comando:

SAVEPOINT IDENTIFICADOR_DO_SAVEPOINT

Para retornar a um ponto de salvamento, devemos utilizar o comando ROLLBACK em conjunto com a cláusula

TO SAVEPOINT, conforme mostrado a seguir:

ROLLBACK [WORK] TO SAVEPOINT IDENTIFICADOR_DO_SAVEPOINT

• Consideremos a instância da tabela ASSUNTO representada na tabela 3.2. Considere que uma transação foi iniciada e que os três comandos a seguir foram executados.

INSERT INTO ASSUNTO (SIGLA, DESCRICAO)

VALUES (‘X’, ‘XML’)

SAVEPOINT PONTO1

UPDATE ASSUNTO SET DESCRICAO = ‘BIOINFORMATICA’

WHERE SIGLA = ‘B’

A nova instância da tabela está representada na tabela 9.1. Então, consideremos que o comando a seguir foi

realizado:

ROLLBACK TO SAVEPOINT PONTO1

Neste caso, o comando UPDATE foi desfeito. O conteúdo temporário da tabela ASSUNTO está representado

na tabela 9.2, mas a transação continua ativa. Neste caso, novos comandos podem ser realizados antes da

finalização da transação. Se, neste ponto, o comando COMMIT for executado, a tabela ASSUNTO será confirmada

com conteúdo idêntico ao da tabela 9.2. Se um comando ROLLBACK for executado, conteúdo da tabela ASSUNTO

voltará a ser da tabela 3.2.

238

Para destruir um ponto de salvamento, devemos utilizar o comando:

Concorrência e Níveis de isolamento

Cada transação realizada em um SGBD tem um nível de isolamento. São quatro os níveis de isolamento

definidos no padrão SQL: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, e SERIAZABLE. O nível de

isolamento mais alto, SERIAZABLE, faz com que as operações sejam executadas como se a transação em questão

fosse a única ocorrendo no sistema. Esse nível de isolamento diminui o nível de concorrência. Por outro lado, os

níveis de isolamento mais baixos, que aumentam os níveis de concorrência podem levar a aparentes problemas

de consistência das informações.

Consideremos um ambiente onde não existe isolamento entre as transações. As alterações realizadas por um

usuário são imediatamente vistas por todos. Neste caso, dentre os fenômenos possíveis de ocorrer, temos:

• Leitura Suja – leitura de dados não confirmados. Suponha que ocorram os seguintes passos: 1. Duas transações, T1 e T2 são iniciadas; 2. A transação T1 modifica a linha L1; 3. A transação T2 lê a linha L1 antes que T1 termine; 4. T1 termina com fracasso e suas operações são desfeitas;

Neste caso, T2 terá lido uma informação que nunca foi confirmada.

• Leitura Não-Repetida: duas leituras de dados da mesma transação não se repetem. Na segunda leitura, dados antigos não existem ou foram modificados. Suponha que ocorram os seguintes passos: 1. Duas transações T1 e T2 são iniciadas; 2. A transação T1 lê a linha L1; 3. A transação T2 modifica a linha L1, atualizando seus dados ou apagando a linha; 4. A transação T2 é concluída com sucesso e suas operações são confirmadas; 5. A transação T1 lê (ou tenta ler) a linha L1. T1 nunca modificou os dados da linha L2, mas não obteve a

mesma leitura duas vezes dentro da mesma transação.

• Leitura Fantasma: na leitura de um conjunto de dados, surgem novas informações no conjunto. Suponha que ocorram os seguintes passos: 1. Duas transações T1 e T2 são iniciadas; 2. A transação T1 lê um conjunto de linhas que atendem a uma condição C1; 3. A transação T2 executa uma operação de atualização que cria uma ou mais que atendem à condição

C1; 4. A transação T1 lê, novamente, um conjunto de linhas que atendem a uma condição C1. Embora T1

não tenha criado linhas que atendam à condição C1, novas linhas participarão do resultado da consulta;

• Perda de atualização: duas transações que ocorrem simultaneamente atualizam o mesmo dado no banco de dados. Isto pode ocorrer em uma sequencia segundo a qual uma das atualizações é perdida: 1. Duas transações, T1 e T2, são iniciadas; 2. A transação T1 lê o dado D1 e armazena seu valor na variável X; 3. A transação T2 lê o dado D1 e armazena seu valor na variável Y; 4. A transação T1 atualiza D1 com o valor de 6*X; 5. A transação T2 atualiza D1 com o valor de 0.5*Y;

239

6. A transação T1 termina com sucesso; 7. A transação T2 termina com sucesso.

Para a transação T1, ao seu final, o valor de D1 deveria ter sido multiplicado por seis. Já para T2, ao seu final,

o valor de D1 deveria ser a metade do seu valor original. Se as transações T1 e T2 fossem executadas em série,

independente da ordem, o valor final de D1 deveria ser a metade do valor original. Entretanto, na ordenação de

comandos demonstrada antes, o valor final de D1 é a metade do original, como se a atualização realizada por T1

nunca tivesse sido realizada.

Os quatros níveis de isolamento da SQL podem ser definidos de acordo com sua relação com os quatro

fenômenos listados. Essa relação é mostrada na tabela 9.3.

O nível de

isolamento a ser utilizado pode ser definido através de uma cláusula nos comandos de início de uma transação,

conforme foi apresentado. Pode, também, ser definido em um comando de alteração das propriedades da

transação, o comando:

SET TRANSACTION ISOLATION LEVEL NIVEL_DE_ISOLAMENTO

Por exemplo, alterar o nível de isolamento de uma transação para SERIAZABLE:

SET TRANSACTION ISOLATION LEVEL SERIAZABLE

Programas e Gatilhos Armazenados

A linguagem SQL é uma linguagem diferente das linguagens tradicionais, como C e Pascal. Ao contrário de

linguagens como estas, no SQL especificamos o resultado que queremos obter, sem nos preocuparmos com a

sequencia de passos a serem para obtenção do resultado. Por isso, a SQL é classificada como uma linguagem

declarativa.

Por outro lado, SGBD´s se tornaram pontos cruciais em ambientes coorporativos, sendo acessos por diversos

sistemas. A partir daí, surgiram, em algumas situações, duas necessidades: (i) mover a especificação e execução

de rotinas que verificam e implementam regras de negócio para o próprio SGBD, centralizando, assim, tais

operações e as disponibilizando para toda a empresa de uma só vez, e (ii) fornecer ao SGBD algum tipo de

comportamento ativo (ou reativo) a determinadas situações.

De forma a permitir a migração das regras de negócio para o gerenciador de banco de dados foram definidos,

na SQL, os conceitos de procedimento e função armazenados. Estes são pequenos programas compilados,

armazenados e executados diretamente no servidor de banco de dados. São invocados através de comandos SQL.

Já para adicionar ao SGBD comportamento reativo, foi introduzido, na linguagem SQL, o conceito de gatilho.

Este é um mecanismo que permite a execução automática de comandos ou, até mesmo, programas, a partir de

eventos que ocorrem na base de dados (como a atualização do conteúdo de uma célula, por exemplo).

Para permitir a definição de blocos de programas a serem armazenados no SGBD, a SQL foi estendida com

comandos típicos de linguagens não-declarativas, como comandos de iteração e de decisão, por exemplo. Estes

blocos compõem o corpo não só de procedimentos e funções armazenados nos servidores, mas, também, de

240

gatilhos. Os SGBD´s apresentam também, conjuntos próprios de comandos de controle. No Oracle, a linguagem

de programação, chama-se PL/SQL. Já no SQL Server, chama-se Transact SQL.

Bloco de Comandos

Para permitir a criação de programas com SQL, foi criado o conceito de blocos de comandos. Além disso,

foram incorporados à linguagem vários comandos de controle similares aos existentes em outras linguagens de

programação.

Blocos de comandos são pequenos programas compostos de um ou mais comandos SQL. Permitem a

especificação de variáveis e cursores próprios aos blocos. Sua estrutura é a seguinte:

BEGIN

DECLARAÇÃO DE VARIÁVEIS

DECLARAÇÃO DE CURSORES

LISTA DE COMANDOS SQL

END

Existe, ainda, no padrão SQL:2003, a possibilidade de declaração de handles, objetos que irão tratar exceções

ocorridas durante a execução de blocos de comandos.

Declaração de Variáveis: em um bloco de comandos, podemos declarar quantas variáveis locais forem

necessárias. Para isso, utilizamos a seguinte estrutura:

DECLARE NOME_VARIAVEL1, NOME_VARIAVEL2, ...,

NOME_VARIAVELN TIPO_DE_DADOS

Cursores OPEN, FETCH e CLOSE: cursores são mecanismos que permitem que as linhas de uma tabela sejam

manipuladas uma a uma. Atuam como ponteiros que apontam para as linhas que formam o resultado de uma

dada consulta. Podemos recuperar e manipular os valores de cada linha apontada por um cursor.

Desta forma, deve existir um comando SELECT associado a um cursor. Para declarar um cursor e seu

comando associado, utilizamos o comando DECLARE CURSOR, conforme mostrado a seguir:

DECLARE CURSOR

FOR COMANDO_SELECT

[FOR UPDATE]

Um cursor não deve ser, somente, declarado. Para manipularmos os dados, devemos, inicialmente, abrir um

cursor. Para isso, utilizaremos o comando OPEN seguido do nome do cursor, conforme a seguir:

OPEN NOME_CURSOR

Neste momento, o resultado do comando SQL que define o cursor estará pronto para ser manipulado. Então,

para posicionarmos o ponteiro, devemos utilizar o comando FETCH. Esse comando irá posicionar o ponteiro em

uma dada linha e atribuir as informações apontadas para um conjunto de variáveis. Então, após a atribuição,

poderemos manipular as variáveis que recebem o valor de um cursor como tratamos quaisquer outras. FETCH

terá a seguinte estrutura:

FETCH NOME_CURSOR

INTO VARIAVEL1, ..., VARIAVELN

O comando FETCH é, usualmente, utilizado em conjunto com um comando de iteração, como os comandos

REPEAT e WHILE, que serão apresentados ainda nesta seção.

Ao final de sua utilização, o cursor deve ser fechado. Para fechar o cursor, utilizamos o comando CLOSE

seguido do nome do cursor, conforme representado a seguir:

241

CLOSE NOME_CURSOR

Através do comando FOR, poderemos declarar, abrir, navegar, e fechar cursores em um único comando. O

comando FOR será apresentado mais adiante.

Atribuição de Valores: podemos utilizar variáveis em blocos de comandos. Para atribuirmos valores a variáveis,

utilizaremos o comando SET da seguinte forma:

SET NOME_VARIAVEL = VALOR;

Por exemplo: Bloco de comando contendo a declaração de uma variável X, de tipo caractere de comprimento

cinco, e atribuição do valor ‘OI’ a X.

BEGIN

DECLARE X CHAR(5);

SET X = ‘OI’;

END

Comando FOR: FOR é um comando bastante útil, pois permite que, através de um só comando, seja declarado

como cursor, que seu conteúdo seja percorrido e que tal cursor seja fechado. O cursor é fechado

automaticamente ao final do comando. FOR deve ser utilizado quando a navegação se faz de forma seqüencial e

através de todas as linhas do resultado da consulta de declaração do cursor. Sua estrutura é mostrado a seguir.

FOR

NOME_CURSOR [CURSOR FOR]

COMANDO_SELECT

DO LISTA DE COMANDOS SQL

END FOR

Ao início de FOR o cursor é declarado e aberto. O ponteiro é posicionado na primeira linha do resultado da

consulta. Então, os comandos SQL contidos na lista de comandos são executados. Ao chegar a END FOR, a

execução retorna para FOR que, desta vez, percebe que o cursor já está aberto e posiciona o ponteiro na próxima

linha do resultado da consulta. Novamente, o conjunto de comandos SQL é executado. O laço se repete até que

todas as linhas resultantes do comando SELECT tenham sido percorridas.

Comando SELECT INTO: atribui um valor a uma variável. O valor atribuído é recuperado a partir de uma consulta.

A consulta deve retornar somente uma linha. A seguir é apresentado um formato para o comando.

SELECT COL1, COL2, ..., COLN

INTO VAR1, VAR2, ..., VARN

FROM NOME_TABELA

Comando IF: para permitir decisões, foi incorporado à linguagem SQL o comando IF. Esta testa se uma condição

booleana é verdadeira e, em função do resultado dos testes, executa um determinado conjunto de comandos.

Sua estrutura é:

IF CONDICAO_BOOLEANA THEN

LISTA DE COMANDOS SQL

[ELSE IF CONDICAO_BOOLEANA THEN

LISTA DE COMANDOS SQL]

[ELSE LISTA DE COMANDOS SQL]

END IF

242

Comando WHILE: permite que a execução de um conjunto de comandos se repita enquanto determina se a

condição é verdadeira. Sua estrutura é:

WHILE CONDICAO_BOOLEANA DO

LISTA DE COMANDOS SQL

END WHILE

Comando REPEAT: permite que a execução de um conjunto de comandos se repita enquanto determinada

condição for falsa. Os comandos serão executados ao menos uma vez, independente do valor da condição.

REPEAT

LISTA DE COMANDOS SQL

UNTIL CONDICAO_BOOLEANA

END REPEAT

Comando LOOP: assim como WHILE e REPEAT, LOOP permite a repetição na execução de um conjunto de

comandos. Entretanto, no caso de LOOP, não existe uma condição a ser testada. Para que a repetição termine,

outro comando deve ser utilizado. Segundo a SQL, o comando LEAVE termina o laço. Na estrutura de LOOP, que é

apresentado a seguir, temos LOOP e END LOOP como delimitadores do comando e conjunto de comandos a

serem executados, representados por uma lista de comandos SQL. LEAVE deve ser posicionado na lista de

comandos SQL.

LOOP

LISTA DE COMANDOS SQL

END LOOP

Procedimentos armazenados e Funções

Os blocos e comandos apresentados anteriormente permitem a construção de rotinas que serão executadas

no servidor de banco de dados. Tais rotinas podem possuir código extenso e serem bastante complexas.

Armazenar tais rotinas no servidor e permitr que sejam invocadas a partir da própria linguagem SQL aumenta em

muito a utilidade de sua construção. Para permitir essa definição e armazenamento, estão definidas, na

linguagem SQL, os conceitos de procedimentos e funções armazenados no servidor.

Procedimentos armazenados

Procedimentos armazenados (stored procedures) são procedimentos análogos aos existentes em linguagens

de programação tradicionais, mas que terão seu código-fonte armazenado no servidor de banco de dados, o que

é capaz de compilá-lo e executá-lo.

Assim como em outras linguagens, os procedimentos poderão receber valores como parâmetros. Esses

parâmetros são definidos no cabeçalho de procedimentos. Eles têm um nome, um tipo de dados e um modo.

Existem três modos para os parâmetros na SQL:2003: (i) parâmetros que permitem apenas que os valores

externos das variáveis sejam passados dentro do procedimento (idêntico às passagens por valor de outras

linguagens); (ii) modo onde as variáveis internas ao procedimento referentes aos parâmetros não recebem

valores das variáveis externas correspondentes, mas alterações de valores ocorridas nos procedimentos são

efetivadas nas variáveis externas correspondentes; e (iii) os valores externos são passados para dentro do

procedimento e as modificações ocorridas internamente são efetivadas para as variáveis externas (modo

semelhante às passagens por referência de outras linguagens). Parâmetros do primeiro modo são identificados

pela palavra reservada IN. A palavra OUT identifica os parâmetros do segundo modo. Já os parâmetros que

pertencem ao terceiro modo são identificados pela palavra reservada INOUT. O primeiro passo para a utilização

de procedimentos armazenados é a sua criação no servidor. Para criar um procedimento armazenado, utilizamos

o comando CREATE PROCEDURE. Uma estrutura básica para este comando é apresentado a seguir.

CREATE PROCEDURE NOME_PROCEDURE(

243

MODO_PARAM1 NOME_PARAM1 TIPO_PARAM1,

MODO_PARAM2 NOME_PARAM2 TIPO_PARAM2,

...

MODO_PARAMN NOME_PARAMN TIPO_PARAMN

BLOCO_DE_COMANDOS_SQL

Consideremos, por exemplo, o procedimento PRECO_MEDIO apresentado a seguir. Ele possui dois

parâmetros. O primeiro, NÚMERO_LIVROS, é um parâmetro de entrada que recebe do ambiente externo o

número total de livros a ser considerado. O segundo parâmetro, PRECO_MEDIO, é um procedimento e este valor

estará visível para a variável externa correspondente ao parâmetro. Após a declaração da linguagem utilizada,

está sendo criado um bloco de comandos SQL. Neste bloco, é declarada uma variável (V_VALOR_TOTAL), é

utilizado um cursor (MEUCURSOR), manipulado através do comando FOR, e o valor médio dos preços dos livros é

calculado. Tal valor é atribuído à variável PRECO_MEDIO, segundo parâmetro do procedimento.

CREATE PROCEDURE PRECO_MEDIO

(IN NÚMERO_LIVROS INTEGER, OUT PRECO_MEDIO REAL)

LANGUAGE SQL

BEGIN

DECLARE V_VALOR_TOTAL REAL;

SET PRECO_MEDIO = 0;

SET V_VALOR_TOTAL = 0;

FOR MEU_CURSOR AS

SELECT PRECO FROM LIVRO

DO

SET V_VALOR_TOTAL = V_VALOR_TOTAL + PRECO;

END FOR;

SET PRECO_MEDIO = V_VALOR_TOTAL / NÚMERO_LIVROS

END

Após a criação do procedimento, este fica armazenado no servidor de banco de dados e está pronto para ser

utilizado, bastando acioná-lo a partir de um bloco de comandos. Para isso, devemos utilizar seu nome e variáveis

correspondentes aos parâmetros entre parênteses, quando for o caso. A seguir temos um exemplo de chamada

ao procedimento PRECO_MEDIO:

BEGIN

DECLARE V_NUMERO INTEGER;

DECLARE MÉDIA REAL;

...

PRECO_MEDIO(V_NUMERO, MÉDIA);

...

END

Para destruirmos um procedimento de nosso banco de dados, devemos utilizar o comando DROP

PROCEDURE seguido do nome do procedimento a ser destruído, conforme mostrado a seguir.

DROP PROCEDURE NOME_PROCEDIMENTO;

Como exemplo, vamos destruir o procedimento PRECO_MEDIO:

DROP PROCEDURE PRECO_MEDIO;

Funções Armazenadas

244

Além de procedimentos, o SQL permite que armazenemos funções no servidor de banco de dados. A função

pode receber e tratar diversos parâmetros de uma mesma maneira que o procedimento. A diferença entre um

procedimento e uma função reside no fato de que a função sempre retorna um valor. Assim, para criar uma

função, utilizamos o comando CREATE FUNCTION ao invés do comando CREATE PROCEDURE. Como a função

retorna um valor, no comando CREATE FUNCTION devemos especificar o tipo de dados retornado pela função. O

exemplo da estrutura de CREATE FUNCTION é similar a CREATE PROCEDURE. A única diferença, além do nome do

comando, reside na introdução da cláusula RETURNS, onde o TIPO_RETORNO representa o tipo de dados

retornado pela função.

CREATE FUNCTION NOME_FUNÇÃO (

MODO_PARAM1 NOME_PARAM1 TIPO_PARAM1,

MODO_PARAM2 NOME_PARAM2 TIPO_PARAM2,

...

MODO_PARAMN NOME_PARAMN TIPO_PARAMN

) RETURN TIPO_RETORNO [LANGUAGE NOME_LINGUAGEM]

BLOCO DE COMANDOS SQL

Dentro do bloco de comandos do corpo da função devemos ter um comando RETURN seguido do valor a ser

retornado pela mesma. Exemplo:

RETURN 3.65;

A chamada para execução de funções é um pouco diferente da chamada para execução de procedimentos.

Como funções retornam um valor, o nome da função aparece ao lado direito do comando de atribuição de

valores. Por exemplo:

BEGIN

DECLARE V_NUMERO INTEGER;

DECLARE MÉDIA REAL;

...

SET MÉDIA = FUNC_PRECO_MEDIO(V_NUMERO);

...

END

Funções armazenadas podem, ainda, ser utilizadas em comandos SQL da mesma forma que as funções da

própria linguagem, já apresentadas anteriormente.

Para destruirmos funções, utilizamos o comando DROP FUNCTION seguido do nome da função.

DROP FUNCTION FUNC_PRECO_MEDIO

Gatilhos

Gatilhos são especificações de ações a serem realizadas sempre que um dado evento ocorrer sobre um dado

objeto. Entre os eventos possíveis, temos a inclusão, atualização ou exclusão de informações de uma tabela.

Um gatilho executa um conjunto de comandos, definido num bloco de comandos similar aos apresentados

anteriormente. Este bloco pode ser executado uma vez para cada evento que disparou o gatilho ou uma vez para

cada linha afetada pelo evento em questão. No primeiro caso, dizemos que se trata de um evento ao nível de

comando e, no segundo, ao nível de linha.

Entretanto, podemos querer que o bloco de comandos seja acionado somente em algumas circunstâncias e

não todas as vezes que o evento disparador do gatilho ocorrer. A linguagem SQL nos permite realizar tal ação

através da inclusão de uma condição booleana na declaração do gatilho. Devido às suas características, os

gatilhos são conhecidos por atenderem à regra ECA – Evento, Condição e Ação.

245

Um gatilho pode ser executado antes ou após a execução do comando que o disparou. Entretanto, em ambos

os casos, podemos querer acessar os dados nos formatos que teriam antes ou após a execução de tal comando.

Ou seja, podemos, por exemplo, após a realização de uma atualização, querer testar se o novo valor de uma dada

coluna é diferente do valor anterior.

Para permitir o acesso à duas versões das informações de uma dada linha dentro de um gatilho, foram

definidas as tabelas de transição NEW e OLD. NEW contém a nova versão das informações e OLD, a versão antiga.

Note que os eventos podem ser operações de inclusão, atualização e exclusão. No caso de uma operação de

inclusão, OLD está vazia, pois a informação não existia antes do evento. Caso o evento seja um operação de

exclusão, NEW está vazia, pois a informação não mais existe após o comando. Quando o evento disparador é uma

operação de atualização, tanto NEW quanto OLD possuem dados. A seguir, apresentaremos a sintaxe básica para

a declaração de um gatilho:

CREATE TRIGGER NOME_GATILHO

MOMENTO_EXECUÇÃO

EVENTO_DISPARADOR

ON TABELA_EVENTO

[REFERENCING NEW AS NOVO_NOME_N OLD AS NOVO_NOME_O]

[NIVEL_GATILHO]

[CONDICAO_EXECUCAO]

BLOCO_DE_COMANDOS_SQL

Por exemplo: Suponha que desejamos armazenar, sempre que um livro sofra um aumento de preço superior a

20%, seu código, seu preço antigo e seu novo preço. Para isto, criamos uma tabela chamada AUDITORIA que tem

três colunas: uma para armazenar o código do livro que sofreu aumento (CODIGO_LIVRO), a segunda para

armazenar o preço do livro antes do aumento (VALOR_ANTIGO) e a última (VALOR_NOVO), para armazenar o

novo preço do livro.

Para que a rotina de armazenamento das mudanças de preço ocorra de forma automática, criamos um

gatilho, chamado TESTA_AUMENTO, que é disparado sempre que a coluna PREÇO, da tabela LIVRO, é atualizada.

Então, para cada linha afetada pelo comando de atualização, se o novo valor for superior a 20% do antigo valor, o

código do livro, o seu preço antigo e seu novo preço são inseridos na tabela auditoria. Nesse gatilho, utilizaremos

N1 como apelido para a tabela NEW e O1 para apelido para a tabela OLD.

O comando de criação do gatilho descrito encontra-se a seguir:

CREATE TRIGGER TESTA_AUMENTO

AFTER UPDATE OF PRECO ON LIVRO

REFERENCING NEW AS N1 OLD AS O1

FOR EACH ROW

WHEN (N1.PRECO > 1.2 * O1.PRECO)

BEGIN

INSERT INTO AUDITORIA (CODIGO_LIVRO, VALOR_ANTIGO, VALOR_NOVO) VALUES (:N1.CODIGO, :O1.PRECO,

:N1.PRECO)

END;

Note que, no exemplo, utilizamos o caractere ‘;’ para referenciar, a partir do bloco de comandos SQL, as

variáveis que representam a tabela de transição. Note, ainda, que, para acessarmos os valores das colunas,

podemos utilizar o formato NOME_TABELA_TRANSICAO.NOME_COLUNA.

Não podemos executar gatilhos através de chamadas diretas, como fazemos com procedimentos e funções

armazenadas. A única maneira de executá-los é através de seu evento disparador.

Para apagar um gatilho, utilizamos o comando DROP TRIGGER seguido do nome do gatilho. Exemplo:

DROP TRIGGER TESTA_AUMENTO;

246

Extensões ao Relacional

No final da década de 80, a programação orientada a objetos alcançou um grande número de adeptos. O

debate entre os que defendiam os SGBD´s orientados a objetos e entre os adeptos dos SGBD´s relacionais tomou

maior importância. A partir deste, surgiram SGBD´s que incorporaram características de orientação a objeto aos

modelos relacionais: os SGBD´s estendidos ou relacionais a objeto.

Vários SGBD´s Relacionais começaram a se tornar relacionais estendidos, até que a SQL incorporou

comandos para criação de tipos do usuário e manipulação de dados não-convencionais, entre outros.

Tipos de Dados do Usuário

A primeira características dos SGBD´s relacionais estendidos é a possibilidade do usuário poder criar seus

próprios tipos de dados.

Tipos de dados definidos pelo usuário podem conter um ou mais atributos, sendo que cada atributo possui,

por sua vez, um tipo de dados próprio, podendo este ser um tipo predefinido ou um tipo de dados do usuário.

Tipos de dados do usuário em conjunto com seus atributos formam estruturas análogas ao struct da linguagem C

ou ao RECORD da linguagem Pascal.

Podemos, também, atribuir métodos a tipos de dados do usuário. Métodos são rotinas (procedimentos ou

funções) que podem ser acionados para tratamento e manipulação de informações.

Outro importante conceito proveniente de orientação a objetos é o de herança. Na orientação a objetos,

uma classe, chamada de subclasse, pode herdar atributos e métodos de outra classe, sua superclasse. Na

SQL:2003, um tipo de dados do usuário pode ser classificado como NOT FINAL poderão ter herdeiros (ou

descendentes), ou seja, seus atributos e métodos poderão ser herdados por outros tipos de dados.

CREATE TYPE NOME_TIPO [UNDER NOME_SUPER_TIPO] AS (

ATRIBUTO1 TIPO_ATRIBUTO1,

ATRIBUTO2 TIPO_ATRIBUTO2,

...

ATRIBUTON TIPO_ATRIBUTON

)

[TIPO_FINAL]

[METHOD NOME_MÉTODO1 (PARAMETROS_METODO1),

METHOD NOME_METODO2 (PARAMETROS_METODO2),

...

METHOD NOME_METODON(PARAMETROS_METODON)]

Consideremos que nossa base de dados armazena a informação de endereços em diversas tabelas. Para

tornar a definição de um endereço mais completa, gostaríamos de dividi-lo em vários campos: logradouro,

número, complemento, CEP, bairro, cidade e estado.

Como temos várias tabelas com colunas que armazenam endereços, decidimos, por exemplo, criar um tipo

de dados chamado TYP_ENDERECO, que contém os campos citados anteriormente com seus atributos. A criação

do tipo de dados TYP_ENDERECO é apresentado a seguir:

CREATE TYP_ENDERECO (

LOGRADOURO VARCHAR(100),

NÚMERO INTEGER,

COMPLEMENTO VARCHAR(15),

CEP CHAR(8),

BAIRRO VARCHAR(30),

CIDADE VARCHAR(40),

ESTADO VARCHAR(2) )

247

FINAL

No exemplo anterior, o tipo de dados TYP_ENDERECO foi definido como FINAL. Isso significa que ele não

pode ter suas propriedades herdadas por outros tipos de dados. TYP_ENDERECO não apresenta nenhum método.

Consideremos, agora, como exemplo, que desejamos tornar a definição de nomes mais bem estruturada,

criando dois atributos para um nome e sobrenome. Chamemos nosso tipo de dados de TYP_NOME. Além disso,

muitas vezes, desejamos obter o nome completo de uma pessoa. Para tal, criamos um método que seja capaz de

concatenar nome e sobrenome. Os atributos e métodos desse tipo de dados podem ser herdados por outros

tipos. A definição do tipo de dados, contendo atributos e métodos, é apresentado a seguir:

CREATE TYPE TIP_NOME(

PRIMEIRO_NOME VARCHAR(30),

SOBRENOME VARCHAR(30)

) NOT FINAL

METHOD NOME_COMPLETO RETURNS VARCHAR

Utilizando tipo de dados do usuário

Os tipos de dados definidos pelo usuário podem ser utilizados na definição de colunas. Assim, podemos criar

uma tabela CLIENTE que possui, dentre as suas colunas, a coluna NOME, de tipo de dados TYP_NOME, e a coluna

ENDERECO que tem, como tipo de dados, tipo TYP_ENDERECO. O comando de criação da tabela CLIENTE é

apresentado a seguir:

CREATE TABLE CLIENTES(

CODIGO INTEGER PRIMARY KEY,

NOME TYP_NOME NOT NULL,

ENDERECO TYP_ENDERECO NOT NULL

CPF CHAR(11) CONSTRAINT UK_CPF UNIQUE,

DATA_NASCIMENTO DATE,

TELEFONE VARCHAR(12) )

Para utilizarmos colunas cujos tipos são definidos pelo usuários em comandos de seleção, inclusão,

atualização e exclusão, devemos utilizar o construtor do tipo, que podemos o nome do próprio tipo de dados.

Alterando o tipo de dados do usuário

Em varias situações pode ser necessários alterar um tipo de dados definido pelo usuário. Podemos, por

exemplo, querer: (i) adicionar atributos, (ii) remover atributos, (iii) adicionar novos métodos e (iv) remover

métodos existentes. Para atender a essas necessidades, foi definido o comando ALTER TYPE.

Podemos utilizar ALTER TYPE em diferentes construções. A estrutura a seguir deve ser utilizada para

adicionarmos um atributo a um tipo.

ALTER TYPE NOME_TIPO

ADD ATTRIBUTE NOME_ATRIBUTO TIPO_ATRIBUTO

Exemplo:

ALTER TYPE TYP_ENDERECO

ADD ATTRIBUTE PAIS VARCHAR(35)

Já para remover um atributo, utilizamos o comando ALTER TYPE com o formato abaixo.

ALTER TYPE NOME_TIPO

DROP ATTRIBUTE NOME_ATRIBUTO [RESTRICT]

248

RESTRICT reforça a idéia de que nenhum atributo pode ser apagado se o tipo de dados estiver sendo utilizado na

definição de qualquer objeto de b d.

Exemplo:

ALTER TYPE TYP_ENDERECO

DROP ATTRIBUTE PAIS

Para adicionar novos métodos, utilizamos o commando com o formato abaixo:

ALTER TYPE NOM_TIPO

ADD METHOD NOME_METODO [PARAMETROS_METODO]

RETURNS TIPO_RETORNO

Após a inclusão do método, seu corpo deve ser especificado. Tal especificação ocorre de forma análoga à

apresentada anteriormente.

Já para removermos um método do tipo definido pelo usuário, utilizamos o comando ALTER TYPE conforme

mostrado a seguir.

ALTER TYPE NOME_TIPO DROP METHOD NOME_METODO

Removendo tipos de dados do usuário

Tipos de dados do usuário podem ser excluídos. Para isso, utilizamos o comando DROP TYPE seguido do

nome do tipo de dados em questão. A seguir, teremos o exemplo de exclusão dos tipos TYP_CLIENTE e

TYP_ENDERECO.

DROP TYPE TYP_CLIENTE

DROP TYPE TYP_ENDERECO

Para que possamos excluir um tipo de dados, ele não deve estar sendo utilizado por nenhuma tabela e nem

por outro tipo de dados.

Armazenando e referenciando objetos

Os objetos (de dados) criados a partir dos tipos definidos pelo usuário possuem um identificador único,

chamado OID (object identifier). Podemos referenciar os objetos, através de seu OID, utilizando o tipo REF.

REF é um ponteiro para objetos de um determinado tipo de dados do usuário. Permite implementar vínculos

entre relações onde em um dos lados existe um objeto. Desta forma, podemos criar colunas ou atributos que

sejam do tipo REF e que armazenam um ponteiro para um objeto de um dado tipo. Ao valor da coluna, podemos

atribuir o valor NULL ou uma referência ao objeto. Podemos utilizar o valor contido em REF para recuperar o

objeto que é apontado e para atualizar seu valor.

Para criarmos um atributo ou coluna que armazene uma referência, devemos utilizar a construção:

NOME_COLUNA REF NOME_TIPO_DE_DADOS_USUARIO

Exemplo: Considere que tenhamos montado um banco de dados para armazenar as equipes que participam de

um torneio. De cada jogador, queremos armazenar o nome e a equipe a que pertence. De cada equipe,

desejamos armazenar o nome e o nome de seu capitão. Para isso, iremos criar dois tipos de dados.

Iniciamos criando o tipo que representa as informações de equipes que, por enquanto, contém apenas o

atributo nome.

CREATE TYPE TYP_EQUIPE AS (

NOME VARCHAR(30)

)

Então, criamos o tipo de dados para representar as informações de cada jogador. Este tipo contém dois

atributos: o nome, e uma referência para um objeto do tipo referenciado.

249

CREATE TYPE TIP_JOGADOR AS (

NOME VARCHAR(50),

EQUIPE REF(TYP_EQUIPE))

Por fim, alteramos TYP_EQUIPE para conter uma referência para um objeto que contém as informações do

capitão da equipe:

ALTER TYPE TYP_EQUIPE ADD ATTRIBUTE CAPITAO REF(TYP_JOGADOR)

A SQL:2003 nos permite criar tabela que sejam repositórios de objetos. Nestas tabelas, somente temos um

objeto. Uma sintaxe simplificada para sua criação é:

CREATE TABLE NOME_TABELA OF NOME_TIPO_DADOS (

REF IS NOME_COLUNA,

NOME_COLUNA_TIPO_REF WITH OPTIONS

SCOPE NOME_TABELA_REFERENCIADA,

NOME_COLUNA_TIPO_REF2 WITH OPTIONS

SCOPE NOME_TABELA_REFERENCIADA2,

NOME_COLUNA_TIPO_REFN WITH OPTIONS

SCOPE NOME_TABELA_REFERENCIADAN

)

Consideremos o tipo de dados TYP_EQUIPE e TYP_JOGADORES, mencionados anteriormente. Vamos, agora,

criar as tabelas EQUIPES e JOGADORES de forma a armazenar objeto dos dois tipos, respectivamente.

Inicialmente, criamos a tabela EQUIPES, definindo OID como nome da coluna de referência.

Comando:

CREATE TABLE EQUIPES OF TYP_EQUIPE (REF IS OID)

Então, criamos a tabela JOGADORES, definindo, também, uma coluna de nome OID como coluna de

referência. Na criação, definimos, ainda, que o atributo EQUIPE aponta para objetos contidos na tabela EQUIPES.

Comando:

CREATE TABLE JOGADORES OF TYP_JOGADOR (REF IS OID,

EQUIPE WITH OPTIONS SCOPE EQUIPES)

Agora, utilizamos o comando ALTER TABLE para modificar a tabela EQUIPES de forma a fazer com que a

coluna CAPITAO referencie objetos contidos na tabela JOGADORES. O comando a ser utilizado, apresentado a

seguir, permite alterar a definição de uma coluna, adicionando um escopo para sua referência.

ALTER TABLE EQUIPES ALTER CAPITAO ADD SCOPE JOGADORES

Vamos, agora, como exemplo, incluir equipes e jogadores nas tabelas. Começamos incluindo três equipes

chamadas ‘Equipe Azul’, ‘Equipe Verde’ e ‘Equipe Laranja’. Os comandos de inclusão são apresentados a seguir.

INSERT INTO EQUIPES (OID, NOME)

VALUES (TYP_EQUIPE(‘1’), ‘EQUIPE AZUL’)

INSERT INTO EQUIPES (OID, NOME)

VALUES (TYP_EQUIPE(‘2’), ‘EQUIPE VERDE’)

INSERT INTO EQUIPES (OID, NOME)

VALUES (TYP_EQUIPE(‘3’), ‘EQUIPE LARANJA’)

Note, nos comandos anteriores, que não incluímos valores para o atributo CAPITAO. Além disso, utilizamos o

nome do tipo de dados como construtor para a obtenção de um OID a partir dos valores ‘1’, ‘2’ e ‘3’.

250

Podemos, também, incluir dados na tabela de jogadores. Vamos incluir três jogadores para a Equipe Azul:

João, André, e Pedro. Para isso, executamos os comandos a seguir:

INSERT INTO JOGADORES (OID, NOME, EQUIPE)

VALUES (

TYP_JOGADOR(‘1’), ‘JOAO’, (SELECT OID FROM EQUIPES WHERE NOME= ‘EQUIPE AZUL’))

INSERT INTO JOGADORES (OID, NOME, EQUIPE) VALUES (

TYP_JOGADOR(‘2’), ‘ANDRÉ’, (SELECT OID FROM EQUIPES WHERE NOME= ‘EQUIPE AZUL’))

INSERT INTO JOGADORES (OID, NOME, EQUIPE) VALUES (

TYP_JOGADOR(‘3’), ‘PEDRO’, (SELECT OID FROM EQUIPES WHERE NOME= ‘EQUIPE AZUL’))

Nos comandos anteriores, utilizamos uma subconsulta para recuperar o objeto referente à equipe dos

jogadores e inseri-lo na tabela JOGADORES. Vamos, agora, marcar o jogador André como capitão da equipe Azul.

UPDATE EQUIPE

SET CAPITAO = ( SELECT OID FROM JOGADORES WHERE NOME = ‘ANDRÉ’)

WHERE NOME = ‘EQUIPE AZUL’

Podemos, também, realizar consultas sobre os dados contidos em JOGADORES e em EQUIPES. Vejamos

exemplos:

• Desejamos selecionar o nome do capitão da ‘Equipe Azul’. SELECT CAPITAO -> NOME

FROM EQUIPES

WHERE NOME = ‘EQUIPE AZUL’

• Desejamos selecionar o nome da equipe do jogador ‘JOAO’. SELECT EQUIPE -> NOME

FROM JOGADORES

WHERE NOME = ‘JOAO’

• Desejamos selecionar o nome do capitão da equipe do jogador ‘JOAO’. SELECT EQUIPE -> CAPITAO -> NOME

FROM JOGADORES

WHERE NOME = ‘JOAO’

Nestes comandos utilizamos ‘->’ para acessar os campos dos objetos referenciados pelas colunas de tipo REF.

No terceiro exemplo, a partir do objeto referente ao jogador ‘JOAO’ acessamos o objeto referente à ‘Equipe Azul’,

na tablea de equipes, e, então, acessamos o objeto referente ao jogdor capitão da equipe, ‘André’, para obter o

seu nome.

A SQL:2003 define, também, dois tipos de coleções de objetos: arrays e multisets. Arrays são coleções onde

cada elemento participante possui uma posição fixa e pode ser acessado por sua posição na coleção. Multisets

são coleções onde não existe ordenação dos elementos. Assim, Multisets são coleções onde não existe ordenação

dos elementos. Assim, não há um índice através do qual acessemos um dado elemento.

Extensão para OLAP

A SQL:2003 apresenta diferentes estruturas para atender a necessidades de aplicações OLAP. Dentre elas,

temos os elementos de agrupamento ROLLUP, CUBE e GROUPING SETS, que são apresentados a seguir.

Anteriormente foi apresentada a cláusula GROUP BY. Ela faz com que dados sejam agrupados através de uma

ou mais colunas e permite que se apliquem funções sobre as linhas que participam de cada grupo. ROLLUP, CUBE,

251

e GROUPING SETS são empregados em conjunto com GROUP BY, provendo diferentes comportamentos a essa

cláusula.

ROLLUP

Quando utilizamos ROLLUP, o resultado obtido contém linhas representando a realização do agrupamento

em vários níveis. Sua sintaxe de utilização é a seguinte:

SELECT COL1, COL2, ..., COLN, FUNCAO1, ..., FUNCAON

FROM NOME_TABELA

GROUP BY ROLLUP (COL1, ..., COLN)

Como exemplo, considere uma base de dados de vendas. Assuma que existe uma tabela onde são

armazenadas informações das vendas realizadas por uma empresa nos diversos estados do país. São

armazenados os produtos vendidos, a data das vendas e as quantidades vendidas em cada operação. Um

exemplo desta tabela é apresentado na Tabela 11.1.

Considere, como exemplo, que desejamos saber a quantidade total vendida em cada combinação de

<produto, mês, estado> existente na tabela. Desejamos que a listagem esteja ordenada por estado, mês de

venda, e nome do produto. Para isso, devemos utilizar a consulta a seguir.

SELECT ESTADO, MONTH(DATA) AS MÊS, PRODUTO, SUM(QUANTIDADE) AS TOTAL

FROM VENDA

GROUP BY ESTADO, MONTH(DATA), PRODUTO

ORDER BY ESTADO, MONTH(DATA), PRODUTO

Considere que, agora, desejamos montar uma listagem que apresente os totais de vendas, não só para as

diversas combinações de <produto, mês, estado>, mas, também, para cada par <mês, estado> e para cada estado

isoladamente. Vamos, então, utilizar o operador ROLLUP:

SELECT ESTADO, MONTH(DATA) AS MÊS, PRODUTO,

SUM(QUANTIDADE) AS TOTAL

FROM VENDA

GROUP BY ROLLUP (ESTADO, MONTH(DATA), PRODUTO)

ORDER BY ESTADO, MONTH(DATA), PRODUTO

Note a diferença entre os resultados da utilização da cláusula GROUP BY com e sem o operador ROLLUP.

Quando utilizamos o operador ROLLUP, além das linhas obtidas com a realização do agrupamento, são obtidas

novas linhas que contêm os subtotais para cada nível de agrupamento. Uma linha contendo o total geral também

participa do resultado.

Grouping Sets

Já a utilização do operador GROUPING SETS faz com que o agrupamento seja realizado, isoladamente, por

cada uma das expressões contidas na cláusula GROUP BY. Ou seja, o resultado de uma consulta com a utilização

de GROUPING SETS em um cláusula GROUP BY com n expressões é idêntico à união dos resultados de n

comandos com a cláusula GROUP BY utilizada isoladamente e, em cada um dos n comandos, somente uma das

colunas n originais está presente em GROUP BY.

GROUPING SETS é utilizado de forma idêntica a ROLLUP, bastando substituir no comando de consulta, ROLLUP

por GROUPING SETS. Vejamos, como exemplo, a utilização de GROUP SETS na nossa tabela de vendas.

SELECT ESTADO, MONTH(DATA) AO MÊS, PRODUTO, SUM(QUANTIDADE) AS TOTAL

FROM VENDA

GROUP BY GROUPING SETS (ESTADO, MONTH(DATA), PRODUTO)

252

ORDER BY ESTADO, MONTH(DATA), PRODUTO

Note, no exemplo, anterior, que o resultado final é idêntico ao que seria obtido se realizássemos a consulta

três vezes, onde, em cada uma, somente o comando GROUP BY e uma das três expressões de agrupamento

(ESTADO, MONTH(DATA), PRODUTO) fossem utilizados.

CUBE

Este operador realizar agrupamentos por cada combinação possível das expressões contidas na cláusula

GROUP BY. Ele apresenta, também, uma linha com informação condensada de toda a tabela. É utilizado de forma

idêntica a ROLLUP e a GROUP SETS. Vejamos um exemplo de consulta utilizando a tabela VENDA:

SELECT ESTADO, MONTH(DATA) AS MÊS, PRODUTO, SUM(QUANTIDADE) AS TOTAL

FROM VENDA

GROUP BY CUBE(ESTADO, MONTH(DATA), PRODUTO)

ORDER BY ESTADO, MONTH(DATA), PRODUTO

Notamos que estão presentes no resultado aqueles que seriam obtidos se realizássemos várias consultas

com a cláusula GROUP BY e diversas combinações entre as colunas ESTADO e PRODUTO e a expressão

MONTH(DATA).

Nos exemplos anteriores, utilizamos a função SUM. Comportamento análogo pode ser obtido se utilizarmos

outras funções agregadas, como SUM. No caso da utilização de AVG em conjunto com ROLLUP, por exemplo,

obtemos a média por produto, mês e estado, a média de vendas por mês e estado, a média por estado e, enfim, a

média final de vendas.

Privilégios e Papéis

Controle de privilégios é um importante mecanismo existente em SGBD´s. através dele é possível garantir

que os usuários realizem apenas as operações que lhe são permitidas.

Todo acesso a um SGBD é realizado em conjunto com a identificação de um usuário ou tipo de usuário, seja

de forma implícita ou explícita. A cada usuário podem estar associados diversos privilégios. De acordo com os

privilégios que possui, o usuário pode, por exemplo, se conectar ao banco de dados, criar outros usuários e ler,

alterar, incluir ou apagar dados de uma ou mais tabela.

É possível ainda, por exemplo, configurar o sistema de forma que um determinado usuário conectado ao

SGBD não saiba da existência de uma ou mais tabelas. Para isso, basta não atribuir a tal usuário os privilégios

mínimos para que tome conhecimento da existência das tabelas em questão.

De forma a facilitar o gerenciamento e a atribuição de privilégios a usuários, o padrão SQL define a

possibilidade de criação de papéis (roles), os quais podem ser tratados como conjuntos de privilégios que podem

ser atribuídos a usuários.

Usuários

O padrão SQL define o conceito de identificadores de usuários como a representação no SGBD de usuários do

mundo real. Desta forma, identificadores de usuários representam usuários do SGBD.

De acordo com o que foi definido no padrão SQL, toda sessão SQL possui um identificador de usuário a ela

associado. De acordo com os privilégios, que o identificador de usuário da sessão possuir, são definidos os

comandos que podem ou não ser executados na referida sessão.

É comum que usuários do banco de dados representem aplicações e não usuários reais. Do ponto de vista do

SGBD, no que se refere ao controle de acesso e segurança, esses tipos de usuários não são diferentes entre si,

devendo ser tratados de maneira similar, havendo também a necessidade de atribuição e revogação de

privilégios.

253

Privilégios

O padrão SQL define um privilegio como sendo uma autorização para que uma dada ação seja executada.

Estas ações geralmente incidem sobre um objeto de banco de dados, como tabelas, visões, gatilhos ou colunas,

por exemplo.

Dentre as possíveis ações indicadas no padrão SQL, temos:

• INSERT [LISTA_DE_COLUNAS] – LISTA DE COLUNAS é opcional. Permite incluir somente valores para as colunas especificadas na LISTA_DE_COLUNAS, caso esta lista tenha sido especificada, ou para todas as colunas de uma tabela/visão, caso a lista não tenha sido especificada.

• UPDATE [LISTA_DE_COLUNAS] – LISTA_DE_COLUNAS é opcional. Permite atualizar as colunas de uma tabela/visão especificadas na LISTA_DE_COLUNAS, caso essa lista tenha sido especificada, ou em todas as colunas, caso essa lista não tenha sido especificada.

• DELETE – Apagar dados em uma tabela.

• SELECT [LISTA_DE_COLUNAS] – LISTA_DE_COLUNAS é opcional. Permite selecionar valores de uma tabela/visão, somente para as colunas especificadas na LISTA_DE_COLUNAS, caso esta lista tenha sido especificada, ou para todas as colunas, caso essa lista não tenha sido especificada.

• REFERENCES [LISTA_DE_COLUNAS] – LISTA_DE_COLUNAS é opcional. Permite referenciar uma tabela na criação de chaves estrangeiras. Somente as colunas especificadas na LISTA_DE_COLUNAS poderão ser referenciadas, caso essa lista tenha sido especificada, ou qualquer coluna, caso a lista não tenha sido especificada.

De forma a melhorar os mecanismos de controle de acesso e segurança, os Gerenciadores de Banco de dados

apresentam diversos privilégios além dos especificados no padrão. Cada SGBD possui seu próprio modelo de

privilégios. Nesses modelos, os privilégios são classificados, geralmente, ao menos como privilégios de objeto e

privilégios de sistema.

De toda a forma, são comuns em SGBD´s privilégios específicos para:

• Conectar-se a uma instância do SGBD ou a uma de suas bases de dados;

• Criar e destruir objetos do banco de dados, como tabelas, índices, visões, gatilhos, e procedimentos armazenados, dentre outros.

• Consultar, atualizar, incluir e excluir dados em tabelas (e em visões, para gerenciadores que suportam a realização dessas operações nas mesmas).

• Executar procedimentos e funções armazenados.

• Alterar as características do sistema e de suas bases de dados, tais como parâmetros de inicialização.

• Realizar operações de geração de cópias de segurança (backup) e recuperação de tais cópias (restore).

Em geral, o usuário analista de sistema ou programador não necessitará possuir todos esses privilégios. A

maioria de tais privilégios se relaciona com ações que são realizadas especificamente pelo administrador do

banco de dados.

Usualmente, é o administrador do banco de dados que concede os privilégios necessários para que outros

usuários realizem suas ações.

Atribuindo privilégios a um usuário

Para atribuirmos um privilégio a um usuário (ou seja, para permitimos que um usuário realize uma

determinada ação), devemos utilizar o comando GRANT, cuja sintaxe é exibida a seguir:

GRANT PRIVILEGIOS

TO NOME_PRIVILEGIADO

[WITH HIERARCHY OPTION][WITH GRANT OPTION]

[GRANTED BY NOME_CONCEDENTE]

É importante notar que, para poder atribuir um privilégio a um dado usuário, o usuário concedente deve

possuir privilégios para tal.

Como exemplo, consideremos que deve ser permitido ao usuário MARIA realizar qualquer operação dentro

da tabela EDITORA. Para atribuir ao referido usuário tais privilégios, pode ser usada a cláusula ALL PRIVILEGES.

254

GRANT ALL PRIVILEGES ON EDITORA TO MARIA

Considere-se, ainda, que este usuário deveria poder, também, selecionar e atualizar dados da tabela LIVRO,

mas que não lhe deve ser permitido incluir ou apagar dados desta tabela. O usuário MARIA poderá, ainda,

conceder privilégios que possui na tabela LIVRO a outros usuários. Para permitir que o usuário MARIA realize tais

ações, executa-se o comando a seguir:

GRANT SELECT, UPDATE ON LIVRO TO MARIA WITH GRANT OPTION

Já no que se refere à tabela ASSUNTO, o usuário MARIA somente pode alterar apenas a coluna DESCRIÇÃO.

Para atribuir à Maria privilégios necessários para realizar essa operação, deve ser executado o seguinte comando:

GRANT UPDATE(DESCRIÇÃO) ON ASSUNTO TO MARIA

Conforme mencionado anteriormente, para que os comandos GRANT apresentados nos exemplos possam

ser executados com sucesso, devem ser executados por um usuário que possua os privilégios adequados.

Usualmente, esse usuário representa o administrador do banco de dados, o qual possui privilégios de sistema

que lhe permitem realizar todas, ou quase todas as operações do banco de dados. No entanto, podem, também,

ser executados por um usuário que tenha recebido tais privilégios com a opção WITH GRANT OPTION, que lhe

permite atribuir privilégios recebidos a outros usuários.

Privilégios referentes à manipulação de dados em objetos podem, ainda, ser executados pelo usuário ‘dono’

do objeto em questão. Usualmente, o ‘dono’ de um objeto de banco de dados é o usuário que o criou.

Removendo privilégios de um usuário

Após o usuário ter recebido o privilegio para executar uma ação, ele irá manter esse privilégio até que o

mesmo seja explicitamente revogado. Para revogar um ou mais privilégios de um usuário, deve-se utilizar o

comando REVOKE. Sua estrutura simplificada é mostrada a seguir:

REVOKE [GRANT OPTION FOR] PRIVILÉGIOS FROM NOME_PRIVILEGIADO

Como exemplo, consideremos que o usuário MARIA não deve mais possuir a habilidade de alterar dados da

tabela EDITORA. Para tal, pode-se executar comando como o apresentado a seguir.

REVOKE UPDATE ON EDITORA FROM MARIA

Consideremos, agora, que não deve mais ser permitido ao usuário MARIA atribuir a todos os outros usuários

o privilégio de atualização da tabela LIVRO. Para tal, pode-se executar o comando apresentado a seguir, que

revoga essa habilidade do usuário em questão, mas também a possibilidade que o referido usuário possa

atualizar dados na tabela LIVRO.

REVOKE GRANT OPTION FOR UPDATE ON LIVRO FROM MARIA

Podemos ainda considerar a situação onde não mais deve ser permitido ao usuário realizar quaisquer ações

na tabela ASSUNTO. Para tal, pode-se executar o comando a seguir, que retira do usuário os privilégios para

executar ações na tabela ASSUNTO.

REVOKE ALL PRIVILEGES ON ASSUNTO FROM MARIA

Papéis

Em um ambiente de banco de dados de produção podem existir diversos usuários e algumas centenas ou até

mesmo milhares de tabelas, entre outros objetos. Muitos dos usuários podem utilizar o mesmo conjunto de

objetos, devendo ter privilégios para executar conjuntos similares de ações.

Atribuir privilégios referentes a cada um dos objetos para cada usuário de forma individual pode ser uma

tarefa bastante trabalhosa. Além disso, a medida que aumenta o número de tabelas ou outros objetos sobre as

quais cada usuário deve ter privilégios, aumenta também a probabilidade de que, por erro ou por esquecimento

255

pode ficar desapercebido num primeiro momento. No entanto, poderá causar problemas mais graves, por

exemplo, quando a aplicação que utilize o usuário em questão para acessar o banco de dados esteja em

produção.

Para facilitar as tarefas de administração do banco de dados relacionadas com a atribuição e revogação de

privilégios, e reduzir o número de erros nessas operações, foi definido o conceito de Papel (Role).

Um Papel visa representar todas as ações que um ou mais usuários podem realizar no banco de dados.

Assim, após criar um papel, é necessário atribuir-lhe todas as ações que representa. Em seguida, esse papel pode

ser atribuído a um ou mais usuários. Todos os usuários a quem o papel for atribuído receberão todos os

privilégios que foram atribuídos ao papel em questão. A qualquer momento é possível atribuir ou revogar

privilégios de um papel, e atribuir ou revogar o papel de um ou mais usuários.

Desta forma, ao ser criado um novo usuário B que deva possuir os mesmos privilégios para execução de

ações que outro usuário A já existente, podemos atribuir ao usuário B os mesmos papéis que tiverem sido

atribuídos ao usuário A.

Criando e utilizando Papéis

Para criamos um papel, utilizamos o comando CREATE ROLE. Sua sintaxe simplificada é a seguinte:

CREATE ROLE NOME-ROLE

Consideremos a base de dados de livros que será utilizada por vários usuários que podem ser agrupados

segundo dois diferentes perfis: um denominado BIBLIOTECA e outro denominado VISITANTE. Para criar esses

papéis, utilizamos o comando a seguir:

CREATE ROLE BIBLIOTECA

CREATE ROLE VISITANTE

Os papéis somente serão úteis se a eles forem associados os privilégios aos quais correspondem. Para

aqueles que possuírem o papel BIBLIOTECA, deverá ser permitido realizar quaisquer operações sobre os dados

das tabelas ASSUNTO, EDITORA, AUTOR, LIVRO e AUTOR_LIVRO. É necessário, então, que tais privilégios sejam

atribuídos ao papel BIBLIOTECA. Isto pode ser feito pelo comando GRANT, de forma similar ao que foi realizado

para a atribuição de privilégios para um usuário. Nesse caso, deve-se usar o nome do papel na posição de NOME-

PRIVILEGIADO.

Os cinco comandos a seguir apresentam a atribuição dos privilégios para o papel BIBLIOTECA.

GRANT ALL PRIVILEGES ON ASSUNTO TO BIBLIOTECA

GRANT ALL PRIVILEGES ON EDITORA TO BIBLIOTECA

GRANT ALL PRIVILEGES ON AUTOR TO BIBLIOTECA

GRANT ALL PRIVILEGES ON LIVRO TO BIBLIOTECA

GRANT ALL PRIVILEGES ON AUTOR_LIVRO TO BIBLIOTECA

Além de atribuir os privilégios ao papel, faz-se necessário atribuir o papel a cada usuário que deva ter o perfil

em questão.

Caso deva ser permitido aos usuários MARIA, JOÃO e ANA o acesso completo aos dados das tabelas

ASSUNTO, EDITORA, AUTOR, LIVRO e AUTOR_LIVRO, podemos atribuir o papel BIBLIOTECA a tais usuários. Para

isso, utilizamos o comando GRANT de forma similar ao apresentado anteriormente. Porém, neste caso,

PRIVILÉGIOS será substituído pelo nome do papel. O NOME_PRIVILEGIADO irá se referir ao usuário que deverá

possuir os mesmos privilégios que foram atribuídos ao papel. O comando a seguir exemplifica a atribuição do

papel BIBLIOTECA aos usuários MARIA, JOÃO e ANA.

GRANT BIBLIOTECA TO MARIA, JOAO, ANA

256

Após a execução do comando anterior, os usuários MARIA, JOÃO e ANA poderão executar todas as ações

definidas nos privilégios atribuídos ao papel BIBLIOTECA.

Se considerarmos que, após uma reavaliação dos critérios de segurança do banco de dados, seja definido, por

exemplo, que o papel BIBLIOTECA não deve ter acesso aos dados da tabela EDITORA, podemos revogar os

privilégios que possui com a utilização do comando REVOKE.

A utilização do comando REVOKE, nesse caso, será similar à apresentada anteriormente, sendo necessário

que se utilize, como NOME_PRIVILEGIADO, o nome do papel em questão. O exemplo a seguir retira os privilégios

sobre a tabela EDITORA do papel BIBLIOTECA.

REVOKE ALL PRIVILEGES ON EDITORA FROM BIBLIOTECA

Ao executarmos o comando anterior, alguns privilégios para executar ações que são revogados do papel

BIBLIOTECA e, consequentemente, de todos os usuários que possuem o referido papel. Ou seja, segundo nosso

exemplo, os privilégios para executar ações sobre a tabela EDITORA foram removidos dos usuários MARIA, ANA e

JOÃO. De fato, se for necessário atribuir um novo privilégio ou revogar um privilégio do papel que os usuários

possuem.

É possível, também, revogar de um usuário a sua participação em um papel. Para isso, também é utilizado o

comando REVOKE. Neste caso, o nome do papel será utilizado em PRIVILEGIOS e o nome do usuário em

NOME_PRIVILEGIADO.

Consideremos que o usuário ANA não deva mais ter os privilégios definidos para o papel BIBLIOTECA. O

exemplo a seguir revoga o referido papel do usuário ANA.

REVOKE BIBLIOTECA FROM ANA

Removendo Papéis

Para removermos papéis, podemos utilizar o comando DROP ROLE. Sua sintaxe básica é:

DROP ROLE NOME_ROLE

O comando a seguir exemplifica a exclusão do papel BIBLIOTECA através do comando DROP ROLE.

DROP ROLE BIBLIOTECA

Ao removermos um papel, ele é automaticamente retirado de todos os usuários a quem tenha sido atribuído.

Desta forma, não será mais permitido aos usuários que executem as ações definidas no papel que foi removido, a

menos que os privilégios para tal sejam diretamente atribuídos aos usuários ou que sejam atribuídos através de

outros papéis.

Com base nos exemplos anteriores, pode-se dizer que ao removermos o papel BIBLIOTECA, o usuário JOÃO

não terá mais privilégios para realizar ações sobre as tabelas LIVROS e LIVRO_AUTOR, dentre outras. Isso porque

nenhum privilegio sobre tais tabelas tinha sido explicitamente atribuído a este usuário. Todas as ações que ele

podia realizar sobre essas tabelas eram permitidas pois o usuário possuía o papel BIBLIOTECA, ao qual foi, agora,

removido do sistema.

257

Transação e Concorrência

Normalmente, considera-se que um conjunto de várias operações no banco de dados é uma única

unidade do ponto de vista do usuário. Por exemplo, a transferência de fundos de uma conta corrente para uma

poupança é uma operação única sob o ponto de vista do cliente; dentro do sistema de banco de dados, porém,

ela envolve várias operações. Evidentemente, é essencial a conclusão de todo o conjunto de operações, ou que,

no caso de uma falha, nenhuma delas ocorra. Seria inaceitável o débito na conta sem o crédito na poupança.

As operações que formam uma única unidade lógica de trabalho são chamadas de transações. Um

sistema de banco de dados precisa garantir a execução apropriada das transações a despeito de falhas – ou a

transação é executada por completo ou nenhuma parte dela é executada. Além disso, ele deve administrar a

execução simultânea de transações de modo a evitar a ocorrência de inconsistências. Retornando a nosso

exemplo de transferência de fundos, uma transação que calcula o total de dinheiro do cliente poderia trabalhar

com o saldo da conta corrente antes do débito feito pela transação de transferência e, também, verificar o saldo

da poupança depois do crédito. Com isso, obteria um resultado incorreto.

Conceito de Transação

Uma transação é uma unidade de execução de programa que acessa e, possivelmente, atualiza vários

itens de dados. Uma transação, geralmente, é o resultado da execução de um programa de usuário escrito em

uma linguagem de manipulação de dados de alto nível ou em uma linguagem de programação (p.e., SQL, COBOL,

C ou Pascal), e é determinada por declarações (ou chamadas de função) da forma begin transaction e end

transaction. A transação consiste em todas as operações ali executadas, entre o começo e o fim da transação.

Para assegurar a integridade dos dados, exigimos que o sistema de banco de dados mantenha as

seguintes propriedades das transações:

• Atomicidade. Ou todas as operações da transação são refletidas corretamente no banco de dados ou

nenhuma o será.

• Consistência. A execução de uma transação isolada (ou seja, sem a execução concorrente de outra

transação) preserva a consistência do banco de dados.

• Isolamento. Embora diversas transações possam ser executadas de forma concorrente, o sistema garante

que, para todo par de transações Ti e Tj, Ti tem a sensação de que Tj terminou sua execução antes de Ti

começar, ou que Tj começou sua execução após Ti terminar. Assim, cada transação não toma

conhecimento de outras transações concorrentes no sistema.

• Durabilidade. Depois da transação completar-se com sucesso, as mudanças que ela faz no banco de

dados persistem, até mesmo se houver falhas no sistema.

Essas propriedades são chamadas frequentemente de propriedades ACID; o acrônimo é derivado da

primeira letra de cada uma das quatro propriedades.

Para obter um melhor entendimento das propriedades ACID e da necessidade dessas propriedades,

vamos considerar um sistema bancário simplificado que consiste em várias contas e um conjunto de transações

que acessam e atualizam essas contas. Por enquanto, vamos supor que o banco de dados reside

permanentemente em disco, mas que alguma parte dele reside, temporariamente, na memória principal.

O acesso ao banco de dados é obtido pelas duas seguintes operações:

• read(X), que transfere o item de dados X do banco de dados para um buffer local alocado à transação que

executou a operação de read.

• write(X), que transfere o item de dados X do buffer local da transação que executou a write de volta ao

banco de dados.

Em um sistema de banco de dados real, a operação write (escrita) não resulta necessariamente na

atualização imediata dos dados no disco; a operação write pode ser armazenada temporariamente na memória e

ser executada depois no disco. Mas, por enquanto, vamos supor que a operação write atualize o banco de dados

imediatamente.

258

Seja Ti uma transação que transfere 50 dólares da conta A para a conta B. Essa transação pode ser

definida como:

Vamos considerar cada uma das propriedades ACID (para facilidade de apresentação, vamos considera-las

em ordem diferente da ordem A-C-I-D).

• Consistência. A exigência de consistência aqui significa que a soma de A com B deve permanecer

inalterada após a execução da transação. Sem a exigência de consistência, uma soma em dinheiro poderia

ser criada ou destruída pela transação! Pode-se verificar facilmente que, se o banco de dados permanece

consistente depois da execução da transação.

Assegurar a permanência da consistência após uma transação em particular é responsabilidade do

programador da aplicação que codifica a transação.

• Atomicidade. Suponha que, exatamente antes da execução da transação Ti, os valores das contas A e B

sejam 1000 e 2000 dólares, respectivamente. Agora suponha que, durante a execução da transação Ti,

uma falha aconteceu impedindo Ti de se completar com sucesso. Exemplos desses tipos de falhas incluem

falta de energia, falhas de máquina e erros de software. Além disso, suponha que a falha tenha ocorrido

depois da execução da operação write(A), mas antes da operação write(B). Nesse caso, os valores das

contas A e B refletidas no banco de dados são 950 e 2000 dólares. Como resultado da falha sumiram 50

dólares. Em particular, notamos que a soma A+B já não é preservada.

Assim, como resultado da falha, o estado do sistema não reflete mais de um estado real do mundo que se

supõe representado no banco de dados. Chamamos esse estado de inconsistente. Devemos assegurar que essas

inconsistências não sejam perceptíveis em um sistema de banco de dados. Porém, observe que o sistema pode,

em algum momento, estar em um estado inconsistente. Mesmo que a transação Ti seja executada até o final, há

um ponto no qual o valor da conta A é 950 dólares e o valor da conta B é 2000 dólares, que é claramente um

estado inconsistente. Porém esse estado deverá ser substituído pelo estado consistente em que o valor da conta

A é 950 dólares e o valor da conta B é 2050 dólares. Assim, se a transação nunca se iniciou ou se for garantida sua

execução completa, esse estado incompatível não seria visível, exceto durante a execução da transação. Essa é a

razão da exigência da atomicidade: se a propriedade de atomicidade for garantida, todas as ações da transação

serão refletidas no banco de dados ou nenhuma delas o será.

A ideia básica por trás da garantia da atomicidade é a seguinte. O sistema de banco de dados mantém um

registro (em disco) dos antigos valores de quaisquer dados sobre os quais a transação executa uma gravação e, se

a transação não se completar, os valores antigos são restabelecidos para fazer com que pareça que ela nunca foi

executada. Assegurar a atomicidade é responsabilidade do próprio sistema de banco de dados, mais

especificamente ela é tratada por um componente chamado de componente de gerenciamento de transações.

• Durabilidade. Se a transação se completar com sucesso, e o usuário que a disparou for notificado da

transferência de fundos, isso significa que não houve nenhuma falha de sistema que tenha resultado em

perda de dados relativa a essa transferência de capitais.

A propriedade de durabilidade garante que, uma vez completada a transação com sucesso, todas as atualizações

realizadas no banco de dados persistirão, até mesmo se houver uma falha de sistema após a transação se

completar.

Suponha agora que uma falha do sistema possa resultar em perda de dados na MP, mas que os dados

gravados em disco nunca sejam perdidos. Podemos garantir a durabilidade observando um dos seguintes itens:

259

1. As atualizações realizadas pela transação foram gravadas em disco, antes da transação completar-se.

2. Informações gravadas no disco, sobre as atualizações realizadas pela transação, são suficientes para que o

banco de dados possa reconstruir essas atualizações quando o sistema de banco de dados for reiniciado

após uma falha.

Assegurar a durabilidade é responsabilidade de um componente do sistema de banco de dados chamado

de componente de gerenciamento de recuperação. O componente de gerenciamento de transação e o

componente de gerenciamento de transação estão estreitamente relacionados.

• Isolamento. Mesmo asseguradas as propriedades de consistência e de atomicidade para cada transação,

quando diversas transações concorrentes são executadas, suas operações podem ser intercaladas de

modo inconveniente, resultando em um estado inconsistente.

Por exemplo, conforme vimos, o banco de dados fica temporariamente inconsistente, enquanto a

transação transfere fundos de A para B, quando o total reduzido já está escrito em A e o total a ser acrescidos

ainda está aguardando ser escrito em B. Se uma segunda transação, em execução concorrente, ler A e B nesse

ponto intermediário e computar A+B, observará um valor inconsistente. Além disso, se essa segunda transação

executar em A e B atualizações baseadas nos valores inconsistentes que leu, o banco de dados pode ficar em um

estado inconsistente mesmo após ambas as transações se completarem.

Uma solução para o problema de execução concorrente de transações é executar as transações em série

– ou seja, uma após a outra. Entretanto, a execução simultânea de transações proporcionam uma melhoria de

desempenho significativa. Por isso, foram desenvolvidas opções que permitem que diversas transações sejam

executadas de modo concorrente.

Discutimos os problemas causados pela execução de transações concorrentes adiante. A propriedade de

isolamento de uma transação garante que a execução simultânea de transações resulte em uma situação no

sistema equivalente ao estado obtido caso as transações tivessem sidos executadas uma de cada vez, em

qualquer ordem. Assegurar a propriedade de isolamento é responsabilidade de um componente do sistema de

banco de dados chamado componente de controle de concorrência e bem como a obediência a seus princípios.

Estado da Transação

Na ausência de falhas, todas as transações completam-se com sucesso. Entretanto, como observamos

anteriormente, nem sempre uma transação pode completar-se com sucesso. Nesse caso, a transação é abortada.

Se asseguramos a propriedade de atomicidade, uma transação é abortada. Se assegurarmos a propriedade de

atomicidade, uma transação abortada não deve ter efeito sobre o estado do banco de dados. Assim, quaisquer

atualizações que a transação abortada tiver feito no banco de dados devem ser desfeitas. Uma vez que as

mudanças causadas por uma transação abortada sejam desfeitas, dizemos que a transação foi desfeita (rolled

back – retornada). Gerenciar transações abortadas é responsabilidade do esquema de recuperação.

Uma transação completada com sucesso é chamada efetivada (committed). Uma transação que foi

efetivada e que realizou atualizações transforma o banco de dados em um novo estado consistente que deve

persistir até mesmo se houver uma falha no sistema.

Uma vez que uma transação chegue à efetivação (commit), não podemos desfazer seus efeitos

abortando-a. O único modo de desfazer os efeitos de uma transação efetivada é executar uma transação de

compensação, porém nem sempre isso é possível. Logo, a responsabilidade pela criação e execução de uma

transação de compensação é deixada a cargo do usuário, não sendo tratada pelo sistema de banco de dados.

Precisamos ser mais precisos sobre o que queremos dizer com término com sucesso de uma transação.

Portanto, estabeleceremos um modelo de transação simples e abstrato. Uma transação deve estar em um dos

seguintes estados:

• Ativa, ou estado inicial; a transação permanece neste estado enquanto estiver executando.

• Em efetivação parcial, após a execução da última declaração.

• Em falha, após a descoberta de que a execução normal já não pode se realizar.

260

• Abortada, depois que a transação foi desfeita e o banco de dados foi restabelecido ao estado anterior do

início da execução da transação.

• Em efetivação, após a conclusão com sucesso.

O diagrama de estado correspondente a uma transação é mostrado na fig. 13.1. Dizemos que uma

transação foi efetivada somente se ela entrou no estado de efetivação. Analogamente, dizemos que uma

transação abortou somente se ela entrou no estado de abortada. Uma transação é dita concluída se estiver em

efetivação abortada.

Uma transação começa no estado ativo. Quando termina sua última declaração, ela entra no estado de

efetivação parcial.

Nesse momento, a transação completou sua execução, mas ainda é possível ser abortada, já que seus

efeitos ainda podem estar na MP, e com isso uma falha de hardware pode impedir que seja completada com

sucesso.

Então, o sistema de banco de dados escreve informações suficientes no disco, de forma que, até mesmo

em uma falha eventual, as atualizações realizadas pela transação possam ser recriadas quando o sistema for

reiniciado. Quando a última dessas informações for escrita, a transação entre no estado de efetivação.

Conforme mencionamos anteriormente, por enquanto estaremos supondo que as falhas não resultam em

perda de dados no disco. Técnicas para lidar com a perda de dados serão discutidas a seguir.

Uma transação entra no estado de falha quando o sistema determina que ela já não pode prosseguir sua

execução normal (p.e., por causa de erros de hardware ou erros lógicos). Essa transação deve ser desfeita. Ela

entra, então, no estado abortada. Nesse momento, o sistema tem duas opções:

• Ele pode reiniciar a transação, mas somente se ela foi abortada como resultado de algum erro de

hardware ou de software não criado pela lógica interna da transação. Uma transação reiniciada é

considerada uma transação nova.

• Ele pode matar a transação. Normalmente, isso é feito em decorrência de algum erro lógico interno que

só pode ser corrigido refazendo o programa de aplicação, ou porque a entrada de dados não era

adequada ou porque os dados desejados não foram encontrados no banco de dados.

Devemos ser cautelosos quando tratamos de escritas externas observáveis, como escrever em um

terminal ou em uma impressora. Uma escrita desse tipo não pode ser apagada, já que é vista externamente ao

sistema de banco de dados. A maioria dos sistemas permite que essas escritas aconteçam somente depois que

essas escritas aconteçam somente depois que a transação entra no estado de efetivação. Um modo de

implementar esse esquema é fazer com que o sistema de banco de dados armazene temporariamente, em um

meio de armazenamento não-volátil, qualquer valor associado a uma escrita externa, e fazer com que ele executa

a escrita real somente depois que a transação entra no estado de efetivação. Se o sistema falhar depois ter

entrado no estado de efetivação, mas antes de completar a escrita externa, quando o sistema for reiniciado, o

261

sistema de banco de dados executará a escrita externa (usando as informações do meio de armazenamento não-

volátil).

Para certas aplicações, pode ser conveniente permitir que transações ativas exibam dados aos usuários,

particularmente em transações de longa duração, de minutos ou horas. Infelizmente, não podemos permitir essas

saídas de dados, a menos que estejamos dispostos a comprometer a atomicidade da transação. A maioria dos

atuais sistemas de transação assegura a atomicidade e, por isso, proíbe essa forma de interação com usuários.

Implementação de Atomicidade e Durabilidade

O componente de gerenciamento de recuperação de um banco de dados implementa o suporte à

atomicidade e durabilidade. Consideraremos primeiro um esquema simples, mas extremamente ineficiente. Esse

esquema supõe que somente uma transação esteja ativa por vez, e baseia-se em cópias do banco de dados é

simplesmente um arquivo no disco. Um ponteiro chamado db_pointer é mantido no disco; ele aponta para a

cópia corrente do banco de dados.

No esquema de banco de dados shadow, uma transação que deseja atualizar o banco de dados primeiro

cria uma cópia completa dele. Todas as atualizações são feitas na nova cópia, deixando a cópia original, chamada

cópia shadow, intata. Se, em qualquer momento, a transação tiver de ser abortada, simplesmente apaga-se a

novo cópia.

Se a transação se completa, sua efetivação será feita conforme segue. Primeiro, o sistema operacional

precisa ter certeza de que todas as páginas da nova cópia do banco de dados tenham sido escritas no discos. Em

sistemas Unix, o comando flush (transportar, arrebatar) é usada para esse propósito. Depois que o flush se

completa, o ponteiro db_pointer é atualizado para apontar para a nova cópia do banco de dados e a esta se torna

a cópia atual. Então, a cópia velha é apagada. Esse esquema é mostrado graficamente na fig. 13.2, em que o

estado do banco de dados, antes e após a atualização, é indicado.

Diz-se que uma transação foi efetivada quando o db_pointer atualizado é escrito no disco. Discutiremos,

agora, como essa técnica trata as falhas de transação e de sistema. Primeiramente, consideremos a falha de

transação. Se a transação falhar antes da atualização do db_pointer, o conteúdo antigo do banco de dados não

será afetado. Simplesmente podemos abortar a transação apagando a cópia nova do banco de dados. Se a

transação foi efetivada, todas as atualizações que ela executou estão no banco de dados apontado pelo

db_pointer. Assim, ou todas as atualizações da transação são efetivadas ou nenhum de seus efeitos estarão

refletidos, a despeito da falha da transação.

Agora, considere uma falha do sistema. Suponha que a falha do sistema ocorra antes do db_pointer

atualizado ser escrito em disco. Quando o sistema reiniciar, ele lerá o db_pointer, verá o conteúdo original do

banco de dados e nenhum dos efeitos da transação será visível no banco de dados. Agora, suponha que o sistema

falha depois que o db_pointer tiver sido atualizado em disco. Antes de atualizar o ponteiro, todas as páginas

atualizadas da nova cópia do banco de dados foram escritas no disco. Como mencionamos anteriormente,

estamos supondo que, uma vez escrito no disco, o conteúdo de um arquivo não seja danificado, nem mesmo se

262

houver uma falha de sistema. Portanto, quando o sistema reiniciar, ele lerá o db_pointer e verá o conteúdo do

banco de dados depois de todas as atualizações executadas pela transação.

Na verdade, a implementação depende da atomicidade da gravação em db_pointer; ou seja, todos os

seus bytes são escritos ou nenhum de seus bytes o será. Se alguns dos bytes do ponteiro forem atualizados por

uma escrita, mas outros não, o ponteiro não será representativo, e tanto a versão antiga do banco de dados

quanto a versão nova podem não ser encontradas quando o sistema for reiniciado. Felizmente, os sistemas de

disco fornecem atualizações atômicas para blocos inteiros, ou pelo menos para um setor de disco. Em outras

palavras, o sistema de disco garante que atualizará o db_pointer atomicamente.

Assim, as propriedades de atomicidade e durabilidade das transações são garantidas na técnica de

implementação com cópia shadow, feita pelo componente de gerenciamento de recuperação.

Um exemplo simples de uma transação, fora do domínio de banco de dados, seria uma sessão de edição

de texto. Uma sessão de edição inteira pode ser modelada como uma transação. As ações executadas pela

transação são a leitura e a atualização de um arquivo. Salvar o arquivo ao término da edição corresponde à

efetivação da transação de edição; sair da sessão do editor sem salvar o arquivo corresponde a abortar a

transação de edição.

Muitos editores de texto usam essencialmente a implementação descrita acima, para garantir que a

sessão de edição seja transacional. Um arquivo novo é usado para armazenar o arquivo atualizado. Ao término da

sessão de edição, se o arquivo atualizado for salvo, um comando de arquivo rename é usado para rebatizar o

arquivo novo com seu nome corrente. Supõe-se que o rename seja implementado como uma operação atômica

pelo sistema de arquivo subjacente, e que ele também apagará o arquivo antigo.

Infelizmente, essa implementação é extremamente ineficiente no contexto dos grandes banco de dados,

já que a execução de uma única transação implica copiar o banco de dados inteiro. Além disso, essa

implementação não permite que transações concorram uma com as outras. Há maneiras práticas de implementar

a atomicidade e a durabilidade que são muito menos onerosas e mais poderosas.

Execuções Concorrentes

Os sistemas de processamento de transações, normalmente, permitem que diversas transações sejam

executadas de modo concorrente. Permitir que múltiplas transações concorram na atualização de dados traz

diversas complicações em relação à consistência desses dados, conforme vimos anteriormente. Assegura a

consistência, apesar da execução concorrente de transações, exige trabalho adicional; é muito mais fácil insistir

na execução de transações sequencialmente, uma de cada vez, cada uma começando somente depois que a

anterior se completou. Porém, há duas boas razoes para permitir a concorrência.

• Uma transação consiste em diversos passos. Alguns envolvem atividade de I/O; outros atividades de CPU.

A CPU e os discos em um sistema de computador podem operar em paralelo. Logo, a atividade de I/O

pode ser feita em paralelo com o processamento na CPU. Assim, o paralelismo entre CPU e o sistema de

I/O pode ser explorado para executar diversas transações em paralelo. Enquanto uma leitura ou escrita

solicitada por uma transação está em desenvolvimento em um disco, outra transação pode estar sendo

processada na CPU, e outro disco pode estar executando uma leitura ou escrita solicitada por uma

terceira transação. Desse modo, há um aumento no throughput do sistema – ou seja, no número de

transações que podem ser executadas em um determinado tempo. De forma correspondente, o uso do

processador e do disco também aumentam; em outras palavras, o processador e o disco ficam menos

tempo inativos ou sem executar trabalho útil.

• Pode haver uma mistura de transações em execução simultânea no sistema, algumas curtas e outras

longas. Se a execução das transações for sequencial, uma transação curta pode ser obrigada a esperar até

que uma transação longa precedente se complete, o que pode gerar atrasos imprevisíveis em sua

execução. Se as transações estão operando em diferentes partes do banco de dados, é melhor deixa-las

concorre de modo a compartilhar os ciclos de CPU e os acessos de disco entre si. A execução concorrente

reduz os atrasos imprevisíveis na execução. Se as transações estão operando em diferentes partes do

263

banco de dados, é melhor deixa-las concorrer de modo a compartilhar os ciclos de CPU e os acessos de

disco entre si. Além disso, reduz também o tempo médio de resposta: o tempo médio para uma

transação ser completada após ser submetida.

A motivação para usar a execução concorrente em um banco de dados é essencialmente a mesma para

usar a multiprogramação em um sistema operacional.

Quando várias transações são processadas de modo concorrente, a consistência do banco de dados pode

ser destruída, mesmo que cada transação individual seja executada com correção. Vamos agora apresentar o

conceito de escalas de execução (schedules), para ajudar na identificação de quais ordens de execução podem

garantir a manutenção da consistência.

O sistema de banco de dados deve controlar a interação entre as transações concorrentes para impedi-las

de destruir sua consistência. Isso é feito por meio de uma variedade de mecanismos chamados de esquemas de

controle de concorrência.

Considere, novamente, o sistema bancário simplificado já apresentado, que possui diversas contas, além

de um conjunto de transações que acessa e atualiza essa contas. Sejam T1 e T2 duas transações que transferem

fundos de uma conta para outra. A transação T1 transfere 50 dólares da conta A para a conta B e é definida da

seguinte forma:

A transação T2 transfere 10 por cento do saldo da conta A para a conta B e é definida da seguinte forma:

Sejam mil e dois mil dólares os valores correntes das contas A e B, respectivamente. Suponha que as duas

transações sejam executadas em sequência, T1 seguida de T2. Essa sequência de execução é representada na fig.

13.3. A sequência dos passos das instruções estão em ordem cronológica a partir do topo da figura, com as

instruções de T1 aparecendo na coluna à esquerda e as instruções de T2 aparecendo na coluna à direita. Depois

que a execução apresentada na fig. 13.3 terminar, os valores nas contas A e B são 855 e 2145 dólares,

respectivamente. Assim, o montante de dinheiro das contas A e B – ou seja, a soma A+B – é preservado depois da

execução de ambas as transações.

264

Analogamente, se as transações forem executadas em outra sequência, desta vez T2 seguida de T1, então

a sequência de execução correspondente é mostrada na fig. 13.4. Novamente, conforme esperado, a soma A+B é

preservada, e os valores finais das contas A e B são 850 e 2150 dólares, respectivamente.

As sequências de execução descritas anteriormente são chamadas de escalas de execução ou escalas. Elas

representam a ordem cronológica por meio da qual as instruções são executadas no sistema. Claramente, uma

determinada escala de execução de um conjunto de transações consiste em todas as instruções dessas transações

e deve preservar a ordem na qual as instruções aparecem em cada transação individual. Por exemplo, na

transação T1, a instrução write(A) deve aparecer antes da instrução read(B), em qualquer escala válida. Na

discussão a seguir, iremos nos referir à primeira sequência de execução (T1 seguida de T2) como escala 1 e à

segunda sequência de execução (T2 seguida de T1) como escala 2.

Essas escalas de execução são sequenciais. Cada escala sequencial consiste em uma sequência de

instruções de várias transações em que as instruções que pertencem a uma única transação aparecem agrupadas.

Assim, para um conjunto de n transações, há n! escalas sequenciais válidas diferentes.

Quando várias transações são executadas simultaneamente, a escala correspondente pode já não ser

sequencial. Se duas transações são executadas simultaneamente, o sistema operacional pode executar uma

transação durante algum tempo e, então, voltar à primeira transação durante algum tempo e assim por diante,

alternadamente. Com diversas transações, o tempo de CPU é compartilhado entre todas.

Várias sequências de execução são possíveis, já que as várias instruções, de ambas as transações podem

ser intercaladas. Geralmente, não é possível exatamente prever quantas instruções de uma transação serão

executadas antes que a CPU alterne para outra transação. Assim, o número de escalas de execução possíveis para

um conjunto de n transações é muito maior que n!

265

Retornando ao nosso exemplo anterior, suponha que as duas transações sejam executadas de modo

concorrente. Uma escala de execução possível é mostrada na fig. 13.5. Após essa execução, chegamos ao mesmo

estado obtido durante a execução sequencial na ordem T1 seguida de T2. A soma A+B é preservada.

Nem todas as execuções concorrentes resultam em um estado correto. Para ilustrar, considere a escala

de execução da fig. 13.6. Depois de sua execução, chegamos a um estado tal que os valores para as contas A e B

são 950 e 2100 dólares, respectivamente. Esse estado final é um estado inconsistente, já que apareceram 50

dólares durante a execução concorrente. Realmente, a soma A+B não é preservada na execução das duas

transações.

Se o controle da execução concorrente é deixado completamente sob a responsabilidade do sistema

operacional, muitas escalas de execução possíveis, inclusive aquelas que deixam o banco de dados em um estado

inconsistente como a descrita anteriormente, são factíveis. É uma tarefa do sistema de banco de dados garantir

que qualquer escala executada deixe o banco de dados em estado consistente. O componente do sistema de

banco de dados que executa esta tarefa é chamado de componente de controle de concorrência.

Podemos assegurar a consistência do banco de dados, sob execução concorrente, garantindo que

qualquer escala executada tenho o mesmo efeito de outra que tivesse sido executada sem qualquer

concorrência. Isto é, uma escala de execução deve, de alguma forma, ser equivalente a uma escala sequencial.

Serialização

O sistema de banco de dados deve controlar a execução concorrente de transações para assegurar que o

estado do banco de dados permaneça consistente. Antes de examinarmos como o sistema de banco de dados

pode cumprir essa tarefa, temos de entender primeiro quais escalas de execução podem garantir a consistente e

quais não irão fazê-lo.

266

Considerando que as transações são programas, é difícil, pelo caráter da computação, determinar quais

são as operações exatas que uma transação executa, e como as operações de várias transações interagem. Por

essa razão, não faremos interpretações sobre o tipo de operações que uma transação pode executar em um item

de dados. Em vez disso, consideraremos apenas duas operações: read (leitura) e write (escrita). Supomos assim

que, entre uma instrução read (Q) e write(Q) em um item de dado Q, uma transação pode executar uma

sequência arbitrária de operações na cópia de Q, que está residindo no buffer local no qual se processa a

transação. Assim, as únicas operações significativas de uma transação, do ponto de vista da escala de execução,

são suas instruções de leitura e escrita. Por isso, mostraremos apenas as instruções read e write nas escalas de

execução, conforme fizemos na representação da escala 3 que é mostrada na fig. 13.7.

Vamos discutir formas de equivalência entre escalas de execução; elas conduzem às noções de

serialização de conflito e de visão serializada.

Serialização de Conflito

Vamos considerar uma escala de execução S com duas instruções sucessivas, Ii e Ij, das transações Ti e Tj

(i≠j), respectivamente. Se Ii e Ij referem-se a itens de dados diferentes, então podemos alternar Ii e Ij sem afetar os

resultados de qualquer instrução da escala. Porém, se Ii e Ij referem-se ao mesmo item de dados Q, então a

ordem de dois passos pode importar. Como estamos lidando apenas com instruções read e write, há quatro casos

a considerar:

Assim, apenas no caso em que ambas, Ii e Ij, são instruções de read a ordem relativa de suas execuções

não é importante.

Dizemos que Ii e Ij entram em conflito caso elas sejam operações pertencentes a diferentes transações,

agindo no mesmo item de dado, e pelo menos uma dessas instruções é uma operação de write.

Para ilustrar o conceito de operações conflitantes consideraremos a escala 3 mostrada na fig. 13.7. A

instrução write (A) de T1 entra em conflito com a instrução read(A) de T2. Porém, a instrução write(A) de T2 não

está em conflito com a instrução read(B) de T1, porque as duas instruções trabalham itens de dados diferentes.

Sejam Ii e Ij instruções consecutivas de uma escala de execução S. Se Ii e Ij são instruções de transações

diferentes e não entram em conflito, então podemos trocar a ordem de Ii e Ij para produzir uma nova escala de

267

execução S’. Esperamos que S seja equivalente a S’, já que todas as instruções aparecem na mesma ordem em

ambas as escalas de execução com exceção de Ii e Ij , cuja ordem não importa.

Como a instrução write(A) de T2 na escala 3 da fig. 13.7 não entra em conflito com a instrução read(B) de

T1, podemos trocar essas instruções para gerar uma escala de execução equivalente, a escala 5, conforme mostra

a fig. 13.8. A despeito do estado inicial do sistema, ambas as escalas, 3 e 5, produzem o mesmo estado final no

sistema.

Continuaremos a trocar instruções não-conflitantes conforme segue:

• Trocar a instrução read(B) de T1 pela instrução read(A) de T2.

• Trocar a instrução write(B) de T1 pela instrução write(A) de T2.

• Trocar a instrução write(B) de T1 pela instrução read(A) de T2.

O resultado final dessas trocas, conforme mostrado na escala 6 da fig. 13.9, é uma escala de execução

sequencial. Assim, mostramos que a escala 3 é equivalente a uma escala de execução sequencial. Essa

equivalência implica que, a despeito do estado inicial do sistema, a escala 3 produzirá o mesmo estado final

produzido por alguma escala sequencial.

Se uma escala de execução S puder ser transformada em outra, S’, por uma série de trocas de instruções

não-conflitantes, dizemos que S e S’ são equivalentes no conflito.

Retornando a nossos exemplos anteriores, observamos que a escala 1 não é equivalente no conflito à

escala 2. Entretanto, a escala 1 é equivalente no conflito à escala 3, porque as instruções read(B) e write(B) de T1

podem ser trocadas pelas instruções read(A) e write(A) de T2.

O conceito de equivalência no conflito leva ao conceito de serialização de conflito. Dizemos que uma

escala de execução S é conflito serializava se ela é equivalente no conflito a uma escala de execução sequencial.

Assim, a escala 3 é conflito serializava, já que ela é equivalente no conflito à escala sequencial 1.

Finalmente, considere a escala 7 da fig. 13.10; ela consiste somente nas operações significativas (ou seja,

read e write) das transações T3 e T4. Essa escala de execução não é conflito serializava, já que não é equivalente à

escala sequencial <T3, T4> ou à escala sequencial <T4, T3>.

268

É possível ter duas escalas de execução que produzam o mesmo resultado, mas que não sejam

equivalentes no conflito. Por exemplo, considere a transação T5, que transfere 10 dólares da conta B para a conta

A. Seja a escala 8 definida na fig. 13.11. Verificamos que a escala 8 não é equivalente no conflito à escala

sequencial <T1, T5>, já que, na escala 8, a instrução write(B) de T5 entra em conflito com a instrução read(B) de T1.

Assim, apenas pela troca de instruções consecutivas não conflitantes, não conseguimos mover todas as instruções

de T1 antes daquelas de T5. Porém, os valores finais das contas A e B depois da execução da escala 8 ou da escala

sequencial <T1, T5> são os mesmos – isto é, 960, e 2040 dólares, respectivamente.

Podemos ver nesse exemplo que há definições menos triviais de equivalência de escala que a

equivalência de conflito. Para o sistema determinar se a escala 8 produz o mesmo resultado que a escala

sequencial <T1, T5>, ele tem de analisar toda computação executada por T1 e T5, em vez de analisar apenas as

operações read e write. Em geral, tal análise é difícil de implementar e é onerosa em termos computacionais.

Porém, há outras definições de equivalência entre escalas de execução baseadas puramente nas operações read

e write.

Visão Serializada

Vamos considerar uma forma de equivalência que é menos restritiva que a equivalência de conflito,

embora, assim como a equivalência de conflito, esteja baseada apenas nas operações read e write das

transações.

Considere duas escalas de execução S e S’, com o mesmo conjunto de transações participando de ambas.

As escalas S e S’ são ditas equivalente na visão se as três condições seguintes forem satisfeitas:

1. Para cada item de dados Q, se a transação Ti fizer uma leitura no valor inicial de Q na escala S, então a

transação Ti também deve, na escala S’, ler o valor inicial de Q.

2. Para cada item de dados Q, se a transação Ti executar um read(Q) na escala S, e aquele valor foi

produzido por meio da transação Tj (se houver), então a transação Ti também deverá, na escala S’, ler o

valor de Q que foi produzido por meio da transação Tj.

3. Para cada item de dados Q, a transação (se houver) que executa a operação final write(Q) na escala S tem

de executar a operação write(Q) final na escala S’.

269

As condições 1 e 2 asseguram que cada transação lê os mesmos valores em ambas as escalas e, então,

executa a mesma computação. A condição 3, em conjunto com as condições 1 e 2, assegura que ambas as escalas

de execução resultem no mesmo estado final de sistema.

Retornando a nossos exemplos anteriores, notamos que a escala 1 não é equivalente em visão à escala 2,

já que, na escala 1, o valor da conta A lido pela transação T2 foi produzido por T1, enquanto isso não ocorre na

escala 2. Porém, a escala 1 é equivalente em visão à escala 3, porque os valor da conta A e B lidos pela transação

T2 foram produzidos por T1 em ambas as escalas.

O conceito de equivalência de visão leva ao conceito de serialização de visão. Dizemos que uma escala de

execução S tem visão serializada se for equivalente, em visão, a uma escala de execução sequencial.

Para ilustrar, suponha que aumentemos a escala 7 com a inclusão da transação T6 obtendo a escala 9,

conforme pode ser visto na fig. 13.12. A escala 9 é a visão serializada. De fato, ela é equivalente em visão à escala

sequencial <T3, T4, T6>, já que uma instrução read(Q) lê o valor inicial de Q em ambas as escalas, e T6 executa a

escrita final de Q em ambas as escalas.

Toda escala conflito serializava é visão serializava, mas há escala visão serializava que não são conflito

serializava. Realmente, a escala 9 não é conflito serializava, uma vez que qualquer par de instruções consecutivas

é conflitante e, assim, não é possível nenhuma troca de instruções.

Observe que, na escala 9, as transações T4 e T6 executam operações write(Q) sem terem executado uma

operação read(Q). Esse tipo de escrita é chamado de escrita cega (blind write). As escritas cegas aparecem em

algumas escalas visão serializava que não são conflito serializava.

Recuperação

Até o momento, estudamos quais escalas de execução são aceitáveis do ponto de vista da consistência do

banco de dados, supondo, de modo implícito, que não ocorram falhas de transação. Veremos agora os efeitos das

falhas de transação durante a execução concorrente.

Se uma transação Ti falhar, por qualquer razão, precisamos desfazer seus efeitos para garantir a

propriedade de atomicidade da transação. Em um sistema que permite execução concorrente, também é

necessário assegurar que qualquer transação Tj que seja dependente de Ti (quer dizer, Tj leu dados escritos por Ti)

também seja abortada. Para alcançar essa segurança, precisamos colocar restrições no tipo de escalas permitidas

no sistema.

Escala de Execução Recuperáveis

Considere a escala 11, mostrada na fig. 13.13, na qual T9 é uma transação que executa apenas uma

instrução: read(A). Suponha que o sistema permita que T9 seja efetivada imediatamente após executar a

instrução read(A). Assim, T9 é efetivada antes que T8 o seja. Agora, suponha que T8 falhe antes da efetivação.

Como T9 leu o valor do item de dados A escrito por T8, temos de abortar T9 para assegurar a atomicidade da

transação. Porém, T9 já foi efetivada e não poderá ser abortada. Assim, temos uma situação em que é impossível

se recuperar corretamente da falha de T8.

270

A escala 11, com a efetivação acontecendo imediatamente após a instrução read(A), é um exemplo de

escala de execução não-recuperável que, portanto, não deveria ser permitida. A maioria dos sistemas de banco

de dados exige que todas as escalas sejam recuperáveis. Uma escala recuperável é aquela na qual, para cada par

de transações Ti e Tj, tal que Tj leia itens de dados previamente escritos por Ti, a operação de efetivação de Ti

apareça antes da operação de efetivação de Tj.

Escalas sem cascata

Mesmo em uma escala recuperável, para o sistema recuperar-se corretamente da falha de transação Ti,

pode ser que seja necessário desfazer diversas transações. Tais situações ocorrem se as transações leram dados

escritos por Ti. Como ilustração, considere a escala parcial da fig. 13.14. A transação T10 escreve um valor para A

que é lido pela transação T11. A transação T11 escreve um valor para A que é lido pela transação T11. Suponha que,

nesse momento, T10 falhe. T10 deverá ser desfeita. Como T11 é dependente de T10, T11 deverá ser desfeita. Como

T12 é dependente de T11, T12 deverá ser desfeita. Esse fenômeno, no qual a falha de uma única transação conduz a

uma série de reversões de transação, é chamado de retorno em cascata (cascading rollback).

O retorno em cascata é indesejável, já que leva a desfazer uma quantia significativa de trabalho. É

conveniente restringir as escalas àquelas nas quais os retornos em cascata não possam acontecer. Tais escalas são

chamadas de escalas sem cascata. Uma escala sem cascata é aquela na qual cada par de transações Ti e Tj, tal que

Tj leia um item de dados previamente escrito por Ti, a operação de efetivação de Ti apareça antes da operação de

leitura de Tj. É fácil verificar que toda escala sem cascata também é recuperável.

Implementação do Isolamento

Até o momento, vimos quais propriedades uma escala deve ter para deixar o banco de dados em um

estado consistente e para permitir o tratamento seguro de possíveis falhas de transação. Especificamente, as

escalas que são conflito ou visão serializava e sem cascata satisfazem essas exigências.

Há vários esquemas de controle de concorrência que podemos usar para garantir que, até mesmo quando

diversas transações são executadas de modo concorrente, sejam geradas apenas escalas aceitáveis, a despeito de

como o sistema operacional compartilha os recursos (como o tempo de CPU) entre as transações.

Como um exemplo trivial de um esquema de controle de concorrência, considere este: uma transação

bloqueia (lock) o banco de dados inteiro antes de começar e libera o bloqueio após sua efetivação. Enquanto uma

transação mantém um bloqueio, nenhuma outra tem permissão para realizar um bloqueio, todas elas são

obrigadas a esperar sua liberação. Como resultado dessa política de bloqueio, apenas uma transação pode

executar um bloqueio de cada vez. Logo, são geradas apenas escalas sequenciais. Estas são trivialmente

serializáveis e é fácil verificar que são também sem cascata.

271

Um esquema de controle de concorrência como esse apresenta um desempenho pobre, já que força as

transações a esperarem o término das precedentes antes que possam começar. Em outras palavras, ele

possibilita um baixo grau de concorrência. A execução concorrente traz vários benefícios em relação ao

desempenho.

O objetivo dos esquemas de controle de concorrência é proporcionar um alto grau de concorrência,

enquanto garante que todas as escalas geradas sejam conflito serializava ou visão serializava, e também sejam

em cascata.

Os esquemas têm diferentes características em termos do grau de concorrência observado e da

quantidade de overhead em que incorrem. Alguns deles permite que apenas escalas conflito serializava sejam

geradas, outros permite que escalas visão serializável, que não são conflito serializava, também sejam geradas.

Definição de Transação em SQL

Uma linguagem de manipulação de dados deve possuir um construtor para especificar o conjunto de

ações que constitui uma transação.

O padrão SQL especifica que uma transação começa de modo subentendido. As transações são

terminadas por uma das seguintes declarações SQL:

• Commit work executa a efetivação da transação corrente e começa uma nova.

• Rollback work aborta a transação corrente.

A palavra-chave work é opcional em ambas as declarações. Se um programa termina sem um desses

comandos, as atualizações são efetivadas ou desfeitas – a escolha não é especificada pelo padrão, e é

dependente da implementação.

O padrão especifica também que o sistema deve assegurar a serialização e retorno sem cascata. A

definição de serialização usada pelo padrão é a que estabelece que uma escala deve ter o mesmo efeito de uma

escala sequencial. Assim, tanto serialização de conflito quanto serialização de visão são aceitáveis.

O padrão SQL-92 também permite que se estabeleça para uma transação uma execução de modo não

serializava em relação a outras transações. Por exemplo, uma transação pode operar em nível de read sem

efetivação (read uncommitted), permitindo que as transações leiam registros mesmo sem suas efetivações. Essa

características é oferecida para transações longas, cujos resultados não precisam ser exatos. Por exemplo, uma

informação aproximada é geralmente suficiente para estatísticas usadas na otimização de consultas. Se essas

transações forem executadas de uma maneira serializava, elas poderiam interferir em outras transações,

provocando atrasos.

O nível de consistência especificado pela SQL-92 é:

• Serializável é o default (padrão).

• Read repetitivo somente permite leitura de registros que sofreram efetivação e, além disso, exige que

nenhuma outra transação consiga atualizar um registro entre duas leituras feitas por uma transação.

Entretanto, a transação pode não ser serializava com respeito a outras transações. Por exemplo, quando

se está procurando registros que satisfaçam algumas condições, uma transação pode achar alguns dos

registros inseridos por uma transação que sofreu efetivação, mas não encontrar os outros.

• Read com efetivação permite que apenas registros que sofreram efetivação sejam lidos, mas não exige

read repetitivo. Por exemplo, entre duas leituras de um registro feitas por uma transação, os registros

podem ter sido atualizados por meio de transações que obtiveram efetivação.

• Read sem efetivação permite a leitura de registros que não sofreram efetivação. É o nível mais baixo de

consistência permitido pela SQL-92.

Teste de Serialização

Ao projetar esquemas de controle de concorrência, devemos mostrar que as escalas geradas por eles são

serializáveis. Para fazê-lo, primeiro temos de entender como determinar, para uma escala S em particular, se ela é

serializava. Nesta seção, apresentaremos métodos para determinar serialização de conflito e serialização de visão.

272

Mostraremos que há um algoritmo simples e eficiente para determinar a serialização de conflito. Entretanto, não

há nenhum algoritmo eficiente para determinar a serialização de visão.

Teste para Serialização de Conflito

Seja S uma escala. Construímos um gráfico direcionado, chamado gráfico de precedência de S. Esse

gráfico consiste em um par G=(V,E), em que V é um conjunto de vértices e E é um conjunto de arestas. O conjunto

de vértices consiste em todas as transações que participam da escala. O conjunto de arestas consiste em todas as

transações que participam da escala. O conjunto de arestas consiste em todas as arestas Ti�Tj para as quais uma

das seguintes condições se verifica:

1. Ti executa write(Q) antes de Tj executar read(Q).

2. Ti executa read(Q) antes de Tj executar write(Q).

3. Ti executa write(Q) antes de Tj executar write(Q).

Se há uma aresta Ti�Tj no gráfico de precedência, então, em qualquer escala sequencial S’ equivalente a

S, Ti deve aparecer antes de Tj.

Por exemplo, o gráfico de precedência para a escala 1 é mostrado na fig. 13.15a. Ele contém a única

aresta T1�T2, já que todas as instruções de T1 são executadas antes da primeira instrução de T2 ser executada. De

forma semelhante, a fig. 13.15b mostra o gráfico de precedência para a escala 2 com a única aresta T2�T1, já que

todas as instruções de T2 são executadas antes da primeira instrução de T1 ser executada.

O gráfico de precedência para a escala 4 é mostrado na fig. 13.16. Ele contém a aresta T1�T2, porque T1

executa read(A) antes de T2 executar write(A). Ele também contém a aresta T2�T1, porque T2 executa read(B)

antes de T1 executar write(B).

Se o gráfico de precedência para S tem um ciclo, então a escala S não é conflito serializava. A ordem de

serialização pode ser obtida por meio da classificação topológica, que estabelece uma ordem linear consistente

com a ordem parcial do gráfico de precedência. Em geral, várias ordens lineares possíveis podem ser obtidas por

meio da classificação topológica. Por exemplo, o gráfico da fig. 13.17a possui ordens lineares aceitáveis, conforme

é ilustrado pelas fig. 13.17b e 13.17c.

Assim, para testar a serialização de conflito, precisamos construir o gráfico de precedência e evocar um

algoritmo de detecção de ciclos. Algoritmos de detecção de ciclos podem ser encontrados em livros-texto sobre

algoritmos. Os algoritmos de detecção de ciclos, como aqueles baseados em depth-first search, são da ordem de

n2 operações, em que n é o número de vértices no gráfico (ou seja, o número de transações). Assim, temos um

esquema prático para determinar a serialização de conflito.

Retornando a nossos exemplos anteriores, observe que os gráficos de precedência para as escalas 1 e 2

(fig. 13.15) realmente não contêm ciclos. O gráfico de precedência para a escala 4 (fig. 13.16), por outro lado,

contém um ciclo que indica que essa escala não é conflito serializava.

Teste para Serialização de Visão

Podemos modificar o teste do gráfico de precedência para serialização de visão, conforme mostraremos a

seguir. Entretanto, o teste resultante é oneroso em relação ao tempo de CPU. De fato, testar serialização de visão

é um problema caro em termos computacionais, como veremos posteriormente.

273

No teste para serialização de conflito, sabemos que, se duas transações, Ti e Tj, têm acesso a um item de

dados Q, e pelo menos uma dessas transações escreve Q, então a aresta Ti�Tj ou a aresta Tj�Ti será inserida no

gráfico de precedência. Porém, isto não mais ocorre no teste para serialização de visão. Como veremos em breve,

essa diferença é a razão da incapacidade em se chegar a um algoritmo eficiente para esse teste.

Considere a escala 9 da fig. 13.12. Se seguirmos a regra do teste para serialização de conflito e criamos o

gráfico de precedência, obteremos o gráfico da fig. 13.18. O gráfico contém um ciclo indicando que a escala 9 não

é conflito serializava. Entretanto, como vimos anteriormente, a escala 9 é visão serializava, já que ela é

equivalente em visão à escala sequencial <T3, T4, T6>. A aresta T4�T3 não deveria ter sido inserida no gráfico, já

que os valores do item Q produzidos por T3 e T4 não foram usados por quaisquer outras transações, e T6 produziu

um valor final novo de Q. As instruções write(Q) de T3 e T4 são chamadas de gravações inúteis.

Com isso, mostramos que não podemos simplesmente usar o esquema de gráfico de precedência citado

anteriormente para testar serialização de visão. Precisamos desenvolver um esquema para decidir se uma aresta

deve ou não ser inserida no gráfico de precedência.

Seja S uma escala. Suponha que a transação Tj leia o valor do item de dado Q escrito por Ti. É claro que, se

S é visão serializava, então, em qualquer escalar que S’ seja equivalente a S, Ti deve preceder Tj. Suponha agora

que, na escala S, a transação Tk executou uma write(Q). Então, na escala S’, Tk deve preceder Ti ou deve seguir Tj.

274

Ela não poderá aparecer entre Ti e Tj, porque dessa forma Tj não leria o valor de Q escrito por Ti e, assim, S não

seria equivalente em visão a S’.

Tais requisitos não podem ser expressos no modelo simples de gráfico de precedência discutido

anteriormente. A dificuldade acontece porque sabemos que, no exemplo precedente, um dos pares de arestas,

Tk�Ti ou Tj�Tk, deverá ser inserido no gráfico, mas não temos, contudo, formulada a regra para determinar qual

a escolha apropriada.

Para formulá-la, precisamos expandir o gráfico de precedência de modo a incorporar as arestas rotuladas.

Chamamos esse gráfico de gráfico de precedência rotulado. Como antes, os nós do gráfico são as transações que

participam da escala. As regras para a inserção de arestas rotuladas são descritas a seguir.

Seja S uma escala que consiste nas transações {T1, T2, ..., Tn}. Sejam Tb e Tf duas transações fictícias, tais

que Tb execute write(Q) para todo Q que sofreu acesso em S e Tf, execute uma read(Q) para todo Q que sofreu

acesso em S. Construímos uma nova escala S’ a partir de S por meio da inserção de Tb no início de S e do

acréscimo de Tf no final de S. Construímos o gráfico de precedência rotulado para a escala S’ conforme segue:

1. Adicione uma aresta , se a transação Tj lê o valor do item de dados Q escrito pela transação Ti.

2. Remova todas as arestas que incidam em transações inúteis. Uma transação Ti é inútil se não houver

caminho, no gráfico de precedência, de Ti para a transação Tf.

3. Para todo item de dados Q, tal que Tj lê o valor de Q escrito por Ti, Tk executa um write(Q) e Tk≠Tb, faça o

seguinte:

a. Se Ti=Tb e Tj≠Tf, então insira a aresta no gráfico de precedência rotulado.

b. Se Ti≠Tb e Tj=Tf, então insira a aresta no gráfico de precedência rotulado.

c. Se Ti=Tb e Tj≠Tf, então insira o par de arestas e no gráfico de precedência rotulado,

em que p é um inteiro maior que 0 que não tenha sido usado anteriormente para rotular arestas.

A regra 3c determina que, se Ti escrever um item de dados lido por Tj, então uma transação Tk que escreva o

mesmo item de dados deve vir antes de Ti ou depois de Tj. As regras 3a e 3b são casos especiais resultantes do

fato de que, necessariamente, Tb e Tf são a primeira e a última transação, respectivamente. Quando aplicamos a

regra 3c, não estamos exigindo que Tk esteja simultaneamente antes de Ti e depois de Tj. Em vez disso,

poderemos escolher onde Tk aparecerá, em uma ordem sequencial equivalente.

Como ilustração, considere novamente a escala 7 (fig. 13.10). O gráfico construído pelos passos 1 e 2 é

mostrado na fig. 13.19a. Ele contém a aresta , já que T3 lê o valor de Q escrito por Tb. ele contém a aresta

, já que T3 foi a última transação que escreveu Q e, assim, Tf leu aquele valor. O gráfico final que

corresponde à escala 7 é mostrado na fig. 13.19b. Ele contém a aresta resultante do passo 3a. Ele contém

a aresta como resultado do passo 3b.

275

Agora, considere a escala 9 (fig. 13.12). O gráfico construído nos passos 1 e 2 é mostrado na fig. 13.20a. O

gráfico final é mostrado na fig. 13.20b. Ele contém as arestas e como resultado do passo 3a. Contém

as arestas (já no gráfico) e como resultado do passo 3b.

Finalmente, considere a escala 10 da fig. 13.21. A escala 10 é visão serializava, já que é equivalente em

visão à escala sequencial <T3, T4, T7>. O gráfico de precedência rotulado correspondente, construído nos passos 1

e 2 é mostrado na fig. 13.22a. O gráfico final é mostrado na fig. 13.22b. As arestas e foram inseridas

como resultado da regra 3a. O par de arestas e foi inserido como resultado de uma única

aplicação da regra 3c.

276

Os gráficos mostrados nas figuras 13.19b e 13.22b contêm os seguintes ciclos mínimos, respectivamente:

O gráfico da fig. 13.20b, por outro lado, não contém ciclos.

Se o gráfico não contém ciclos, a escala correspondente é visão serializava. Realmente, o gráfico da figura

13.20b não contém ciclos, e sua escala correspondente, escala 9, é visão serializava. Entretanto, se o gráfico

contiver um ciclo, essa condição não implica necessariamente que a escala correspondente não seja visão

serializava. Realmente, o gráfico da fig. 13.19b contém um ciclo, contudo sua escala correspondente, escala 7,

não é visão serializava. O gráfico da fig. 13.22b, por outro lado, contém um ciclo, mas sua escala correspondente,

escala 10, é visão serializava.

Como, então, determinamos se uma escala é visão serializava? A resposta está em uma intepretação

apropriada do gráfico de precedência. Suponha que haja n pares de arestas distintas. Ou seja, aplicamos n vezes a

regra 3c na construção do gráfico de precedência. Haverá então 2n gráficos diferentes, sendo que cada gráfico

contém apenas uma aresta de cada par. Se algum desses gráficos for acíclico, então a escala correspondente será

visão serializava. A ordem de serialização é determinada pela remoção das transações fictícias Tb e Tf e pela

classificação topológica do gráfico acíclico restante.

Volte ao gráfico da fig. 13.22b. como há exatamente um par distinto, há dois gráficos diferentes que

devem ser considerados. Os dois gráficos são mostrados na fig. 13.23. Como o gráfico da fig. 13.23a é acíclico,

sabemos que a escala correspondente, escala 10, é visão serializava.

O algoritmo descrito anteriormente obriga testar exaustivamente todos os possíveis gráficos distintos.

Para isso mostrou-se que o problema do teste de um gráfico acíclico nesse conjunto recai sobre a classe de

277

problemas NP-completos. Qualquer algoritmo para um problema NP-completo quase certamente tomará um

tempo exponencial proporcional ao tamanho do problema.

De fato, foi mostrado que o problema do teste para serialização de visão é, ele próprio, NP-completo.

Assim, muito provavelmente não há um algoritmo eficiente para testar serialização de visão. Entretanto, os

esquemas de controle de concorrência ainda podem usar as condições suficientes para serialização de visão. Ou

seja, se as condições suficientes forem satisfeitas, a escala é visão serializava, mas pode haver escalas visão

serializava que não satisfaçam as condições suficientes.

278

Controle de Concorrência

Já vimos que uma propriedade fundamental da transação é o isolamento. Quando diversas transações são

executadas de modo concorrente em um banco de dados, a propriedade do isolamento pode não ser preservada.

É necessário que o sistema controle a interação entre transações concorrentes; esse controle é alcançado por

meio de uma larga gama de mecanismo chamados esquemas de controle de concorrência.

Todos os esquemas de controle de concorrência têm por base a propriedade de serialização

(serializability). Isto é, todos os esquemas apresentados aqui garantem que a ordenação de processamento é

serializada.

Protocolo com Base em Bloqueios (Lock)

Um meio de garantir a serialização é obrigar que o acesso aos itens de dados seja feito de maneira

mutuamente exclusiva; isto é, enquanto uma transação acessa um item de dados, nenhuma outra transação pode

modifica-lo. O método mais usado para sua implementação é permitir o acesso a um item de dados somente se

ele estiver bloqueado.

Bloqueios

Há vários modos por meio dos quais um item de dado pode ser bloqueado. Vamos nos restringir a dois

deles:

1. Compartilhado. Se uma transação Ti obteve um bloqueio compartilhado (denotado por S) sobre o item Q,

então Ti pode ler, mas não escrever Q.

2. Exclusivo. Se uma transação Ti obteve um bloqueio exclusivo (denotado por X) do item Q, então Ti pode

tanto ler como escrever Q.

Precisamos que toda transação solicite o bloqueio do item Q de modo apropriado, dependendo do tipo

de operação realizada em Q. A solicitação é direcionada para o gerenciador do controle de concorrência. A

transação pode realizar suas operações somente depois que o gerenciador de controle de concorrência. A

transação pode realizar suas operações somente depois que o gerenciador de controle de concorrência conceder

(grants) o bloqueio para transação.

Dado um conjunto de bloqueios, podemos definir uma função de compatibilidade sobre eles. Seja A e B

uma representação arbitrária dos modos de bloqueio. Suponha que uma transação Ti solicite um bloqueio do

modo A sobre o item Q, sobre o qual a transação Tj (Ti≠Tj) mantém um bloqueio do modo B.

Se uma transação Ti consegue um bloqueio sobre Q imediatamente, a despeito da presença de um

bloqueio do modo B, então dizemos que o modo A é compatível com o modo B. Essa função pode ser

convenientemente representada por uma matriz. A relação de compatibilidade entre os dois modos de bloqueio

usados aqui é apresentada na matriz comp da fig. 14.1. Um elemento comp(A,B) da matriz possui valor

verdadeiro se, e somente se, o modo A é compatível com o modo B.

Note que o modo compartilhado é compatível com o modo compartilhado, mas não com o modo

exclusivo. A qualquer hora podem ser feitos diversos bloqueios compartilhados simultaneamente (por diferentes

transações) sobre um item de dado em particular. Uma solicitação de bloqueio exclusivo precisa esperar até que

um bloqueio compartilhado termine para ser efetivada.

Uma transação solicita um bloqueio compartilhado do item de dado Q executando a instrução lock-S(Q).

Analogamente, um bloqueio exclusivo é solicitado pela instrução lock-X(Q). um item de dado Q pode ser

desbloqueado por outra transação, o gerenciador de controle de concorrência não concederá o bloqueio até que

todos os bloqueios incompatíveis mantidos pela outra transação sejam desfeitos.

279

A transação Ti pode desbloquear um item de dado a qualquer momento. Note que uma transação precisa

manter o bloqueio do item de dado durante todo o tempo de acesso àquele item. Além disso, o desbloqueio

imediatamente após o acesso final nem sempre é interessante, já que pode comprometer a serialização.

Como ilustração, considere novamente o sistema bancário apresentado anteriormente.

Sejam A e B duas contas que são acessadas pelas transações T1 e T2. A transação T1 transfere 50 dólares

da conta A para a conta B e tem a forma:

A transação T2 apresenta o saldo total das contas A e B – isto é, a soma A+B – e é definida por:

Suponha que os saldos de A e B sejam 100 e 200 dólares, respectivamente. Se essas duas transações são

executadas serialmente, na ordem T1, T2 ou T2, T1, então a transação T2 mostrará o valor 300 dólares. Se, no

entanto, essas transações forem executadas concorrentemente, a escala de execução 1, mostrada na fig. 14.2,

pode ocorrer. Nesse caso, a transação T2 mostrará o resultado 250 dólares, que não é correto. A razão desse erro

provém da falta de bloqueio em tempo hábil do item de dado B, com isso T2 mostra uma situação inconsistente.

A escala de execução mostra as ações que são executadas pelas transações, assim como os pontos em

que os bloqueios são concedidos pelo gerenciador de controle de concorrência. Uma transação que pede um

bloqueio não pode executar sua próxima ação até que o bloqueio seja concedido pelo gerenciador de controle de

concorrência; daí o bloqueio precisa ser concedido no intervalo de tempo entre a operação de pedido de

bloqueio e a ação seguinte da transação. Em que momento, exatamente, o bloqueio é concedido imediatamente

antes da ação seguinte da transação. Assim, retiraremos a coluna que indica as ações do gerenciador de controle

de concorrência de todas as escalas de execução apresentadas.

Suponha, agora, que os desbloqueios sejam realizados ao final da transação. A transação T3 é similar à

transação T1, com desbloqueio ao final da transação, e é definida como:

A transação T2 apresenta o saldo total das contas A e B – isto é, a soma A+B – e é definida por:

280

Suponha que os saldos de A e B sejam 100 e 200 dólares, respectivamente. Se essas duas transações são

executadas serialmente, na ordem T1, T2 ou T2, T1, então a transação T2 mostrará o valor 300 dólares. Se, no

entanto, essas transações forem executadas concorrentemente, a escala de execução 1, mostrada na fig. 14.2,

pode ocorrer. Nesse caso, a transação T2 mostrará o resultado 250 dólares, que não é correto. A razão desse erro

provém da falta de bloqueio em tempo hábil do item de dado B, com isso T2 mostra uma situação inconsistente.

A escala de execução mostra as ações que são executadas pelas transações, assim como os pontos em

que os bloqueios são concedidos pelo gerenciador de controle de concorrência. Uma transação que pede um

bloqueio não pode executar sua próxima ação até que o bloqueio seja concedido pelo gerenciador de controle de

concorrência; daí o bloqueio precisa ser concedido no intervalo de tempo entre a operação de pedido de

bloqueio e a ação seguinte da transação. Em que momento, exatamente, o bloqueio é concedido dentro desse

intervalor não é importante; o bloqueio é considerado seguro mesmo que concedido imediatamente antes da

ação seguinte da transação. Assim, retiraremos a coluna que indica as ações do gerenciador de controle de

concorrência de todas as escalas de execução apresentadas aqui.

Suponha, agora, que os desbloqueios sejam realizados ao final da transação. A transação T3 é similar à

transação T1, com desbloqueio ao final da transação, e é definida como:

A transação T4 corresponde à T2, com desbloqueio ao final da transação, e é definida como:

281

Você pode notar que a sequência de leituras e escritas da escala de execução 1, que resulta no total

incorreto de 250 dólares, não ocorre usando T3 e T4. Outras escalas são possíveis. T4 não apresentará um

resultado inconsistente, qualquer que seja a escala de execução.

Infelizmente, o uso de bloqueio pode causar situações indesejáveis. Considere a escala parcial de T3 e T4

na fig. 14.3. Já que T3 mantém um bloqueio exclusivo sobre B, e T4 solicita um bloqueio compartilhado em B, e T4

solicita um bloqueio compartilhado em B, T4 espera que T3 libere B. Analogamente, como T4 mantém um bloqueio

compartilhado de A, e T3 está solicitando um bloqueio exclusivo em A, T3 está esperando que T4 libere A. Assim,

chegamos a uma situação em que nenhuma dessas transações pode processar em sua forma normal. Essa

situação é chamada de deadlock (impasse). Quando um deadlock ocorre, o sistema precisa desfazer uma das duas

transações. Uma vez desfeita a transação, os itens de dados são, então, avaliados por outras transações, que

podem continuar com suas execuções. Retornaremos aos meios de tratamento do deadlock mais adiante.

Se não usarmos o bloqueio, ou desbloqueio, dos itens de dados, tão logo seja possível, após sua leitura ou

escrita, poderemos chegar a resultados inconsistentes. Por outro lado, se não desbloquearmos um item de dados

antes de solicitarmos um bloqueio a outro item de dados, o deadlock poderá ocorrer. Há formas de evitar o

deadlock em algumas situações. Entretanto, em geral, deadlocks são problemas inerentes ao bloqueio, necessário

se desejarmos evitar estados inconsistentes. Os deadlocks podem ser preferíveis a estados inconsistentes, já que

podem ser tratados por meio do rollback (reversão) da transação, enquanto os estados inconsistentes podem

originar problemas reais, não tratados pelo sistema de banco de dados.

Exigimos que cada transação do sistema siga determinado conjunto de regras, chamado de protocolo de

bloqueio, indicando quando uma transação pode ou não bloquear ou desbloquear cada um dos itens de dados. O

protocolo de bloqueio restringe o número de escalas de execução possíveis. O conjunto de todas as escalas desse

tipo é um subconjunto de todas as escalas serializadas possíveis. Apresentaremos diversos protocolos de bloqueio

que permitem somente escalas com serialização de conflitos. Antes de fazê-lo, precisamos de algumas definições.

Seja {T0, T1, ..., Tn} um conjunto de transações participantes de uma escala de execução S. Dizemos que Ti

precede Tj em S, denotando Ti�Tj, se há um item de dado Q tal que Ti consegue bloqueio do tipo A sobre Q e

depois Tj consegue bloqueio do tipo B sobre Q e comp (A,B) = falso. Se Ti�Tj, então essa precedência implica que,

em qualquer escala serial equivalente, Ti precisa aparecer antes de Tj. Observe que esse gráfico é similar ao usado

anteriormente para testar serialização de conflito. Dizemos que um protocolo de bloqueio garante serialização de

conflito se, e somente se, para todas as escalas de execução legais, as relações associadas � são acíclicas.

Concessão de Bloqueios

Quando uma transação solicita bloqueio sobre um determinado item de dado em particular, e nenhuma

outra transação mantém o mesmo item de dado bloqueado de modo conflitante, tal bloqueio pode ser

concedido. Entretanto, é preciso ter cuidado para evitar o seguinte cenário. Suponha que a transação T2 tenha um

bloqueio compartilhado sobre um item de dado e outra transação T1 solicite um bloqueio exclusivo do mesmo

282

item. Claro que T1 terá de esperar até que o bloqueio compartilhado feito por T2 seja liberado. Enquanto isso,

uma transação T3 pode solicitar que um bloqueio compartilhado feito por T2 seja liberado. Enquanto isso, uma

transação T3 pode solicitar um bloqueio compartilhado sobre o mesmo item de dado. O bloqueio pedido é

compatível com o bloqueio concedido a T2, de modo que o bloqueio compartilhado pode ser concedido a T3.

Nessa altura, T2 pode liberar o bloqueio, mas T1 terá de esperar agora, até que T3 termine. Novamente, aparece

uma nova transação T4 que solicita um bloqueio compartilhado sobre o mesmo item de dado e ele é concedido

antes que T3 libere o dado. De fato, é possível que haja uma sequência de transações solicitando bloqueios

compartilhados sobre um item de dado, e que cada uma delas libere seu bloqueio um pouco antes de que novo

bloqueio seja concedido à outra transação, de modo que T1 nunca consegue seu bloqueio exclusivo. A transação

T1 poderá nunca ser processada, e ela é chamada de inane (starved).

Podemos evitar a inanição de transações da seguinte forma. Quando uma transação Ti solicita o bloqueio

do item de dados Q de um modo particular M, o bloqueio é concedido contato que:

1. Não haja nenhuma outra transação com bloqueio sobre Q cujo modo de bloqueio seja conflitante com

M.

2. Não haja nenhuma outra transação que esteja esperando um bloqueio sobre Q e que tenha feito sua

solicitação de bloqueio antes de Ti.

Protocolo de Bloqueio em Duas Fases

Um dos protocolos que garante a serialização é o protocolo de bloqueio em duas fases (two-phase locking

protocol). Esse protocolo exige que cada transação emita suas solicitações de bloqueio e desbloqueio em duas

fases:

1. Fase de expansão. Uma transação pode obter bloqueios, mas não pode liberar nenhum.

2. Fase de encolhimento. Uma transação pode liberar bloqueios, mas não consegue obter nenhum bloqueio

novo.

Inicialmente, uma transação está na fase de expansão. A transação adquire os bloqueios de que precisa.

Tão logo a transação libera um bloqueio, ela entra na fase de encolhimento e não poderá solicitar novos

bloqueios.

Por exemplo, as transações T3 e T4 têm duas fases. Por outro lado, as transações T1 e T2 não têm duas

fases. Note que as instruções de desbloqueio não precisam aparecer no final da transação. Por exemplo, no caso

da transação T3, podemos colocar a instrução unlock(B) logo após a instrução lock-X(A) e ainda assim manter a

propriedade do bloqueio em duas fases.

Podemos mostrar que o protocolo do bloqueio em duas fases garante a serialização de conflitos.

Considere qualquer transação. O ponto da escala no qual a transação obteve seu bloqueio final (o fim da fase de

expansão) é chamado de ponto de bloqueio da transação. Assim, as transações podem ser ordenadas de acordo

com seus pontos de bloqueio – essa ordenação é, de fato, uma ordenação serializada de transações.

O bloqueio em duas fases não garante a ausência de deadlock. Observe que as transações T3 e T4

possuem duas fases, mas na escala de execução 2 (fig. 14.3) elas estão em um deadlock.

Recordamos que, além de serem serializada, é desejável que as escalas de execução não sejam em

cascata. O rollback em cascata pode ocorrer sob o protocolo de bloqueio em duas fases. Como ilustração,

considere a escala da fig. 14.4. Cada transação observa o protocolo de bloqueio em duas fases, mas a falha de T5

depois do passo read(A) da transação T7 ocasiona o rollback em cascata de T6 e T7.

283

Os rollbacks em cascata podem ser evitados por uma modificação no bloqueio em duas fases chamado

protocolo de bloqueio em duas fases severo (strict two-phase locking). O protocolo de bloqueio em duas fases

severo exige, em adição ao bloqueio feito em duas fases, que todos os bloqueios de modo exclusivo tomados por

uma transação sejam mantidos até que a transação seja efetivada. Essa exigência garante que qualquer dado

escrito por uma transação que não foi ainda efetivada seja bloqueado de modo exclusivo até que a transação seja

efetivada, evitando que qualquer outra transação leia o dado em transação.

Outra variante do bloqueio em duas fases é o protocolo de bloqueio em duas fases rigoroso, que exige

que todos os bloqueios sejam mantidos até que a transação seja efetivada. Pode ser facilmente verificado que,

com o bloqueio em duas fases rigoroso, as transações podem ser serializadas na ordem de sua efetivação. A

maioria dos sistemas de banco de dados implementa ou o bloqueio em duas fases severo ou o rigoroso.

Considere as duas transações seguintes para as quais mostramos somente algumas das mais significativas

operações de leitura (read) e escrita (write). A transação T8 é definida como:

A transação T9 é definida como:

Se empregarmos o protocolo de bloqueio em duas fases, então T8 precisará bloquear a1 de modo

exclusivo. Portanto, qualquer execução concorrente de ambas as transações atinge uma execução serial. Note,

entretanto, que T8 precisa de um bloqueio exclusivo de a1 somente ao final de sua execução, quando ela escreve

a1. Assim, se T8 estiver bloqueando ai de modo compartilhado e depois mudar esse bloqueio para o modo

exclusivo, poderemos obter mais concorrência, já que T8 e T9 poderiam manter acesso simultâneo a a1 e a2.

Essa observação remete-nos ao refinamento do protocolo básico do bloqueio em duas fases, no qual a

conversão de bloqueios é permitida. Podemos proporcionar um mecanismo para promover um bloqueio

compartilhado para exclusivo de promoção (upgrade) e de exclusivo para compartilhamento de rebaixamento

(downgrade). A conversão de bloqueio não pode ser arbitrária. Pelo contrário, a promoção só pode acontecer

durante a fase de expansão, enquanto o rebaixamento somente ocorre na fase de encolhimento.

Retornando a nosso exemplo, as transações T8 e T9 podem ser executadas concorrentemente sob o

protocolo de bloqueio em duas fases refinado, como mostra a escala incompleta da fig. 14.5, em que somente

algumas das instruções de bloqueio são mostradas.

Note que uma transação tentando a promoção de um bloqueio do item Q pode ser forçada a esperar.

Essa espera forçada ocorre se Q estiver bloqueado por outra transação em modo compartilhado.

284

Tanto quanto o protocolo de bloqueio em duas fases, o bloqueio em duas fases com conversão de

bloqueio não pode ser arbitrária. Pelo contrário, a promoção só pode acontecer durante a fase de expansão,

enquanto o rebaixamento somente ocorre na fase de encolhimento.

Retornando a nosso exemplo, as transações T8 e T9 podem ser executadas concorrentemente sob o

protocolo de bloqueio em duas fases refinado, como mostra a escala incompleta da fig. 14.5, em que somente

algumas das instruções de bloqueio são mostradas.

Note que uma transação tentando a promoção de um bloqueio do item Q pode ser forçada a esperar.

Essa espera forçada ocorre se Q estiver bloqueado por outra transação em modo compartilhado.

Tanto quanto o protocolo de bloqueio em duas fases, o bloqueio em duas fases com conversão de

bloqueio só gera escalas com serialização de conflito, e as transações podem ser serializadas por seus pontos de

bloqueio. Além disso, se bloqueios exclusivos são mantidos até o final da transação, as escalas são em cascata.

Descreveremos agora um esquema simples, mas muito usado, que gera as instruções de bloqueio e

desbloqueio, automaticamente, para uma transação. Quando uma transação Ti emite uma operação read(Q), o

sistema emite uma instrução lock-S(Q) seguida de uma instrução read(Q). Quando Ti emite uma operação

write(Q), o sistema verifica se Ti ainda mantém um bloqueio compartilhado. Se ainda há, o sistema emite uma

instrução upgrade(Q), seguida de uma instrução write(Q). De outro modo, o sistema emite uma instrução lock-

X(Q), seguida de uma instrução write(Q). Todos os bloqueios obtidos por uma transação são desbloqueados

depois da transação ser efetivada ou abortada.

Para um conjunto de transações, pode haver escalas de serialização de conflito que não sejam obtidas por

meio do protocolo de bloqueio em duas fases. Entretanto, para obter escalas de serialização de conflito por meio

de protocolos de bloqueio sem usar duas fases, precisamos obter informações adicionais sobre as transações ou

impor alguma estrutura, ou ordem, sobre o conjunto de itens de dados dos banco de dados. Na ausência de tais

informações, o bloqueio em duas fases é necessário para a serialização de conflito – se Ti é uma transação que

não está em duas fases, é sempre possível encontrar outra transação Tj que esteja em duas fases, tal que haja

uma escala viável para Ti e Tj que não seja conflitante por serialização.

O bloqueio em duas fases severo e o bloqueio em duas fases rigoroso (com conversão de bloqueios) são

usados extensivamente em sistemas de banco de dados comerciais.

Protocolos com Base em Gráficos (Graph-Based Protocols)

Como dissemos, na ausência de informações a respeito do modo de acesso aos itens de dados, o

protocolo de bloqueio em duas fases é necessário e suficiente para garantir a serialização. Assim, se desejamos

desenvolver protocolos que não usam duas fases, precisamos de informações adicionais sobre como cada

transação desenvolverá seu acesso ao banco de dados. Há diversos modelos que diferem no tocante à quantidade

de informações a proporcionar. O modelo mais simples exige que tenhamos conhecimento anterior sobre a

ordem na qual os itens de banco de dados serão acessados. Fornecidas essas informações, é possível construir

protocolos de bloqueio que não sejam em duas fases, mas que, no entanto, garantem a serialização de conflito.

Para adquirir esse conhecimento prévio, impomos uma ordenação parcial � sobre o conjunto

D={d1,d2,...,dn} de todos os itens de dados. Se di�dj, então qualquer transação que mantenha acesso a ambos, di

285

e dj, deverá acessar primeiro di e depois dj. Essa ordenação parcial pode resultar da organização física ou lógica

dos dados, ou pode ser imposta somente para fins de controle de concorrência.

A ordenação parcial implica que o conjunto D pode ser visto agora como um gráfico acíclico, chamado

gráfico de banco de dados. Por maior simplicidade, restringiremos nossa atenção somente àqueles gráficos que

são árvores raízes. Apresentaremos um protocolo simples, chamado de protocolo de árvore, que é restrito para

emprego somente nos bloqueios exclusivos.

No protocolo de árvore é permitida somente a instrução de bloqueio lock-X. Cada transação Ti pode

bloquear um item de dado no máximo uma vez e deve observar as seguintes regras:

1. O primeiro bloqueio feito por Ti pode ser sobre qualquer dado.

2. Subsequentemente, um certo item de dado Q pode ser bloqueado por Ti somente se os pais de Q

estiverem bloqueados por Ti.

3. Itens de dados podem ser desbloqueados a qualquer momento.

4. Um item de dado que foi bloqueado e desbloqueado por Ti não pode ser rebloqueado por Ti

subsequentemente.

Como colocamos anteriormente, todas as escalas de execução que forem legais sob o protocolo de árvore

que serão conflito serializadas.

Para ilustrar esse protocolo, considere o gráfico do banco de dados da fig. 14.6. As quatro transações

seguintes respeitam o protocolo de árvore desse gráfico. Mostraremos somente as instruções de bloqueio e

desbloqueios:

Uma escala possível em que participam essas quatro transações é a mostrada na fig. 14.7. Note que,

durante a execução, a transação T10 mantém bloqueio sobre duas subárvores separadas.

Observe que a escala da fig. 14.7 é conflito serializada. Não apenas pode ser mostrado que o protocolo de

árvore garante a serialização de conflito, mas também que esse protocolo garante a ausência de deadlock.

286

O protocolo de bloqueio em árvore apresenta a vantagem de realizar o desbloqueio mais cedo do que é

feito no protocolo de bloqueio em duas fases. O desbloqueio feito mais cedo pode reduzir os tempos de espera e

aumentar a concorrência. Além disso, uma vez que o protocolo é resistente a deadlocks, nenhum rollback é

necessário. Entretanto, o protocolo tem a desvantagem de, em alguns casos, uma transação pode manter o

bloqueio de um item de dado que não acessa. Por exemplo, uma transação que precise do acesso aos itens de

dados A e J cujo gráfico do banco de dados é o da fig. 14.6, precisa não somente bloquear A e J, mas também os

itens de dados B, D e H. Esse bloqueio adicional resulta no aumento de overhead relativo aos bloqueios, na

possibilidade de tempo de espera adicional e decréscimo potencial da concorrência. Além disso, sem o

conhecimento prévio de como os itens de dados serão bloqueados, as transações terão de bloquear a raiz da

árvore, o que reduz consideravelmente a concorrência.

Para um conjunto de transações, há escalas conflitos serializadas que não podem ser obtidas pelo

protocolo de árvore. Ainda, há escalas possíveis sob o protocolo de bloqueio em duas fases que também não são

possíveis sob o protocolo de árvore, e vice-versa.

Protocolos com Base em Timestamp (Registro de Tempo)

Nos protocolos de bloqueio, descritos até agora, a ordem entre cada par de transações conflitantes é

determinada durante a execução do primeiro bloqueio que ambas solicitam e que envolve modos incompatíveis.

Outro método para a determinação da ordem serializada é selecionar uma ordenação entre transações em

andamento. O método mais usado é o esquema de ordenação por timestamp.

Timestamp

A cada transação Ti dos sistema associamos um único timestamp fixo, denotado por TS(Ti). Esse

timestamp é criado pelo sistema de banco de dados antes que a transação Ti inicie sua execução. Se uma

transação Ti recebeu o TS(Ti) em uma nova transação Tj entre no sistema, então TS(Ti)<TS(Tj).

Há duas formas simples de implementar esse esquema:

1. Usar a hora do relógio do sistema (clock) como timestamp, isto é, o timestamp de uma transação é igual à

hora em que a transação entra no sistema.

2. Usar um contador lógico que é incrementado sempre que se usa um novo timestamp, isto é, o timestamp

da transação é igual ao valor do contador no momento em que a transação entre no sistema.

Os timestamps das transações determinam a ordem de serialização. Assim, se TS(Ti)<TS(Tj), o sistema

precisa garantir que a escala produzida seja equivalente a uma escala serial em que a transação Ti aparece antes

da transação Tj.

Para implementação desse esquema, associamos a cada item Q dois valores para timestamp:

• W-timestamp(Q) denota o maior timestamp de qualquer transação que execute uma write(Q) com

sucesso.

• R-timestamp(Q) denota o maior timestamp de qualquer transação que execute uma read(Q) com

sucesso.

Esses timestamps são atualizados sempre que uma nova instrução read(Q) ou write(Q) for executada.

O Protocolo de Ordenação por Timestamp

O protocolo de ordenação por timestamp assegura que quaisquer operações de leitura e escrita sejam

executadas por ordem de timestamp. Esse protocolo opera da seguinte forma:

1. Suponha que a transação Ti emita uma read(Q).

a. Se TS(Ti)<W-timestamp(Q), então Ti precisa ler um valor de Q que já foi sobreposto. Assim, a

operação read é rejeitada e Ti é desfeita.

b. Se TS(Ti)≥W-timestamp(Q), então a operação read é executada e R-timestamp(Q) recebe o maior

valor entre R-timestamp(Q) e TS(Ti).

2. Suponha que a transação Ti emita um write(Q).

287

a. Se TS(Ti)<R-timestamp(Q), então o valor de Q que Ti está produzindo foi necessário antes e o

sistema assumiu que aquele valor nunca seria produzido. Logo, a operação write é rejeitada e Ti é

desfeita.

b. Se TS(Ti)<W-timestamp(Q), então Ti está tentando escrever um valor obsoleto em Q. Logo, essa

operação write é rejeitada e Ti é desfeita.

c. De outro modo, a operação write é executada e W-timestamp(Q) é registrado em TS(Ti).

Uma transação Ti que foi desfeita pelo esquema de controle de concorrência, decorrente de uma

operação read ou write, recebe um novo timestamp e é reiniciada.

Para ilustrar esse protocolo, considere as transações T14 e T15. A transação T14 mostra o conteúdo total das

contas A e B e é definida como:

A transação T15 transfere 50 dólares da conta A para a conta B e então apresenta o resultado de ambas:

Nas escalas criadas obedecendo ao protocolo de timestamp, assumimos que uma transação recebe um

timestamp imediatamente antes de sua primeira instrução. Assim, na escala 3 da fig. 14.8, TS(T14)<TS(T15) e a

escala é possível sob o protocolo de timestamp.

Notamos que a execução precedente pode também ser realizada pelo protocolo de bloqueio em duas

fases. Há, entretanto, escalas que são viáveis sob o protocolo de bloqueio em duas fases, mas inviáveis sob o

protocolo de timestamp, e vice-versa.

O protocolo de ordenação por timestamp garante a serialização de conflito. Essa asserção provém do fato

de que operações conflitantes são processadas pela ordem do timestamp. O protocolo garante também

resistência a deadlocks, já que uma transação nunca espera. O protocolo consegue gerar escalas que não podem

ser recuperadas (desfeitas), entretanto ele pode receber uma extensão para fazer escalas cascateadas.

Regra de Escrita de Thomas (Thomas’ Write Rule)

Apresentaremos agora uma modificação no protocolo de ordenação por timestamp que aumenta a

concorrência em potencial em relação àquele que descrevemos anteriormente. Consideremos a escala 4 da fig.

14.9 e apliquemos a ela o protocolo da ordenação por timestamp. Uma vez que T16 começa antes de T17, podemos

considerar que TS(T16)<TS(T17). A operação read(Q) de T16 é executada, assim como a operação write(Q) de T17.

Quando T16 tenta executar sua operação write(Q), descobrimos que TS(T16)<W-timestamp(Q), já que W-

timestamp(Q) = TS(T17). Assim, a operação write(Q) de T16 é rejeitada e a transação T16 precisa ser desfeita.

288

Embora o rollback de T16 seja requerido pelo protocolo da ordenação por timestamp, ele é desnecessário.

Uma vez que T17 já escreveu Q, o valor que T16 está tentando escrever nunca será lido. Qualquer transação Ti com

TS(Ti)<TS(T17) deverá ler o valor de Q que foi escrito por T17 em vez de o valor escrito por T16.

Essa observação sugere uma modificação no protocolo de ordenação por timestamp no qual operações

write obsoletas podem ser ignoradas sob determinadas circunstâncias. As regras de protocolo para as operações

read permanecem inalteradas. As regras de protocolo para as operações write, entretanto, são ligeiramente

diferentes das do protocolo de ordenação por timestamp vista anteriormente:

1. Se TS(Ti)<R-timestamp(Q), então o valor de Q que Ti está produzindo foi necessário anteriormente, e

assumiu-se que o valor nunca seria produzido. Logo, a operação write é rejeitada e Ti é desfeita.

2. Se TS(Ti)<W-timestamp(Q), então Ti está tentando escrever um valor obsoleto para Q. Logo, a operação

write pode ser ignorada.

3. De outro modo, a operação write é executada e W-timestamp(Q) recebe o valor de TS(Ti).

A diferença entre essas regras e as apresentadas anteriormente está na segunda regra. O protocolo de

ordenação por timestamp exige que Ti seja desfeita se emitir uma write(Q) e TS(Ti)<W-timestamp(Q). Entretanto,

aqui, nos casos em que TS(Ti)≥W-timestamp(Q). Entretanto, aqui, nos casos em que TS(Ti)≥R-timestamp(Q),

ignoramos writes obsoletas. Essa modificação no protocolo de ordenação por timestamp é chamada de regra de

escrita de Thomas.

A regra de escrita de Thomas faz uso da serialização de visão, eliminando, com efeito, as operações de

write obsoletas das transações que as emitem. Essa modificação torna possível a geração de escalas de execução

serializadas que não poderiam ocorrer sob outros protocolos apresentados aqui. Por exemplo, a escala 4 da fig.

14.9 é não-conflito serializada e, assim, não é viável sob qualquer protocolo de bloqueio em duas fases, protocolo

de árvore ou de ordenação por timestamp. Sob a regra escrita de Thomas, a operação write(Q) da T16 poderia ser

ignorada. O resultado é uma escala cuja visão é equivalente à escala serial <T16, T17>.

Protocolos com Base em Validação

Nos casos em que a maioria das transações é somente de leitura, a taxa de conflitos entre as transações

pode ser baixa. Assim, algumas dessas transações, se executadas sem a supervisão de um esquema de controle

de concorrência, poderiam deixar o sistema sempre em estado consistente. Um esquema de controle de

concorrência impõe overhead relativo à execução de mais código e possível atraso nas transações. Pode ser

interessante usar um esquema alternativo que resulte em menor overhead. Uma dificuldade enfrentada para a

redução de overhead é que não sabemos a priori quais transações serão envolvidas em conflito. Para obter essa

informação, precisamos de um esquema para a monitoração do sistema.

Consideramos que cada transação Ti é executada em duas ou três fases diferentes, dependendo se é uma

transação somente de leitura ou de atualização. Essas fases são, em ordem, as seguintes:

1. Fase de leitura. Durante essa fase, a execução da transação Ti tem início. Os valores de diversos itens de

dados são lidos e armazenados em variáveis locais para Ti. Todas as operações de escrita são processadas

com variáveis locais temporárias, sem alterar de fato o banco de dados.

2. Fase de validação. A transação Ti processa um teste de validação para determinar se pode copiar no

banco de dados as variáveis locais temporárias que mantêm os resultados das operações de escrita sem,

com isso, causar a violação da serialização.

3. Fase de escrita. Se a transação Ti obtém sucesso na validação (passo 2), então a atualização é aplicada de

fato ao banco de dados. Caso contrário, Ti é desfeita.

Cada transação precisa passar pelas três fases, na ordem mostrada. Entretanto, as três fases de

transações em execução concorrentes podem ser intercaladas.

289

As fases de leitura e escrita são autoexplicativas. A única fase que exige mais explicações é a de validação.

Para realizar os testes de validação, precisamos saber quando ocorreram as diversas fases da transação Ti.

Precisamos, portanto, associar três timestamps diferentes para a transação Ti:

1. Start(Ti), o momento em que Ti teve início.

2. Validation(Ti), o momento em que Ti terminou sua fase de leitura e começou sua fase de validação.

3. Finish(Ti), o momento em que Ti terminou sua fase de escrita.

Determinamos a ordem de serialização pela técnica de ordenação por timestamp, usando o valor do

timestamp da Validation(Ti). Assim, o valor de TS(Ti)=Validation(Ti) e, se TS(Tj)<TS(Tk), então qualquer escala criada

precisa ser equivalente à escala serializada na qual a transação Tj aparece antes da transação Tk. A razão para

escolhermos Validation(Ti) em vez de Start(Ti) como timestamp da transação Ti é que, com isso, podemos esperar

menor tempo de resposta, com a condição de que as taxas de conflito entre transações sejam com certeza

pequenas.

O teste de validação para Ti exige que, para todas as transações Ti com TS(Ti)<TS(Tj), uma das duas

condições a seguir seja realizada:

1. Finish(Ti)<Start(Ti). Já que Ti completa sua execução antes de Tj começar, a ordem de serialização é com

certeza mantida.

2. Não há interseção entre o conjunto de itens de dados escritos por Ti e o conjunto de dados lidos por Tj, e

Ti completa sua fase de escrita antes de Tj começar sua fase de validação

(Start(Tj)<Finish(Ti)<Validation(Tj)). Essa condição garante que as escritas de Ti e Tj não sejam sobrepostas.

Uma vez que a escrita de Ti não afeta a leitura de Tj e que Tj não pode afetar a leitura de Ti, a ordem de

serialização é com certeza mantida.

Como ilustração, considere novamente as transações T14 e T15. Suponha que TS(T14)<TS(T15). Então, a fase de

validação consegue produzir a escala de execução 5, que é apresentada na fig. 14.10. Note que a escrita das

variáveis reais é realizada somente após a fase de validação de T15. Assim, T14 lê valores desatualizados de A e B e

essa é serializada.

O esquema de validação evita, automaticamente, os rollbacks em cascata, já que as escritas reais

acontecem somente depois que a transação que emitiu a solicitação de escrita tenha sido efetivada.

Granularidade Múltipla

Nos esquemas de concorrência descritos até agora, estivemos usando cada item de dado individual como

uma unidade à qual a sincronização é aplicada.

Há circunstâncias, no entanto, em que pode ser vantajoso o agrupamento de diversos itens de dados,

tratando-os como uma unidade de sincronização individual. Por exemplo, se uma transação Ti precisa do acesso a

todo o banco de dados e um protocolo de bloqueio é usado, Ti precisará bloquear cada um dos itens do banco de

dados. Logicamente, esse bloqueio é um consumidor de tempo. Seria melhor Ti emitir uma única solicitação de

bloqueio a todo o banco de dados. Por outro lado, se uma transação Tj precisa do acesso a somente alguns itens

de dados, não é necessário bloquear todo o banco de dados, porque, desse modo, a concorrência é perdida.

É preciso um mecanismo que permita ao sistema definir diferentes múltiplos de granulação. Podemos

desenvolver um desses mecanismos permitindo diversos tamanhos aos itens de dados e definindo uma hierarquia

290

de granularidade de dados, em que as granulações menores sejam aninhadas às maiores. Tal hierarquia pode ser

representada graficamente como uma árvore. Note que a árvore que descrevemos aqui é bastante diferente da

usada no protocolo de árvore. O nó sem ramificações de uma árvore de granularidade múltipla representa o dado

associado a seus descendentes. No protocolo de árvore, cada nó representa um item de dado independente.

Como ilustração, considere a árvore da fig. 14.11, consistindo em nós em quatro níveis. O nível mais alto

representa o banco de dados como um todo. Abaixo, há nós do tipo área; o banco de dados é constituído

exatamente dessas áreas. Cada área, por sua vez, possui nós do tipo arquivo como filhos. Cada área é constituída

exatamente daqueles arquivos que são seus nós filhos. Nenhum arquivo está em mais de uma área. Finalmente,

cada arquivo possui nós do tipo registro. Como antes, o arquivo é constituído exatamente daqueles registros que

são seus nós filhos, e nenhum registro pode estar em mais de um arquivo.

Cada nó de uma árvore pode ter bloqueio individual. Como foi feito no protocolo de bloqueio em duas

fases, usaremos os modos de bloqueio exclusivo e compartilhado. Quando uma transação bloqueia um nó, tanto

no modo compartilhado quanto no exclusivo, a transação também bloqueará todos os descendentes daquele nó

no mesmo modo de bloqueio. Por exemplo, se a transação Ti bloqueio de forma explícita o arquivo Fc da fig.

14.11, no modo exclusivo, então ela está bloqueando de forma implícita, no modo exclusivo, todos os registros

pertencentes àquele arquivo. Ela não precisará fazer, de forma explícita, o bloqueio individual dos registros de Fc.

Suponha que uma transação Tj queira bloquear o registro rb6 do arquivo Fb. Dado que Ti bloqueou Fb de

forma explícita, segue que rb6 está também bloqueado (de forma implícita). Mas, quando Tj emite uma solicitação

de bloqueio para rb6, este não bloqueado de modo explícito! Como o sistema determinará se Tj pode bloquear rb6?

Tj precisará percorrer a árvore da raiz até o registro rb6. Se algum modo nó do caminho estiver bloqueado de

modo incompatível, então Tj precisará esperar.

Suponha, agora, que a transação Tk deseja bloquear todo o banco de dados. Para isso, ela precisa

simplesmente bloquear a raiz hierárquica. Note, entretanto, que Tk não deve conseguir o bloqueio no nó raiz, já

que Ti já está bloqueado, como acontece com parte da árvore (especificamente, o arquivo Fb). Mas, agora, como o

sistema determinará se o nó raiz poderá ser bloqueado? Uma solução seria pesquisar a árvore inteira. Essa

solução, entretanto, se antepõe ao proposito do esquema do bloqueio de granularidade múltipla. Um meio mais

eficiente seria introduzir uma nova classe de modo de bloqueio, chamado modo de bloqueio intencional. Se um

nó é bloqueado no modo intencional, o bloqueio explícito será feito no nível mais baixo da árvore (isto é, na

granularidade mais fina). Bloqueios intencionais serão feitos em todos os antecessores do nó antes que aquele nó

seja bloqueado de forma explícita. Assim, uma transação não precisa pesquisar a árvore inteira para determinar

se poderá bloquear um nó. Uma transação que queira bloquear um nó – digamos, Q – precisa percorrer o

caminho, pela árvore, do nó até Q. Enquanto se percorre a árvore, os bloqueios das transações são feitos de

modo intencional.

Há um modo intencional associado ao modo compartilhado e um relacionado ao modo exclusivo. Se um

nó é bloqueado no modo compartilhado-intencional (intention-shared – IS), o bloqueio explícito está sendo feito

no nível mais baixo da árvore, mas com somente bloqueios de modo compartilhado.

Analogamente, se um nó é bloqueado no modo exclusivo-intencional (intention-exclusive – IX), então o

bloqueio explícito está sendo feito no nível mais baixo, no modo exclusivo ou compartilhado. Finalmente, se um

nó está bloqueado nos modos de bloqueio é apresentada na fig. 14.12.

291

O protocolo de bloqueio de granularidade múltipla garante a serialização. Cada transação Ti pode

bloquear um nó Q, usando as seguintes regras:

1. A função de compatibilidade de bloqueio da fig. 14.12 precisa ser observada.

2. A raiz da árvore precisa ser bloqueada primeiro e pode ser bloqueada em qualquer modo.

3. Um nó Q pode ser bloqueado por Ti no modo S ou IS somente se o pai de Q for bloqueado por Ti no modo

IX ou IS.

4. Um nó Que pode ser bloqueado por Ti no modo X, SIX ou IX somente se o pai de Q estiver bloqueado por

Ti no modo IX ou no modo SIX.

5. Ti pode bloquear um nó somente se ele não desbloqueou outro nó anteriormente (isto é, Ti tem duas

fases).

6. Ti pode desbloquear um nó Que somente se nenhum dos filhos de Que estiver bloqueado por Ti.

Observe que o protocolo de granularidade múltipla exige que os bloqueios sejam feitos de cima para

baixo (top-down – da raiz para as folhas), enquanto a liberação deve ser de baixo para cima (bottom-up – das

folhas para a raiz).

Para ilustrar o protocolo, considere a árvore da fig. 14.11 e as seguintes transações:

• Suponha que a transação T18 leia o registro ra2 do arquivo Fa. Então, T18 precisa bloquear o banco de

dados, a área A1, o arquivo Fa no modo IS (nessa ordem) e, finalmente, bloquear ra2 no modo S.

• Suponha que a transação T19 altere o registro ra9 do arquivo Fa. Então, T19 precisa bloquear o banco de

dados, a área A1, o arquivo Fa no modo IX e, finalmente, bloquear ra9 no modo X.

• Suponha que a transação T20 leia todos os registros do arquivos Fa. Então, T20 precisa bloquear o banco de

dados e a área A1 (nesse ordem) no modo IS e, finalmente, bloquear Fa no modo S.

• Suponha que a transação T21 leia todo o banco de dados. Então, poderá fazê-lo depois de bloquear o

banco de dados no modo S.

Podemos notar que as transações T18, T20, e T21 mantêm acesso ao banco de dados concorrentemente. A

transação T19 pode concorrer com T18, mas não com T20 nem T21.

Esse protocolo aumenta a concorrência e reduz o overhead por bloqueio. Isso é particularmente útil em

aplicações que misturam:

• Transações curtas que mantêm acesso em poucos itens de dados.

• Transações longas que produzem relatórios a partir de um arquivo ou de um conjunto de arquivos.

Há protocolos de bloqueio similares que são aplicados a sistemas de banco de dados nos quais a

granularidade é organizada na forma de gráficos acíclicos.

Esquemas de Multiversão

Os esquemas de controle de concorrência discutidos até aqui garantem a serialização atrasando a

operação ou abortando a transação responsável por tal operação. Por exemplo, uma operação de read pode ser

retratada se o valor apropriado em questão ainda estiver sendo escrito; ou pode ser rejeitada se o valor

apropriado em questão ainda estiver sendo escrito; ou pode ser rejeitada (isto é, a transação que emitiu tal

solicitação deve ser abortada) porque o valor lido já foi alterado. Essas dificuldades podem ser evitadas se o

sistema providenciar cópias anteriores de cada item de dado.

Em um sistema de banco de dados multiversão, cada operação write(Q) cria uma nova versão de Q.

Quando é emitida uma operação read(Q), o sistema seleciona uma das versões de Q para ser lida seja tal que

292

assegure a serialização. É crucial, por razões de desempenho, que uma transação possa determinar fácil e

rapidamente qual versão do item de dados poderá ser lido.

Multiversão com Ordenação por Timestamp

A técnica mais usada nos esquemas de multiversão é o timestamp. A cada transação Ti do sistema é

associado um timestamp único e estático, denotado por TS(Ti). Esse timestamp é associado antes do início da

execução da transação, conforme já descrito.

Para cada idem de dado Q, uma sequência de versões < Q1, Q2, ..., Qm> é associada. Cada versão Qk

contém três campos de dados:

• Content (conteúdo) é o valor da versão Qk.

• W-timestamp(Qk) é o timestamp da transação que criou a versão Qk.

• R-timestamp(Qk) é o timestamp mais alto de alguma transação que tenha lido a versão Qk com sucesso.

Uma transação – digamos, Ti – cria uma nova versão Qk do item de dado Q emitindo uma operação

write(Q). O campo conteúdo da versão mantém o valor escrito por Ti. O W-timestamp e o R-timestamp são

inicializados por TS(Ti). O valor de R-timestamp é atualizado sempre que uma transação Tj lê o conteúdo de Qk e

R-timestamp é atualizado sempre que uma transação Tj lê o conteúdo de Qk e R-timestamp(Qk)<TS(Tj).

O esquema de multiversão com timestamp apresentado a seguir garante a serialização. O esquema opera

da forma descrita a seguir. Suponha que uma transação Ti emita uma operação read(Q) ou write(Q). Seja Qk a

versão de Q cujo timestamp de escrita é o mais alto timestamp, menor ou igual a TS(Ti).

1. Se a transação Ti emitir uma read(Q), então o valor recebido será o conteúdo da versão Qk.

2. Se a transação Ti emitir um write(Q) e TS(Ti)<R-timestamp(Qk), o conteúdo de Qk é sobreposto; caso

contrário, outra versão de Q é criada.

A justificativa para a regra 1 é clara. Uma transação lê a versão mais recente anterior a ela. A segunda

regra força o aborto de uma transação se for “tarde demais” para que se faça uma escrita. Mais precisamente, se

Ti tentar escrever uma versão que alguma outra transação já tenha lido, então não poderemos permitir que essa

escrita seja bem-sucedido.

As versões que não forem mais necessárias serão removidas conforme a regra seguinte. Suponha que

haja duas versões, QK e Qj, de um item de dados e que ambas as versões tenha o W-timestamp menor que o

timestamp da última transação do sistema. Então, a mais antiga entre as versões QK e Qj não será usada

novamente e pode ser eliminada.

O esquema multiversão ordenada por timestamp possui a adequada propriedade de garantir que uma

solicitação de leitura nunca falhe e nunca espere. Em um sistema de banco de dados típico, em que as operações

de leitura são mais frequentes que as de escrita, essa vantagem é de grande importância prática.

O esquema, entretanto, possui duas propriedades indesejáveis. Primeiro, a leitura de um item de dados

exige também a atualização do campo R-timestamp, resultando em dois acessos ao disco, em vez de apenas um.

Segundo, os conflitos entre transações são resolvidos por rollback, em vez da imposição de tempo de espera. Essa

alternativa pode ser onerosa. Um algoritmo para amenizar o problema será descrito na próxima seção.

Multiversão com Bloqueio em Duas Fases

O protocolo de multiversão com bloqueio em duas fases tenta combinar as vantagens do controle de

concorrência multiversão com as vantagens do bloqueio em duas fases. Esse protocolo diferencia transações

somente de leitura das transações de atualização. As transações de atualização executam um bloqueio em duas

fases rigorosas, isto é, elas mantêm todos os bloqueios até o final da transação. Assim, podem ser serializadas de

acordo com sua ordem de efetivação. Cada item de dado possui um único timestamp. O timestamp não é, nesse

caso, baseado no horário, mas em um contador, que será chamado de ts_counter, que é incrementado durante o

processo de efetivação.

293

Marcamos o timestamp das transações somente de leitura por meio do valor corrente do contador, ou

seja, lendo o valor de ts_counter antes de começar sua execução; para a leitura, elas seguem o protocolo de

multiversão ordenada por timestamp. Com isso, quando uma transação Ti desse tipo emite uma read(Q), o valor

recebido é o conteúdo da versão cujo timestamp é o inferior a TS(Ti) mais próximo.

Quando uma transação de atualização lê um item, ela impõe um bloqueio compartilhado ao item e lê a

última versão do item. Quando uma transação de atualização deseja escrever um item, ela primeiro consegue o

bloqueio exclusivo desse item e, então, cria uma nova versão do item de dados. A escrita é realizada a partir da

nova versão e o timestamp da nova versão recebe ∞ como valor inicial, que é maior que qualquer outro

timestamp possível.

Quando uma transação de atualização Ti completa suas ações, ela realiza o processo de efetivação da

seguinte forma: primeiro, Ti adiciona 1 ao valor de ts_counter e transfere esse valor aos timestamp de todas as

versões que criou; então, Ti adiciona 1 ao ts_counter. Somente uma transação de atualização por vez pode

realizar o processo de efetivação.

Como consequência, as transações somente de leitura que começarem depois de Ti incrementar o

ts_counter acessarão o valor atualizado por Ti, enquanto aquelas que começarem antes do incremento de

ts_counter, feito por Ti, verão o valor anterior à atualização de Ti. Nesse caso, as transações somente de leitura

jamais precisarão esperar por bloqueios.

As versões são eliminadas de modo similar à multiversão com ordenação por timestamp. Suponha que

haja duas versões, QK e Qj, de um item de dado e que ambas as versões tenham timestamp menor que o da

última transação somente de leitura processada no sistema. Logo, a mais antiga entre as duas versões QK e Qj

não será mais usada e pode ser eliminada.

A multiversão com bloqueio em duas fases ou variações são aplicadas a alguns sistemas de banco de

dados comerciais.

Manuseio do Deadlock

Um sistema está em estado de deadlock se há um conjunto de transações, tal que toda a transação desse

conjunto está esperando outra transação também nele contida. Mais precisamente, há um conjunto de

transações esperando {T0,T1,...,Tn}, tal que T0 está esperando um item de dado mantido por T1, T1 está esperando

um item de dado mantido por T2, ..., Tn-1 está esperando um item de dados mantido por Tn e Tn está esperando

por um item de dado mantido por T0. Nenhuma dessas transações poderá prosseguir em uma situação dessas. O

único remédio para essa situação indesejável é uma ação drástica do sistema, como reverter uma das transações

envolvidas no deadlock.

Há dois métodos principais para o tratamento do deadlock. Podemos usar o protocolo de prevenção de

deadlock para garantir que o sistema nunca entrará em tal situação. Ou podemos permitir que o sistema entre

em estado de deadlock e, então, removê-lo dessa situação, recuperando-o por meio dos esquemas de detecção

de deadlock e recuperação de deadlock. Como vimos, ambos os métodos podem acabar por reverter uma

transação (rollback). A prevenção é mais utilizada se a probabilidade do sistema que entrar deadlock for

relativamente alta; caso contrário, a detecção e recuperação são mais eficientes.

Note que os esquemas de detecção e recuperação implicam overhead relativo, não somente ao tempo de

processamento do sistema para manutenção das informações necessárias e para a execução do algoritmo de

detecção, mas também devido às perdas potenciais inerentes advindas da recuperação de um deadlock.

Prevenção de Deadlock

Há duas abordagens para a prevenção de deadlock. Uma garante que nenhum ciclo de espera poderá

ocorrer pela ordenação de solicitações de bloqueios, ou obrigando que todos os bloqueios sejam obtidos juntos.

Outra aproxima-se da recuperação do deadlock, fazendo com que a transação seja refeita, em vez de esperar um

bloqueio, sempre que a espera possa potencialmente ocorrer um deadlock.

294

O esquema mais simples sob a primeira abordagem obriga cada transação a bloquear todos os itens de

dados antes de sua execução. Além disso, ou todos são bloqueados de uma vez ou nenhum o será. Há duas

desvantagens nesse protocolo. A premiria, normalmente, é a dificuldade em prever, antes da transação começar,

quais itens de dados precisarão de bloqueio. Segundo, a utilização do item de dados pode ser bastante reduzida,

já que muitos dos itens de dados podem ser bloqueados e não ser usados pro um longo período.

Outro esquema de prevenção de deadlock é feito pela imposição de ordenação parcial de todos os itens

de dados e pela obrigação de que a transação bloqueie um item de dado somente na ordem especificada na

ordenação parcial. Vimos um desses esquemas no protocolo de árvore.

A segunda abordagem para a prevenção de deadlock é a preempção e o rollback de transações. Na

preempção, quando uma transação T2 solicita um bloqueio que está sendo mantido pela transação T1, o bloqueio

concedido a T1 pode ser revisto por meio do rollback de T1, e concedido a T2. Para controle da preempção,

consideramos um único timestamp para cada transação. O sistema usa esses timestamps somente para decidir se

a transação pode esperar ou será revertida. O bloqueio é ainda usado para controle de concorrência. Se uma

transação for revertida, ela manterá seu timestamp antigo quando for reiniciada. São propostos dois esquemas

diferentes de prevenção de deadlock usando timestamp:

1. O esquema esperar-morrer (wait-die) tem por base uma técnica de não-preempção. Quando uma

transação Ti solicita um item de dado mantido por Tj, Ti pode esperar somente se possuir um timestamp

menor que o de Tj (isto é, Ti é mais antiga que Tj). Caso contrário, Ti será revertida (morta). Por exemplo,

suponha que as transações T22, T23 e T24 tenham timestamps 5, 10 e 15, respectivamente. Se T24 solicita

um item de dado mantido por T23, então T24 será desfeita. Se T24 solicitar um item de dado mantido por

T23, então T24 esperará.

Sempre que as transações forem revertidas, é importante garantir que não haja inanição (starvation) –

isto é, que nenhuma transação seja desfeita continuamente e jamais possa continuar seu processamento.

Ambos os esquemas, esperar-morrer e ferir-esperar, evitam a inanição: qualquer que seja o momento, é

possível encontrar a transação com menor timestamp. Essa transação não será revertida em nenhum dos

esquemas. Uma vez que os timestamps sempre crescem, e dado que as transações não recebem dois novos

timestamps se foram revertidas, a transação revertida, em determinado momento, terá o menor timestamp.

Assim, ela não será revertida novamente.

Há entretanto, diferenças significativas entre as formas dos dois esquemas operar.

• No esquema esperar-morrer, a transação mais antiga precisará esperar até que a mais nova libere seus

itens dados. Assim, quanto mais antiga a transação, maior a possibilidade de esperar. Em contraste, no

esquema ferir-esperar, a transação mais antiga nunca espera a mais nova.

• No esquema esperar-morrer, se uma transação Ti morre e é desfeita porque solicitou um item de dado

preso por uma transação Tj, então Ti pode reemitir a mesma sequência de solicitações quando for

reiniciada. Se os itens de dados ainda estiverem presos por Tj, então Ti morrerá novamente. Assim, Ti

poderá morrer diversas vezes antes de conseguir o item de dados necessário. Compare essa série de

eventos com o que acontece no esquema ferir-esperar. A transação Ti será ferida e revertida porque Tj

solicitou um item de dados preso por ela. Quando Ti reinicia e solicita o item de dado preso por Tj, Ti

esperará. Com isso, deve haver menos reversões do esquema ferir-esperar.

O maior problema com ambos os esquemas é que podem ocorrer rollbacks desnecessários.

Esquemas com Base em Tempo Esgotado (Timeout)

Outro enfoque simples para o tratamento de deadlocks tem por base o tempo esgotado para o bloqueio

(lock timeouts). Dessa forma, uma transação que tenha solicitado um bloqueio espera por ele determinado

período de tempo. Se o bloqueio não for conseguido dentro desse intervalo, é dito que o tempo da transação está

esgotado, assim ela mesma se reverte e se reinicia. Se de fato estiver ocorrendo um deadlock, uma ou mais

transações nele envolvidas terão seu tempo esgotado e se revertem, permitindo a continuação de outras. Esse

295

esquema pode ser considerado alguma coisa entre prevenção de deadlock, dado que o deadlock nunca ocorre, e

detecção e recuperação, já discutidas.

O esquema de tempo esgotado é particularmente fácil de ser implementado, trabalha bem se as

transações forem curtas e longas esperas são frequentemente em função dos deadlocks. Entretanto, em geral é

difícil decidir por quanto tempo a transação deve esperar. Esperas muito longas implicam atrasos desnecessários,

dado que esteja ocorrendo um deadlock. Esperas muito curtas resultam em transações sendo revertidas mesmo

sem deadlock, desperdiçando recursos. A inanição também é possível nesse esquema. Então ocorre a aplicação

limitada do esquema com base em tempo esgotado.

Detecção de Deadlock e Recuperação

Se um sistema não usa um protocolo resistente ao deadlock, ou seja, que garanta que deadlocks não

aconteçam, então um esquema para detecção e recuperação precisa ser aplicado. Um algoritmo que examina o

estado do sistema é evocado periodicamente para determinar se um deadlock está ocorrendo. Se estiver, então o

sistema precisa tentar recuperar-se. Para isso, ele precisa:

• Manter informações sobre a alocação corrente dos itens de dados para transações, assim como qualquer

solicitação de itens de dados pendente.

• Proporcionar um algoritmo que use essas informações para determinar se o sistema entrou em estado de

deadlock.

• Recuperar-se de um deadlock quando o algoritmo de detecção determinar que ele ocorreu.

Detecção de Deadlock

Os deadlocks podem ser precisamente descritos em, termos de um gráfico chamado gráfico de espera.

Esse gráfico consiste em um par G=(V,E), em que V é um conjunto de vértices e E, um conjunto de arestas. O

conjunto de vértices consiste em todas as transações do sistema. Cada elemento do conjunto E de arestas é um

par ordenado Ti�Tj. Se Ti�Tj está em E, então o sentido da aresta, da transação Ti para Tj, implica que a

transação Ti está esperando que a transação Tj libere o item de dado de que ela precisa.

Quando a transação Ti solicita um item de dado que está preso pela transação Tj, então a aresta Ti�Tj é

inserida no gráfico de espera. Essa aresta é removida somente quando a transação Tj não estiver mais esperando

um item de dado necessário à transação Ti.

Há um deadlock no sistema se, e somente se, o gráfico de espera contiver um ciclo. Cada transação

envolvida em um ciclo está em deadlock. Para detectar deadlocks, o sistema precisa manter o gráfico de espera e,

periodicamente, evocar um algoritmo que verifique a existência de ciclos.

Para ilustrar esses conceitos, considere o gráfico de espera da fig. 14.13, que exibe a seguinte situação:

• A transação T25 está esperando as transações T26 e T27.

• A transação T27 está esperando a transação T26.

• A transação T26 está esperando a transação T28.

Uma vez que não há ciclos, o sistema não está em estado de deadlock.

Suponha agora que a transação T28 esteja solicitando um item preso por T27. A aresta T28�T27 será

adicionada ao gráfico de espera, alterando o estado do sistema, como mostrado na fig. 14.14. A essa altura, o

gráfico contém o ciclo:

implicando que as transações T26, T27 e T28 estão todas em deadlock.

296

Consequentemente, impõe-se a questão: quando evocaremos o algoritmo de detecção será evocado com

mais frequência que o usual. Os itens de dados alocados nas transações em deadlock não estarão disponíveis para

outras transações até que o deadlock seja resolvido. Além disso, o número de ciclos no gráfico pode crescer

também. Na pior das hipóteses, evocaríamos o algoritmo de detecção sempre que uma solicitação de alocação

não puder ser atendida imediatamente.

Recuperação após um Deadlock

Quando um algoritmo de detecção determina a existência de um deadlock, o sistema precisa recuperar-

se desse deadlock. A solução mais comum é reverter uma ou mais transação para quebrar o deadlock. Devem ser

tomadas três ações:

1. Selecionar uma vítima. Dado um conjunto de transações em deadlock, precisamos determinar quais

transações (ou transação) serão desfeitas para quebrar o deadlock. Poderíamos reverter as transações

que representam o menor custo. Infelizmente, o termo mínimo custo não é preciso. Muitos fatores

podem determinar o custo de um rollback, incluindo:

a. A quanto tempo a transação está em processamento e quanto tempo será ainda necessário para

que a tarefa seja completada.

b. Quantos itens de dados a transação usou.

c. Quantos itens ainda a transação usará até que se complete.

d. Quantas transações serão envolvidas no rollback.

2. Rollback. Uma vez decidido que uma transação em particular será revertida, precisamos determinar até

que ponto ela deverá ser revertida. A solução mais simples é revertê-la totalmente: abortá-la para depois

reiniciá-la. Entretanto, é mais eficaz reverter a transação somente o suficiente para a quebra do deadlock.

Mas esse método exige que o sistema mantenha informações adicionais sobre o estado de todas as

transações em execução.

3. Inanição. Em um sistema no qual a seleção de vítimas tem por base fatores de custo, pode acontecer de

uma mesma transação ser sempre escolhida vítima. Assim, essa transação nunca se completa. Essa

situação é chamada de inanição. Precisamos garantir que uma transação seja escolhida vítima somente

um número finito (pequeno) de vezes. A solução mais comum é incluir o número de reversão no fator de

custos.

Operações de Inserção e Remoção

Até agora restringimos nossa atenção a operações read e write. Essas restrições limita a ação das

transações sobre os itens de dados existentes no banco de dados. Algumas transações precisam não somente de

acesso a itens de dados existentes, mas também da capacidade de criar novos itens de dados. Outras precisam

remover itens de dados. Para examinar como tais transações afetam o controle de concorrência, introduzimos as

seguintes operações adicionais:

• delete(Q) remove o item de dados Q do banco de dados.

• insert(Q) insere um novo item de dados Q em um banco de dados e designa um valor inicial para ele.

Uma transação Ti que queira operar uma read(Q) depois da remoção de Q resulta em erro lógico em Ti.

Analogamente, se uma transação Ti quiser realizar uma operação de read(Q) antes da inserção de Q, também

haverá erro lógico em Ti. Também será um erro lógico tentar remover um item de dados inexistente.

297

Remoção

Para entender como uma instrução de remoção (delete) afeta o controle de concorrência, precisamos

definir quando ela entra em conflito com outra instrução. Seja Ii e Ij instruções de Ti e Tj, respectivamente, que

aparecem nessa ordem na escala de execução S. Seja Ii= delete(Q). Consideremos as seguintes instruções Ij.

• Ij = read(Q). Ii e Ij entram em conflito. Se Ii começou antes de Ij, Tj incorrerá em erro lógico. Se Ij começou

antes de Ii, Tj poderá executar a operação read com sucesso.

• Ij = write(Q). Ii e Ij entram em conflito. Se Ii começou antes de Ij, Tj incorrerá em erro lógico. Se Ij começou

antes de Ii, Tj poderá executar a operação write com sucesso.

• Ij = delete(Q). Ii e Ij entram em conflito. Se Ii começou antes de Ij, Tj incorrerá em erro lógico. Se Ij começou

antes de Ii, Ti incorrerá em erro lógico.

• Ij = insert(Q). Ii e Ij entram em conflito. Suponha que o item de dado Q não exista antes da execução de Ii e

Ij. Então, se Ii começou antes de Ij, ocorrerá erro lógico em Ti. Se Ij começou antes de Ii não ocorrerá

nenhum erro lógico. Da mesma forma, se Q existir antes da execução de Ii e Ij, poderá ocorrer erro lógico

se Ij começou antes de Ii, caso contrário não.

Podemos concluir que, se o bloqueio em duas fases for usado, é preciso um bloqueio exclusivo sobre o

item de dados antes que ele possa ser removido. Sob o protocolo de ordenação por timestamp, um teste que ele

possa ser removido. Sob o protocolo de ordenação por timestamp, um teste similar ao indicado para a write

precisará ser realizado. Suponha que a transação Ti emita um delete(Q).

• Se TS(Ti)<R-timestamp(Q), então o valor de Q que Ti removeu já havia sido lido pela transação Tj com

TS(Tj)>TS(Ti). Então, a operação delete será rejeitada e Ti será revertida.

• Se TS(Ti)<W-timestamp(Q), então uma transação Tj com TS(Ti)>TS(Tj) já gravou Q. Com isso, essa operação

delete será rejeitada e Ti será desfeita.

• De outro modo a operação delete será executada.

Inserção

Vimos que uma operação insert(Q) entra em conflito com uma operação delete(Q). Analogamente,

insert(Q) entra em conflito com uma operação read(Q) ou uma operação write(Q). nenhuma read ou write pode

ser realizada sobre um item de dados antes que ele exista.

Uma vez que insert(Q) estabeleça um valor para o item de dado Q, a insert é tratada de modo similar à

write para efeito de controle de concorrência:

• Sob o protocolo de bloqueio em duas fases, se Ti realizar uma operação insert(Q), Ti estará impondo um

bloqueio exclusivo para o novo item de dado Q criado.

• Sob o protocolo de ordenação por timestamp, se Ti realizar uma operação insert(Q), os valores de R-

timestamp(Q) e W-timestamp(Q) serão registros em TS(Ti).

O Fenômeno do Fantasma

Considere a transação T29 que executa a consulta SQL a seguir:

A transação T29 obriga o acesso a todas as tuplas da relação conta pertencentes a agência Perryridge.

Seja T30 uma transação que executa a seguinte inserção SQL:

Seja S uma escala de execução envolvendo T29 e T30. Esperamos um conflito em potencial pelas seguintes

razões:

• Se T29 usar a tupla recentemente inserida por T30 para calcular sum(saldo), então T29 lerá o valor inserido

por T30. Assim, em uma escala de execução serializada equivalente a S, T30 deve começar antes de T29.

298

• Se T29 não usar a tupla recentemente inserida por T30 para calcular sum(saldo) em uma escala de

execução serializada equivalente a S, T29 deve começar antes de T30.

O segundo caso é curioso. T29 e T30 não acessam a nenhum tupla em comum, ainda assim entram em

conflito. Com efeito, T29 e T30 entram em conflito com uma tupla fantasma. Assim, o fenômeno que acabamos de

descrever é chamado de fenômeno do fantasma. Se o controle de concorrência é feito com granularidade de

tupla, esse conflito pode não ser detectado.

Para prevenir esse fenômeno, fazemos com que T29 evite que outras transações criem novas tuplas na

relação conta com nome_agência= “Perryridge”.

Para encontrar todas as tuplas de conta com nome_agência = “Perryridge”, T29 precisa pesquisar toda a

relação conta, ou ao menos um índice na relação. Até agora, consideramos de modo implícito que os itens de

dados aos quais a transação mantém acesso sejam somente tuplas. Entretanto, T29 é um exemplo de transação

que procura a informação de quantas tuplas há na relação e T30 exemplifica uma transação que atualiza essa

informação. É lógico que não é suficiente bloquear as tuplas que sofrem acesso; o bloqueio também é necessário

para informações sobre os quais tuplas estão na relação.

A solução mais simples para esse problema seria associar um item de dado à própria relação. Transações,

como a T29, que leem informações acerca de quais tuplas estão na relação deveriam, então, bloquear no modo

compartilhado o item de dado correspondente à relação conta. Transação, com a T30, que atualizam as

informações acerca de quais tuplas estão na relação deveriam bloquear o item de dado no modo exclusivo. Desse

modo, T29 e T30 entram em conflito devido a itens de dados reais, e não fantasmas.

Não confunda o bloqueio de uma relação inteira, como no bloqueio de granularidade múltipla, com o

bloqueio de um item de dado correspondente à relação. Por meio do bloqueio do item de dado, uma transação

impede somente que outras transações alterem informações sobre as tuplas que pertencem à relação. O

bloqueio das tuplas é ainda necessário. As transações que mantêm acesso direto a uma tupla podem conseguir o

bloqueio de uma tupla mesmo que outra transação tenha bloqueio exclusivo sobre um item de dado

correspondente àquela transação propriamente dita.

A maior desvantagem do bloqueio de um item de dado correspondente a uma relação é o baixo grau de

concorrência – duas transações que inserem tuplas diferentes em uma relação não podem ser concorrentes.

Uma solução melhor é a técnica do bloqueio de índices. Qualquer transação que inserir uma tupla em

uma relação deve inserir informações em todos os índices mantidos pela relação. Eliminamos o fenômeno do

fantasma por meio da imposição de um protocolo de bloqueios para os índices.

Todo valor da chave de pesquisa está associado a um registro do índice ou a um bucket. Uma consulta,

normalmente, usará um ou mais índices para o acesso à relação. Uma consulta, normalmente, usará um ou mais

índices para o acesso à relação. Uma inserção precisará introduzir uma nova tupla em todos os índices de uma

relação.

Em nosso exemplo, assumimos que há um índice em conta para nome_agência. Logo, T30 precisa

modificar o bucket de Perryridge. Se T29 lê o bucket de Perryridge para localizar todas as tuplas pertencentes à

agência Perryridge, então T29 e T30 entrarão em conflito naquele bucket.

O protocolo de bloqueio de índices possui a vantagem de criar índices sobre uma relação por meio da

troca de instâncias do fenômeno de fantasmas por conflito de bloqueios em índices bucket. O protocolo opera da

seguinte forma:

• Toda relação precisa ter ao menos um índice.

• Uma transação Ti pode bloquear em modo S uma tupla ti de uma relação somente se ela possui um

bloqueio modo S sobre o índice bucket que contém um ponteiro para ti.

• Uma transação Ti pode bloquear em modo X uma tupla ti de uma relação somente se ela possui um

bloqueio modo X sobre o índice bucket que contém um ponteiro para ti.

• Uma transação Ti não pode inserir uma tupla ti em uma relação r sem atualizar todos os índices de r. Ti

precisa obter um bloqueio modo X sobre todos os índices bucket que ela modifica.

• É preciso observar as regras do protocolo de bloqueio em duas fases.

299

Há variante da técnica de bloqueio de índices para a eliminação do fenômeno do fantasma em outros

protocolos de controle de concorrência já apresentados.

Concorrência em Estruturas de Índices

É possível tratar do acesso às estruturas de índices como qualquer outra estrutura de um banco de dados

e aplicar as técnicas de controle de concorrência discutidas anteriormente. Entretanto, uma vez que os índices

têm acesso frequente, eles se tornam ponto de grande contenção de bloqueios, originando um baixo nível de

concorrência. Felizmente, os índices não tem de receber o mesmo tipo de tratamento das demais estruturas do

banco de dados, já que não proporcionam um alto nível de abstração para o mapeamento de chaves de pesquisa

de tuplas do banco de dados. É perfeitamente aceitável que uma transação verifique um índice duas vezes e

perceba que essa estrutura de índice foi alterada nesse meio tempo, contanto que o índice aponte um conjunto

correto de tuplas. Assim, é aceitável manter acesso concorrente não-seriado em um índice, contanto que a

precisão do índice seja mantida.

Mostramos a técnica para o gerenciamento de acessos concorrentes em árvores-B+.

As técnicas que apresentamos para árvores-B+ têm por base o bloqueio, mas nem o bloqueio em duas

fases nem o protocolo de árvore são empregados.

300

Sistema de Recuperação

Um sistema de computador, como qualquer outro equipamento mecânico ou elétrico, está sujeito a

falhas. Há grande variedade de falhas, incluindo quebra de disco, falha de energia, erro de software, fogo na sala

de equipamento ou mesmo sabotagem. Em cada um desses casos, informações podem ser perdidas. Portanto, o

sistema de banco de dados deve precaver-se para garantir que as propriedades de atomicidade e durabilidade

das transações sejam preservadas, a despeito de tais falhas. Uma parte integrante de um sistema de banco de

dados é o esquema de recuperação que é responsável pela restauração do banco de dados para um estado

consistente que havia antes da ocorrência da falha.

Classificação de Falha

Vários tipos de falhas podem ocorrer em um sistema, cada um dos quais exigindo um tratamento

diferente. O tipo de falha mais simples de tratar é aquele que não resulta na perda de informação no sistema. As

falhas mais difíceis de tratar são aquelas que resulta em perda de informação. Vamos considerar somente os

seguintes tipos de falha:

• Falha de transação. Dois tipos de erros podem causar uma falha de transação:

o Erro lógico. A transação não pode mais continuar com sua execução normal devido a alguma

condição interna, como uma entrada inadequada, um dado encontrado, overflow ou limite de

recurso excedido.

o Erro de sistema. O sistema entrou em um estado inadequado (por exemplo, deadlock), com isso,

uma transação não pode continuar com sua execução normal. A transação, entretanto, pode ser

reexecutada posteriormente.

• Queda do sistema. Há algum mau funcionamento de hardware ou um bug no software de banco de

dados ou no sistema operacional que causou a perda do conteúdo no armazenamento volátil e fez o

processamento da transação parar. O conteúdo de armazenamento não-volátil permanece intato e não é

corrompido.

A condição originada por erros de hardware e bugs no software que fazem o sistema parar, mas não

corrompem o conteúdo do armazenamento não-volátil, é conhecida como condição falhar-parar. Sistemas bem

projetados têm numerosas verificações internas em nível de hardware e software que fazem o sistema parar

quando há um erro. Consequentemente, a condição falhar-parar é uma condição razoável.

• Falha de disco. Um bloco de disco perde seu conteúdo em função da quebra do cabeçote ou da falha

durante uma operação de transferência de dados. São usadas, para recuperação do sistema após a falha,

as cópias dos dados em outros discos ou backups de arquivos em meios terciários, como fitas.

Para determinar como o sistema deve recuperar-se das falhas, necessitamos identificar os modos de falha

possíveis dos equipamentos usados para armazenar dados. Depois, devemos considerar como esses modos de

falha afetam o conteúdo do banco de dados. Então, poderemos desenvolver algoritmos para assegurar a

consistência do banco de dados e a atomicidade da transação, a despeito das falhas. Esses algoritmos são

conhecidos como algoritmos de recuperação, embora tenham duas partes:

1. Ações tomadas durante o processamento normal da transação a fim de garantir que haja informação

suficiente para permitir a recuperação de falhas.

2. Ações tomadas em seguida à falha, recuperando o conteúdo do banco de dados para um estado que

assegure sua consistência, a atomicidade da transação e durabilidade.

Estrutura de Armazenamento

Os vários itens do banco de dados podem ser armazenados e sofre acesos em diferentes meios de

armazenamento. Para compreender como garantir propriedades de atomicidade e durabilidade de uma

transação, devemos compreender melhor como funcionam esses meios de armazenamento e seus métodos de

acesso.

301

Tipos de Armazenamento

Há vários tipos de meios de armazenamento; eles são distinguidos por sua velocidade relativa, capacidade

e resistência à falha.

• Armazenamento volátil. A informação residente em armazenamento volátil usualmente não sobrevive a

quedas no sistema. Exemplos de tal armazenamento são MP e memória cache. O acesso à armazenagem

volátil é extremamente rápido, tanto devido à velocidade de acesso da memória em si quanto ao acesso

direto a qualquer item de dado possível no armazenamento volátil.

• Armazenamento não-volátil. A informação residente em armazenamento não-volátil sobrevive a quedas

de sistema. Exemplos de tal armazenamento são o disco e fitas magnéticas. Os discos são usados para

armazenamento online, ao passo que as fitas são usadas para armazenamento de arquivo. Entretanto,

ambos estão sujeitos à falha (por exemplo, quebra de cabeçote), que pode resultar em perda de

informação. No atual estado da tecnologia, o armazenamento não-volátil é mais lento que o

armazenamento volátil por muitas ordens de magnitude. Essa distinção ocorre porque discos e fitas são

equipamentos eletromecânicos, em vez de inteiramente baseados em chips, como o armazenamento

volátil. Outros meios não-voláteis são usados, normalmente apenas no backup de dados.

• Armazenamento estável. A informação residente em armazenamento estável nunca é perdida (nunca é

entendida aqui como uma agulha no palheiro, já que teoricamente nunca não pode ser garantido – por

exemplo, é possível, embora extremamente improvável, que um buraco negro engula a Terra e destrua

permanentemente todos os dados!). Embora o armazenamento estável seja teoricamente impossível de

obter, pode-se chegar perto dele usando técnicas que torna extremamente improvável a perda de dados.

Frequentemente, as distinções entre os vários tipos de armazenamento são menos claras na prática que

em nossa apresentação. Certos sistemas fornecem backup de bateria, de forma que parte da MP pode resistir a

quedas de sistema e falhas de energia. Formas alternativas de armazenamento não-volátil, como meio ótico,

fornecem maior grau de confiabilidade que os discos.

Implementação do Armazenamento Estável

Para implementar o armazenamento estável, temos de replicar a informação em vários meios de

armazenamento ano-voláteis (usualmente discos), como modos possíveis de falha independentes, e controlar a

atualização das informações, garantindo que uma eventual falha durante a transferência de dados não danifique

as informações.

Os sistemas RAID garantem que a falha de um único disco (mesmo durante a transferência de dados) não

resulte em perda de dados. A forma mais simples e mais rápida de RAID é o disco espelhado, que mantém duas

cópias de cada bloco em discos separados. Outras formas de RAID oferecem custos menores, mas com menor

desempenho.

Os sistemas RAID, entretanto, não podem se proteger contra perda de dados devido a desastres como

incêndios ou enchentes. Muitos sistemas armazenam backups em fitas e diferentes locais para proteger-se contra

tais desastres. Entretanto, já que as fitas não podem ser transportadas continuamente, as atualizações ocorridas

entre o desastre e o último backup podem ser perdidas. Sistemas mais seguros mantêm uma cópia de cada bloco

de armazenamento estável em um site remoto, enviando-a por uma rede de computadores, além de armazenar o

bloco em um sistema de disco local. Já que os blocos são enviados ao sistema remoto, como e quando são

enviados para o armazenamento local, uma vez completada a operação de saída, essa saída não é perdida,

mesmo na ocorrência de um desastre, como um incêndio ou uma enchente.

Vamos discutir como o meio de armazenamento pode ser protegido de uma falha durante a transferência

de dados. A transferência de blocos entre a memória e o armazenamento de disco pode resultar em:

• Conclusão bem-sucedida. A informação transferida chegou de forma segura ao seu destino.

• Falha parcial. Uma falha ocorreu no meio de transferência e o bloco de destino contém informação

incorreta.

302

• Falha total. A falha ocorreu cedo o suficiente, de modo que o bloco de destino permanece intato.

Exigimos que, se uma falha na transferência de dados ocorrer, o sistema a detecte e chama um

procedimento de recuperação para restabelecer o bloco, levando-o a um estado consistente. Para isso, o sistema

deve manter dois blocos físicos para cada bloco lógico do banco de dados; no caso de discos espelhados, ambos

os blocos então no mesmo local; no caso de backup remoto, um dos blocos é local, enquanto o outro está em um

site remoto. Uma operação de saída é executa como segue:

1. Escreve a informação dentro do primeiro bloco físico.

2. Quando a primeira escrita se completar com sucesso, escreve a mesma informação no segundo bloco

físico.

3. A saída é completada somente após a segunda escrita completar-se com sucesso.

Durante a recuperação, cada par de blocos físico é examinado. Se ambos são iguais e não há erro detectável,

então nenhuma ação adicional é necessária. Se um bloco contém um erro detectável, então trocamos seu

conteúdo pelo conteúdo do segundo bloco. Se ambos os blocos não contêm erros detectáveis, mas diferem em

conteúdo, então trocamos o conteúdo do primeiro bloco pelo valor do segundo. Esse procedimento de

recuperação assegura que uma escrita em armazenamento estável seja bem-sucedida (isto é, atualize todas as

cópias), ou não resulte em mudança alguma.

Exigir a comparação entre cada par de blocos correspondentes durante a recuperação é custoso.

Podemos reduzir consideravelmente o custo mantendo uma varredura de escritas de bloco que estão em

progresso, usando uma pequena quantidade de RAM não-volátil.

Os protocolos para escrita de um bloco em um site remoto são similares aos protocolos para escrita de

blocos em sistema de disco espelhado.

Podemos facilmente expandir esse procedimento para que permita o uso de um número arbitrariamente

grande de cópias de cada bloco de armazenamento estável. Embora o uso de um número grande de cópias

reduza a probabilidade de uma falha para muitos menos que quando se usam duas cópias, em geral é suficiente

simular o armazenamento estável somente com duas cópias.

Acesso de dados

O sistema de banco de dados reside permanentemente em armazenamento não-volátil (usualmente

discos) e é particionado em unidades de armazenamento de comprimento fixo chamadas de blocos. Os blocos

são unidades de transferência de dados para e a partir do disco e podem conter vários itens de dados. Assumimos

que nenhum item de dado abrange dois ou mais blocos. Essa premissa é verdadeira para a maioria das aplicações

de processamento de dados, como em nosso exemplo bancário.

As transações transferem informações do disco para a MP e, então, reenviam essas informações de volta

para o disco. As operações de entrada e saída (entrar e sair da memória) são feitas em unidades de bloco. Os

blocos residentes no disco são chamados de blocos físicos; os blocos residentes temporariamente na MP são

chamados de blocos de buffer. A área de memória na qual os blocos residem temporariamente é chamada de

buffer de disco.

Movimentos de bloco entre disco e memória principal são iniciados por meio de duas operações

seguintes:

1. input(B) transfere o bloco físico B para a MP.

2. output(B) transfere o bloco de buffer B para o disco e troca-o, no disco, pelo físico apropriado.

Esse esquema é ilustrado na fig. 15.1.

303

Cada transação Ti tem uma área de trabalho privada na qual cópias de todos os itens de dados acessados

e atualizados são mantidas. Essa área de trabalho é criada quando a transação é iniciada; ele é removida quando

a transação é iniciada; ela é removida quando a transação é efetivada ou abortada. Cada item de dados x mantido

na área de trabalho da transação Ti é denotado por xi. A transação Ti interage com o sistema de banco de dados

pela transferência de dados para e de sua área de trabalho até o buffer de sistema. Transferimos os dados usando

as duas operações a seguir:

1. read(X) designa o valor do item de dado X para a variável local xi. Essa operação é executada como segue:

a. Se o bloco Bx no qual reside X não está na memória principal, então é emitido um input(Bx).

b. Designa a xi o valor de X a partir do bloco de buffer.

2. write(X) designa o valor da variável local xi para o item de dado X no bloco de buffer. Essa operação é

executada como segue:

a. Se o bloco Bx no qual reside X não está na memória principal, então emite um input(Bx).

b. Designa o valor de xi para X no buffer Bx.

Observe que ambas as operações podem exigir a transferência de um bloco do disco para a MP.

Entretanto, elas não exigem a transferência de um bloco da MP para o disco.

O bloco de buffer será eventualmente escrito no disco se o gerenciador de buffer necessitar de espaço

em memória para outros propósitos ou porque o sistema de banco de dados deseja refletir a mudança em B

sobre o disco. Dizemos que o sistema de banco de dados força saídas do buffer B se ele emite um output(B).

Quando uma transação necessita do acesso a um item de dado X pela primeira vez, ela deve executar

read(X). Todas as atualizações de X são, então, realizadas sobre xi. Após o último acesso X feito pela transação, ela

executará um write(X) para refletir a mudança em X no banco de dados propriamente dito.

A operação output(Bx) para o buffer de bloco Bx em que X reside não precisa ter efeito imediato após a

execução do write(X), já que o bloco Bx pode conter outros itens de dados que ainda estão sendo acessados.

Então, a saída real aparecerá mais tarde. Observe que, se o sistema cair após a operação write(X) ter sido

executada, mas antes do output(Bx), o novo valor de X nunca será escrito no disco e, portanto, é perdido.

Recuperação e Atomicidade

Considere novamente nosso sistema bancário simplificado e a transação Ti transfere 50 dólares da conta

A para a conta B, com valores iniciais de A e B sendo mil e dois mil dólares, respectivamente. Suponha que uma

queda de sistema tenha ocorrido durante a execução de Ti, após output(BA), mas antes do output(BB), em que BA

e BB denotam os blocos de buffer em que A e B residem. Já que os conteúdos de memória foram perdidos, não

sabemos o destino da transação; então, poderíamos chamar um dos dois possíveis procedimentos de

recuperação:

• Reexecutar Ti. Este faz com que o valor de A torne-se 900 dólares, em vez de 950 dólares. Então, o

sistema entra em um estado inconsistente.

• Não reexecutar Ti. No estado corrente do sistema, os valores A e B são de 950 e 2000 dólares,

respectivamente. Então, o sistema entra em um estado inconsistente.

Em ambos os casos, o banco de dados é deixado em estado inconsistente, logo esse esquema de recuperação

simples não funciona. Essa dificuldade ocorre porque modificamos o banco de dados sem ter certeza de que a

304

transação será efetivada de fato. Entretanto, se Ti realizou diversas modificações no banco de dados, podem ser

necessárias várias operações de saída e pode ocorrer uma falha após algumas dessas modificações terem sido

feitas, mas antes de todas serem realizadas.

Para atingir nosso objetivo de atomicidade, primeiro devemos mandar informações que descrevam essas

modificações para um armazenamento estável, sem modificar o banco de dados em si. Como veremos, esse

procedimento nos permitirá enviar todas as modificações feitas por uma transação efetivada, apesar de possíveis

falhas. Há duas maneiras de realizar tais saídas. Vamos assumir que as transações são executadas serialmente,

isto é, somente uma única transação está ativa de cada vez.

Recuperação Baseada em Log

A estrutura mais usada para gravar modificações no banco de dados é o log (diário). O log é uma

sequência de registros de log que mantém um arquivo atualizado das atividades no banco de dados. Há vários

tipos de registros de log que mantém um arquivo atualizado das atividades no banco de dados. Há vários tipos de

registro de log. Um registro de atualização de log descreve uma única escrita do banco de dados e tem os

seguintes campos:

• Identificador de transação é um identificador único da transação que realiza operação de escrita (write).

• Identificação de item de dado é um identificador único do item de dado escrito. Normalmente, é a

localização do item de dado no disco.

• Valor antigo é o valor do item de dado anterior à escrita.

• Valor novo é o valor que o item de dado terá após a escrita.

Há outros registros de log para arquivar eventos significativos durante o processamento de transação,

como o início da transação, sua efetivação ou aborto. Indicamos os vários tipos de registros de log como seguem:

• <Ti start>. A transação Ti começou.

• <Ti, Xj, V1, V2>. A transação Ti foi efetivada.

• <Ti abort>. A transação Ti foi abortada.

Sempre que uma transação realiza uma escrita, é essencial que o registro de log para aquela escrita seja

criado antes do banco de dados ser modificado. Havendo o registro de log, podemos enviar a modificação ao

banco de dados quando ela for conveniente. Também conseguiremos inutilizar (undo) uma modificação que já

tenha sido enviada ao banco de dados. Podemos desfazê-la usando o campo de valor antigo do registro de log.

Para que os registros de log sejam úteis na recuperação após falhas de sistema e disco, o log deve residir

em armazenamento estável. Por enquanto, assumiremos que todo registro de log será escrito no final do arquivo

de log em armazenamento estável, tão logo seja criado. Veremos quando é seguro afrouxar essa exigência para

reduzir o overhead imposto ao registro em log. Introduziremos também duas técnicas de uso de log para garantir

atomicidade de transações apesar das falhas. Repare que o log contém um registro completo de toda atividade

do banco de dados. Com isso, o volumo de dados armazenado no log pode tornar-se absurdamente grande.

Mostraremos quando é seguro apagar informações de log.

Modificações adiadas do banco de dados

A técnica de adiar modificações garante a atomicidade de transações quando todas as modificações do

banco de dados são escritas no log, adiando a execução de todas as operações write de uma transação até sua

efetivação parcial. Lembre-se de que uma transação é considerada parcialmente efetivada quando a última ação

da transação tiver sido executada. A versão da técnica de modificação que descrevemos aqui pressupõe que as

transações sejam executadas serialmente.

Quando uma transação é parcialmente efetivada, as informações no log associadas àquela transação são

usadas para a execução das escritas adiadas. Se o sistema cair antes de completar a transação ou se a transação

for abortada, então as informações do log serão simplesmente ignoradas.

305

A execução da transação Ti funciona como segue. Um registro <Ti start> é escrito no log antes de Ti ter

início. Uma operação write(X) feita por Ti resulta na escrita de um novo registro no log. Finalmente, quando Ti é

parcialmente efetivada, um registro <Ti commit> é escrito no log.

Quando uma transação Ti é parcialmente efetivada, os registros no log a ela associados são usados para

execução das escritas adiadas. Como uma falha pode ocorrer enquanto essa execução está em andamento,

devemos ter certeza, antes de começa-la, de que todos os registros de log estejam escritos em armazenamento

estável. Uma vez escritas, as atualizações reais podem ocorrer de fato e a transação entra no estado de

efetivação.

Observe que somente o novo valor do item de dado é necessário para a técnica de modificação adiada.

Logo, podemos simplificar a estrutura geral do registro de atualização do log, que vimos anteriormente, por meio

da omissão do campo de valor antigo.

Para ilustração, reconsidere nosso exemplo de sistema bancário simplificado. Seja T0 uma transação que

transfere 50 dólares da conta A para a conta B. Essa transação é definida como segue:

Seja T1 uma transação que debita cem dólares da conta C. Essa transação é definida como:

Suponha que essas transações sejam executadas serialmente, T0 é seguida por Ti e os valores das contas

A, B e C antes da execução, eram de 1000, 2000 e 700 dólares, respectivamente. A porção do log contendo as

informações relevantes sobre essas duas transações é apresentada 15.2.

Como resultado da execução de T0 e T1, há várias ordens possíveis em que as saídas reais podem ocorrer,

tanto para o sistema de banco de dados como para o log. Tal ordem é apresentada na fig. 15.3. Note que o valor

de A é alterado somente após o registro <T0, A, 950> ter sido colocado no log.

Usando o log, o sistema pode lidar com qualquer falha que resulte em perda de informação no

armazenamento volátil. O esquema de recuperação usa o seguinte procedimento:

• redo(Ti) define o valor de todos os itens de dados atualizados pela transação Ti para os novos valores.

O conjunto de itens de dados atualizados por Ti e seus respectivos novos valores podem ser encontrados

no log.

306

A operação redo (refazer) deve ser idempotente, isto é, executá-la várias vezes deve ser equivalente a

executá-la uma vez só. Essa característica é exigida se formos garantir comportamento correto, mesmo que uma

falha ocorra durante o processo de recuperação.

Após a ocorrência de uma falha, o subsistema de recuperação consulta o log para determinar quais

transações têm de ser refeitas. A transação Ti deverá ser refeita se, e somente se, o log contiver os registros <Ti

start> e <Ti commit>. Assim, se o sistema cair depois que a transação completar sua execução, as informações no

log serão usadas na restauração do sistema para o estado consistente anterior.

Como ilustração, retornemos a nosso exemplo bancário com as transações T0 e T1 executadas uma após a

outra, T0 seguida por T1. A figura 15.2 mostra o log resultante da execução completa de T0 e T1.

Suponhamos que o sistema caia antes que as transações terminem, de forma que possamos ver como a

técnica de recuperação restabelece o banco de dados para um estado consistente. Assuma que a queda logo após

o registro de log do passo write (B) da transação T0 ter sido escrito em armazenamento estável. O log, no

momento da queda, é mostrado na fig. 15.4a. Quando o sistema retorna, nenhuma ação refazer tem de ser

tomada, já que nenhum registro de efetivação aparece no log. Os valores das contas A e B permanecem mil e dois

mil dólares, respectivamente. Os registros de log da transação incompleta T0 podem ser removidos do log.

Agora, assumamos que a queda venha logo após o registro de log para o passo write(C) da transação T1

ter sido escrito em armazenamento estável. Nesse caso, o log, no momento da queda, está como na fig. 15.4.

Quando o sistema retorna, a operação redo (T0) é realizada, já que o registro <T0 commit> aparece no log em

disco. Após essa operação, os valores das contas A e B são 950 e 2050 dólares, respectivamente. O valor da conta

C permanece 700 dólares. Como antes, os registros de log da transação incompleta T1 podem ser removidos do

log.

Por último, assumamos que uma queda ocorra logo após o registro de log <T1 commit> ser escrito em

armazenamento estável. O log, no momento dessa queda, está como mostra a fig. 15.4c. Quando o sistema

retorna, dois registros de efetivação estão no log: um para T0 e um para T1. Portanto, as operações redo(T0) e

redo(T1) devem ser processadas. Após essas operações, os valores das contas A, B e C são, respectivamente, 950,

2050 e 600 dólares.

Finalmente, consideremos o caso em que uma segunda queda de sistema ocorre durante a recuperação

da primeira queda. Algumas mudanças devem ter sido feitas no banco de dados como resultado das operações

redo, mas pode ser que nem todas as alterações tenham ocorrido. Quando o sistema retornar da segunda queda,

a recuperação se dará exatamente como nos exemplos anteriores.

Para cada registro de efetivação <Ti commit> encontrado no log, a operação redo(Ti) será processada. Em

outras palavras, as ações de recuperação são reinicializadas a partir do começo. Já que redo escreve valores no

banco de dados independente de seus valores correntes, o resultado de uma segunda tentativa de redo será igual

ao alcançado caso o redo seja bem-sucedido já na primeiro vez.

Modificação Imediata de Banco de Dados

A técnica de atualização imediata permite que as modificações no banco de dados sejam enviadas

enquanto as transações ainda estão no estado ativo. As escritas emitidas por transações ativas são chamadas de

modificações não-efetivadas. Na ocorrência de uma queda ou de uma falha de transação, o sistema deverá usar o

campo relativo ao valor antigo dos registros de log para restauração dos itens de dados modificados, levando-os

307

ao valor anterior ao início da transação. Essa restauração é conseguida por meio da operação undo (desfazer)

descrita a seguir.

Antes que uma transação Ti inicie sua execução, o registro <Ti start> é escrito no log. Durante sua

execução, qualquer operação write(X) feita por Ti é precedida pela escrita apropriada do novo registro corrente

no log. Quando Ti é parcialmente efetivada, o registro <Ti commit> é escrito no log.

Já que as informações do log são usadas na reconstrução do estado do banco de dados, não podemos

permitir que a atualização real do banco de dados ocorra antes da escrita correspondente, em armazenamento

estável, do registro de log. Portanto, exigimos que, antes da execução de uma operação output(B), os registros de

log correspondentes a B sejam escritos em armazenamento estável.

Como ilustração, reconsideremos nosso sistema bancário simplificado, com transações T0 e T1 executadas

uma após a outra com T0 seguida por T1. A porção do log contendo as informações importantes relativas a essas

duas transações é apresentada na fig. 15.5.

Uma ordem possível de ocorrência das saídas reais, tanto para o sistema de banco de dados quanto para

o log, resultantes da execução de T0 e T1, é descrita na fig. 15.6. Observe que essa ordem não poderia ser obtida

na técnica de modificação adiada.

Usando o log, o sistema pode tratar de qualquer falha que não resulte na perda de informação em

armazenamento não-volátil. O esquema de recuperação usa dois procedimentos de recuperação:

• undo(Ti) retorna aos valores antigos todos os itens de dados atualizados pela transação Ti.

• redo(Ti) ajusta os valores de todos os itens de dados atualizados pela transação Ti para os valores novos.

O conjunto de itens de dados atualizados por Ti e seus respectivos valores antigos e novos podem ser

encontrados no log.

As operações undo e redo devem ser idempotentes para garantir o comportamento correto mesmo se

uma falha ocorrer durante o processo de recuperação.

Após a falha, o esquema de recuperação consulta o log para determinar quais transações necessitam ser

refeitas e quais necessitam ser inutilizadas. Essa classificação de transações é conseguida como segue:

• A transação Ti tem de ser inutilizada se o log contém o registro <Ti start>, mas não contém o registro <Ti

commit>.

• A transação Ti tem de ser refeita se o log contém tanto o registro <Ti start> quanto o registro <Ti commit>.

Como ilustração, retornaremos a nosso exemplo bancário, com as transações T0 é seguida por T1. Suponhamos

que o sistema caia antes do término das transações. Deveremos considerar três casos. O estado dos logs para

cada um desses casos é mostrado na fig. 15.7.

308

Primeiro, assumamos que a queda ocorra logo após o registro de log para o passo write(B) da transação

T0 ter sido escrito em armazenamento estável (fig. 15.7a). quando o sistema retorna, ele encontra o registro <T0

start> no log, mas nenhum registro <T0 commit> correspondente. Então, a transação T0 deverá ser inutilizada, de

modo que um undo(T0) será processado. Como resultado, os valores nas contas A e B (no disco) são restaurados

em mil e dois mil dólares, respectivamente.

A seguir, assumamos que a queda venha logo após o registro de log para o passo write(C) da transação T1

ter sido escrito em armazenamento estável (fig. 15.7b). Quando o sistema retornar, duas ações de recuperação

necessitam ser tomadas. A operação undo (T1) deve ser realizada, já que o registro <T1 start> aparece no log, mas

não há o registro <T1 commit>, e a operação redo(T0) também deve ser realizada, já que o log contém tanto o

registro <T0 start> como o registro <T0 commit>. No fim do processo de recuperação completo, os valores das

contas A, B e C serão 950, 2050 e 700 dólares, respectivamente. Observe que a operação undo(T1) é realizada

antes de redo(T0). Nesse exemplo, o resultado seria o mesmo se a ordem fosse revertida. Entretanto, fazer

primeiro as operações undo e depois as operações redo é importante no algoritmo de recuperação que veremos

adiante.

Finalmente, assumamos que a queda ocorra logo após o registro de log <T1 commit> ter sido escrito em

armazenamento estável (fig. 15.7c). Quando o sistema retorna, tanto T0 como T1 necessitam ser refeitas, já que os

registros <T0 start> e <T0 commit> aparecem no log, assim como os registros <T1 start> e <T1 commit>. Após os

procedimentos de recuperação redo(T0) e redo(T1), os valores nas contas A, B e C serão 950, 2050 e 600 dólares,

respectivamente.

Checkpoints

Quando uma falha de sistema ocorre, devemos consultar o log para determinar aquelas transações que

necessitam ser refeitas e aquelas que necessitem ser inutilizadas. A princípio, para isso, deveríamos pesquisar

todo o log. Há duas grandes dificuldades nessa abordagem:

1. O processo de pesquisa consome tempo.

2. Muitas das transações que, de acordo com nosso algoritmo, necessitam ser refeitas já escreveram suas

atualizações no banco de dados. Embora refazê-las não cause dano algum, a recuperação torna-se mais

longa.

Para reduzir esses tipos de overhead, introduzimos os checkpoints (pontos de controle). Durante a

execução, o sistema mantém o log usando uma das duas técnicas descritas anteriormente. Além disso, o sistema

cria checkpoints periodicamente, que exigem a execução da seguinte sequência de ações:

1. Saída, para armazenamento estável, de todos os registros residentes na memória principal;

2. Saída, para disco, de todos os blocos de buffer modificados.

3. Saída, para armazenamento estável, de um registros de log <checkpoint>.

Não é permitido às transações processar quaisquer ações de atualização, como escrever em um bloco de

buffer ou escrever um registro de log, enquanto um checkpoint está em progresso.

A presença de um registro <checkpoint> no log permite que o sistema dinamize seu procedimento de

recuperação. Considere uma transação Ti efetivada antes do checkpoint. Para tal transação, o registro <Ti

commit> aparece no log antes do registro <checkpoint>. Quaisquer modificações feitas por Ti ou já foram escritas

no banco de dados antes do checkpoint ou o foram como parte do checkpoint propriamente dito. Então, no

momento de recuperação, não haverá necessidade de uma operação redo sobre Ti.

309

Essa observação permite-nos refinar nossos esquemas de recuperação anteriores (continuamos a assumir

que as transações são executadas serialmente). Após uma falha, o esquema de recuperação examina o log para

determinar a última transação Ti anterior ao checkpoint mais recente. Isso poderá ser feito por uma pesquisa

retroativa no log, a partir de seu final até o primeiro registro <checkpoint> (já que estamos pesquisando em

ordem cronológica inversa, o registro encontrado será o último registro <checkpoint> do log); então o sistema

continuará a pesquisar retroativamente até encontrar o próximo registro <Ti start>. Esse registro identifica uma

transação Ti.

Uma vez identificada a transação Ti, as operações redo e undo devem ser aplicadas à transação Ti e a

todas as transações Tj que começaram depois da dela. Indiquemos essas transações pelo conjunto T. O restante

(parte anterior) do log pode ser ignorado e até mesmo apagado, se for conveniente. Quais operações de

recuperação devem, exatamente, ser processadas dependerá da técnica de modificação em uso, se imediata ou

adiada. As operações de recuperação exigidas, se a técnica de modificação imediata é empregada, são as

seguintes:

• Para todas as transações Tk em T que não têm nenhum registro <Tk commit> no log será executado

undo(Tk).

• Para todas as transações Tk em T tais que o registro <Tk commit> aparece no log será executado redo(Tk).

Obviamente, a operação undo não será aplicada caso a técnica de modificação adiada esteja em uso.

Como ilustração, considere o conjunto de transações {T0,T1,..., T100} executado na ordem dos subescritos.

Suponha que o checkpoint mais recente tenha ocorrido durante a execução da transação T67. Então, somente as

transações T67, T68, ..., T100 necessitam ser consideradas durante o esquema de recuperação. Cada uma delas será

refeita se tiver sido efetivada; caso contrário, será inutilizada.

Paginação Shadow

Uma alternativa às técnicas de recuperação de queda baseada em log é a paginação shadow (sombra). A

técnica de paginação shadow é essencialmente uma melhoria de técnica de cópia shadow. Sob certas

circunstâncias, a paginação shadow pode exigir menos acessos a disco que os métodos baseados em log que

acabamos de discutir. Entretanto, também há desvantagens nessa abordagem, como veremos. Por exemplo, é

difícil aplicar a paginação shadow pode exigir menos acessos a disco que os métodos baseados em log que

acabamos de discutir. Entretanto, também há desvantagens nessa abordagem, como veremos. Por exemplo, é

difícil aplicar a paginação shadow em transações concorrentes.

Como antes, o banco de dados é particionado em um número de blocos de comprimento fixo, chamados

de páginas. O termo página é emprestado dos sistemas operacionais, já que estamos usando um esquema de

paginação para gerenciamento de memória. Assumamos que haja n páginas, numeradas de 1 até n. (Na prática, n

pode ser da ordem de centenas de milhares.) Essas páginas não necessitam ser armazenadas em alguma ordem

particular no disco (há muitas razões para que isso não aconteça). Entretanto, deve haver uma forma de

encontrar, para um i qualquer, a i-ésima página do banco de dados. Usamos, para esse proposito, uma tabela de

página, como mostrado na fig. 15.8. A tabela de página tem n entradas – uma para cada página do banco de

dados. Cada entrada contém um ponteiro para uma página no disco. A primeira entrada contém um ponteiro

para a primeira página do banco de dados, a segunda entrada aponta para a segunda página e assim por diante. O

exemplo da fig. 15.8 mostra que a ordem lógica das páginas do banco de dados não necessita corresponder à

ordem física em que as páginas estão armazenadas no disco.

A ideia básica da técnica de paginação shadow é manter duas tabelas de página durante o processamento

da transação: a tabela de páginas atuais e a tabela de páginas shadow. Quando a transação começa, ambas as

tabelas são idênticas. A tabela de página shadow não é alterada durante toda a duração da transação. A tabela de

página atual é alterada quando a transação processa uma operação write. Todas as operações de entrada (input)

e saída (output) usam a tabela de páginas atuais para localizar páginas do banco de dados no disco.

310

Suponha que a transação processe uma operação write(X) e que X resida na i-ésima página. A operação

write é executada como segue:

1. Se a i-ésima página (isto é, a página em que X reside) ainda não está na MP, então será emitido um

input(X).

2. Se essa é a primeira escrita processada na i-ésima página por essa transação, então a tabela de páginas

atuais será modificada conforme segue:

a. Encontra-se uma página sem uso no disco. Normalmente, o sistema de banco de dados tem

acesso a uma lista de páginas sem uso (livres).

b. Remove-se a página encontrada no passo 2a a partir da lista de quadros de páginas livres.

c. Modifica-se a tabela de páginas atuais, tal que a i-ésima entrada aponte para a página encontrada

no passo 2a.

3. Designa-se o valor de xj para X na página de buffer.

Comparemos a ação precedente para a operação write com aquela já descrita. A única diferença é

adicionamos um novo passo. Os passos 1 e 3 anteriores correspondem aos passos 1 e 2 descritos anteriormente.

O passo adicional, passo 2, manipula a tabela de páginas atuais. A fig. 15.9 mostra as tabelas, shadow e atual,

para uma transação que realize uma escrita na quarta página de um banco de dados constituído de dez páginas.

Intuitivamente, a abordagem de recuperação por meio de páginas shadow consiste em manter uma

tabela de páginas atuais. A fig. 15.9 mostra as tabelas, shadow e atual, para uma transação que realize uma

escrita na quarta página de um banco de dados constituído de dez páginas.

Intuitivamente, a abordagem de recuperação por meio de páginas shadow consiste em manter uma

tabela de páginas shadow em armazenamento não-volátil, tal que se possa recuperar o estado do banco de dados

anterior à execução da transação, devido a uma queda ou aborto da transação. Quando uma transação é

efetivada, a tabela de páginas atuais é escrita em armazenamento não-volátil. A tabela de páginas atuais torna-se,

então, a nova tabela de páginas shadow e, então, uma nova transação pode começar. É importante que a tabela

de páginas shadow seja armazenada em meio não-volátil, já que ela fornece o único meio de localização das

páginas do banco de dados. A tabela de páginas atuais pode ser mantida na MP (armazenamento volátil). Não

importa se a tabela de páginas atuais é perdida em uma queda, já que o sistema se recupera usando a tabela de

página shadow.

311

Uma recuperação bem-sucedida exige que, após uma queda, encontremos a tabela de páginas shadow no

disco. Uma forma simples de encontra-la é manter, em uma localização fixa de armazenamento estável, o

endereço em disco da tabela de página shadow. Quando o sistema retornar após uma queda, copiamos a tabela

de páginas shadows na MP e a usamos para o processamento subsequente da transação. Devido a nossa

definição de operação write, garantimos que a tabela de páginas shadow apontará para as páginas do banco de

dados que correspondem ao estado do banco de dados anterior a qualquer transação ativa no momento da

queda. Então, o aborto de transações é automático. Ao contrário dos esquemas baseados em log, nenhuma

operação undo precisa ser chamada.

Para efetivar uma transação, devemos fazer o seguinte:

1. Garantir que todas as páginas de buffer da MP que foram alteradas pela transação sejam enviadas para a

saída em disco. (Observe que essas operações de saída não alterarão as páginas do banco de dados

apontadas pela tabela de páginas shadow.)

2. Enviar a tabela de páginas atuais para saída em disco. Observe que não devemos sobrescrever a tabela de

páginas atuais, já que poderemos precisar dela para a recuperação após uma queda.

3. Enviar os endereços de disco da tabela de página atuais para a localização fixa em armazenamento

estável que contém o endereço da tabela de páginas shadow. Essa ação sobrescreve o endereço da tabela

de páginas shadow antiga. Portanto, a tabela de páginas atuais tornou-se a tabela de páginas shadow e a

transação foi efetivada.

Se uma queda ocorrer antes do término do passo 3, reverteremos ao estado anterior ao da execução da

transação. Se a queda ocorrer após o término do passo 3, os efeitos da transação ainda assim serão preservados;

nenhuma operação redo precisará ser atividade.

A paginação shadow oferece diversas vantagens sobre as técnicas baseadas em log. O overhead relativo

ao envio dos registros de log é eliminado e a recuperação de falhas é significativamente mais rápida (já que

nenhum operação undo e redo é necessária). Entretanto, há também inconvenientes nessa técnica.

• Overhead de efetivação. A efetivação de uma única transação usando paginação shadow exige que

diversos blocos sejam enviados – blocos de dados reais, tabela de páginas atuais e o endereço de disco da

tabela de páginas atuais. Os esquemas baseados em log precisam enviar somente os registros de log que,

para as pequenas e mais comuns transações, cabem dentro de um bloco.

• Fragmentação de dado. Consideramos estratégias para manter próximas no disco as páginas do banco de

dados relacionadas fisicamente. Essa proximidade permite maior rapidez na transferência de dados. A

paginação shadow gera alterações na localização das páginas do banco de dados quando elas sofrem

312

atualizações. Com isso, ou perdemos a proximidade das páginas ou precisaremos recorrer a esquemas

mais complexos, com maior overhead para gerenciamento do armazenamento físico.

• Coleta de lixo. Cada vez que uma transação é efetivada, as páginas do banco de dados contendo a versão

antiga do dado, alterado pela transação, tornam-se inacessíveis. Na fig. 15.9, a página apontada pela

quarta entrada da tabela de páginas shadow se tornará inacessível se a transação daquele exemplo for

efetivada. Tais páginas são consideras lixo, já que elas não fazem parte do espaço livre nem contêm

informação útil. O lixo também pode ser um efeito colateral das quedas. Periodicamente, é necessário

encontrar todas as páginas lixo e adicioná-las à lista de páginas livres. Esse processo, chamado de coleta

de lixo, impõe overhead e complexidade adicionais ao sistema. Há vários algoritmos-padrão para coleta

de lixo.

Além dos inconvenientes que mencionamos, a adaptação da paginação shadow a sistemas com

transações concorrentes é mais difícil do que registro de log. Em tais sistemas, algum tipo de registro de log é

normalmente necessário, mesmo que a paginação shadow seja usada. O protótipo do Sistema R, por exemplo,

usava uma combinação de paginação shadow com um esquema de registro de log similar ao já apresentado. É

relativamente simples ampliar os esquemas de recuperação baseados em log para que trabalhem com transações

concorrentes. Por essas razões, a paginação shadow não é tão usada.

Recuperação com Transações Concorrentes

Até agora, consideramos a recuperação em um ambiente no qual uma única transação é executada por

vez. Agora, discutiremos como modificar o esquema de recuperação baseado em log para tratar de diversas

transações concorrentes. Independente do número de transações concorrentes, o sistema tem um único buffer

de disco e um único log. Os blocos de buffer são compartilhados por todas as transações. Permitiremos, além de

atualizações imediatas, que um bloco de buffer tenha itens de dados atualizados por uma ou mais transações.

Interação com Controle de Concorrência

O esquema de recuperação depende muito do esquema de controle de concorrência em uso. Para

reverter (rollback) uma transação com falha, devemos inutilizar as atualizações processadas por ela. Suponha que

uma transação T0 tenha de ser desfeita e um item de dado Q que foi atualizado por T0 tenha de recuperar seu

valor antigo. Usando os esquemas baseados em log, recuperamos esses valor usando as informações undo de um

registro de log.

Suponha agora que uma segunda transação T1 tenha também processado uma atualização sobre Q antes

de T0 ser desfeita. Então, a atualização processada por T1 será perdida quando T0 for revertida.

Portanto, exigimos que, se uma transação T atualizou um item de dado Q, nenhuma outra transação

consiga atualizar o mesmo item de dado até que T tenha sido efetivada ou revertida. Podemos facilmente cumprir

essa exigência pelo bloqueio em duas fases severo – isto é, bloqueio em duas fases com bloqueios exclusivos

mantidos até o final da transação.

Reversão de Transação

Revertemos uma transação com falha, Ti, usando o log. O log é reexaminado, do fim para o começo; e

para cada registro da forma <Ti, Xj, V1, V2> encontrado, o item de dado Xj é restaurado em seu valor antigo V1.

Esse exame termina quando o registro de log <Ti, start> é encontrado.

Reexaminar o log de trás para frente é importante, já que uma transação pode ter atualizado um item de

dados mais de uma vez. Como ilustração, considere o par de registros de log:

Os registros de log representam uma modificação do item de dado A por Ti, seguido de outra modificação

de A por Ti. Reexaminar o log do final para o começo ajusta A corretamente para 10. Se os logs fossem

examinados no sentido inverso, A seria ajustado para 20, cujo valor é incorreto.

313

Se o bloqueio em duas fases severo é usado para controle de concorrência, os bloqueios mantidos pela

transação T podem ser liberados somente após a transação ter sido revertida, como descrito. Uma vez que uma

transação T (que é revertida) tenha atualizado um item de dado, nenhuma outra transação poderá atualizar o

mesmo item de dado, devido às exigências de controle de concorrência mencionadas anteriormente. Portanto, a

recuperação do valor antigo do item de dado não apagará os efeitos de qualquer transação.

Checkpoints

Usamos checkpoints para reduzir o número de registros de log que devem ser examinados quando o

sistema se recupera de uma queda. Já que não tínhamos levado qualquer ocorrência em conta, foi necessário

considerar somente as seguintes transações durante a recuperação:

• Aquelas transações que iniciaram após o checkpoint mais recente.

• Aquela transação, se houver alguma, que estava ativa no momento da escrita do checkpoint mais

recente.

A situação é mais complexa quando as transações podem ser executadas de modo concorrente, já que

várias transações poderiam estar ativas no momento em que o checkpoint mais recente foi gerado.

Em um sistema com processamento de transações concorrentes, exigimos que o registro de log relativo

ao checkpoint seja da forma <checkpoint L>, em que L é a lista de transações ativas no momento do checkpoint.

Novamente, assumimos que as transações não processam atualizações tanto nos blocos de buffer como no log

enquanto o checkpoint está em andamento.

A exigência de que as transações não devem realizar quaisquer atualizações nos blocos de buffer, ou no

log, durante o checkpoint pode ser preocupante, já que o processamento de transação terá de parar enquanto

um checkpoint estiver em progresso. Um fuzzy checkpoint é aquele que é permitido às transações processarem

atualizações mesmo quando os blocos de buffer estão sendo escritos.

Recuperação por Reinício

Quando um sistema se recupera de uma queda, ele constrói duas listas: a lista inutilizar (undo-list), que

consiste em transações a serem inutilizadas, e a lista refazer (redo-list), que consiste em transações a serem

refeitas.

Essas duas listas são construídas na recuperação como segue. Inicialmente, ambas estão vazias.

Examinando o log de trás para frente, cada registro, até que seja encontrado o primeiro registro <checkpoint>:

• Para cada registro da forma <Ti commit> encontrado, adicionamos Ti à lista refazer.

• Para cada registro da forma <Ti start> encontrado, se Ti não estiver na lista refazer, então adicionamos Ti

na lista inutilizar.

Quando todos os registros de log apropriados tiverem sido examinados, checamos a lista L no registro de

checkpoint em questão. Para cada transação Ti em L, se Ti não estiver na lista refazer, então será adicionada à lista

inutilizar.

Uma vez construídas as listas refazer e inutilizar, os procedimentos de recuperação prosseguem como

segue:

1. Reexaminar o log a partir do registro mais recente e processar um undo para cada registro de log

pertencente à transação Ti na lista inutilizar. Os registros de log de transações na lista refazer serão

ignorados nessa fase. O exame para quando os registros <Ti start> são encontrados para cada

transação Ti na lista inutilizar.

2. Localizar o registro <checkpoint L> mais recente no log. Note que este passo pode implicar exame do

log na ordem cronológica crescente, se o registro de checkpoint foi ultrapassado no passo 1.

3. Examinar o log a partir do registro <checkpoint L> mais recente até o final e processar redo para cada

registro de log pertencente a uma transação Ti que está na lista refazer. Ignorar os registros de log de

transações na lista inutilizar nessa fase.

314

É importante processar o log no passo 1, na ordem cronológica decrescente, para garantir que o estado

resultante do banco de dados estará correto.

Após desfazer todas as transações da lista inutilizar, as transações da lista refazer são refeitas. É

importante, nesse caso, processar o log na ordem cronológica crescente. Completado o processo de recuperação,

o processamento da transação é reassumido.

Quando se usa o algoritmo precedente, é importante inutilizar uma transação da lista inutilizar antes de

refazer transações da lista refazer. De outra forma, o seguinte problema pode ocorrer. Suponha que o item de

dado A tenha inicialmente o valor 10. Suponha que uma transação Ti tenha atualizado o item de dado A para 20 e

depois foi abortada; a reversão da transação restauraria A para o valor 10. Suponha, então, que outra transação Tj

tenha atualizado o item de dado A para 30 e tenha sido efetivada; em seguida, o sistema cai. O estado do log no

momento da queda é:

Se o passo redo é processado primeiro, A será ajustado para 30; então, no passo undo, A será ajustado

para 10, o que está errado. O valor final de Q deveria ser 30, que podemos alcançar processando undo antes de

redo.

Gerenciamento de Buffer

Vamos considerar diversos detalhes sutis, embora essenciais, para a implementação de um esquema de

recuperação de queda que garanta consistência de dados e imponha uma quantidade mínima de overhead

devido a interações com o banco de dados.

Bufferização de Registro de Log

Anteriormente, assumimos que qualquer registro de log é enviado para a saída de armazenamento

estável no momento em que é criado. Essa situação impõe grande overhead à execução do sistema pelas

seguintes razões. Normalmente, a saída para armazenamento estável ocorre em unidades de blocos. Em muitos

casos, um registro de log é muito menor que um bloco. Então, a saída de cada registro de log se traduz em uma

saída muito maior no nível físico. Além do mais a saída de um bloco para armazenamento estável pode significar

várias operações de saída no nível físico.

O custo relativo ao processamento de uma saída de bloco para armazenamento estável é suficientemente

alto; é melhor que saiam diversos registros de log de uma só vez. Para isso, escrevemos os registros de log em um

buffer de log na MP, onde permanecem temporariamente, até serem enviados para armazenamento estável.

Múltiplos registros de log podem ser reunidos no buffer de log e enviados para armazenamento estável em uma

única operação de saída. A ordem dos registros de log no armazenamento estável deve ser exatamente a mesma

na qual foram escritos no buffer de log.

Em consequência do uso da bufferização do log, um registro de log pode residir somente em MP

(armazenamento volátil) por um tempo considerável antes de ser enviado para a saída em armazenamento

estável. Já que tais registros de log são perdidos se os sistema cair, devemos impor exigências adicionais às

técnicas de recuperação para garantia de atomicidade da transação:

• A transação Ti entra em estado de efetivação após o registro de log <Ti commit> ter sido enviado para

saída em armazenamento estável.

• Antes do registro de log <Ti commit> sofrer armazenamento estável, todos os registros de log

pertencentes à transação Ti deverão ter sido enviados para armazenamento estável.

• Antes de um bloco de dados na MP podem ser enviado para o banco de dados (em armazenamento não-

volátil), todos os registros de log pertencentes a dados naquele bloco devem ter sido enviados para

armazenamento estável.

315

• Antes de um bloco de dados na MP poder ser enviado para o banco de dados (em armazenamento não-

volátil), todos os registros de log pertencentes a dados naquele bloco devem ter sido enviados para

armazenamento estável.

A última regra é chamada de regra write-ahead logging (WAL). (Precedência de escrita do log, a regra WAL exige

somente a informação undo no log tenha sido enviada para saída em armazenamento estável, permitindo que a

informação redo seja escrita mais tarde. A diferença é relevante em sistemas nos quais a informação redo e undo

é armazenada em registros de log separados.)

Escrever o log bufferizado no disco às vezes é chamado de forçar o log (log force). As regras precedentes

criam situações em que certos registros de log devem ser enviados para saída em armazenamento estável. Não

há problema decorrente do envio dos registros de log mais cedo que o necessário. Logo, quando o sistema achar

necessário enviar um registro de log para armazenamento estável, ele enviará um bloco inteiro de registros de

log, se houver registros de log suficientes na MP para preencher um bloco. Se os registros de log forem

insuficientes na MP para preencher um bloco. Se os registros de logo forem insuficientes para preencher o bloco,

os registros de log na MP serão combinados a um bloco parcialmente completo e serão enviados para saída em

armazenamento estável.

Bufferização de Banco de Dados

Já descrevemos a hierarquia de armazenamento em dois níveis. O banco de dados está armazenado em

armazenamento não-volátil (disco) e os blocos de dados são levados à MP quando necessário. Já que a MP é

normalmente muito menor que o banco de dados inteiro, pode ser necessário sobrescrever um bloco B1 na MP

quando outro bloco B2 necessitar ser levado à memória. Se B1 tiver sido modificado, ele deverá ser enviado para o

banco de dados antes da entrada de B2. Essa hierarquia de armazenamento é o conceito-padrão de sistema

operacional de memória virtual.

As regras para a saída de registros de log limitam a liberdade do sistema para saída de blocos de dados. Se

a entrada do bloco B2 fizer com que o bloco B1 seja selecionado para a saída, todos os registros de log

pertencentes aos dados em B1 devem ser enviados para armazenamento estável antes de B1 ser enviado para

saída. Então, a sequência de ações pelo sistema, seria como segue:

• Enviar registros de log para armazenamento estável até acabarem os registros de log pertencentes ao

bloco B1.

• Enviar o bloco B1 para saída no disco.

• Enviar o bloco B2 do disco para a entrada em MP.

É importante que nenhuma gravação no bloco B1 esteja em progresso enquanto a sequência precedente

de ações estiver em execução. Essa condição é satisfeita quando se usa um meio especial de bloqueio, como

segue. Antes que uma transação escreva um item de dado, ela deve adquirir um bloqueio exclusivo sobre o bloco

no qual o item de dados reside. O bloqueio poderá ser liberado imediatamente após a atualização. Antes de um

bloco ser enviado para a saída, o sistema obtém um bloqueio exclusivo sobre o bloco, para garantir que nenhuma

transação o altere. Completada a saída do bloco, o bloqueio é liberado. Os bloqueios que são mantidos por pouco

tempo são frequentemente chamados de trancas (latches). As trancas são tratadas de forma distintas dos

bloqueios usados pelo sistema de controle de concorrência. Com isso, elas podem ser liberadas sem considerar

qualquer protocolo de bloqueio, como bloqueio em duas fases, exigido pelo sistema de controle de concorrência.

Para ilustrar a necessidade da sequência anterior de ações, consideremos nosso exemplo bancário com as

transações T0 e T1. Suponha que o estado do log seja:

e que a transação T0 emita uma read(B). Assuma que o bloco em que B reside não está na MP e que a MP esteja

completa. Suponha que o bloco em que reside A seja escolhido para ser enviado ao disco. Se o sistema envia esse

bloco para saída no disco e, então, o sistema cai, os valores no banco de dados para as contas A, B e C são 950,

2000 e 700 dólares, respectivamente. Esse estado do banco de dados é inconsistente. Entretanto, devido às

exigências precedentes, o registro de log <T0, A, 1000, 950> deverá ser enviado para armazenamento estável

316

antes da saída do bloco em que A reside. O sistema pode usar o registro de log durante a recuperação para levar

o banco de dados de volta a um estado consistente.

Regra de Sistema Operacional em Gerenciamento de Buffer

Podemos gerenciar o buffer do banco de dados usando uma das duas abordagens:

1. O sistema de banco de dados reserva parte da MP para servir como um buffer gerenciado por ele, e não

pelo sistema operacional. O sistema de banco de dados gerencia a transferência de bloco de dados, de

acordo com as exigências que já discutimos.

Essa abordagem tem o inconveniente de limitar a flexibilidade de uso da MP. O buffer deve ser mantido

pequeno o suficiente para que outras aplicações tenha MP suficiente para sua necessidades. Entretanto, mesmo

quando outras aplicações não estiverem rodando, o banco de dados não poderá fazer uso de todas a memória

disponível. Da mesma forma, aplicações que não sejam banco de dados podem não usar aquela parte da MP

reservada para o buffer de banco de dados, mesmo se algumas páginas no buffer do banco de dados não

estiverem em uso.

2. O sistema de banco de dados implementa seu buffer dentro da memória virtual do sistema operacional.

Já que o sistema operacional possui informações sobre as exigências de memória de todos os processos

no sistema, idealmente ele deveria estar capacitado a decidir quais blocos de buffer devem ser forçados à

saída para o disco e quando isso deve ser feito. Mas, para atender à precedência de escrita do log que

discutimos, o sistema operacional não poderia escrever páginas de buffer no banco de dados por si só,

mas, ao contrário, solicitar que o sistema de banco de dados forçasse a saída dos blocos do buffer. O

sistema de banco de dados, por sua vez, só forçaria a saída dos blocos do buffer para o banco de dados

após escrever os registros de log relevantes em armazenamento estável.

Infelizmente, quase todos os sistemas operacionais da geração atual mantêm controle completo sobre a

memória virtual. O sistema operacional reserva espaço em disco para armazenar páginas de memória virtual que

não estão na MP; este espaço é chamado de espaço de swap (troca). Se o sistema operacional decidir retirar da

memória um bloco Bx, ele será enviado para o espaço de swap no disco, e não há nenhuma forma do sistema de

banco de dados conseguir controle dessa saída de blocos do buffer.

No entanto, se o buffer de banco de dados estiver na memória virtual, as transferências entre arquivos de

banco de dados e o buffer na memória virtual são gerenciadas pelo sistema de banco de dados, que cumprirá as

exigências de precedência de escrita do log que discutimos.

Essa abordagem pode implicar saídas extras de dados para o disco. Se um bloco Bx é retirado pelo sistema

operacional, esse bloco não será enviado para o banco de dados. Ao contrário, ele será enviado para o banco de

dados. Quando o sistema de banco de dados tiver de enviar Bx para o disco, pode ser que o sistema operacional

tenha de, primeiro, retirar Bx de seu espaço de swap. Então, ao contrário de uma única saída de Bx da memória,

exigiremos duas saídas de Bx (uma pelo sistema operacional, e outra pelo sistema de banco de dados), além de

uma entrada extra de Bx na memória.

Embora ambas as abordagens padeçam de alguns inconvenientes, uma ou outra deverá ser escolhida, a

menos que o sistema operacional seja projetado para aceitar as exigências de log do banco de dados. Somente

uns poucos sistemas operacionais atuais, como o sistema operacional Mach, aceitam tais exigências.

Falha com Perda de Armazenamento Não-volátil

Até agora, consideramos somente falhas que têm como consequência a perda de informações residentes

em armazenamento volátil; o conteúdo do armazenamento não-volátil permanece intacto. Embora as falhas com

perda de armazenamento não-volátil sejam raras, precisamos estar preparados para trata-las. Vamos discutir, por

enquanto, apenas armazenamento em disco. Nossas discussões se aplicam também a outros tipos de

armazenamento não-volátil.

O esquema básico é descarregar (dump) todo conteúdo do banco de dados para armazenamento estável

periodicamente – digamos, uma vez por dia. Por exemplo, podemos descarregar o banco de dados para uma ou

317

mais fitas magnéticas. Se ocorrer uma falha que resulte na perda de blocos físicos do banco de dados, a descarga

mais recente será usada na restauração do banco de dados para seu último estado consistente possível. Uma vez

completada essa restauração, o sistema usa o log para trazer o sistema de banco de dados para um estado

consistente recente.

Mais precisamente, nenhuma transação pode estar ativa durante o procedimento de descarga, e um

procedimento similar para checkpoint (pontos de controle) deve ocorrer:

1. Enviar todos os registros de log residentes em MP para saída de armazenamento estável.

2. Enviar todos os blocos de buffer para saída em disco.

3. Copiar o conteúdo do banco de dados em armazenamento estável.

4. Enviar um registro de log <dump> para saída em armazenamento estável.

Os passos 1, 2 e 4 correspondem aos três passos usados para checkpoints anteriormente.

Para se recuperar da perda de armazenamento não-volátil, restauramos o banco de dados em disco

usando a descarga (dump) mais recente. O log é, então, consultado e todas as transações que foram efetivadas

desde a última descarga são refeitas. Observe que nenhuma operação undo precisa ser executada.

Uma descarga do conteúdo do banco de dados também é chamada de archival dump (descarga histórica),

já que podemos arquivar as descargas e usá-las mais tarde para examinar informações antigas do banco de

dados. Descargas de um banco de dados e checkpoints dos buffers são similares.

O procedimento de descarga simples aqui descrito é oneroso pelas seguintes razões. Primeiro, todo o

banco de dados deverá ser copiado em armazenamento estável, resultando em considerável transferência de

dados. Segundo, já que o processamento das transações é suspenso durante a descarga, ciclos de CPU são

perdidos. Têm sido desenvolvidos esquemas de fuzzy dump (descarga indistinta) que permitem a existência de

transações ativas enquanto a descarga está em progresso. São similares a esquemas de checkpoints.

Técnicas de Recuperação Avançadas

As técnicas de recuperação já descritas requerem que, enquanto uma transação atualização um item de

dado, nenhuma outra transação consiga atualizar o mesmo item de dado, até que a primeira seja efetivada ou

revertida. Satisfazemos essa condição por meio do bloqueio em duas fases severo. Embora o bloqueio em duas

fases severo seja aceitável para os registros de relações ele causa um significativo decréscimo de concorrência

quando aplicado a certas estruturas especiais, como páginas de índice de árvore-B+.

Para aumentar a concorrência, podemos usar o algoritmo de controle e de concorrência árvore-B+

permitindo que os bloqueios sejam liberados mais cedo, sem usar duas fases. Como isso, entretanto, as técnicas

de recuperação estudadas tornam-se inaplicáveis. Várias técnicas de recuperação alternativas têm sido propostas

tornam-se inaplicáveis. Várias técnicas de recuperação alternativas têm sido propostas, aplicáveis mesmo com

liberação prematura de bloqueio. Descrevemos uma dessas técnicas de recuperação agora.

Log com Undo Lógico

Para ações nas quais os bloqueios são liberados mais cedo, não podemos processar as ações undo

simplesmente regravando o valor antigo dos itens de dados. Considere uma transação T que insira uma entrada

em uma árvore-B+ e, seguindo o protocolo de controle de concorrência árvore-B+, libere alguns dos bloqueios

após o término da operação de inserção, mas antes da transação ser efetivada. Após a liberação dos bloqueios,

outras transações podem realizar inserções ou remoções adicionais, causando desse modo mudanças adicionais

nas páginas da árvore B+.

Mesmo que a operação libere alguns dos bloqueios previamente, ela deverá reter bloqueios suficientes

para garantir que não seja permitido a nenhuma outra transação executar qualquer operação conflitante (como

ler ou remover o valor inserido). Por essa razão, o protocolo de controle de concorrência árvore-B+ mantém os

bloqueios no nível de folha da árvore-B+ até o fim da transação.

Agora vamos considerar como processar os rollbacks de transações. Se os valores antigos dos nós

internos da árvore-B+ (antes da operação de inserção ser executada) forem restaurados durante a reversão da

318

transação, algumas das atualizações processadas pelas últimas operações de inserção ou exclusão executadas por

outras transações poderiam ser perdidas. Em vez disso, a operação de inserção tem de ser inutilizada logicamente

– isto é, pela execução de uma operação de remoção.

Portanto, quando a ação de inserção for completada, antes de liberar qualquer bloqueio, ela deverá

gravar um registro de log <Oi, operation-end, U>, em que U indica a informação de inutilização e Oi indica um

identificador para a operação. Por exemplo, se a operação inseriu uma entrada em uma árvore-B+, a informação

undo U indicaria o que remover da árvore-B+. Esse log de informação sobre operações é chamado de log físico e

os correspondentes registros de log são chamados de registros de log físicos.

As operações de inserção e remoção são exemplos de uma classe de operações que exige operações undo

lógicas, já que liberam os bloqueios previamente; chamamos tais operações de operações lógicas. Antes que uma

operação lógica tenha início, ela escreve um registro de log <Oi, operation-begin>, em que Oi é o identificador

para a operação. Enquanto a operação está sendo executada, o log é feito da maneira convencional para todas as

atualizações processadas pela operação. Então, as informações usuais dos valores antigos e novos são escritos

para cada atualização. Quando a operação termina, um registro de log de final de operação (operation-end) é

escrito como explicado anteriormente.

Reversão de Transação

Vamos considerar primeiro a reversão de transações durante operação normal (isto é, não durante a

recuperação do sistema após uma falha). O log é examinado de trás para a frente (ordem cronológica

decrescente) e os registros de log pertencente à transação são usado para restaurar os valores antigos dos itens

de dados. Ao contrário de antes, escrevemos registros especiais de log somente redo da forma <Ti, Xj, V> com o

valor V sendo sobreposto ao item de dado Xj durante a reversão. Esses registros de log algumas vezes são

chamados de registros de log de compensação. Sempre que um registro de log <Oi, operation-end, U> é

encontrado, ações especiais são tomadas:

1. Revertemos a operação usando a informação undo U do registro de log. As atualizações processadas

durante a reversão da operação são registradas no log exatamente da mesma forma que as atualizações

processadas quando a operação foi executada primeiro. Além do mais, registros de log referentes ao

início e ao final da operação são gerados exatamente como durante a execução normal da operação.

2. Quando a verificação retroativa do log continua, saltamos todos os registros de log da transação até

encontrados o registro de log <Oi, operation-begin>. Após o registro de log de início da operação ser

encontrado, os registros de log da transação são processados novamente de maneira usual.

Quando a transação Ti for revertida, um registro <Ti abort> será adicionado ao log.

Se ocorrerem falhas enquanto uma operação lógica está em progresso, o registro de logo de fim de

operação para aquela operação não será encontrado quando a transação for revertida. Entretanto, para cada

atualização processada pela operação, a informação undo – na forma do valor antigo nos registros de log físicos –

está disponível no log. Observe que saltar os registros de log físicos, quando o registro de log de fim de operação

é encontrado durante a reversão, garante que os valores antigos do registro de log físico não sejam usados na

reversão, uma vez que a operação terminou.

Se o bloqueio é usado para controle de concorrência, os bloqueios mantidos por uma transação T podem

ser liberados somente após a transação tiver sido revertida, conforme descrito.

Checkpoints

Os checkpoints são processados conforme já descrito. Atualizações no banco de dados são

temporariamente suspensas e as seguintes ações são executadas:

1. Enviar para saída em armazenamento estável todos os registros de log residentes na memória principal.

2. Enviar para saída em armazenamento estável um registro de log <checkpoint L>, em que L é uma lista de

todas as transações ativas.

3. Enviar para saída em disco todos os blocos de buffer modificados.

319

Recuperação de Reinício

As ações de recuperação, quando o sistema de banco de dados é reiniciado após uma falha, são

executadas em duas fases:

1. Na fase redo, reexecutamos as atualizações de todas as transações pela varredura do log avançando a

partir do último checkpoint. Os registros de log que serão reexecutados englobam os registros de log das

transações que foram revertidas antes do sistema cair e aquelas que não foram efetivas quando ocorreu

a queda do sistema. Os registros de log reexecutados incluem os registros de log usuais da forma <Ti, Xj,

V1, V2> e os registros de log especiais da forma <Ti, Xj, V2>; o valor V2 é escrito para o item de dados Xj em

qualquer caso. Essa fase também determina todas as transações que estão na lista de transações no

registro de checkpoint ou que foram iniciadas mais tarde, mas não têm nenhum registro <Ti abort> ou <Ti

commit> no log. Todas essas transações têm de ser revertidas e seus identificadores de transação são

colocados em uma lista undo.

2. Na fase undo, refazemos todas as transações da lista undo. Processamos a reversão reexaminando o log

do final para o começo. Sempre que um registro de log pertencente a uma transação da lista undo é

encontrado, as ações undo são processadas exatamente como se o registro de log fosse encontrado

durante a reversão de uma transação que falhou. Então, os registros de log de uma transação anteriores a

um registro final de operação, mas após o correspondente registro de início de operação, são ignorados.

Quando um registro de log <Ti start> é encontrado para uma transação Ti na lista undo, um registro de log

<Ti abort> é escrito no log. O exame do log para quando registros de log <Ti start> são encontrados para todas as

transações na lista undo.

A fase redo da recuperação de reinício reexecuta todos os registros de log físico, a partir do mais recente

registro de checkpoint. Em outras palavras, essa fase de recuperação de reinício repete todas as ações de

atualização que foram executadas após o checkpoint e cujos registros de log alcançaram log estável. Essas ações

incluem as transações incompletas e as ações executadas para reverter transações com falha. As ações são

repetidas na mesma ordem em que foram executas; consequentemente, esse processo é chamado de história

repetitiva (repeating history). A história repetitiva simplifica enormemente os esquemas de recuperação.

Fuzzy Checkpoint

A técnica checkpoint descrita exige que todas as atualizações ao banco de dados sejam temporariamente

suspensas enquanto o checkpoint está em progresso. É possível modificar a técnica para permite que as

atualizações iniciem no momento em que o registro checkpoint é escrito, mas antes dos blocos de buffer

modificados serem escritos no disco. Então, o checkpoint gerado é um fuzzy checkpoint (ponto de controle

indistinto).

A ideia é a seguinte. Em vez de reexaminar o log de trás para frente a fim de encontrar um registro de

checkpoint, armazenamos a localização em log do último registro de checkpoint em uma posição fixa no disco.

Entretanto, essa informação não é atualizada quando o registro checkpoint é escrito. Ao contrário, antes do

registro checkpoint ser escrito, uma lista com todos os blocos de buffer modificados é criada. A informação último

checkpoint é atualizada somente após todos os blocos de buffer, da lista de blocos de buffer modificados, terem

sido escritos no disco. O protocolo de precedência de escrita do log deve ser seguido quando os blocos de buffer

são enviados para saída.

Observe que, em nosso esquema, o registro de log lógico é usado somente para propósitos de undo, ao

passo que o registro de log físico é usado para propósitos redo (refazer) e undo (inutilizar). Há esquemas de

recuperação que usam registro de log lógico para propósitos redo. Entretanto, tais esquemas não podem ser

usados com fuzzy checkpoint e, portanto, não são muito empregados.