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.
python
mysql
sqlalchemy
Paulo
fonte
fonte
Respostas:
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
Query
funciona é 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,
Query
oferece uma opção para alterar esse comportamento por meioyield_per()
. Essa chamada fará com que oQuery
produza 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.
fonte
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 umwhile
loop 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
ostream_results
trabalha com Postgres eopyscopg2
adaptador, 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.
fonte
pymysql
), esta deve ser a resposta aceita IMHO.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
fonte
.all()
é necessário. Percebo que a velocidade melhorou muito após a 1ª ligação..all()
a variável things é uma consulta que não suporta len ()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
fonte
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.
fonte
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.
fonte