Este documento discute falsas suposições comuns sobre bancos de dados relacionais, especialmente PostgreSQL. Algumas dessas falsas suposições incluem: que a subtração de dias ou horas de um timestamp sempre dará o mesmo resultado; que fusos horários sempre serão próximos; e que IDs serão contínuos e em ordem de inserção. O documento também fornece melhores práticas, como sempre considerar timestamps absolutos e usar sequences para geração de IDs.
1. O que você acha que sabe
sobre banco de dados
As falsas suposições mais comuns sobre o comportamento
do PostgreSQL durante o desenvolvimento de sistemas
Veja também: TOP5 - Falsas Suposições de Programadores
Matheus de Oliveira
2016-10-06
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20. ἵ +10k restaurantes
+400 funcionários (and )
Ἶ +150 cidades
ὥ +1,5 milhões de usuários
ὤ +120k pedidos num único dia
...e crescendo freneticamente a cada dia!
we're hiring!
22. Todo dia tem 24 horas, certo?
Então tanto faz se subtrairmos 1 dia ou 24 horas de um
timestamp que teremos o mesmo resultado, certo?
postgres=# SELECT ts, ts interval '1 day' FROM datahora;
ts | ?column?
+
20151019 00:00:0002 | 20151018 01:00:0002
(1 row )
postgres=# SELECT ts, ts interval '24 hours' FROM datahora;
ts | ?column?
+
20151019 00:00:0002 | 20151017 23:00:0003
(1 row )
23. Falsa suposições sobre fuso horários:
minha aplicação nunca vai ser usada com diferentes fuso
horários
Ok, mas pelo menos os "o sets" vão ser próximos
os possíveis "o sets" são -12:00, -11:00, -10:00, ..., 10:00,
11:00, 12:00
os servidores estarão todos com o mesmo fuso horário
o fuso horário do servidor não vai mudar
...
24. a data/hora do cliente e do servidor são iguais
Ok, não iguais, mas bem próximas
Ok, ok, mas pelo menos a diferença permanece a mesma,
sempre!
...http://in niteundo.com/post/25326999628/falsehoods-
programmers-believe-about-time
25. E o que devemos fazer?
Sempre considerar data/hora absoluto, e não o que você
enxerga no relógio
Tipos timestamp no PostgreSQL:
timestampou timestamp WITHOUT time zone
timestamptzou timestamp WITH time zone
Qual usar?
Preferir fortemente TIMESTAMP WITH TIME
ZONE
29. E o que devemos fazer?
Nunca utilize o tipo money, pois este depende de
con gurações de locale
Usar tipos real(float4) ou double precision
(float8) somente quando precisa de muita velocidade
nas operações, não se importa com precisão, e sabe que
os valores estão no intervalo aceitável do tipo
Para a maioria dos casos, usar o numeric(ou
decimal, que é um alias)
30. Fique atento
Não adianta usar o tipo correto no banco de dados e não
na aplicação
Lembre-se que DECIMAL(N, M)signi ca Ndígitos,
sendo Mdeles a parte decimal
Por exemplo, decimal(5, 2)aceita de 3 dígitos
inteiros e 2 decimais:de -999,99a 999,99
32. max(id)+1
Código para gerar um idsequencial:
SELECT coalesce(max(id), 0) + 1 INTO NEW.id
FROM minha_tabela;
ERRADO! grande problema de condição de corrida!
Duas sessões podem recuperar o mesmo valor
34. Só isso...
Solução para auto-increment
CREATE TABLE minha_tabela (
id serial primary key,
...
);
;)
Se a coluna já existir:
BEGIN;
CREATE SEQUENCE minha_tabela_seq;
ALTER TABLE minha_tabela
ALTER id SET DEFAULT nextval('minha_tabela_seq');
SELECT setval('minha_tabela_seq', max(id))
FROM minha_tabela;
ALTER SEQUENCE minha_tabela_seq
OWNED BY minha_tabela.id;
COMMIT;
35. Falsas suposições
ids são contínuos (sem buracos)
SELECT ... FROM tabela(sem ORDER BY), traz na
ordem de inserção
Ok, mas pelo menos ORDER BYid me traz a ordem de
inserção
Sério? Ah, mas ORDER BY ide ORDER BY
data_insercaosão equivalentes
36. UPSERT - UPdate or inSERT
Veri ca se um registro já existe e atualiza, se não insere:
UPDATE acesso
SET contador = contador + 1
WHERE url = p_url AND usuario = p_usuario;
IF (NOT FOUND) THEN
INSERT INTO acesso(url, usuario, contador)
VALUES(p_url, p_usuario, 1);
END IF;
PROBLEMA! dois usuários podem tentar inserir
juntos
37. Solução!
A partir da versão 9.5 do PostgreSQL, podemos
simplesmente fazer:
INSERT INTO acesso AS a(url, usuario, contador)
VALUES(p_url, p_usuario, 1)
ON CONFLICT (url)
DO UPDATE SET contador = a.contador + EXCLUDED.contador;
38. Solução!
Para versões mais antigas era mais complicado:
LOOP
UPDATE acesso
SET contador = contador + 1
WHERE url = p_url AND usuario = p_usuario;
EXIT WHEN FOUND; /* finaliza se atualizou algo */
/* se não atualizou, tenta inserir: */
BEGIN
INSERT INTO acesso(url, usuario, contador)
VALUES(p_url, p_usuario, 1);
EXIT; /* inseriu, sai do loop */
EXCEPTION WHEN unique_violation THEN
/* já existe, continua no loop para atualizar */
END;
END LOOP;
39. Sumarização de dados
Sumarização com UPDATE:
SELECT p.qtd_estoque INTO v_estoque
FROM produto AS p
WHERE p.produto_id = NEW.produto_id;
v_estoque := v_estoque + NEW.qtd;
UPDATE produto
SET qtd_estoque = v_estoque
WHERE p.produto_id = NEW.produto_id;
PROBLEMA! duas sessões podem pegar o mesmo
valor
Esse código pode parecer incomum, mas é efetivamente o que muitas aplicações que usam ORM
acabam fazendo por baixo dos panos, então você deve aprender a identi car esse padrão.
41. Validação de dados com várias linhas
Trigger na própria tabela entrada_saida_estoque (AFTER INSERT)
SELECT sum(est.qtd)
INTO v_saldo
FROM entrada_saida_estoque est
WHERE est.produto_id = NEW.produto_id;
IF (v_saldo + NEW.qtd < 0) THEN
RAISE EXCEPTION 'Saldo não pode ser negativo';
END IF;
PROBLEMA! duas sessões concorrentes conseguem
fazer o saldo ser menor que zero
42. Solução: forçar bloqueio
Bloquear com FOR UPDATEantes de validar:
Trigger na própria tabela entrada_saida_estoque (AFTER INSERT)
PERFORM 1 FROM produto p
WHERE p.produto_id = NEW.produto_id
FOR UPDATE;
SELECT sum(est.qtd)
INTO v_saldo
FROM entrada_saida_estoque est
WHERE est.produto_id = NEW.produto_id;
IF (v_saldo + NEW.qtd < 0) THEN
RAISE EXCEPTION 'Saldo não pode ser negativo';
END IF;
43. Solução: pré-sumarizar
Pré-sumarizar o resultado (com um UPDATEsemelhante ao
anterior):
Trigger na própria tabela entrada_saida_estoque (AFTER INSERT)
UPDATE produto
SET qtd_estoque = qtd_estoque + NEW.qtd
WHERE p.produto_id = NEW.produto_id
RETURNING qtd_estoque INTO v_saldo;
IF (v_saldo < 0) THEN
RAISE EXCEPTION 'Saldo não pode ser negativo';
END IF;
44. Solução: usar SSI
Usar o primeiro código (que parece errado), mas no nível de
transação SERIALIZABLE
A partir do PostgreSQL 9.1, este nível utiliza Serializable
Snapshot Isolation (SSI)
Quando duas transações têm chance de con ito (por
exemplo a primeira iria ler os dados da segunda, ou vice-
versa), em nível SERIALIZABLE, recebe-se o erro:
Mais detalhes:
ERROR: could not serialize access due to
read/write dependencies among transactions
DETAIL: Cancelled on identification as a pivot,
during commit attempt.
HINT: The transaction might succeed if retried.
https://wiki.postgresql.org/wiki/SSI
46. BETWEENvs >= ... <=
Internamente o PostgreSQL convert BETWEENem
>= ... <=:
EXPLAIN SELECT * FROM orders WHERE value BETWEEN 50 AND 100;
QUERY PLAN
Seq Scan on orders (...)
Filter: ((value >= '50'::numeric) AND (value <= '100'::numeric))
(2 rows)
src/backend/parser/gram.y:
Esse código entre a e , mas a lógica nal permanece.
| a_expr BETWEEN opt_asymmetric b_expr AND b_expr %prec BETWEEN
{
$$ = (Node *) makeA_Expr(AEXPR_AND, NIL,
(Node *) makeSimpleA_Expr(AEXPR_OP, ">=", $1, $4, @2),
(Node *) makeSimpleA_Expr(AEXPR_OP, "<=", $1, $6, @2),
@2);
foi modi cado versão 9.4 versão 9.5
47. Timestamp entre datas
Todos pedidos do primeiro semestre de 2016.
SELECT * FROM orders
WHERE date(delivery_date) BETWEEN '20160101' AND '20160701';
ERRADO!...Incluí pedidos do dia 01/07/2016!
Lógica correta, mas não conseguirá usar um índice (não
funcional) em delivery_date
SELECT * FROM orders
WHERE date(delivery_date) BETWEEN '20160101' AND '20160630';
48. Soluções
Todos pedidos do primeiro semestre de 2016.
SELECT * FROM orders
WHERE delivery_date BETWEEN '20160101'
AND '20160630 23:59:59.999999';
Correto! Mas tem que lembrar sempre que a precisão é
microssegundos (não segundos, nem milissegundos)!
Bem mais simples!!!
Ambos se bene ciam de índice em delivery_date.
SELECT * FROM orders
WHERE delivery_date >= '20160101'
AND delivery_date < '20160701';
49. COUNT(1)ou COUNT(primary_key)vs
COUNT(*)
Qual é mais rápido?
SELECT count(1) FROM orders
WHERE delivery_date >= '20160101'
AND delivery_date < '20160701';
SELECT count(order_id) FROM orders
WHERE delivery_date >= '20160101'
AND delivery_date < '20160701';
SELECT count(*) FROM orders
WHERE delivery_date >= '20160101'
AND delivery_date < '20160701';
52. COUNT(*)
SELECT count(*) FROM orders
WHERE delivery_date >= '20160101'
AND delivery_date < '20160701';
Não tem validação de NULL.
Para o countespeci camente o *seria semelhante a só
fazer count()(não aceita essa porque o padrão SQL trata o
count(*)dessa forma...De nições meu caro...xD )
Muitos dizem que COUNT(*)é mais lento que as
outras duas formas, isso para o PostgreSQL é
somente uma lenda, e mais, COUNT(*)pode ser até
mais rápido (a diferença deve ser imperceptível na
maioria dos casos entretanto).
53. Explicando melhor...
O PostgreSQL trata a chamada de qualquer função de
agregação sem parâmetros como FUNCAO(*).
Não acredita? Veja mais um trecho do
src/backend/parser/gram.y:
func_name '(' '*' ')'
{
/*
* We consider AGGREGATE(*) to invoke a parameterless
* aggregate. This does the right thing for COUNT(*),
* and there are no other aggregates in SQL that accept
* '*' as parameter.
* [...]
*/
FuncCall *n = makeFuncCall($1, NIL, @1);
[...]
}
Veja esse código no .repositório git do PostgreSQL, versão 9.6