Desempenho
O lote contra o N+1, paginação por cursor, cache e o custo de uma consulta.
O N+1, em uma frase#
Uma consulta que pede 50 pedidos e, de cada um, o cliente, faz 51 consultas ao banco: uma para os pedidos e uma por cliente. O servidor parece rápido e o banco morre.
busca:
pedidos(limite: 50):
numero
cliente:
nomeO lote, em uma frase#
O resolvedor não busca — ele pede. Os pedidos feitos na mesma volta são juntados num só, e cada um recebe a sua parte.
action buscar_clientes(ids):
yield Banco.varios(ctx["banco"], "clientes", ids)
action cliente_do_pedido(pedido, args, ctx):
yield Lavra.pedir(ctx, "clientes", pedido.cliente_id)
Lavra.campo(esq, "Pedido", "cliente", "Cliente!", resolve := cliente_do_pedido)ctx := Lavra.contexto({"banco": conexao})
_ := Lavra.lote(ctx, "clientes", buscar_clientes)
r := Lavra.executar(esq, consulta, contexto := ctx)
out r["extensoes"]["lotes"]{clientes: {chamadas: 1, chaves: 50, economia: 49}}Cinquenta pedidos, uma ida ao banco. O economia está ali para ser olhado: um lote que devolve o valor certo e mesmo assim consulta cinquenta vezes passaria em qualquer teste que só olhasse o resultado.
Duas decisões do lote#
- Ele vive no contexto, não no módulo. Um lote de processo guardaria o cliente depois que ele mudou, e serviria o valor velho para outra pessoa. O contexto morre com a consulta, que é exatamente a vida útil que um cache de leitura pode ter aqui.
- A ordem da resposta é a ordem do pedido. A função recebe as chaves e devolve os valores na MESMA ordem — ou um vault de chave → valor, que não depende de ordem nenhuma.
Paginação por cursor#
Paginar por posição (pule 20, traga 20) parece mais simples e quebra do jeito mais difícil de ver: se alguém insere uma linha entre a página 1 e a 2, um item desaparece — ele desceu para a posição que já foi lida.
action pedidos_de(usuario, args, ctx):
todos := Banco.pedidos(usuario.id)
yield Lavra.pagina(todos, primeiros := args["primeiros"],
depois := args["depois"])
Lavra.tipo_pagina(esq, "Pedido")
Lavra.campo(esq, "Usuario", "pedidos", "PaginaPedido!",
args := {"primeiros": {"tipo": "Integer", "padrao": 20},
"depois": "String"},
resolve := pedidos_de)busca:
usuario(id: 1):
pedidos(primeiros: 20, depois: "eyJ..."):
itens:
numero
info:
tem_proxima
cursor_fim
totalCom cursor, a página seguinte começa exatamente onde a anterior parou — independentemente do que aconteceu no meio.
Cache#
Há três camadas, e elas não se substituem:
| Camada | Vive | Para quê |
|---|---|---|
| o lote | uma consulta | o mesmo dado pedido várias vezes na mesma resposta |
V.cache / memoize | o processo | o cálculo caro que muda devagar |
| cabeçalho HTTP | o cliente | a resposta inteira, quando ela pode envelhecer |
O custo de uma consulta#
Nem toda consulta custa o mesmo, e o tamanho do texto não diz nada: três linhas pedindo uma lista de mil itens custam mais que trinta linhas de campos escalares.
Lavra.campo(esq, "Usuario", "relatorio", "Relatorio!",
resolve := gerar_relatorio, custo := 50)
Lavra.limites(esq, complexidade := 1000)Um campo que devolve lista multiplica o custo do que vem dentro pelo argumento de limite — e, sem limite declarado, por dez. É o que faz pedidos { itens { produto { … } } } custar caro sem que ninguém precise contar à mão.
out r["extensoes"]{ms: 12.4, campos: 143, profundidade: 4, complexidade: 260,
lotes: {clientes: {chamadas: 1, chaves: 50, economia: 49}}}