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.
| onde está o dado | tempo real | em escala humana |
| Cache L1 | 1 ns | 1 s |
| Memória principal | 100 ns | 2 min |
| SSD NVMe | 100 µs | 27 h |
| Disco magnético | 10 ms | 116 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.
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.
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?
Sai a página que entrou há mais tempo. O acerto não mexe na fila.
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 é a que foi alterada em memória e ainda não foi gravada no disco.
kill -9O cache pode gravar no disco uma página suja de transação que ainda não fez COMMIT.
Exige saber desfazer
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 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.
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.
O dado não chega ao disco, e a perda aparece numa consulta muito depois.
A alteração morre ali, e o bit de sujeira não significa nada. Devolva a mesma referência.
soltaO cache enche de páginas fixadas, e a primeira expulsão falha.
Duas cópias da mesma página, com valores diferentes.
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.
NOTES.md com a decisão mais difícilNo NOTES.md, digam qual capacidade vocês escolheram e por quê.
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.
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]
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()