Em um
artigo anterior
, descrevi o conceito e a implementação de um banco de dados construído com base em funções, não em tabelas e campos, como nos bancos de dados relacionais. Deu muitos exemplos mostrando as vantagens dessa abordagem em relação ao clássico. Muitos os acharam não suficientemente convincentes.
Neste artigo, mostrarei como esse conceito permite equilibrar rápida e convenientemente a escrita e a leitura no banco de dados sem nenhuma alteração na lógica do trabalho. Eles tentaram implementar funcionalidades semelhantes no DBMS comercial moderno (em particular, Oracle e Microsoft SQL Server). No final do artigo, mostrarei o que aconteceu com eles, para dizer o mínimo, não muito.
Descrição do produto
Como antes, para uma melhor compreensão, começarei a descrição com exemplos. Suponha que precisamos implementar uma lógica que retorne uma lista de departamentos com o número de funcionários e o salário total.
Em um banco de dados funcional, ele terá a seguinte aparência:
A complexidade de executar essa consulta em qualquer DBMS será equivalente a
O (número de funcionários) , pois para esse cálculo você precisa varrer a tabela inteira de funcionários e agrupá-los por departamento. Também haverá algumas pequenas adições (acreditamos que há muito mais funcionários que departamentos), dependendo do plano escolhido
O (número de log de funcionários) ou
O (número de departamentos) para agrupamento e assim por diante.
É claro que a sobrecarga para execução pode ser diferente em diferentes DBMSs, mas a complexidade não muda de forma alguma.
Na implementação proposta, o DBMS funcional formará uma subconsulta, que calculará os valores necessários para o departamento e, em seguida, fará um JOIN com a tabela de departamento para obter o nome. No entanto, para cada função, ao declarar, é possível especificar um marcador MATERIALIZED especial. O sistema criará automaticamente um campo apropriado para cada uma dessas funções. Quando um valor da função muda, o valor do campo muda na mesma transação. Ao acessar esta função, uma apelação já será feita no campo calculado.
Em particular, se você colocar MATERIALIZED para as funções
countEmployees e
employeeSum , na tabela com a lista de departamentos serão adicionados dois campos nos quais o número de funcionários e o salário total serão armazenados. Com qualquer alteração nos funcionários, seus salários ou afiliação aos departamentos, o sistema alterará automaticamente os valores desses campos. A consulta acima começará a acessar esses campos diretamente e será executada para
O (número de departamentos) .
Quais são as limitações? Apenas uma coisa: essa função deve ter um número finito de valores de entrada para os quais seu valor está definido. Caso contrário, será impossível construir uma tabela que armazene todos os seus valores, pois não pode haver uma tabela com um número infinito de linhas.
Um exemplo:
Esta função é definida para um número infinito de valores do número N (por exemplo, qualquer valor negativo é adequado). Portanto, não pode ser colocado MATERIALIZADO. Portanto, essa é uma limitação lógica e não técnica (ou seja, não porque não fomos capazes de implementar isso). Caso contrário, não há restrições. Você pode usar agrupamento, classificação, AND e OR, PARTITION, recursão, etc.
Por exemplo, na tarefa 2.2 do artigo anterior, você pode colocar MATERIALIZED nas duas funções:
O próprio sistema criará uma tabela com chaves dos tipos
Customer ,
Product e
INTEGER , adicionará dois campos a ela e atualizará os valores de campo nelas com quaisquer alterações. Após chamadas adicionais para essas funções, elas não serão calculadas, mas os valores dos campos correspondentes serão lidos.
Usando esse mecanismo, você pode, por exemplo, livrar-se da recursão (CTE) nas consultas. Em particular, considere os grupos que compõem a árvore usando o relacionamento filho / pai (cada grupo tem um link para seu pai):
Em um banco de dados funcional, a lógica de recursão pode ser definida da seguinte maneira:
Como MATERIALIZED é afixado para a função
isParent , será criada uma tabela com duas chaves (grupos), na qual o campo
isParent será verdadeiro somente se a primeira chave for um descendente da segunda. O número de entradas nesta tabela será igual ao número de grupos vezes a profundidade média da árvore. Se for necessário, por exemplo, calcular o número de descendentes de um determinado grupo, você poderá acessar esta função:
Não haverá CTE na consulta SQL. Em vez disso, haverá um simples GROUP BY.
Usando esse mecanismo, você também pode desnormalizar facilmente o banco de dados, se necessário:
Quando você chama a função de
data para a linha do pedido, a leitura será da tabela com as linhas do pedido do campo para o qual existe um índice. Ao alterar a data do pedido, o próprio sistema recalcula automaticamente a data não normalizada na linha.
Os benefícios
Por que todo esse mecanismo é necessário? Nos DBMSs clássicos, sem reescrever consultas, um desenvolvedor ou DBA só pode alterar índices, determinar estatísticas e informar ao planejador de consultas como executá-las (além disso, as HINTs estão disponíveis apenas em DBMSs comerciais). Por mais que tentem, eles não poderão atender à primeira solicitação do artigo para
O (número de departamentos) sem alterar as solicitações e adicionar gatilhos. No esquema proposto, no estágio de desenvolvimento, você não precisa pensar na estrutura do armazenamento de dados e em quais agregações usar. Tudo isso pode ser facilmente alterado em tempo real, diretamente em operação.
Na prática, é o seguinte. Algumas pessoas desenvolvem lógica diretamente com base na tarefa. Eles não são versados em algoritmos e sua complexidade, nem em planos de execução, nem em tipos de join'ov, nem em nenhum outro componente técnico. Essas pessoas são mais analistas de negócios do que desenvolvedores. Então, tudo entra em teste ou operação. O log de consultas longas está ativado. Quando uma solicitação longa é detectada, outras pessoas (mais técnicas - na verdade, DBA) decidem incluir MATERIALIZED em alguma função intermediária. Assim, a gravação fica um pouco mais lenta (já que é necessário atualizar um campo adicional em uma transação). No entanto, não apenas essa solicitação é significativamente acelerada, mas também todas as outras que usam essa função. Ao mesmo tempo, tomar uma decisão sobre qual função específica se materializar é relativamente simples. Dois parâmetros principais: o número de possíveis valores de entrada (exatamente quantos registros haverá na tabela correspondente) e com que frequência é usado em outras funções.
Análogos
Os DBMSs comerciais modernos têm mecanismos semelhantes: VIEW MATERIALIZED com FAST REFRESH (Oracle) e INDEXED VIEW (Microsoft SQL Server). No PostgreSQL, o MATERIALIZED VIEW não pode ser atualizado em uma transação, mas apenas mediante solicitação (e mesmo com restrições muito rígidas), portanto, não consideramos isso. Mas eles têm vários problemas, o que limita bastante o seu uso.
Primeiro, você pode ativar a materialização apenas se você já criou uma VIEW regular. Caso contrário, você terá que reescrever as solicitações restantes para acessar a exibição recém-criada para usar essa materialização. Ou deixe como está, mas será pelo menos ineficaz se já houver determinados dados calculados, mas muitas consultas nem sempre os usam, mas calculam novamente.
Em segundo lugar, eles têm um grande número de restrições:
Oracle5.3.8.4 Restrições gerais na atualização rápida
A consulta de definição da visão materializada é restrita da seguinte maneira:
- A visualização materializada não deve conter referências a expressões não repetidas como
SYSDATE
e ROWNUM
. - A exibição materializada não deve conter referências aos tipos de dados
RAW
ou LONG
RAW
. - Ele não pode conter uma subconsulta da lista
SELECT
. - Ele não pode conter funções analíticas (por exemplo,
RANK
) na cláusula SELECT
. - Ele não pode fazer referência a uma tabela na qual um índice
XMLIndex
está definido. - Não pode conter uma cláusula
MODEL
. - Ele não pode conter uma cláusula
HAVING
com uma subconsulta. - Ele não pode conter consultas aninhadas que possuem
ANY
, ALL
ou NOT
EXISTS
. - Não pode conter uma cláusula
[START WITH …] CONNECT BY
. - Ele não pode conter várias tabelas de detalhes em sites diferentes.
ON
vistas materializadas ON
COMMIT
não podem ter tabelas de detalhes remotas.- As visualizações materializadas aninhadas devem ter uma junção ou agregação.
- As visualizações de junção materializada e as visualizações agregadas materializadas com uma cláusula
GROUP
BY
não podem ser selecionadas em uma tabela organizada por índice.
5.3.8.5 Restrições à atualização rápida em vistas materializadas apenas com junções
A definição de consultas para visualizações materializadas apenas com junções e nenhum agregado possui as seguintes restrições na atualização rápida:
- Todas as restrições de " Restrições gerais à atualização rápida ".
- Eles não podem ter cláusulas ou agregados
GROUP
BY
. - Rowids de todas as tabelas na lista
FROM
devem aparecer na lista SELECT
da consulta. - Os logs de exibição materializada devem existir com linhas para todas as tabelas base na lista
FROM
da consulta. - Você não pode criar uma exibição materializada atualizável rápida de várias tabelas com junções simples que incluem uma coluna de tipo de objeto na
SELECT
.
Além disso, o método de atualização que você escolher não será idealmente eficiente se:
- A consulta de definição usa uma junção externa que se comporta como uma junção interna. Se a consulta de definição contiver essa associação, considere reescrever a consulta de definição para conter uma associação interna.
- A lista
SELECT
da visualização materializada contém expressões em colunas de várias tabelas.
5.3.8.6 Restrições à atualização rápida em vistas materializadas com agregados
A definição de consultas para visualizações materializadas com agregações ou junções possui as seguintes restrições na atualização rápida:
A atualização rápida é suportada para as visualizações materializadas ON
COMMIT
e ON
DEMAND
, no entanto, as seguintes restrições se aplicam:
- Todas as tabelas na visão materializada devem ter logs de visão materializada e os logs de visão materializada devem:
- Contenha todas as colunas da tabela referenciada na visualização materializada.
- Especifique com
ROWID
e INCLUDING
NEW
VALUES
. - Especifique a cláusula
SEQUENCE
se a tabela tiver uma combinação de inserções / cargas diretas, exclusões e atualizações.
- Apenas
SUM
, COUNT
, AVG
, VARIANCE
, VARIANCE
, MIN
e MAX
são suportados para atualização rápida. COUNT(*)
deve ser especificado.- As funções agregadas devem ocorrer apenas como a parte mais externa da expressão. Ou seja, agregados como
AVG(AVG(x))
ou AVG(x)
+ AVG(x)
não são permitidos. - Para cada agregado, como
AVG(expr)
, a COUNT(expr)
correspondente COUNT(expr)
deve estar presente. A Oracle recomenda que SUM(expr)
seja especificado. - Se
VARIANCE(expr)
ou STDDEV(expr
) for especificado, COUNT(expr)
e SUM(expr)
deverão ser especificados. A Oracle recomenda que SUM(expr *expr)
seja especificado. - A coluna
SELECT
na consulta de definição não pode ser uma expressão complexa com colunas de várias tabelas base. Uma solução possível para isso é usar uma exibição materializada aninhada. - A lista
SELECT
deve conter todas as colunas GROUP
BY
. - A visualização materializada não se baseia em uma ou mais tabelas remotas.
- Se você usar um tipo de dados
CHAR
nas colunas de filtro de um log de exibição materializada, os conjuntos de caracteres do site mestre e a exibição materializada deverão ser os mesmos. - Se a visualização materializada tiver um dos seguintes, a atualização rápida será suportada apenas em inserções DML convencionais e cargas diretas.
- Vistas materializadas com agregados
MIN
ou MAX
- Visualizações materializadas que possuem
SUM(expr)
mas não COUNT(expr)
- Visualizações materializadas sem
COUNT(*)
Essa visão materializada é denominada visão materializada somente para inserção. - Uma visualização materializada com
MAX
ou MIN
é rapidamente atualizável após excluir ou misturar instruções DML, se não tiver uma cláusula WHERE
.
A atualização rápida máxima / min após a exclusão ou a DML mista não tem o mesmo comportamento que o caso somente de inserção. Exclui e recalcula os valores máx / min para os grupos afetados. Você precisa estar ciente de seu impacto no desempenho. - As visualizações materializadas com visualizações nomeadas ou subconsultas na cláusula
FROM
podem ser atualizadas rapidamente, desde que as visualizações possam ser completamente mescladas. Para obter informações sobre quais visualizações serão mescladas, consulte Referência de linguagem SQL do banco de dados Oracle . - Se não houver associações externas, você poderá ter seleções e associações arbitrárias na cláusula
WHERE
. - As vistas agregadas materializadas com junções externas são atualizáveis rapidamente após o DML convencional e as cargas diretas, desde que apenas a tabela externa tenha sido modificada. Além disso, restrições exclusivas devem existir nas colunas de junção da tabela de junção interna. Se houver junções externas, todas as junções devem ser conectadas por
AND
e devem usar o operador de igualdade ( =
). - Para visualizações materializadas com
CUBE
, ROLLUP
, conjuntos de agrupamentos ou concatenação deles, aplicam-se as seguintes restrições:
- A lista
SELECT
deve conter um distintivo de agrupamento que pode ser uma função GROUPING_ID
em todas as expressões GROUP
BY
ou funções GROUPING
uma para cada expressão GROUP
BY
. Por exemplo, se a cláusula GROUP
BY
da exibição materializada for " GROUP
BY
CUBE(a, b)
", a lista SELECT
deverá conter " GROUPING_ID(a, b)
" ou " GROUPING(a)
AND
GROUPING(b)
"para que a visualização materializada seja rapidamente atualizada. GROUP
BY
não deve resultar em agrupamentos duplicados. Por exemplo, " GROUP BY a, ROLLUP(a, b)
" não é atualizável rapidamente porque resulta em agrupamentos duplicados " (a), (a, b), AND (a)
".
5.3.8.7 Restrições à atualização rápida em vistas materializadas com UNION ALL
As vistas materializadas com o operador definido UNION
ALL
suportam a opção REFRESH
FAST
se as seguintes condições forem atendidas:
- A consulta de definição deve ter o operador
UNION
ALL
no nível superior.
O operador UNION
ALL
não pode ser incorporado a uma subconsulta, com uma exceção: O UNION
ALL
pode estar em uma subconsulta na cláusula FROM
, desde que a consulta de definição esteja no formato SELECT * FROM
(exibição ou subconsulta com UNION
ALL
), conforme a seguir exemplo:
CRIAR VISTA view_with_unionall AS
(SELECT c.rowid crid, c.cust_id, 2 anos
FROM clientes c WHERE c.cust_last_name = 'Smith'
UNIÃO TUDO
SELECIONAR c.rowid crid, c.cust_id, 3 anos
FROM clientes c WHERE c.cust_last_name = 'Jones');
CRIAR VISTA MATERIALIZADA unionall_inside_view_mv
ATUALIZE RAPIDAMENTE A DEMANDA COMO
SELECT * FROM view_with_unionall;
Observe que a visualização view_with_unionall
atende aos requisitos de atualização rápida. - Cada bloco de consulta na consulta
UNION
ALL
deve atender aos requisitos de uma visualização materializada atualizável rápida com agregados ou uma visualização materializada atualizável rápida com junções.
Os logs apropriados da vista materializada devem ser criados nas tabelas, conforme necessário para o tipo correspondente de vista materializada rápida e atualizável.
Observe que o banco de dados Oracle também permite o caso especial de uma visualização materializada de tabela única com junções somente desde que a coluna ROWID
tenha sido incluída na lista SELECT
e no log de visualização materializada. Isso é mostrado na consulta de definição da visualização view_with_unionall
. - A lista
SELECT
de cada consulta deve incluir um marcador UNION
ALL
e a coluna UNION
ALL
deve ter um valor numérico ou sequência constante distinto em cada ramificação UNION
ALL
. Além disso, a coluna do marcador deve aparecer na mesma posição ordinal na lista SELECT
de cada bloco de consulta. Consulte " Marcação UNION ALL e reescrita de consulta " para obter mais informações sobre marcadores UNION
ALL
. - Alguns recursos, como junções externas, consultas de exibição materializada agregada somente para inserção e tabelas remotas não são suportadas para visualizações materializadas com
UNION
ALL
. Observe, no entanto, que as visualizações materializadas usadas na replicação, que não contêm junções ou agregados, podem ser atualizadas rapidamente quando UNION
ALL
ou tabelas remotas são usadas. - O parâmetro de inicialização de compatibilidade deve ser definido como 9.2.0 ou superior para criar uma visualização materializada atualizável rápida com
UNION
ALL
.
Eu não quero ofender os fãs do Oracle, mas, a julgar pela lista de restrições, parece que esse mecanismo não foi escrito no caso geral usando algum tipo de modelo, mas milhares de indianos, onde todos tinham permissão para escrever seu próprio segmento, e cada um deles podia e fez. Usar esse mecanismo para lógica real é como caminhar em um campo minado. A qualquer momento, você pode obter uma mina, atingindo uma das limitações não óbvias. Como isso funciona também é um problema separado, mas está fora do escopo deste artigo.
Microsoft SQL ServerRequisitos Adicionais
Além das opções SET e dos requisitos de função determinística, os seguintes requisitos devem ser atendidos:
- O usuário que executa
CREATE INDEX
deve ser o proprietário da visualização. - Quando você cria o índice, a opção
IGNORE_DUP_KEY
deve ser definida como OFF (a configuração padrão). - As tabelas devem ser referenciadas por nomes de duas partes, esquema . nome da tabela na definição da visualização.
- As funções definidas pelo usuário mencionadas na visualização devem ser criadas usando a opção
WITH SCHEMABINDING
. - Quaisquer funções definidas pelo usuário mencionadas na visualização devem ser referenciadas por nomes de duas partes, <schema> . <função> .
- A propriedade de acesso a dados de uma função definida pelo usuário deve ser
NO SQL
e a propriedade de acesso externo deve ser NO
. - As funções do Common Language Runtime (CLR) podem aparecer na lista de seleção da exibição, mas não podem fazer parte da definição da chave de índice em cluster. As funções CLR não podem aparecer na cláusula WHERE da exibição ou na cláusula ON de uma operação JOIN na exibição.
- As funções e métodos do CLR dos tipos definidos pelo usuário do CLR usados na definição da visualização devem ter as propriedades configuradas conforme mostrado na tabela a seguir.
- A visualização deve ser criada usando a opção
WITH SCHEMABINDING
. - A visualização deve fazer referência apenas às tabelas base que estão no mesmo banco de dados que a visualização. A visualização não pode fazer referência a outras visualizações.
- A instrução SELECT na definição de exibição não deve conter os seguintes elementos Transact-SQL:
1 A exibição indexada pode conter colunas flutuantes ; no entanto, essas colunas não podem ser incluídas na chave de índice em cluster. - Se
GROUP BY
estiver presente, a definição VIEW deve conter COUNT_BIG(*)
e não deve conter HAVING
. Essas restrições GROUP BY
são aplicáveis apenas à definição de exibição indexada. Uma consulta pode usar uma exibição indexada em seu plano de execução, mesmo que não atenda a essas restrições GROUP BY
. - Se a definição de exibição contiver uma cláusula
GROUP BY
, a chave do índice clusterizado exclusivo poderá fazer referência apenas às colunas especificadas na cláusula GROUP BY
.
Aqui você pode ver que os índios não foram atraídos, como decidiram fazer de acordo com o esquema "faremos pouco, mas bom". Ou seja, eles têm mais minas em campo, mas sua localização é mais transparente. A coisa mais angustiante é essa limitação:
A visualização deve fazer referência apenas às tabelas base que estão no mesmo banco de dados que a visualização. A visualização não pode fazer referência a outras visualizações.
Em nossa terminologia, isso significa que uma função não pode acessar outra função materializada. Isso corta toda a ideologia pela raiz.
Além disso, essa limitação (e mais adiante no texto) reduz bastante os casos de uso:
A instrução SELECT na definição de exibição não deve conter os seguintes elementos Transact-SQL:
JUNTAS EXTERIORES, UNIÃO, ORDEM POR e outros são proibidos. Talvez fosse mais fácil indicar o que pode ser usado do que o que não é. A lista provavelmente seria muito menor.
Resumindo: um enorme conjunto de restrições em cada DBMS (observo comercial) versus nenhum (com exceção de um lógico e não técnico) na tecnologia LGPL. No entanto, deve-se notar que implementar esse mecanismo na lógica relacional é um pouco mais complicado do que no funcional descrito.
Implementação
Como isso funciona? O PostgreSQL é usado como uma "máquina virtual". Dentro, existe um algoritmo complexo que cria consultas. Aqui está o
código fonte . E não há apenas um grande conjunto de heurísticas com vários ifs. Portanto, se você tiver alguns meses para estudar, poderá tentar entender a arquitetura.
Funciona eficientemente? Efetivamente o suficiente. Infelizmente, provar isso é difícil. Só posso dizer que, se você considerar os milhares de solicitações que estão em aplicativos grandes, em média, elas são mais eficazes do que um bom desenvolvedor. Um excelente programador de SQL pode escrever qualquer consulta com mais eficiência, mas com mil consultas, ele simplesmente não terá motivação nem tempo para fazer isso. A única coisa que posso dar agora como evidência de eficácia é que, com base na plataforma construída neste DBMS, vários projetos de
sistemas ERP funcionam nos quais existem milhares de funções MATERIALIZED diferentes, com milhares de usuários e bancos de dados terrabytes com centenas de milhões de registros trabalhando em um servidor de processador duplo comum. No entanto, qualquer pessoa pode testar / refutar a eficácia baixando a
plataforma e o PostgreSQL,
permitindo o log de consultas SQL e tentando alterar a lógica e os dados.
Nos artigos a seguir, também falarei sobre como você pode suspender restrições de funções, trabalhar com sessões de alteração e muito mais.