Banco de Dados II — 2026.2

Módulo 2
Cache de páginas

👨‍🏫 Prof. Gustavo Pinto 🏛️ UFPA 📅 22 de setembro de 2026 🕐 7h30 – 9h

Os dois números que vocês trouxeram

Dez mil registros, varredura completa. Quantas páginas tem o arquivo, e quantas leituras a varredura fez?

páginas do arquivo                  21   // 1 de metadados, 20 de dados
leituras, versão com iterador       20
leituras, versão que relê a página  10000

E na segunda varredura, dentro do mesmo processo, os dois números se repetem inteiros. O programa já tinha lido tudo, e leu tudo de novo.

É esse desperdício que o M2 apaga. Hoje a gente entende o mecanismo, e quinta vocês implementam.

Por que uma leitura a mais importa tanto

onde está o dadotempo realem escala humana
Cache L11 ns1 s
Memória principal100 ns2 min
SSD NVMe100 µs27 h
Disco magnético10 ms116 dias

A coluna da direita é a mesma escala, esticada como se o acesso a L1 levasse um segundo. Buscar uma página no SSD é um dia inteiro de espera, e no disco magnético são quatro meses.

O trabalho de um SGBD é, quase todo, evitar a terceira e a quarta linha desta tabela.

O que é o cache de páginas

Uma região de memória que guarda cópias de páginas do arquivo, e um conjunto de regras para decidir quem entra, quem fica e quem sai.

O nome que aparece na literatura é buffer pool. É a estrutura mais quente de qualquer banco de dados.

Por que não deixar isso para o sistema operacional

O sistema operacional já tem um cache de páginas. Bastaria usar mmap e deixar ele resolver. Nenhum banco sério faz isso, por quatro motivos.

Leitura curta e direta sobre isso: Crotty, Leis e Pavlo, Are You Sure You Want to Use MMAP in Your DBMS?, CIDR 2022.

As quatro estruturas

Frames

Posições de memória do tamanho de uma página. A capacidade do cache é o número de frames.

Tabela de páginas

Mapeia número da página para o frame em que ela está. É por aqui que o cache responde se acertou.

Indexada pela página, nunca pelo frame

Bit de sujeira

Diz se a cópia em memória difere da cópia no disco. Página limpa pode sair sem custo.

Contador de fixação

Quantos usuários estão com a página aberta neste instante. Página fixada não pode ser expulsa.

Três dicionários e um contador. O M2 é o módulo mais curto do semestre, e o que mais quebra os outros quando sai errado.

O ciclo de vida de uma página

buf = cache.fixa(7)        // achou no cache, ou leu do disco e fixou
   ... usa o buffer ...     // enquanto está fixada, ninguém a expulsa
cache.solta(7, sujou=True)  // devolve, e avisa se alterou

Para que serve a fixação

O iterador do M5 está no meio da página 7. O cache enche e escolhe expulsar a 7. O que acontece?

Página fixada é página intocável. Se todas estiverem fixadas e o cache encher, a resposta certa é falhar alto, e não expulsar assim mesmo.

Página suja

Página suja é a que foi alterada em memória e ainda não foi gravada no disco.

Dois nomes que voltam em novembro

Steal

O cache pode expulsar, e gravar no disco, uma página suja de uma transação que ainda não fez COMMIT.

Ganha memória, e exige saber desfazer

No-force

O COMMIT não obriga a gravar todas as páginas alteradas pela transação.

Ganha tempo, e exige saber refazer

O minidb vai ser steal e no-force, como quase todo banco real. As duas escolhas são feitas aqui, no M2, e a conta chega no M7: é exatamente por causa delas que o log precisa existir.

Quando o cache enche

FIFO

Sai quem entrou primeiro. Simples, e ignora completamente o uso.

LRU

Sai quem foi usado há mais tempo. Aposta que o passado recente prevê o futuro próximo.

É o que o M2 pede

CLOCK

Aproximação barata do LRU: um bit de referência por frame e um ponteiro que gira. Sem lista para reordenar a cada acesso.

CLOCK:  ponteiro passa pelo frame
        bit de referência 1  →  zera o bit e segue  // ganhou uma segunda chance
        bit de referência 0  →  expulsa este        // e o ponteiro para aqui

Onde o LRU erra feio

Cache de 3 frames. A tabela tem 4 páginas, e a consulta varre a tabela inteira, duas vezes.

acessos   1  2  3  4  1  2  3  4
LRU       F  F  F  F  F  F  F  F      // oito faltas em oito acessos
  • Cada página é expulsa exatamente antes de ser pedida de novo
  • Um frame a mais, e a taxa de acerto salta de 0 para 50 por cento

O nome disso é inundação por varredura. É o motivo de bancos reais usarem LRU-K, 2Q ou CLOCK com proteção de varredura.

No minidb, LRU basta. Mas eu quero que vocês saibam onde ele quebra, porque isso vai aparecer na medição de vocês.

E vai aparecer nos números de vocês

Vinte páginas de dados, varredura completa rodada duas vezes no mesmo processo. Qual a taxa de acerto da segunda varredura?

cache com 16 frames   →   0 acertos,  20 faltas     // 0 por cento
cache com 20 frames   →  20 acertos,   0 faltas     // 100 por cento
  • Quatro frames de diferença, e o resultado vai de nada a tudo
  • Com 16 frames, o LRU expulsa a página 1 para ler a 17, e a varredura seguinte começa pedindo a página 1
  • Um cache menor que a tabela, com LRU e varredura, pode não acertar nenhuma vez

O esqueleto do M2

class Cache:
    def __init__(self, pager, capacidade=16):
        self.pager, self.cap = pager, capacidade
        self.frames = {}              # pagina -> buffer
        self.suja   = set()
        self.fixada = {}              # pagina -> contador
        self.uso    = OrderedDict()   # ordem de uso, mais antigo primeiro
        self.acertos = self.faltas = 0

    def fixa(self, n):
        if n in self.frames:
            self.acertos += 1
            self.uso.move_to_end(n)
        else:
            self.faltas += 1
            if len(self.frames) == self.cap:
                self._expulsa()
            self.frames[n] = self.pager.le(n)
            self.uso[n] = True
        self.fixada[n] = self.fixada.get(n, 0) + 1
        return self.frames[n]

O esqueleto do M2

    def solta(self, n, sujou=False):
        if sujou:
            self.suja.add(n)
        self.fixada[n] -= 1

    def _expulsa(self):
        for n in list(self.uso):                  # do mais antigo para o mais novo
            if self.fixada.get(n, 0) == 0:
                if n in self.suja:
                    self.pager.escreve(n, self.frames[n])
                    self.suja.discard(n)
                del self.frames[n], self.uso[n]
                return
        raise RuntimeError("todas as páginas estão fixadas")

    def descarrega(self):                         # no fim do programa, e no commit do M6
        for n in list(self.suja):
            self.pager.escreve(n, self.frames[n])
        self.suja.clear()
        self.pager.sync()

O que muda no código que já existe

antes    Tabela  →  Pagina  →  Pager  →  disco
depois   Tabela  →  Pagina  →  Cache  →  Pager  →  disco

Esse é o primeiro módulo que mexe em código já entregue. Todo módulo daqui para frente vai ser assim.

Armadilhas conhecidas

Expulsar suja sem gravar

O dado some em silêncio, e só aparece em uma consulta muito depois. Grave antes de tirar o frame.

Devolver uma cópia

Se fixa devolve cópia do buffer, a alteração morre ali e o bit de sujeira não significa nada. Devolva a mesma referência.

Esquecer o solta

O cache enche de páginas fixadas, e a primeira expulsão falha. Pares de fixa e solta, sempre.

Deixar o Pager acessível

Uma única chamada direta ao Pager, e existem duas cópias da mesma página em memória, com valores diferentes.

Nenhuma dessas quebra os testes de aceitação de forma óbvia. Todas quebram o módulo seguinte.

Como saber que está certo

Usem capacidade pequena nos testes, três ou quatro frames. Com capacidade grande, a expulsão nunca acontece e o teste não prova nada.

O M2 e as próximas duas aulas

Quinta, 24/09

laboratório
  • Tabela de páginas, frames e o par fixa e solta
  • LRU e a expulsão
  • Contadores de acerto e falta

Terça, 29/09

laboratório
  • Bit de sujeira e escrita na expulsão
  • Integração com o M1, e o Pager fechado
  • Medição antes e depois

29/09, 23h59

entrega do M2
  • Pull request com os testes passando
  • NOTES.md com a decisão mais difícil
  • Commits que mostrem o caminho

No NOTES.md deste módulo, digam qual capacidade vocês escolheram e por quê.

Dúvidas?

Antes de sair, o exercício de hoje

📧 gpinto@ufpa.br

Exercício de hoje

Cache com 3 frames e política LRU. A sequência de acessos é 1, 2, 3, 1, 4, 2, 5, 1, 2, 3.

Tragam a conta resolvida no papel para quinta. O código do M2 fica bem mais curto depois que essa sequência estiver clara.