PostgreSQL: Configuração para importação massiva - 160.000.000 de registros em minutos

Por em

postgresql_data_vacuum

O Governo brasileiro mantém uma publicação mensal dos dados do CNPJ - cadastro nacional de pessoa jurídica. Para novembro de 2024, são 35 arquivos compactados em format ".zip" (link para download no final do texto)

Um dos nossos clientes, nos trouxe a demanda para melhorar sua aplicaçao que mantém um banco de dados local atualziado com os dados do CNPJ.

O banco do nosso cliente atende outras aplicações internas e deveria ser mantido em um servidor Microsoft SQL Server. Fizemos a manutenção do código com algumas melhorias e houve algum ganho no tempo de atualização dos dados - mas ainda levava algumas horas. Não exatamente um problema para um dado atualizado a cada 4 ou 5 semanas.

A dificuldade da importação tanto pelo tamanho dos arquivos quanto peloo tempo de importação, foi pauta de algumas discussões dentro do time da Necto. Então decidimos fazer outros testes mesmo já tendo entergue uma solução para o nosso cliente.

A necto há anos usa o PostgreSQL como banco de dados, é nossa primeira escolha sempre que possível. É um banco de dados robusto, de código aberto, e que não perde em nada para outros SGDBs do mercado. E resolvemos fazer alguns testes com o PostgreSQL para importar os dados do CNPJ.

O comando COPY do PostgreSQL

Os dados do CNPJ são disponibilizados em arquivos CSV compactados em formato ZIP. Hoje (2024) são 35 arquivos ZIP, que cotem algo em torno de 170.000.000 de registros - CNPJs únicos em torno de ~60.000.000.

Com os dados em mãos, usamos o comando `COPY` do PostgreSQL

COPY moves data between PostgreSQL tables and standard file-system files. COPY TO copies the contents of a table to a file, while COPY FROM copies data from a file to a table (appending the data to whatever is in the table already). COPY TO can also copy the results of a SELECT query.
source: https://www.postgresql.org/docs/16/sql-copy.html
language
sql
original_code
COPY table_name [ ( column_name [, ...] ) ] FROM { 'filename' | PROGRAM 'command' | STDIN } [ [ WITH ] ( option [, ...] ) ] [ WHERE condition ] COPY { table_name [ ( column_name [, ...] ) ] | ( query ) } TO { 'filename' | PROGRAM 'command' | STDOUT } [ [ WITH ] ( option [, ...] ) ]
rendered_code
<div class="codehilite"><pre><span></span><span class="k">COPY</span><span class="w"> </span><span class="k">table_name</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">(</span><span class="w"> </span><span class="k">column_name</span><span class="w"> </span><span class="p">[,</span><span class="w"> </span><span class="p">...]</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="p">]</span> <span class="k">FROM</span><span class="w"> </span><span class="err">{</span><span class="w"> </span><span class="s1">&#39;filename&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">PROGRAM</span><span class="w"> </span><span class="s1">&#39;command&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">STDIN</span><span class="w"> </span><span class="err">}</span> <span class="p">[</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="w"> </span><span class="k">option</span><span class="w"> </span><span class="p">[,</span><span class="w"> </span><span class="p">...]</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="p">]</span> <span class="p">[</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">condition</span><span class="w"> </span><span class="p">]</span> <span class="k">COPY</span><span class="w"> </span><span class="err">{</span><span class="w"> </span><span class="k">table_name</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">(</span><span class="w"> </span><span class="k">column_name</span><span class="w"> </span><span class="p">[,</span><span class="w"> </span><span class="p">...]</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="p">(</span><span class="w"> </span><span class="n">query</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="err">}</span> <span class="k">TO</span><span class="w"> </span><span class="err">{</span><span class="w"> </span><span class="s1">&#39;filename&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">PROGRAM</span><span class="w"> </span><span class="s1">&#39;command&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">STDOUT</span><span class="w"> </span><span class="err">}</span> <span class="p">[</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="w"> </span><span class="k">option</span><span class="w"> </span><span class="p">[,</span><span class="w"> </span><span class="p">...]</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="p">]</span> </pre></div>

Algumas configurações foram feitas na tentativa de melhorar a performance da importação.

São muitas possibilidade de configuração do Postgresql, algumas devem ser feitas na inicialização do PostgreSQL outras podem ser feitas em tempo de execução.

Veja aqui uma lista completa dos parâmetros que são possíveis alterar para execução do PostgreSQL (https://www.postgresql.org/docs/16/runtime-config.html).

No caso de importações massivas de atenção a configuração do "WAL" (Write ahed log - https://www.postgresql.org/docs/16/wal-configuration.html)

language
text
original_code
PostgreSQL Config Settings autovacuum.......................off checkpoint_completion_target............0.9 checkpoint_timeout...............900 s constraint_exclusion.......partition default_statistics_target........100 effective_cache_size..........524288 kB effective_io_concurrency...........1 fsync............................off full_page_writes.................off huge_pages.......................try maintenance_work_mem...........65536 kB max_wal_size....................5120 MB min_wal_size....................1024 MB random_page_cost...................4 shared_buffers.................98304 kB synchronous_commit................on wal_buffers.....................2048 kB work_mem........................4096 kB
rendered_code
<div class="codehilite"><pre><span></span>PostgreSQL Config Settings autovacuum.......................off checkpoint_completion_target............0.9 checkpoint_timeout...............900 s constraint_exclusion.......partition default_statistics_target........100 effective_cache_size..........524288 kB effective_io_concurrency...........1 fsync............................off full_page_writes.................off huge_pages.......................try maintenance_work_mem...........65536 kB max_wal_size....................5120 MB min_wal_size....................1024 MB random_page_cost...................4 shared_buffers.................98304 kB synchronous_commit................on wal_buffers.....................2048 kB work_mem........................4096 kB </pre></div>

Estes parâmetros podem ser definidos no arquivo de configuração do PostgreSQL. Como estamos usando uma imagem docker, passamos como parâmetro no `command` do docker-compose .

language
yaml
original_code
# docker compose file # PostgreSQL config settings before start. postgis16: image: postgis/postgis:16-3.5-alpine mem_limit: 2.5GB # limit the memory used by PostgreSQL container mem_reservation: 1.5GB shm_size: '1gb' # its needed to build indexes and Primary Keys. ... # other volumes: - data-files:/usr/src/data_files/ container_name: postgis16 command: - "postgres" - "-c" - "autovacuum=OFF" - "-c" - "min_wal_size=1GB" - "-c" - "max_wal_size=5GB" - "-c" - "shared_buffers=768MB" - "-c" - "fsync=OFF" - "-c" - "full_page_writes=off" - "-c" - "checkpoint_timeout=15min" # min must be lowercase
rendered_code
<div class="codehilite"><pre><span></span><span class="c1"># docker compose file</span> <span class="c1"># PostgreSQL config settings before start.</span> <span class="nt">postgis16</span><span class="p">:</span> <span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">postgis/postgis:16-3.5-alpine</span> <span class="w"> </span><span class="nt">mem_limit</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">2.5GB</span><span class="w"> </span><span class="c1"># limit the memory used by PostgreSQL container</span> <span class="w"> </span><span class="nt">mem_reservation</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">1.5GB</span> <span class="w"> </span><span class="nt">shm_size</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;1gb&#39;</span><span class="w"> </span><span class="c1"># its needed to build indexes and Primary Keys.</span> <span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">...</span><span class="w"> </span><span class="c1"># other</span> <span class="w"> </span><span class="nt">volumes</span><span class="p">:</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">data-files:/usr/src/data_files/</span> <span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">postgis16</span> <span class="w"> </span><span class="nt">command</span><span class="p">:</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;postgres&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;-c&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;autovacuum=OFF&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;-c&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;min_wal_size=1GB&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;-c&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;max_wal_size=5GB&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;-c&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;shared_buffers=768MB&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;-c&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;fsync=OFF&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;-c&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;full_page_writes=off&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;-c&quot;</span> <span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">&quot;checkpoint_timeout=15min&quot;</span><span class="w"> </span><span class="c1"># min must be lowercase</span> </pre></div>

Outros parâmetros, são alterados em tempo de execução ...

language
python
original_code
# python code, before start import # PostgreSQL config settings runtime cur.execute("ALTER DATABASE cnpj SET constraint_exclusion='OFF';") cur.execute("ALTER DATABASE cnpj SET maintenance_work_mem='512MB';") cur.execute("ALTER DATABASE cnpj SET synchronous_commit='OFF';") cur.execute("ALTER DATABASE cnpj SET work_mem='4GB';") cur.execute("SELECT pg_reload_conf();")
rendered_code
<div class="codehilite"><pre><span></span><span class="c1"># python code, before start import</span> <span class="c1"># PostgreSQL config settings runtime</span> <span class="n">cur</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&quot;ALTER DATABASE cnpj SET constraint_exclusion=&#39;OFF&#39;;&quot;</span><span class="p">)</span> <span class="n">cur</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&quot;ALTER DATABASE cnpj SET maintenance_work_mem=&#39;512MB&#39;;&quot;</span><span class="p">)</span> <span class="n">cur</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&quot;ALTER DATABASE cnpj SET synchronous_commit=&#39;OFF&#39;;&quot;</span><span class="p">)</span> <span class="n">cur</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&quot;ALTER DATABASE cnpj SET work_mem=&#39;4GB&#39;;&quot;</span><span class="p">)</span> <span class="n">cur</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&quot;SELECT pg_reload_conf();&quot;</span><span class="p">)</span> </pre></div>

e ainda, alguns outros logo antes da execução dos comandos SQL como o `INSERT INTO`.

language
sql
original_code
ALTER TABLE cnae DISABLE TRIGGER ALL;
rendered_code
<div class="codehilite"><pre><span></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">cnae</span><span class="w"> </span><span class="n">DISABLE</span><span class="w"> </span><span class="k">TRIGGER</span><span class="w"> </span><span class="k">ALL</span><span class="p">;</span> </pre></div>
Results

Abaixo os resultados e o tempo de importação em 2 maquinas distintas.

As especificações estão abaixo.

Database Created
Table Number of Records
cnae 1,358
motivo 60
municipio 5,570
pais 254
natureza_juridica 89
qualificacao 67
simples 41,093,712
empresa 60,294,472
estabelecimento 63,333,636
socio 24,933,508
Setup
CPU Memory Tempo (min)
3.1 GHz Dual-Core Intel Core i7 16 GB 1867 MHz DDR3 39
2.60 GHz Intel i5 * 12 32 GB 3200 MHz DDR4 21
Python Code and Data Corrections

Tivemos problemas durante a criação dos índices, e tivemos que altera a "shared memory" do docker - `shm_size: '1gb'` - veja no arquivo "docker-compose" no repositório.

O Python foi usado principalmente como um orquestrador, mas também faz algumas pequenas correções no dado e nome dos arquivos. O primeiro é devido ao um registro de CNPJ duplicado, o que impedia a criação da PK. O o outro é que alguns nomes de arquivos originais tem um caracter `$` no meio do nome.

Ainda há um problema de codificação em um dos arquivos CSV, que perdura entre as verões publicadas, então o Python executa um script Perl - `perl -i -pe 's/\\x00//g' {fle}` para corrigir o problema em tempo de execução.

language
bash
original_code
perl -i -pe ‘s/\\x00//g’ {fle}
rendered_code
<div class="codehilite"><pre><span></span>perl<span class="w"> </span>-i<span class="w"> </span>-pe<span class="w"> </span>‘s/<span class="se">\\</span>x00//g’<span class="w"> </span><span class="o">{</span>fle<span class="o">}</span> </pre></div>

Escrever em Python nos deu maior maleabilidade para controlar o processo todo. Mas sim, seria possível fazer tudo somente usando PSQL.