Banco de Dados II — 2026.2

Módulo 2
Cache de páginas

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

Dez mil registros, guardados em 20 páginas.
A varredura faz quantas leituras de disco?

lê a página uma vez, e percorre os 510 registros dela       20
lê a página de novo a cada registro                      10000

Mesma resposta, 500 vezes mais leitura.

A segunda varredura, no mesmo processo, repete os dois números. Nada continuou em memória.

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 estica a escala como se L1 levasse um segundo.

O trabalho de um SGBD é, quase todo, evitar as duas últimas linhas.

Onde o cache entra

tabela     10000 registros, 510 por página

arquivo    [ p0 ][ p1 ][ p2 ][ p3 ][ p4 ]  ...  [ p19 ]      4096 bytes cada
                    |            |
                    v            v
cache      [ frame 0 ][ frame 1 ][ frame 2 ]                 cópias em memória
               p1         p3        livre

tabela de páginas    { 1: 0,  3: 1 }                         pagina -> frame

Na literatura, buffer pool.

Glossário da aula

Fixar e soltar (do inglês, pin e unpin)

A varredura do slide 2: lê os 510 registros de uma página antes de passar para a próxima.

def varre(cache, tabela):
    for n in range(tabela.n_paginas):
        buf = cache.fixa(n)                 ┐  estou usando a página n
        for slot in range(510):             │
            yield le_registro(buf, slot)    │  o cache não pode expulsar a n
        cache.solta(n)                      ┘  terminei, pode expulsar

Solta em todo caminho de saída, inclusive no de erro. Em Python, try/finally.

O iterador acima está parado na página 7, entre o fixa e o solta. Outra consulta pede a página 12, e o cache está cheio. Quem sai?

Quando o cache enche

FIFO

Sai a página que entrou há mais tempo. O acerto não mexe na fila.

LRU

Sai a página lida há mais tempo. Cada acerto devolve a página para o fim da fila. É o que o M2 pede.

3 frames, acessos 1, 2, 3, 1, 4
à esquerda fica a página lida há mais tempo, a próxima a sair

acesso   frames      o cache fez
   1     [1]         falta, leu do disco
   2     [1 2]       falta, leu do disco
   3     [1 2 3]     falta, e o cache encheu
   1     [2 3 1]     acerto, sem tocar no disco   <- a 1 foi para o fim da fila
   4     [3 1 4]     falta, expulsou a 2

Com FIFO, o acesso 4 teria expulsado a página 1, que tinha acabado de ser lida.

Onde o LRU não acerta nenhuma vez, e o que os bancos usam no lugar: O'Neil, O'Neil e Weikum, The LRU-K Page Replacement Algorithm for Database Disk Buffering, SIGMOD 1993. dl.acm.org/doi/10.1145/170035.170081

Página suja, steal e no-force

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

Steal

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

Exige saber desfazer

No-force

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

Exige saber refazer

O minidb é steal e no-force. É por isso que o log precisa existir, no M6.

Steal e no-force, na prática

steal      T1 altera a página 5, e ainda não fez COMMIT
           o cache enche, expulsa a 5 e grava no disco
           T1 falha          o disco guarda dado de uma transação que não existiu
           o log precisa saber DESFAZER
no-force   T1 altera a página 5 e faz COMMIT
           a página 5 continua só em memória
           kill -9           o disco não tem o dado de um COMMIT já confirmado
           o log precisa saber REFAZER

Sem steal, a memória fica presa. Sem no-force, todo COMMIT grava. Com os dois, é o log que paga a conta.

Por que não deixar isso para o sistema operacional

O sistema operacional já tem um cache de páginas, e daria para usar mmap. Os bancos em produção não fazem isso.

Crotty, Leis e Pavlo, Are You Sure You Want to Use MMAP in Your DBMS?, CIDR 2022.

Armadilhas conhecidas

Expulsar suja sem gravar

O dado não chega ao disco, e a perda aparece numa consulta muito depois.

Devolver uma cópia

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.

Ler o arquivo sem passar pelo cache

Duas cópias da mesma página, com valores diferentes.

Como saber que está certo

Testem com três ou quatro frames. Com capacidade grande, a expulsão nunca acontece.

Para saber mais. Com todas as páginas fixadas, a próxima falta deve levantar erro em vez de expulsar. E o cache conta acertos e faltas, cuja soma é igual ao número de chamadas a fixa. Os dois estão no esqueleto do apêndice.

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
  • 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, digam qual capacidade vocês escolheram e por quê.

Exercício de hoje

Cache com 3 frames e política LRU. Acessos: 1, 2, 3, 1, 4, 2, 5, 1, 2, 3.

Tragam resolvido no papel para quinta. Dúvidas, gpinto@ufpa.br.

Apêndice

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]
Apêndice

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()