iterador / gerador integrado com eficiência de memória SqlAlchemy?

91

Eu tenho uma tabela MySQL de registro de ~ 10M com a qual faço interface usando SqlAlchemy. Descobri que as consultas em grandes subconjuntos desta tabela consomem muita memória, embora eu achasse que estava usando um gerador embutido que buscava de forma inteligente pedaços pequenos do conjunto de dados:

for thing in session.query(Things):
    analyze(thing)

Para evitar isso, acho que tenho que construir meu próprio iterador que divide em pedaços:

lastThingID = None
while True:
    things = query.filter(Thing.id < lastThingID).limit(querySize).all()
    if not rows or len(rows) == 0: 
        break
    for thing in things:
        lastThingID = row.id
        analyze(thing)

Isso é normal ou há algo que estou perdendo em relação aos geradores integrados do SA?

A resposta a esta pergunta parece indicar que o consumo de memória não é esperado.

Paulo
fonte
Tenho algo muito semelhante, exceto que produz "coisa". Funciona melhor do que todas as outras soluções
iElectric
2
Não é Thing.id> lastThingID? E o que são "linhas"?
sinérgico de

Respostas:

118

A maioria das implementações DBAPI armazena totalmente as linhas à medida que são buscadas - então, normalmente, antes que o SQLAlchemy ORM consiga um resultado, todo o conjunto de resultados está na memória.

Mas então, a maneira como Queryfunciona é que ele carrega totalmente o conjunto de resultados fornecido por padrão antes de retornar seus objetos. A lógica aqui diz respeito a consultas que são mais do que simples instruções SELECT. Por exemplo, em junções a outras tabelas que podem retornar a mesma identidade de objeto várias vezes em um conjunto de resultados (comum com carregamento antecipado), o conjunto completo de linhas precisa estar na memória para que os resultados corretos possam ser retornados de outra forma, coleções e tal pode ser apenas parcialmente preenchido.

Portanto, Queryoferece uma opção para alterar esse comportamento por meio yield_per(). Essa chamada fará com que o Queryproduza linhas em lotes, onde você fornece o tamanho do lote. Como afirma a documentação, isso só é apropriado se você não estiver fazendo nenhum tipo de carregamento antecipado de coleções, portanto, basicamente, se você realmente souber o que está fazendo. Além disso, se as linhas de pré-buffer de DBAPI subjacentes, ainda haverá essa sobrecarga de memória, de modo que a abordagem é apenas um pouco melhor dimensionada do que não usá-la.

Eu quase nunca uso yield_per(); em vez disso, uso uma versão melhor da abordagem LIMIT que você sugeriu acima, usando funções de janela. LIMIT e OFFSET têm um grande problema de que valores muito grandes de OFFSET fazem com que a consulta fique cada vez mais lenta, pois um OFFSET de N faz com que ela percorra N linhas - é como fazer a mesma consulta cinquenta vezes em vez de uma, cada vez lendo um número cada vez maior de linhas. Com uma abordagem de função de janela, eu pré-busco um conjunto de valores de "janela" que se referem a partes da tabela que desejo selecionar. Em seguida, emito instruções SELECT individuais, cada uma puxando de uma dessas janelas por vez.

A abordagem da função de janela está no wiki e eu a uso com grande sucesso.

Observe também: nem todos os bancos de dados suportam funções de janela; você precisa do Postgresql, Oracle ou SQL Server. IMHO usando pelo menos Postgresql definitivamente vale a pena - se você estiver usando um banco de dados relacional, você também pode usar o melhor.

zzzeek
fonte
Você menciona que Query instancia tudo para comparar identidades. Isso poderia ser evitado classificando na chave primária e apenas comparando resultados consecutivos?
Tobu
o problema é se você gerar uma instância com identidade X, o aplicativo a obtém e, em seguida, toma decisões com base nessa entidade e talvez até a modifique. Mais tarde, talvez (na verdade geralmente), mesmo na próxima linha, a mesma identidade volta no resultado, talvez para adicionar mais conteúdo às suas coleções. O aplicativo, portanto, recebeu o objeto em um estado incompleto. a classificação não ajuda aqui porque o maior problema é o funcionamento do carregamento antecipado - o carregamento "unido" e "subconsulta" têm problemas diferentes.
zzzeek
Eu entendi a coisa "próxima linha atualiza as coleções", caso em que você só precisa olhar adiante por uma linha do banco de dados para saber quando as coleções estão completas. A implementação do carregamento antecipado teria que cooperar com a classificação, de forma que as atualizações da coleção sempre fossem feitas nas linhas adjacentes.
Tobu
a opção yield_per () está sempre disponível para quando você tiver certeza de que a consulta que está emitindo é compatível com a entrega de conjuntos de resultados parciais. Passei uma maratona de vários dias de sessão tentando habilitar esse comportamento em todos os casos, sempre houve obscuros, ou seja, até que seu programa use um deles, arestas que falhou. Em particular, não se pode presumir confiar no pedido. Como sempre, sou bem-vindo a contribuições reais de código.
zzzeek
1
Como estou usando o postgres, parece que é possível usar a transação somente leitura de leitura repetida e executar todas as consultas em janela nessa transação.
schatten
25

Não sou um especialista em banco de dados, mas ao usar o SQLAlchemy como uma camada de abstração simples do Python (ou seja, não usar o objeto ORM Query), descobri uma solução satisfatória para consultar uma tabela de 300 milhões de linhas sem explodir o uso de memória ...

Aqui está um exemplo fictício:

from sqlalchemy import create_engine, select

conn = create_engine("DB URL...").connect()
q = select([huge_table])

proxy = conn.execution_options(stream_results=True).execute(q)

Em seguida, uso o fetchmany()método SQLAlchemy para iterar os resultados em um whileloop infinito :

while 'batch not empty':  # equivalent of 'while True', but clearer
    batch = proxy.fetchmany(100000)  # 100,000 rows at a time

    if not batch:
        break

    for row in batch:
        # Do your stuff here...

proxy.close()

Este método me permitiu fazer todo tipo de agregação de dados sem qualquer sobrecarga de memória perigosa.

NOTE o stream_resultstrabalha com Postgres eo pyscopg2adaptador, mas eu acho que não vai trabalhar com qualquer DBAPI, nem com qualquer driver de banco de dados ...

Há um caso de uso interessante nesta postagem do blog que inspirou meu método acima.

edouardtheron
fonte
1
Se alguém estiver trabalhando em postgres ou mysql (com pymysql), esta deve ser a resposta aceita IMHO.
Yuki Inoue de
1
Salvou minha vida, estava vendo minhas consultas cada vez mais lentas. Eu instrumentei o acima em pyodbc (de sql server para postgres) e está funcionando como um sonho.
Ed Baker
Esta foi para mim a melhor abordagem. Como estou usando o ORM, precisei compilar o SQL para o meu dialeto (Postgres) e executar diretamente da conexão (não da sessão), conforme mostrado acima. O "como fazer" de compilação encontrei nesta outra questão stackoverflow.com/questions/4617291 . Melhorar na velocidade era grande. Mudar de JOINS para SUBQUERIES também foi um grande aumento no desempenho. Também recomendo usar sqlalchemy_mixins, usar o smart_query ajudou muito a construir a consulta mais eficiente. github.com/absent1706/sqlalchemy-mixins
Gustavo Gonçalves
14

Estive pesquisando sobre travessia / paginação eficiente com SQLAlchemy e gostaria de atualizar esta resposta.

Acho que você pode usar a chamada de slice para limitar adequadamente o escopo de uma consulta e pode reutilizá-la com eficiência.

Exemplo:

window_size = 10  # or whatever limit you like
window_idx = 0
while True:
    start,stop = window_size*window_idx, window_size*(window_idx+1)
    things = query.slice(start, stop).all()
    if things is None:
        break
    for thing in things:
        analyze(thing)
    if len(things) < window_size:
        break
    window_idx += 1
Joel
fonte
Isso parece muito simples e rápido. Não tenho certeza se .all()é necessário. Percebo que a velocidade melhorou muito após a 1ª ligação.
hamx0r
@ hamx0r Sei que este é um comentário antigo, então deixo-o para a posteridade. Sem .all()a variável things é uma consulta que não suporta len ()
David
9

No espírito da resposta de Joel, eu uso o seguinte:

WINDOW_SIZE = 1000
def qgen(query):
    start = 0
    while True:
        stop = start + WINDOW_SIZE
        things = query.slice(start, stop).all()
        if len(things) == 0:
            break
        for thing in things:
            yield thing
        start += WINDOW_SIZE
Pietro Battiston
fonte
Things = query.slice (start, stop) .all () retornará [] no final e o loop while nunca será interrompido
Martin Reguly
4

Usar LIMIT / OFFSET é ruim, porque você precisa encontrar todas as colunas {OFFSET} antes, portanto, quanto maior for OFFSET - maior será a solicitação. Usar a consulta em janela para mim também dá resultados ruins em uma mesa grande com grande quantidade de dados (você espera os primeiros resultados por muito tempo, isso não é bom no meu caso para uma resposta da Web fragmentada).

Melhor abordagem fornecida aqui https://stackoverflow.com/a/27169302/450103 . No meu caso, resolvi o problema simplesmente usando o índice no campo datetime e buscando a próxima consulta com datetime> = previous_datetime. Estúpido, porque já usei esse índice em casos diferentes antes, mas pensei que para buscar todos os dados a consulta em janela seria melhor. No meu caso, eu estava errado.

Victor Gavro
fonte
3

AFAIK, a primeira variante ainda obtém todas as tuplas da tabela (com uma consulta SQL), mas cria a apresentação ORM para cada entidade ao iterar. Portanto, é mais eficiente do que construir uma lista de todas as entidades antes de iterar, mas você ainda precisa buscar todos os dados (brutos) na memória.

Portanto, usar LIMIT em mesas grandes parece uma boa ideia para mim.

Pankrat
fonte