O que eu gostaria que alguém tivesse me contado sobre Postgres
(challahscript.com)- A documentação oficial do Postgres é excelente, mas o PDF do Postgres 17 tem 3.200 páginas, o que dificulta para iniciantes aprenderem só pela documentação, antes da prática, tudo sobre modelagem de schemas, comportamento do SQL e armadilhas operacionais
- A menos que haja um motivo específico, os dados devem ser normalizados; a desnormalização, que mantém dados duplicados para melhorar a performance de leitura, exige aceitar o custo de inconsistências e maior complexidade de escrita
- Palavras-chave SQL não diferenciam maiúsculas de minúsculas, mas NULL é mais próximo de “desconhecido”, então compará-lo como o
nullde linguagens comuns pode produzir resultados inesperados - No
psql, só de usar bem pager,\x,.psqlrc,\pset null, autocompletar, comandos com barra invertida e\copy, a legibilidade da saída, a navegação e a exportação para CSV ficam muito mais confortáveis - Índices, locks, transações e JSONB são poderosos, mas desconhecer planos de consulta e restrições operacionais pode levar a queda de performance ou problemas de disponibilidade
Contexto antes da imensa documentação oficial
- A documentação oficial do Postgres, na versão atual 17, tem 3.200 páginas quando impressa como PDF em US Letter; em A4, tem 3.024 páginas
- Há muito conhecimento prático que é útil saber antes de usar Postgres, e parte dele pode se aplicar também a outros DBMS SQL, embora o escopo de aplicação nem sempre seja claro
Normalize os dados por padrão
- Normalização é o processo de remover dados duplicados ou desnecessários de um schema de banco de dados
- Se você armazenar
user_emaildiretamente na tabeladocuments, quando um usuário mudar o email será preciso atualizar todas as linhas de documentos daquele usuário- Em vez disso, cada linha de
documentspode referenciar uma linha de outra tabela, comousers, por meio de uma chave estrangeirauser_id
- Em vez disso, cada linha de
- Não é necessário decorar todas as formas normais, como “1st normal form”, mas o processo comum de normalização pode levar a um schema mais fácil de manter
- Desnormalização é uma abordagem que mantém dados duplicados para leitura rápida, sem recalcular certas informações todas as vezes
- Em um app de turnos de funcionários, em vez de calcular as horas trabalhadas acumuladas no ano somando a duração de todos os turnos a cada vez, é possível calcular e armazenar esse valor periodicamente ou quando as horas de trabalho mudarem
- Esses dados podem ficar dentro do Postgres ou em uma camada de cache como Redis
- A desnormalização quase sempre tem custos; os principais são a possibilidade de inconsistência dos dados e o aumento da complexidade de escrita
Conselhos de “não faça isso” do projeto Postgres
- A wiki oficial do Postgres tem uma lista “Don’t do this”
- Não há problema se você não entender todos os itens; se não entende algum deles, provavelmente também é menos provável que cometa aquele erro
- Vale lembrar especialmente estes conselhos
- Use o tipo
textpara armazenar texto - Use
timestampz/time with time zonepara armazenar timestamps - Nomeie tabelas em snake_case
- Use o tipo
Comportamentos fáceis de confundir em SQL
-
Palavras-chave SQL não precisam estar em maiúsculas
- Palavras-chave SQL não diferenciam maiúsculas de minúsculas
- As consultas abaixo têm o mesmo significado
SELECT * FROM my_table WHERE x = 1 AND y > 2 LIMIT 10; select * from my_table where x = 1 and y > 2 limit 10; SELECT * from my_table WHERE x = 1 and y > 2 LIMIT 10;- Essa característica não é exclusiva do Postgres
-
NULL é diferente de null/nil em linguagens comuns
- O
NULLdo SQL é mais próximo de “desconhecido” do que denullounilem linguagens de programação comuns NULL = NULLnão retornatrue, mas simNULL- Comparações em que um dos lados é
NULLtambém costumam resultar emNULL - Para comparar
NULL, é preciso usar as seguintes operaçõesx IS NULL:truesexforNULLx IS NOT NULL:truesexnão forNULLx IS NOT DISTINCT FROM y: parecido comx = y, mas trataNULLcomo um valor comumx IS DISTINCT FROM y: parecido comx != y/x <> y, mas trataNULLcomo um valor comum
- A cláusula
WHEREsó retorna linhas quando a condição étrueSELECT * FROM users WHERE title != 'manager'não retorna linhas em quetitleéNULL- Isso acontece porque o resultado de
NULL != 'manager'éNULL
COALESCEretorna o primeiro valor nãoNULLentre vários argumentos
COALESCE(NULL, 5, 10) = 5 COALESCE(2, NULL, 9) = 2 COALESCE(NULL, NULL) IS NULL - O
Usando melhor o psql
-
Melhore a legibilidade da saída
- Se a saída fica difícil de ler ao consultar tabelas com muitas colunas ou valores longos, o pager pode estar desativado
- Um pager de terminal permite rolar textos grandes ou tabelas do
psqldentro de uma viewport - Em tabelas com muitas colunas, é possível ativar o expanded mode com
\pset expandedou\x - Para usar isso como padrão, basta adicionar
\xao~/.psqlrcno diretório home
-
Deixe a saída de NULL explícita
- A configuração padrão não deixa claro na saída se um valor é
NULL - No
psql, é possível definir a string exibida paraNULL
\pset null '[NULL]'- Strings Unicode também funcionam; para usar como padrão, adicione o mesmo comando ao
~/.psqlrc
- A configuração padrão não deixa claro na saída se um valor é
-
Use autocompletar e comandos com barra invertida
- O
psqloferece suporte a autocompletar, como um console interativo - Ao digitar parte de uma palavra-chave ou nome de tabela e pressionar Tab, ele pode completar o restante
- Alguns comandos úteis com barra invertida são
\?: lista de todos os shortcuts\d: mostra relations, ou seja, tabelas e sequências, e seus proprietários\d+: adiciona tamanho e alguns metadados ao\d\d table_name: mostra o schema da tabela, tipos das colunas, se aceitam null, valores padrão, índices e restrições de chave estrangeira\e: edita a consulta no editor padrão definido na variável de ambiente$EDITOR\h SQL_KEYWORD: mostra a sintaxe daquela palavra-chave SQL e links para a documentação
- O
-
Exportação para CSV e aliases em SELECT
- Com
\copy, é possível salvar resultados de consultas em CSV
\copy (select * from some_table) to 'my_file.csv' CSV- Para incluir os nomes das colunas na primeira linha, adicione a opção
HEADER
\copy (select * from some_table) to 'my_file.csv' CSV HEADER\copypermite evitar os privilégios elevados necessários para o comandoCOPY, mais padronizado- Colunas de saída de um
SELECTpodem receber aliases comAS
SELECT vendor, COUNT(*) AS number_of_backpacks FROM backpacks GROUP BY vendor ORDER BY number_of_backpacks DESC;- Em
GROUP BYeORDER BY, é possível referenciar o número da coluna que aparece após oSELECT
SELECT vendor, COUNT(*) AS number_of_backpacks FROM backpacks GROUP BY 1 ORDER BY 2 DESC;- Essa forma abreviada é útil, mas é melhor não incluí-la em consultas implantadas em produção
- Com
Índices nem sempre são usados só porque foram adicionados
-
Índices e planos de consulta
- Um índice é uma estrutura de dados que funciona como um diretório de atalhos para encontrar linhas de uma tabela com base em um campo específico
- O tipo de índice mais comum é o B-tree, que funciona para condições de igualdade exatas como
WHERE a = 3e condições de intervalo comoWHERE a > 5 - Não é possível instruir diretamente o Postgres a usar um índice específico
- Com base nas estatísticas que mantém para cada tabela, o Postgres estima se um índice será mais rápido do que uma sequential scan que lê a tabela do início ao fim
- Ao colocar
EXPLAINantes deSELECT ... FROM ..., você pode ver o plano de consulta de como o Postgres pretende executar a consulta - Para ler planos de consulta, podem ajudar o guia de EXPLAIN ANALYZE da thoughtbot, a documentação da pganalyze, a documentação oficial e o explain.depesz.com
-
Tabelas pequenas e índices multicoluna
- Em tabelas com poucas linhas, como um DB local de desenvolvimento, índices podem não ajudar muito
- Com algo como 100 linhas, o Postgres pode decidir que uma sequential scan é mais rápida do que usar um índice
- O Postgres oferece suporte a índices multicoluna
CREATE INDEX CONCURRENTLY ON tbl (a, b);- Condições como
WHERE a = 1 AND b = 2podem ser mais rápidas do que ter índices separados emaeb - Isso acontece porque é possível percorrer uma única B-tree e combinar as condições de busca de forma eficiente
- Um índice
(a, b)torna consultas que filtram apenas poratão rápidas quanto um índice somente ema - Consultas como
WHERE b = 5também podem ficar mais rápidas, mas talvez não de forma ideal- Como o índice é chaveado primeiro por
ae depois porb, é preciso passar por todos os valores deapara encontrar os valores deb
- Como o índice é chaveado primeiro por
- Se você precisa consultar várias combinações de colunas, é comum manter tanto
(a, b)quanto um índice somente emb - Dependendo da necessidade, também é possível depender de índices individuais em
aeb
-
Use text_pattern_ops para prefix match
- Ao armazenar diretórios hierárquicos usando a abordagem de materialized path, talvez seja necessário encontrar todos os descendants que começam com um determinado prefixo
SELECT * FROM directories WHERE path LIKE '/1/2/3/%'- Mesmo criando um índice B-tree padrão na coluna
path, ele pode não ser usado para essa consulta
CREATE INDEX CONCURRENTLY ON directories (path);- Para permitir a ordenação caractere a caractere necessária para prefix match ou pattern match, é preciso especificar uma operator class
CREATE INDEX CONCURRENTLY ON directories (path text_pattern_ops);
Problemas operacionais causados por locks e transações
-
Locks no Postgres
- Um lock ou mutex é um mecanismo que permite que apenas um cliente execute uma operação perigosa por vez
- Em um banco de dados, atualizações de objetos como rows, tables e views precisam ter sucesso por completo ou falhar por completo; para evitar situações em que operações simultâneas tenham sucesso apenas parcial, locks são adquiridos sobre os objetos relacionados
- O Postgres tem vários níveis de locks de tabela, dos menos restritivos aos mais restritivos
ACCESS SHARE:SELECTROW SHARE:SELECT ... FOR UPDATEROW EXCLUSIVE:UPDATE,DELETE,INSERTSHARE UPDATE EXCLUSIVE:CREATE INDEX CONCURRENTLYSHARE:CREATE INDEX, semCONCURRENTLYACCESS EXCLUSIVE: muitas formas deALTER TABLE,ALTER INDEX
- Em uma mesma tabela, as seguintes ações podem acontecer ou precisar esperar
UPDATEduranteSELECT: possívelUPDATEduranteCREATE INDEX CONCURRENTLY: possívelSELECTduranteCREATE INDEX: possívelSELECTduranteALTER TABLE: geralmente esperaALTER TABLEduranteSELECT: geralmente espera
- Algumas formas de
ALTER TABLEpodem exigir locks mais fracos; as informações completas estão na documentação oficial sobre locks explícitos e no guia de conflitos de lock por operação
-
ALTER TABLE lento e fila de locks
- Se um
ALTER TABLEdemorar muito, atéSELECTs que leem a mesma tabela podem ser bloqueados - Se for uma tabela central como
users, referenciada por todas as requisições de um app web, as requisições podem ficar esperando, dar timeout e retornar 503 - Causas comuns de um
ALTER TABLElento incluem- adicionar uma coluna com non-constant default
- alterar o tipo de uma coluna
- adicionar uma uniqueness constraint
- A partir do Postgres 11, o problema em que todos os defaults deixavam a adição de coluna lenta foi corrigido; non-constant default ainda pode ser problemático
- Mesmo que o
ALTER TABLEem si seja uma operação rápida, ele só executa depois de obter o lock- Se um
SELECTlento de um dashboard interno antigo já estiver em execução, oALTER TABLEprecisará esperar
- Se um
- Como locks no Postgres formam uma fila, consultas posteriores na mesma tabela que chegarem atrás do
ALTER TABLEem espera também podem ter que esperar - O mesmo cenário pode ser explorado em mais detalhes em Migrations and exclusive locks
- Se um
-
Transações longas também são perigosas
- Uma transação é uma forma de agrupar várias instruções de banco de dados em all-or-nothing; ela começa com
BEGINe termina comCOMMIT - Alterações feitas durante uma transação não são visíveis para outros clientes e são publicadas no banco de dados no
COMMIT - Isso é adequado para operações como uma transferência bancária, em que a redução do saldo de uma conta e o aumento do saldo de outra precisam ter sucesso juntas ou ser cancelados juntos
- Quando uma transação obtém um lock, ela mantém esse lock até o
COMMIT - Se você der
BEGIN, fizerUPDATEem uma row específica e sair da mesa, oDELETEdessa row por outro cliente ficará parado até a transação receber commit - Transações mantidas abertas por mais tempo do que o necessário podem bloquear consultas ou atualizações de outros clientes
- Uma transação é uma forma de agrupar várias instruções de banco de dados em all-or-nothing; ela começa com
JSONB é uma ferramenta afiada
-
Problemas de performance e schema do JSONB
- JSONB é flexível, mas pode ter grandes desvantagens se usado de forma errada
- O Postgres não rastreia estatísticas de colunas JSONB, então uma consulta de igualdade em uma única coluna JSONB pode ser muito mais lenta do que uma consulta sobre um conjunto de colunas comuns
- Um caso mostra um exemplo de lentidão 2000 vezes maior por causa de JSONB
- Uma coluna JSONB pode conter praticamente qualquer coisa, o que é poderoso, mas oferece poucas garantias sobre a estrutura
- Em tabelas comuns, é possível prever o resultado de consultas olhando o schema; com JSONB, não fica claro se o nome da key é camelCase ou snake_case, ou se um estado é boolean ou enum
- As características de tipagem estática dos dados comuns do Postgres não se aplicam ao JSONB da mesma forma
-
A estranheza das comparações de tipos em JSONB
- Ao tentar encontrar, na coluna JSONB
datada tabelabackpacks, linhas cujo campobrandsejaJanSport, a consulta abaixo não funciona
select * from backpacks where data['brand'] = 'JanSport';- O Postgres espera que o tipo à direita da comparação corresponda ao tipo à esquerda, e o lado direito precisa ser um documento JSON válido
- Um documento JSON precisa ser um objeto, array, string, número, boolean ou null, então
JanSportsozinho não é JSON válido - A consulta correta compara com uma string JSON ou converte o lado esquerdo para
textdo Postgres
select * from backpacks where data['brand'] = '"JanSport"'; select * from backpacks where data['brand'] = '"JanSport"'::jsonb; select * from backpacks where data->>'brand' = 'JanSport';- O
NULLdo SQL e onulldo JSONB se comportam de forma diferente'null'::jsonb = 'null'::jsonbétrue, masNULL = NULLéNULL
- O JSONB tem muitos operadores e funções próprios, difíceis de memorizar de uma vez
- O Postgres tem tanto
JSON, que armazena valores JSON como texto, quantoJSONB, que os converte para um formato binário eficiente - O JSONB tem vantagens como possibilidade de indexação, e o formato JSON pode ser visto como um caso especial
- Ao tentar encontrar, na coluna JSONB
2 comentários
Vou precisar ler isso algum dia, sobre o que não se deve fazer.
Opiniões do Hacker News
O PostgreSQL, em geral, diferencia maiúsculas de minúsculas, mas escrever palavras-chave SQL em maiúsculas costuma ser uma tentativa de melhorar a legibilidade por meio de reconhecimento visual de padrões
Não é obrigatório, mas, se eu tivesse que depurar a query de outra pessoa, provavelmente a colocaria em um prettifier para examinar rapidamente a definição sem tropeçar em detalhes triviais da forma da sintaxe
Assim como a formatação de código em outras linguagens, uma estrutura visual como indentação consistente reduz o tempo gasto entendendo as partes óbvias e ajuda a focar no que importa
Mas eu realmente detesto quando identificadores misturam maiúsculas e minúsculas de verdade, como
actuallyUsingCaseInIdentifiers, e não quero ver colunas que exigem aspas duplas para serem verificadas na CLISe vou digitar rapidamente uma query temporária que ninguém verá e descartá-la, não me preocupo com maiúsculas/minúsculas, mas SQL que é commitado no repositório usa comandos em ALL CAPS
Agora que temos cores, isso não é mais necessário, mas é uma lembrança antiga e não tenho material de referência
Ainda assim, não se deve misturar identificadores entre aspas e sem aspas, e a consulta de estruturas internas também não é muito padronizada, então isso não tem grande importância
Eu não conhecia a seção “don’t do this” da wiki do PostgreSQL, e ela é bem útil: https://wiki.postgresql.org/wiki/Don%27t_Do_This
Por exemplo, parece correto desabilitar recursos como herança de tabelas em novos schemas e exigir uma configuração deliberadamente complicada para reativá-los
Muito do que aparece aqui não se aplica só ao PostgreSQL
É o caso dos comportamentos estranhos de
NULLe da ordem das colunas em índices; em especial, a interação entre NULL e índices/restrições únicas também não é intuitiva no MySQLPor exemplo, se você criar uma restrição única em
(email, username)numa tabela de usuários em queemailnão pode ser NULL eusernamepode ser NULL, será possível inserir várias vezes o mesmoemailcomusernameNULL. Isso porque NULL não é igual a outro NULLhttps://www.postgresql.org/docs/devel/sql-createtable.html#S...
Casos de uso que exigem o comportamento oposto são muito mais raros
É problemático apenas dizer “normalize os dados se não houver uma boa razão” e seguir em frente
Mesmo na página linkada pelo autor há 11 formas normais, incluindo a forma não normalizada, e a maioria das pessoas nem sabe o que elas são; 7 delas nem serão usadas
Não se deve fazer as pessoas saírem procurando formas normais mais altas
No projeto para o qual migrei recentemente, também tive que corrigir alguns problemas desse tipo, e quase nunca há motivo para duplicar dados
A primeira dica é fazer VACUUM todos os dias
Quando comecei, eu não sabia disso e nunca fazia VACUUM no banco de dados do reddit; um dia não tive escolha a não ser executá-lo e, enquanto esperava terminar, o reddit ficou fora do ar por quase um dia
No porte do reddit, é surpreendente que os IDs de transação não tenham se esgotado antes
Eu gostaria que desenvolvedores se preocupassem mais com normalização e parassem de enfiar tudo em colunas JSONB
Desenvolvedores mais experientes sabiam que a resposta certa era não duplicar nada além de chaves e desnormalizar só muito a contragosto
Depois surgiram bancos como o Mongo, oferecendo algo “parecido com um banco de dados” em que normalização era difícil ou sem sentido, incentivando esses juniores; como resultado, designs de banco de dados horríveis e torres de lixo impossíveis de manter floresceram por um breve período
Agora o pêndulo voltou e redescobrimos as vantagens de bancos de dados normalizados, mas colunas JSON ainda são uma válvula de escape onde más práticas podem crescer
Primeiro, para armazenar JSON. Quando um servidor web chama uma API de terceiros, salvar a resposta original da API em uma coluna JSONB e processá-la a partir dali deixa um registro auditável para depurar problemas vindos daquela API
Segundo, para armazenar tipos soma (sum types). O fato de SQL não oferecer suporte a tipos soma pode ser considerado uma das maiores falhas ao modelar dados em bancos SQL
Existem várias soluções de contorno, e “simplesmente colocar numa coluna JSONB e validar na aplicação” é uma delas, mas nenhuma dessas soluções é particularmente ótima
Se você não estiver escrevendo consultas ruins dentro do JSONB em vez de promover os valores para colunas separadas, não vejo isso, por si só, como um grande problema
Isso acontece porque, se os requisitos de persistência forem atendidos com sucesso, não há uma pressão forte para pensar em um bom design
É questionável se faz sentido construir outro DBMS em cima de um DBMS, mas, de todo modo, esse é o estado atual
Se uma nova coluna prejudicar a performance ou causar problemas, deve ser possível reverter
Se houver ferramentas de CLI envolvidas, também é preciso lidar com quanto downtime é aceitável, se é possível fazer uma atualização de versão sincronizada em toda a empresa, ou se será necessário oferecer suporte aos schemas antigo e novo por algum tempo
Se o banco de dados não fizer parte do produto principal da equipe, tudo isso pode simplesmente estar ausente
Escrevi este texto para ajudar iniciantes: https://tomcam.github.io/postgres/
O artigo é realmente muito bom, e eu não sabia que a documentação do PostgreSQL tinha 3.200 páginas
Venho usando há algum tempo e aprendendo conforme preciso; também gosto bastante da documentação oficial, e gosto de ler artigos relacionados quando preciso de um tema específico
Acho que ajudaria os leitores se o autor acrescentasse em https://challahscript.com/what_i_wish_someone_told_me_about_... que um índice nas colunas
(b, a)funciona bem ao consultar apenas porbIsso fica meio implícito quando ele fala de consultas apenas por
a, mas não faria mal deixar mais explícitoNão usei muito a parte de JSON/JSONB, então não cheguei a olhar muito
Pensando nos SQLs ridículos que já vi no mundo real, acho que seria bom começar lendo o artigo de Codd e tentando entender o que é o modelo relacional
São só 11 páginas, e só de lê-lo já haveria menos sofrimento neste mundo
Quase tudo neste texto também se aplica a outros bancos de dados MVCC, como MySQL
Os detalhes podem variar, mas o MySQL também sofre com transações longas e pega bloqueios de metadados durante
ALTER, além de problemas divertidos parecidos