Acelerar a inserção em massa usando o ORM do Django?

Estou planejando fazer o upload de um bilhão de registros de ~ 750 arquivos (cada ~ 250MB) para um database usando o ORM do django. Atualmente, cada arquivo leva ~ 20min para processar, e eu queria saber se há alguma maneira de acelerar esse processo.

Tomei as seguintes medidas:

  • Use @ transaction.commit_manually e confirme uma vez a cada 5000 registros
  • Defina DEBUG = False para que o django não acumule todos os comandos sql na memory
  • O loop que executa os registros em um único arquivo é completamente contido em uma única function (minimizar as alterações na pilha)
  • Refreate-se de atingir o database para consultas (usou um hash local de objects já no database em vez de usar get_or_create )
  • Set force_insert = True no save () na esperança de salvar django alguma lógica
  • Explicitamente definir o id na esperança de salvar django alguma lógica
  • Minimização e otimização geral de código

O que mais posso fazer para acelerar as coisas? Aqui estão alguns dos meus pensamentos:

  • Use algum tipo de compilador ou versão Python que seja mais rápido (Psyco?)
  • Substituir o ORM e usar o SQL diretamente
  • Use algum código de terceiros que seja melhor ( 1 , 2 )
  • Implore a comunidade do django para criar uma function bulk_insert

Quaisquer indicações sobre estes itens ou qualquer outra ideia seriam bem vindas 🙂

Isso não é específico para o Django ORM, mas recentemente eu tive que inserir em massa> 60 milhões de linhas de 8 colunas de dados de mais de 2000 arquivos em um database sqlite3. E aprendi que as três coisas a seguir reduziram o tempo de inserção de mais de 48 horas para ~ 1 hora:

  1. aumentar a configuração do tamanho do cache do seu database para usar mais RAM (os padrões sempre muito pequenos, usei 3 GB); em sqlite, isso é feito por PRAGMA cache_size = n_of_pages;

  2. fazer journalling em RAM em vez de disco (isso causa um pequeno problema se o sistema falhar, mas algo que considero insignificante, já que você já tem os dados de origem no disco); em sqlite isso é feito por PRAGMA journal_mode = MEMORY

  3. por último e talvez mais importante: não construa índice durante a inserção. Isso também significa não declarar UNIQUE ou outra restrição que possa fazer com que o DB crie um índice. Construa o índice somente depois que você terminar de inserir.

Como alguém mencionou anteriormente, você também deve usar cursor.executemany () (ou apenas o atalho conn.executemany ()). Para usá-lo, faça:

 cursor.executemany('INSERT INTO mytable (field1, field2, field3) VALUES (?, ?, ?)', iterable_data) 

Os iterable_data podem ser uma lista ou algo parecido, ou até mesmo um leitor de arquivos aberto.

cursor.executemany() para o DB-API e use cursor.executemany() . Veja o PEP 249 para detalhes.

Fiz alguns testes no Django 1.10 / Postgresql 9.4 / Pandas 0.19.0 e obtive os seguintes timings:

  • Insira 3000 linhas individualmente e obtenha IDs de objects populados usando o Django ORM: 3200ms
  • Inserir 3000 linhas com Pandas DataFrame.to_sql() e não obter IDs: 774ms
  • Insira 3000 linhas com o gerenciador do Django .bulk_create(Model(**df.to_records())) e não obtenha IDs: 574ms
  • Insira 3000 linhas com to_csv para o buffer StringIO e COPY ( cur.copy_from() ) e não obtenha IDs: 118ms
  • Insira 3000 linhas com to_csv e COPY e obtenha IDs através de simples SELECT WHERE ID > [max ID before insert] (provavelmente não thread safe, a menos que COPY mantenha um bloqueio na tabela evitando inserções simultâneas?): 201ms
 def bulk_to_sql(df, columns, model_cls): """ Inserting 3000 takes 774ms avg """ engine = ExcelImportProcessor._get_sqlalchemy_engine() df[columns].to_sql(model_cls._meta.db_table, con=engine, if_exists='append', index=False) def bulk_via_csv(df, columns, model_cls): """ Inserting 3000 takes 118ms avg """ engine = ExcelImportProcessor._get_sqlalchemy_engine() connection = engine.raw_connection() cursor = connection.cursor() output = StringIO() df[columns].to_csv(output, sep='\t', header=False, index=False) output.seek(0) contents = output.getvalue() cur = connection.cursor() cur.copy_from(output, model_cls._meta.db_table, null="", columns=columns) connection.commit() cur.close() 

As statistics de desempenho foram todas obtidas em uma tabela que já continha 3.000 linhas em execução no OS X (i7 SSD 16GB), em média, dez execuções usando timeit .

Eu recebo minhas chaves primárias inseridas atribuindo um id de lote de importação e classificando por chave primária, embora eu não esteja 100% certo que as chaves primárias sempre serão atribuídas na ordem em que as linhas são serializadas para o comando COPY .

Há também um snippet de inserção em massa em http://djangosnippets.org/snippets/446/ .

Isso dá a um comando de inserção pares de múltiplos valores (INSERT INTO x (val1, val2) VALORES (1,2), (3,4) –etc etc). Isso deve melhorar muito o desempenho.

Ele também parece estar muito documentado, o que é sempre uma vantagem.

Além disso, se você quiser algo rápido e simples, você pode tentar isso: http://djangosnippets.org/snippets/2362/ . É um gerenciador simples que usei em um projeto.

O outro trecho não era tão simples e estava realmente focado em inserções em massa para relacionamentos. Esta é apenas uma inserção em massa simples e apenas usa a mesma consulta INSERT.