Quando você faz o oposto e consegue o mesmo ...
Tendo a tarefa de processar dados analíticos (computacionais / agregados), é necessário encontrar um compromisso entre capacidade de resposta, velocidade e conveniência.
Alguns sistemas são bem indexados e encontrados, outros podem calcular e agregar dados rapidamente, enquanto outros são simples. Em algum lugar, é necessário organizar o pré-carregamento e a indexação de dados com todas as dificuldades presentes e, em algum lugar, o usuário recebe uma abstração de seu modelo de origem e dados agregados sobre os armazenamentos e bancos de dados físicos internos ou externos usados diretamente durante os cálculos. De qualquer forma, o usuário, do programador ao analista, precisa executar um trabalho relativamente grande, começando com a preparação de dados brutos e a compilação de consultas, o cálculo de modelos e a visualização do resultado em widgets, é claro, "Sexy" - bonito, responsivo e compreensível - caso contrário todo o trabalho feito vai pelo ralo. E, muitas vezes, por sorte, passando pela dificuldade de escolher uma solução, notamos como uma tarefa simples e compreensível à primeira vista se transforma em um monstro assustador, que é inútil para lutar com os meios disponíveis, e precisamos inventar urgentemente algo - uma bicicleta "com blackjack e prostitutas" ©. Nossa bicicleta funcionou, até dá voltas nos solavancos e lida com obstáculos que antes só se podia adivinhar.

A seguir, será descrito um lado do dispositivo interno original do fictício "Cubo de Rubik" - processamento computacional para visualização interativa de dados.
Uma tarefa simples deve ser resolvida com simplicidade, e uma tarefa difícil também deve ser simples, mas mais longa ...
Começando a criar um sistema com pequenas forças, passamos do simples ao complexo. Criando um construtor, estávamos internamente convencidos de que entendemos bem o objetivo do sistema, lutando simultaneamente com o desejo de não fazer muito e o desejo oposto de automatizar tudo e tudo, criando uma estrutura para tudo. Além disso, uma de nossas estruturas maravilhosas estava pronta e até testada em produção - jsBeans. Então, começamos a desenvolver o próximo sistema de processamento de dados, que cresceu e agora é ao mesmo tempo um produto auto-suficiente - um designer e uma plataforma para o desenvolvimento de toda uma classe de sistemas de processamento de dados. Condicionalmente, no artigo, o chamaremos de “Cubo de Rubik”, a fim de ficar sem publicidade, mas para descrever soluções interessantes, em nossa opinião,.
Cubo, fatia, medição
A principal tarefa - ter conjuntos de dados não relacionados, incluindo bancos de dados e arquivos externos heterogêneos, para formar um modelo multidimensional a partir de elementos interconectados dos dados de origem e os resultados de seu processamento analítico para visualização em painéis dinâmicos e widgets interconectados.
Simplificando, por exemplo, como um painel clicável:

Esse modelo de dados multidimensional em nosso sistema é chamado de "Cubo" e representa literalmente uma coleção abstrata de conjuntos de dados mutáveis chamados "Fatia", interconectados por campos / colunas de saída comuns (exibidos) ou campos internos chamados "Dimensões" e usados para filtrar e vinculando fatias entre si.
Uma fatia pode ser representada como uma tabela ou exibição virtual ( CTE ) com parâmetros e um corpo de solicitação variável, dependendo das condições de filtragem. O principal é que os dados de saída mudam, dependendo das condições de pesquisa de contexto (dentro do widget) e do filtro global, que é criado selecionando valores nos widgets e usando funções lógicas básicas (AND / OR / NOT) e combinações.
O filtro global permite "girar o cubo de Rubik", como no vídeo :
Se o campo de saída de uma fatia for ao mesmo tempo uma medida em outra fatia, tiver o mesmo nome, os valores desse campo serão percebidos pelo sistema como "fatos" (se estivéssemos falando sobre OLAP ), configurados na forma de um filtro global que altera os conjuntos de dados originais durante os cálculos e agregação . Como resultado, há uma interação dinâmica de widgets, na qual os valores dos indicadores exibidos dependem dos elementos e filtros selecionados.
Uma fatia é um conjunto de dados que podem ser alterados "por medições" - iniciais ou os resultados de cálculos analíticos; caracterizado por campos / colunas de saída, uma lista de medições suportadas e um conjunto de parâmetros com valores padrão; descrito por uma consulta relativamente elegante em um editor visual que suporta filtragem, classificação, agrupamento / agregação, interseções (JOIN), uniões (UNION), recursão e outras manipulações.
Fatias que se usam como fontes descrevem a estrutura interna de um cubo, por exemplo:

Exemplo de fatiador no editor:

Uma fatia suporta as duas medidas especificadas explicitamente nos campos de saída e herda as medidas das fontes de consulta - isso significa que a saída da fatia pode ser alterada mesmo como resultado de alterações em outras fontes de fatias. Em outras palavras, os resultados da fatia podem ser filtrados não apenas pelos campos de saída, mas também pelos campos internos - medições das fontes, em algum lugar na profundidade da consulta, até as tabelas principais do banco de dados.
A estrutura da consulta é expandida e alterada pelo sistema automaticamente no momento da execução, dependendo do filtro global atual e dos parâmetros de entrada, arrastando-os mais profundamente para a consulta de acordo com o modelo de cubo, medições declaradas e fatias.
Um exemplo de um filtro global simples, literalmente, quando um usuário confirma ou seleciona valores em vários widgets:

O filtro global é armazenado em uma solicitação JSON:

A solicitação chega à fonte primária (ao banco de dados) já na forma preparada, tendo passado por várias etapas principais:
- Solicitar montagem, incluindo seleção e incorporação de fatias ideais, levando em consideração o filtro global atual (quando o filtro está ausente ou simples, você pode selecionar fatias simples / rápidas; quando o filtro é complexo - fatias com uma estrutura complexa e medições adicionais);
- Incorporar um filtro global e adicionar filtros aos corpos de consultas e subconsultas;
- Incorporar macros e expressões de consulta de modelo;
- Otimização de consultas, incluindo a remoção de campos e expressões não utilizados;
- Operações adicionais com a consulta para as especificidades dos bancos de dados primários (por exemplo, se estamos falando de SQL e o banco de dados não contém WITH, as consultas nomeadas são incorporadas).
E a etapa final é a tradução da solicitação no formato da fonte primária, por exemplo, no SQL:

Quando as fontes são diferentes
Como regra, tudo é simples e claro quando você precisa trabalhar com um único data warehouse. Porém, quando houver vários deles e eles forem fundamentalmente diferentes - você precisará aplicar truques diferentes para cada tarefa específica. E você sempre deseja ter uma solução universal que seja sempre adequada, de preferência pronta para uso, com o máximo de pequenas modificações. Para fazer isso, outra abstração o implora: sobre os data warehouses, em primeiro lugar, implementando a harmonização dos formatos e linguagens de consulta e, em segundo lugar, garantindo a interdependência dos dados, pelo menos no nível de condições de filtragem adicionais nas consultas a uma fonte por valores de outra.
Para isso, desenvolvemos uma linguagem de consulta universal, adequada tanto para representar um modelo virtual de dados de cubo quanto para trabalhar com armazenamentos condicionalmente arbitrários, convertendo a solicitação no formato e idioma desejados. Por uma feliz coincidência, a linguagem de consulta, originalmente destinada ao mapeamento e filtragem simples de dados de várias fontes, tornou-se facilmente uma linguagem completa de pesquisa e processamento de dados que permite construir construções computacionais da mais simples à mais complexa em várias páginas e com muitas subconsultas.
As fontes podem ser divididas em três tipos:
- arquivos de dados que requerem download para o sistema;
- Bancos de dados que suportam o processamento completo de dados e outras operações;
- armazenamentos que suportam apenas extração de dados com ou sem filtragem, incluindo vários tipos de serviços externos.
Tudo é inequívoco com o primeiro tipo - o módulo de importação é integrado ao sistema, que analisa vários formatos de entrada e mergulha os resultados no repositório. Para importação, também foi desenvolvido um construtor especial, que deve ser discutido separadamente.
O segundo tipo são bancos de dados independentes, para os quais você só precisa traduzir a solicitação original para o formato e o idioma desejados, o dialeto.
O terceiro tipo requer pelo menos dados de pós-processamento. E todos os tipos ao mesmo tempo também podem exigir pós-processamento - interseções, uniões, agregação e cálculos finais. Isso acontece quando o processamento de dados em um banco de dados precisa ser realizado, levando em consideração os resultados da filtragem em outro externo.
O exemplo mais simples é quando uma pesquisa difusa é realizada em um banco de dados e na saída é necessário obter uma agregação de indicadores armazenados em outro banco de dados em outro servidor, levando em consideração os resultados da pesquisa.
Para implementar o trabalho desse esquema, um algoritmo simples é implementado em nosso sistema - a solicitação inicial é preparada simultaneamente por vários intérpretes, cada um dos quais pode se recusar a executar a solicitação quando é incompatível, ou retornar um iterador com dados, ou transformar a solicitação e iniciar o trabalho da próxima cadeia de preparação de solicitações por outro intérprete. . Como resultado, para uma única solicitação, obtemos de um a vários iteradores preguiçosos que formam o mesmo resultado, mas de maneiras diferentes, nas quais o melhor é selecionado (de acordo com vários critérios definidos pelo desenvolvedor na configuração).
A estratégia de seleção do iterador é especificada nos parâmetros de configuração ou consulta. Atualmente, várias estratégias principais são suportadas:
- primeiro, qualquer último;
- por tipo de prioridade do banco de dados;
- por prioridade das cadeias que formaram os iteradores;
- pela função de peso da "ponderação de solicitação";
- de acordo com o primeiro resultado - todos os iteradores são lançados em paralelo e o primeiro resultado é esperado; como resultado, o iterador mais rápido é usado e o restante é fechado.
Como resultado dessa combinação para uma solicitação de entrada, obtemos várias opções para sua execução, usando diferentes fontes e com diferentes estratégias de execução - escolhendo o banco de dados principal / destino no qual a parte principal da solicitação será executada e a montagem final dos resultados.
Se o DBMS de destino suportar a conexão de fontes externas, será possível criar um circuito reverso no qual o DBMS esteja conectado à API do sistema para receber pequenas quantidades de dados do sistema, por exemplo, para filtrar grandes volumes "no local". Essa integração é transparente para o usuário final e o analista - o modelo do cubo não muda e todas as operações são executadas automaticamente pelo sistema.

Para casos mais complexos, o sistema implementa um intérprete de consulta na memória interno no notável mecanismo de banco de dados H2 Embedded, que permite integrar qualquer banco de dados suportado imediatamente. Literalmente, funciona assim - a solicitação é dividida em partes por grupos de fontes, enviadas para execução, após as quais a montagem e o processamento final dos resultados são realizados na memória, em H2.
À primeira vista, esse esquema de integração de dados no nível do intérprete interno parece "difícil", e isso é verdade se você tiver que trabalhar com grandes volumes de dados de entrada e a necessidade de executar cálculos após interseções e associações de conjuntos de fontes externas. De fato, essa circunstância é parcialmente nivelada - ao mesmo tempo, a solicitação é executada por vários manipuladores em versões diferentes; portanto, o intérprete é usado apenas nos casos mais extremos, como uma solução universal pronta para uso. Por fim, qualquer integração é limitada pelos custos de transporte típicos de preparação, transmissão pela rede e recebimento de dados, e essa é uma tarefa completamente diferente.
Lado técnico
Do lado técnico, que você provavelmente não pode prescindir, abordando esse tópico, o sistema também foi projetado de acordo com o princípio - para fazer mais, mas simplificar tudo o máximo possível.
O sistema de processamento de dados é implementado na parte superior da estrutura cliente-servidor do jsBeans como um conjunto de módulos adicionais e projetos de montagem específicos. O jsBeans, por sua vez, é implementado em Java, funciona como um servidor de aplicativos, em geral é um monte de Rhino, Jetty e Akka, e também inclui a tecnologia de bean cliente-servidor desenvolvida por nossa equipe e uma rica biblioteca de componentes montados ao longo de vários anos de aplicação bem-sucedida.
O Cubo de Rubik é completamente e completamente implementado em JavaScript na forma de muitos js-bins (arquivos * .jsb), alguns dos quais operam apenas no servidor. A outra parte está no cliente e o restante é um componente componente, funcionando como um todo distribuído, cujas partes interagem umas com as outras, transparentes para o desenvolvedor, mas sob seu controle. Os binários Js podem ter estratégias de vida diferentes, por exemplo, com ou sem uma sessão do usuário e muito mais. O bean é isomórfico; permite que o cliente e o servidor trabalhem com ele como uma instância virtual de uma classe regular. A lixeira é descrita por um único arquivo e inclui três seções - para campos e métodos em execução no cliente, para campos do servidor, bem como uma seção de campos sincronizados comuns.
Como o artigo já se mostrou detalhado para não aborrecer os leitores, é hora de concluir, com a intenção de descrever em breve os detalhes e as soluções arquiteturais mais interessantes na implementação do JsBeans e nossos projetos baseados nele - o subsistema de visualização construído, processos analíticos, designer ontológico de áreas de assunto, linguagem consultas, importação de dados e outra coisa ...
Porque
Isso nunca aconteceu antes, e aqui novamente ...
Inicialmente, havia poucos conjuntos de dados primários. As áreas e as tarefas foram completamente especificadas. Parece, por que tal tormento? A tarefa parecia simples, todos queriam obter o resultado imediatamente - especialmente quando a solução rápida estava na superfície, e a certa exigia perseverança e decisões equilibradas, observando a configuração original. Seguimos na direção oposta, de soluções complexas e longas a simples e rápidas, no caminho de generalizar problemas específicos.
A principal condição é que novos painéis sejam construídos rapidamente, mesmo que a nova área de assunto e as necessidades analíticas sejam muito diferentes das anteriores. Obviamente, você não vai adivinhar nem a metade dos requisitos futuros; o sistema deve ser flexível em primeiro lugar. O refinamento da biblioteca de componentes, algoritmos analíticos, conexão de novos tipos de fontes é parte integrante da adaptação do sistema. Em outras palavras, o grupo funcionou - os analistas criam consultas e painéis e os programadores percebem rapidamente novas necessidades para eles. E nós, como programadores, inicialmente procuramos simplificar nosso trabalho no futuro, tentando não prejudicar a usabilidade.
E o sistema foi imediatamente criado universal e adaptável - construímos um “construtor por construtor”, desenvolvendo uma estrutura sobre uma estrutura criada anteriormente com um objetivo semelhante, mas ainda mais geral.
A classificação das escolas de Moscou com base nos resultados do Exame Estatal Unificado e das Olimpíadas é um exemplo de painel construído da maneira descrita acima a partir do descarregamento do portal de dados abertos do governo de Moscou.
Cube-Rubik é uma plataforma básica para o desenvolvimento de informações e sistemas analíticos. Projetado como uma ramificação e uma continuação lógica do jsBeans. Inclui todos os módulos necessários para resolver os problemas de coleta, processamento, análise (computacional e orientada a processos) e visualização.
O jsBeans é uma estrutura da Web isomórfica de pilha completa que implementa a tecnologia de bean JavaScript cliente-servidor, desenvolvida com uma licença aberta como ferramenta universal. Durante o uso, ele se provou bem, na maioria dos casos, idealmente adequado às tarefas que temos diante de nós.