Pular para o conteúdo

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.

consulta lavra
busca:
    pedidos(limite: 50):
        numero
        cliente:
            nome

O 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.

dataforge
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)
dataforge
ctx := Lavra.contexto({"banco": conexao})
_ := Lavra.lote(ctx, "clientes", buscar_clientes)

r := Lavra.executar(esq, consulta, contexto := ctx)
out r["extensoes"]["lotes"]
text
{clientes: {chamadas: 1, chaves: 50, economia: 49}}
Vinte idas, ou uma. À esquerda, um pedido por item. À direita, o lote junta as chaves e vai uma vez.

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.

dataforge
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)
consulta lavra
busca:
    usuario(id: 1):
        pedidos(primeiros: 20, depois: "eyJ..."):
            itens:
                numero
            info:
                tem_proxima
                cursor_fim
            total

Com 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:

CamadaVivePara quê
o loteuma consultao mesmo dado pedido várias vezes na mesma resposta
V.cache / memoizeo processoo cálculo caro que muda devagar
cabeçalho HTTPo clientea 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.

dataforge
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.

dataforge
out r["extensoes"]
text
{ms: 12.4, campos: 143, profundidade: 4, complexidade: 260,
 lotes: {clientes: {chamadas: 1, chaves: 50, economia: 49}}}