Introdução
Existem duas escolas de pensamento sobre a realização de cálculos em seu banco de dados:pessoas que acham ótimo e pessoas que estão erradas. Isso não quer dizer que o mundo das funções, procedimentos armazenados, colunas geradas ou computadas e gatilhos é tudo sol e rosas! Essas ferramentas estão longe de ser infalíveis, e implementações mal consideradas podem ter um desempenho ruim, traumatizar seus mantenedores e muito mais, o que explica a existência de controvérsias.
Mas os bancos de dados são, por definição, muito bons em processar e manipular informações, e a maioria deles disponibiliza esse mesmo controle e poder para seus usuários (SQLite e MS Access em menor grau). Os programas de processamento de dados externos começam com o pé atrás, tendo que extrair informações do banco de dados, geralmente através de uma rede, antes que possam fazer qualquer coisa. E onde os programas de banco de dados podem aproveitar ao máximo as operações de conjunto nativas, indexação, tabelas temporárias e outros frutos de meio século de evolução do banco de dados, programas externos de qualquer complexidade tendem a envolver algum nível de reinvenção da roda. Então, por que não colocar o banco de dados para funcionar?
Veja por que você pode não deseja programar seu banco de dados!
- A funcionalidade do banco de dados tende a se tornar invisível -- especialmente os gatilhos. Essa fraqueza aumenta aproximadamente com o tamanho das equipes e/ou aplicativos que interagem com o banco de dados, pois menos pessoas se lembram ou estão cientes da programação no banco de dados. A documentação ajuda, mas só até certo ponto.
- SQL é uma linguagem desenvolvida especificamente para manipular conjuntos de dados. Não é especialmente bom em coisas que não estão manipulando conjuntos de dados e é menos bom quanto mais complicadas essas outras coisas ficam.
- Os recursos do RDBMS e os dialetos SQL são diferentes. Colunas geradas simples são amplamente suportadas, mas a portabilidade de lógica de banco de dados mais complexa para outras lojas leva tempo e esforço no mínimo.
- As atualizações de esquema de banco de dados geralmente são mais complicadas do que as atualizações de aplicativos. A lógica que muda rapidamente é melhor mantida em outro lugar, embora possa valer a pena dar outra olhada quando as coisas se estabilizarem.
- Gerenciar programas de banco de dados não é tão simples quanto se poderia esperar. Muitas ferramentas de migração de esquema fazem pouco ou nada pela organização, levando a diffs extensos e revisões de código onerosas (os gráficos de dependência do sqitch e o retrabalho de objetos individuais o tornam uma exceção notável, e a migração procura contornar o problema inteiramente). Nos testes, frameworks como pgTAP e utPLSQL melhoram os testes de integração de caixa preta, mas também representam um compromisso extra de suporte e manutenção.
- Com uma base de código externa estabelecida, qualquer mudança estrutural tende a ser trabalhosa e arriscada.
Por outro lado, para as tarefas a que se adequa, o SQL oferece velocidade, concisão, durabilidade e a oportunidade de "canonizar" fluxos de trabalho automatizados. A modelagem de dados é mais do que prender entidades como insetos a papelão, e a distinção entre dados em movimento e dados em repouso é complicada. O descanso é um movimento realmente mais lento em grau mais fino; as informações estão sempre fluindo daqui para lá, e a programação do banco de dados é uma ferramenta poderosa para gerenciar e direcionar esses fluxos.
Alguns mecanismos de banco de dados dividem a diferença entre SQL e outras linguagens de programação acomodando também essas outras linguagens de programação. O SQL Server suporta funções escritas em qualquer linguagem .NET Framework; A Oracle possui procedimentos armazenados em Java; O PostgreSQL permite extensões com C e é programável pelo usuário em Python, Perl e Tcl, com plugins adicionando scripts de shell, R, JavaScript e muito mais. Completando os suspeitos do costume, é SQL ou nada para MySQL e MariaDB, MS Access é apenas programável em VBA, e SQLite não é programável pelo usuário.
Usar linguagens não-SQL é uma opção se o SQL for inadequado para alguma tarefa ou se você quiser reutilizar outro código, mas não o ajudará a contornar os outros problemas que tornam a programação de banco de dados uma faca de muitos gumes. Se alguma coisa, recorrer a eles complica ainda mais a implantação e a interoperabilidade. Scriptor de advertência:deixe o escritor tomar cuidado.
Funções vs Procedimentos
Assim como em outros aspectos da implementação do padrão SQL, os detalhes exatos variam um pouco de RDBMS para RDBMS. Em geral:
- As funções não podem controlar as transações.
- Funções retornam valores; procedimentos podem modificar os parâmetros designados
OUT
ouINOUT
que pode ser lido no contexto de chamada, mas nunca retorna um resultado (exceto SQL Server). - As funções são invocadas de dentro de instruções SQL para realizar algum trabalho nos registros que estão sendo recuperados ou armazenados, enquanto os procedimentos são independentes.
Mais especificamente, o MySQL também não permite a recursão e algumas instruções SQL adicionais em funções. O SQL Server proíbe funções de modificar dados, executar SQL dinâmico e manipular erros. O PostgreSQL não separou procedimentos armazenados de funções até 2017 com a versão 11, então as funções do Postgres podem fazer quase tudo que os procedimentos podem, exceto o controle de transações.
Então, qual usar quando? As funções são mais adequadas à lógica que aplica registro por registro à medida que os dados são armazenados e recuperados. Fluxos de trabalho mais complexos que são chamados por eles mesmos e movem dados internamente são melhores como procedimentos.
Padrões e geração
Mesmo cálculos simples podem causar problemas se forem executados com frequência suficiente ou se existirem várias implementações concorrentes. Operações em valores em uma única linha - pense na conversão entre unidades métricas e imperiais, multiplicando uma taxa por horas trabalhadas para subtotais de faturas, calculando a área de um polígono geográfico - podem ser declaradas em uma definição de tabela para resolver um ou outro problema :
CREATE TABLE pythag ( a INT NOT NULL, b INT NOT NULL, c DOUBLE PRECISION NOT NULL GENERATED ALWAYS AS (sqrt(pow(a, 2) + pow(b, 2))) STORED);
A maioria dos RDBMSs oferece uma escolha entre colunas geradas "armazenadas" e "virtuais". No primeiro caso, o valor é calculado e armazenado quando a linha é inserida ou atualizada. Esta é a única opção com PostgreSQL, a partir da versão 12, e MS Access. As colunas geradas virtuais são computadas quando consultadas como em visualizações, portanto, não ocupam espaço, mas serão recalculadas com mais frequência. Ambos os tipos são fortemente restritos:os valores não podem depender de informações fora da linha a que pertencem, eles não podem ser atualizados e RDBMSs individuais podem ter limitações ainda mais específicas. O PostgreSQL, por exemplo, proíbe o particionamento de uma tabela em uma coluna gerada.
As colunas geradas são uma ferramenta especializada. Mais frequentemente, tudo o que é necessário é um padrão caso um valor não seja fornecido na inserção. Funções como
now()
aparecem frequentemente como padrões de coluna, mas a maioria dos bancos de dados permite funções personalizadas e internas (exceto MySQL, onde apenas current_timestamp
pode ser um valor padrão). Vamos pegar o exemplo bastante seco, mas simples, de um número de lote no formato YYYYXXX, onde os quatro primeiros dígitos representam o ano atual e os três últimos um contador de incremento:o primeiro lote produzido este ano é 2020001, o segundo 2020002 e assim por diante . Não há nenhum tipo padrão ou função interna que gere um valor como este, mas uma função definida pelo usuário pode numerar cada lote
CREATE SEQUENCE lot_counter;CREATE OR REPLACE FUNCTION next_lot_number () RETURNS TEXT AS $$BEGIN RETURN date_part('year', now())::TEXT || lpad(nextval('lot_counter'::REGCLASS)::TEXT, 2, '0');END;$$LANGUAGE plpgsql;CREATE TABLE lots ( lot_number TEXT NOT NULL DEFAULT next_lot_number () PRIMARY KEY, current_quantity INT NOT NULL DEFAULT 0, target_quantity INT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), completed_at TIMESTAMPTZ, CHECK (target_quantity > 0));
Referenciando dados em funções
A abordagem de sequência acima tem uma fraqueza importante (e
lot_counter
ainda terá o mesmo valor de 31 de dezembro. No entanto, há mais de uma maneira de rastrear quantos lotes foram criados em um ano e consultando lots
ele mesmo o next_lot_number
função pode garantir um valor correto após o ano. CREATE OR REPLACE FUNCTION next_lot_number () RETURNS TEXT AS $$BEGIN RETURN ( SELECT date_part('year', now())::TEXT || lpad((count(*) + 1)::TEXT, 2, '0') FROM lots WHERE date_part('year', created_at) = date_part('year', now()) );END;$$LANGUAGE plpgsql;ALTER TABLE lots ALTER COLUMN lot_number SET DEFAULT next_lot_number();
Fluxos de trabalho
Mesmo uma função de instrução única tem uma vantagem crucial sobre o código externo:a execução nunca deixa a segurança das garantias ACID do banco de dados. Comparar
next_lot_number
acima às possibilidades de uma aplicação cliente ou mesmo de um processo manual, executando Programas armazenados com várias instruções abrem um imenso espaço de possibilidades, já que o SQL inclui todas as ferramentas que você precisa para escrever código procedural, desde tratamento de exceção até pontos de salvamento (é até Turing completo com funções de janela e expressões de tabela comuns!). Fluxos de trabalho inteiros de processamento de dados podem ser executados no banco de dados, minimizando a exposição a outras áreas do sistema e eliminando viagens de ida e volta demoradas entre o banco de dados e outros domínios.
Grande parte da arquitetura de software em geral trata de gerenciar e isolar a complexidade, evitando que ela ultrapasse os limites entre os subsistemas. Se algum fluxo de trabalho mais ou menos complicado envolve puxar dados para um backend de aplicativo, script ou cron job, digerir e adicionar a ele e armazenar o resultado - é hora de perguntar o que realmente precisa se aventurar fora do banco de dados.
Como mencionado acima, esta é uma área onde as diferenças entre os tipos de RDBMS e os dialetos SQL vêm à tona. Uma função ou procedimento desenvolvido para um banco de dados provavelmente não será executado em outro sem alterações, seja substituindo o
TOP
do SQL Server para um padrão LIMIT
cláusula ou retrabalhando completamente como o estado temporário é armazenado em uma migração corporativa de Oracle para PostgreSQL. Canonizar seus fluxos de trabalho em SQL também o compromete com sua plataforma e dialeto atuais de forma mais completa do que quase qualquer outra escolha que você possa fazer. Cálculos em consultas
Até agora, analisamos o uso de funções para armazenar e modificar dados, seja vinculado a definições de tabela ou gerenciando fluxos de trabalho de várias tabelas. Em certo sentido, esse é o uso mais poderoso que eles podem fazer, mas as funções também têm um lugar na recuperação de dados. Muitas ferramentas que você já pode usar em suas consultas são implementadas como funções, de built-ins padrão como
count
para extensões como o jsonb_build_object
do Postgres , PostGIS' ST_SnapToGrid
, e mais. É claro que, como eles estão mais integrados ao próprio banco de dados, eles são escritos principalmente em linguagens diferentes do SQL (por exemplo, C no caso do PostgreSQL e PostGIS). Se você frequentemente se encontra (ou acha que pode se encontrar) precisando recuperar dados e então realizar alguma operação em cada registro antes que seja realmente pronto, considere transformá-los ao sair do banco de dados! Projetando algum número de dias úteis fora de uma data? Gerando uma diferença entre dois
JSONB
Campos? Praticamente qualquer cálculo que dependa apenas das informações que você está consultando pode ser feito em SQL. E o que é feito no banco de dados - contanto que seja acessado de forma consistente - é canônico no que diz respeito a qualquer coisa construída sobre o banco de dados. É preciso dizer:se você estiver trabalhando com um back-end de aplicativo, seu kit de ferramentas de acesso a dados pode limitar a quantidade de quilometragem que você obtém ao aumentar os resultados da consulta com funções. A maioria dessas bibliotecas pode executar SQL arbitrário, mas aquelas que geram instruções SQL comuns baseadas em classes de modelo podem ou não permitir a personalização da consulta
SELECT
listas. Colunas ou visualizações geradas podem ser uma resposta aqui. Gatilhos e consequências
Funções e procedimentos são bastante controversos entre designers e usuários de banco de dados, mas as coisas realmente decolar com gatilhos. Um gatilho define uma ação automática, geralmente um procedimento (o SQLite permite apenas uma única instrução), a ser executada antes, depois ou no lugar de outra ação.
A ação de inicialização geralmente é uma inserção, atualização ou exclusão de uma tabela, e o procedimento de disparo geralmente pode ser definido para ser executado para cada registro ou para a instrução como um todo. O SQL Server também permite gatilhos em exibições atualizáveis, principalmente como forma de impor medidas de segurança mais detalhadas; e ele, PostgreSQL e Oracle oferecem algum tipo de evento ou
Um uso comum de baixo risco para gatilhos é como uma restrição extra poderosa que impede que dados inválidos sejam armazenados. Em todos os principais bancos de dados relacionais, apenas chaves primárias e estrangeiras e
UNIQUE
restrições podem avaliar informações fora do registro do candidato. Não é possível declarar em uma definição de tabela que, por exemplo, apenas dois lotes podem ser criados em um mês - e a solução mais simples de banco de dados e código é vulnerável a uma condição de corrida semelhante à abordagem de contagem e definição para lot_number
acima de. Para impor qualquer outra restrição que envolva a tabela inteira ou outras tabelas, você precisa de um CREATE FUNCTION enforce_monthly_lot_limit () RETURNS TRIGGERAS $$DECLARE current_count BIGINT;BEGIN SELECT count(*) INTO current_count FROM lots WHERE date_trunc('month', created_at) = date_trunc('month', NEW.created_at); IF current_count >= 2 THEN RAISE EXCEPTION 'Two lots already created this month'; END IF; RETURN NEW;END;$$LANGUAGE plpgsql;CREATE TRIGGER monthly_lot_limitBEFORE INSERT ON lotsFOR EACH ROWEXECUTE PROCEDURE enforce_monthly_lot_limit();
Assim que você começar a executar o
lots
em si pode ser a operação final de um gatilho iniciado por uma inserção em orders
, sem nenhum usuário humano ou back-end de aplicativo habilitado para gravar em lots
diretamente. Ou como items
são adicionados a um lote, um gatilho pode lidar com a atualização de current_quantity
, e inicie algum outro processo quando atingir o target_quantity
. Triggers e funções podem ser executados no nível de acesso de seu definidor (no PostgreSQL, um
SECURITY DEFINER
declaração ao lado de LANGUAGE
de uma função ), que dá aos usuários limitados o poder de iniciar processos mais amplos - e torna a validação e o teste desses processos ainda mais importantes. A pilha de chamadas trigger-action-trigger-action pode se tornar arbitrariamente longa, embora a verdadeira recursão na forma de modificar as mesmas tabelas ou registros várias vezes em qualquer fluxo seja ilegal em algumas plataformas e, em geral, uma má ideia em quase todas as circunstâncias. O aninhamento de gatilhos ultrapassa rapidamente nossa capacidade de compreender sua extensão e efeitos. Bancos de dados que fazem uso intenso de gatilhos aninhados começam a se deslocar do domínio do complicado para o complexo, tornando-se difícil ou impossível de analisar, depurar e prever.
Programabilidade prática
Os cálculos no banco de dados não são apenas mais rápidos e expressos de forma mais concisa:eles eliminam a ambiguidade e estabelecem padrões. Os exemplos acima liberam os usuários de banco de dados de terem que calcular números de lote por conta própria ou de se preocuparem com a criação acidental de mais lotes do que podem manipular. Desenvolvedores de aplicativos, em particular, muitas vezes foram treinados para pensar em bancos de dados como "armazenamento estúpido", fornecendo apenas estrutura e persistência e, portanto, podem se encontrar - ou pior, não perceber que estão - articulando desajeitadamente fora do banco de dados o que eles poderiam fazer mais eficazmente em SQL.
A programabilidade é um recurso injustamente negligenciado dos bancos de dados relacionais. Existem razões para evitá-lo e mais para limitar seu uso, mas funções, procedimentos e gatilhos são ferramentas poderosas para limitar a complexidade que seu modelo de dados impõe aos sistemas nos quais está incorporado.