Cuando haces lo contrario y obtienes lo mismo ...
Al tener la tarea de procesamiento de datos analíticos (computacionales / agregados), debe encontrar un compromiso entre capacidad de respuesta, velocidad y conveniencia.
Algunos sistemas están bien indexados y encontrados, otros pueden calcular y agregar datos rápidamente, mientras que otros son simples. En algún lugar es necesario organizar la precarga e indexación de los datos con todas las dificultades concomitantes, y en algún lugar se proporciona al usuario una abstracción de su modelo de fuente y datos agregados sobre los almacenes y bases de datos físicos incorporados o externos utilizados directamente durante los cálculos. En cualquier caso, el usuario, desde el programador hasta el analista, tiene que hacer un trabajo relativamente grande, comenzando con la preparación de datos en bruto y compilando consultas, modelos informáticos, y terminando con la visualización del resultado en widgets, por supuesto, "Sexy" - hermoso, receptivo y comprensible - de lo contrario todo el trabajo realizado se irá por el desagüe. Y a menudo, por suerte, pasando por la agonía de elegir una solución, notamos cómo una tarea simple y comprensible a primera vista se convierte en un monstruo espeluznante, que es inútil para luchar con los medios disponibles, y necesitamos inventar urgentemente algo: una bicicleta "con blackjack y putas". ©. Nuestra bicicleta funcionó, incluso supera los baches bastante bien y se enfrenta a obstáculos que antes solo se podían adivinar.

A continuación se describirá un lado del dispositivo interno original del "Cubo de Rubik" ficticio: procesamiento computacional para la visualización interactiva de datos.
Una tarea simple debe resolverse de manera simple, y una difícil también debe ser simple, pero más larga ...
Comenzando a crear un sistema con fuerzas pequeñas, pasamos de simple a complejo. Al crear un constructor, estábamos internamente convencidos de que entendemos bien el propósito del sistema, luchando simultáneamente con el deseo de no hacer demasiado y el deseo opuesto de automatizar todo y todo, creando un marco para todo. Además, uno de nuestros maravillosos frameworks estaba listo e incluso probado en producción: jsBeans. Entonces, comenzamos a desarrollar el próximo sistema de procesamiento de datos, que ha crecido y ahora es al mismo tiempo un producto autosuficiente: un diseñador y una plataforma para desarrollar una clase completa de sistemas de procesamiento de datos. Condicionalmente, en el artículo lo llamaremos "Cubo de Rubik" para prescindir de la publicidad, pero para describir soluciones interesantes, en nuestra opinión.
Cubo, rebanada, medida
La tarea principal: tener conjuntos de datos no relacionados, incluidas bases de datos y archivos externos heterogéneos, para formar un modelo multidimensional a partir de elementos interconectados de los datos de origen y los resultados de su procesamiento analítico para la visualización en paneles dinámicos y widgets interconectados.
En pocas palabras, por ejemplo, como un panel de control en el que se puede hacer clic:

Tal modelo de datos multidimensionales en nuestro sistema se llama "Cube" y literalmente representa una colección abstracta de conjuntos de datos mutables llamados "Slice", interconectados por campos / columnas de salida común (mostrados) o campos internos llamados "Dimensiones" y utilizados para filtrar y enlazando rebanadas entre sí.
Un segmento se puede representar como una tabla o vista virtual ( CTE ) con parámetros y un cuerpo de solicitud variable, dependiendo de las condiciones de filtrado. Lo principal es que los datos de salida cambian, dependiendo de las condiciones de búsqueda de contexto (dentro del widget) y el filtro global, que se construye seleccionando valores en los widgets y usando funciones lógicas básicas (AND / OR / NOT) y combinaciones.
El filtro global le permite "rotar el cubo de Rubik", como en el video :
Si el campo de salida de un segmento es al mismo tiempo una medida en otro segmento, tiene el mismo nombre, entonces el sistema percibe los valores de este campo como "hechos" (si hablamos de OLAP ), establecido en forma de un filtro global que cambia los conjuntos de datos originales durante los cálculos y la agregación . Como resultado, hay una interacción dinámica de widgets, en la que los valores de los indicadores mostrados dependen de los elementos y filtros seleccionados.
Un segmento es un conjunto de datos que se pueden cambiar "por mediciones", iniciales o los resultados de cálculos analíticos; caracterizado por campos / columnas de salida, una lista de mediciones compatibles y un conjunto de parámetros con valores predeterminados; descrito por una consulta relativamente elegante en un editor visual que admite filtrado, clasificación, agrupación / agregación, intersecciones (JOIN), uniones (UNION), recursión y otras manipulaciones.
Los sectores que se usan entre sí como fuentes describen la estructura interna de un cubo, por ejemplo:

Ejemplo de segmentación en el editor:

Un segmento admite ambas mediciones especificadas explícitamente en los campos de salida y hereda las mediciones de las fuentes de consulta; esto significa que la salida del segmento se puede cambiar incluso como resultado de cambios en otras fuentes de sectores. En otras palabras, los resultados de la división se pueden filtrar no solo por los campos de salida, sino también por los campos internos-medidas de las fuentes, en algún lugar de la profundidad de la consulta, hasta las tablas de la base de datos primaria.
El sistema expande y cambia la estructura de la consulta automáticamente en el momento de la ejecución, según el filtro global actual y los parámetros de entrada, arrastrándolos más profundamente en la consulta según el modelo de cubo, las medidas declaradas y los segmentos.
Un ejemplo de un filtro global simple, literalmente, cuando un usuario ha confirmado o seleccionado valores en varios widgets:

El filtro global se almacena en una solicitud JSON:

La solicitud llega a la fuente primaria (a la base de datos) ya en forma preparada, después de haber pasado por varias etapas principales:
- Solicite el ensamblaje, incluida la selección e incrustación de sectores óptimos, teniendo en cuenta el filtro global actual (cuando el filtro está ausente o simple, puede seleccionar sectores simples / rápidos; cuando el filtro es complejo: sectores con una estructura compleja y medidas adicionales);
- Incrustar un filtro global y agregar filtros a los cuerpos de consultas y subconsultas;
- Incrustar macros y expresiones de consulta de plantilla;
- Optimización de consultas, incluida la eliminación de campos y expresiones no utilizados;
- Operaciones adicionales con la consulta para los detalles de las bases de datos primarias (por ejemplo, si estamos hablando de SQL y la base de datos no contiene WITH, entonces se incorporan las consultas con nombre).
Y la etapa final es la traducción de la solicitud al formato de la fuente primaria, por ejemplo, en SQL:

Cuando las fuentes son diferentes
Como regla general, todo es simple y claro cuando tiene que trabajar con un único almacén de datos. Pero, cuando hay varios de ellos y son fundamentalmente diferentes, debe aplicar diferentes trucos para cada tarea específica. Y siempre desea tener una solución universal que siempre sea adecuada, preferiblemente lista para usar, con un máximo de modificaciones menores. Para hacer esto, otra abstracción lo suplica: sobre los almacenes de datos, en primer lugar, implementando la armonización de los formatos de consulta y los idiomas, y en segundo lugar, asegurando la interdependencia de los datos, al menos al nivel de condiciones de filtrado adicionales en consultas a una fuente por valores de otra.
Para hacer esto, hemos desarrollado un lenguaje de consulta universal, adecuado tanto para representar un modelo virtual de datos de cubo como para trabajar con almacenamientos arbitrariamente condicionales traduciendo la solicitud al formato e idioma deseados. Por una afortunada coincidencia, el lenguaje de consulta, originalmente pensado para el mapeo simple y el filtrado de datos de varias fuentes, se ha convertido fácilmente en un lenguaje completo de búsqueda y procesamiento de datos que permite construir construcciones computacionales desde lo más simple hasta lo más complejo en varias páginas y con muchas subconsultas.
Las fuentes se pueden dividir en tres tipos:
- archivos de datos que requieren descarga al sistema;
- Bases de datos que admiten el procesamiento completo de datos y otras operaciones;
- almacenes que solo admiten extracción de datos con o sin filtrado, incluidos varios tipos de servicios externos.
Todo es inequívoco con el primer tipo: el módulo de importación está integrado en el sistema, que analiza varios formatos de entrada y sumerge los resultados en el repositorio. Para la importación, también se ha desarrollado un constructor especial, que debe discutirse por separado.
El segundo tipo son las bases de datos autónomas, para las cuales solo necesita traducir la solicitud original al formato deseado y al idioma de la solicitud, el dialecto.
El tercer tipo requiere al menos datos de procesamiento posterior. Y todos los tipos al mismo tiempo también pueden requerir un procesamiento posterior: intersecciones, uniones, agregación y cálculos finales. Esto sucede cuando el procesamiento de datos en una base de datos debe realizarse teniendo en cuenta los resultados de filtrado en otro externo.
El ejemplo más simple es cuando se realiza una búsqueda difusa en una base de datos, y en la salida es necesario obtener una agregación de indicadores almacenados en otra base de datos en otro servidor, teniendo en cuenta los resultados de la búsqueda.
Para implementar el trabajo de dicho esquema, se implementa un algoritmo simple en nuestro sistema: la solicitud inicial es preparada simultáneamente por varios intérpretes, cada uno de los cuales puede negarse a ejecutar la solicitud cuando es incompatible, o devolver un iterador con datos, o transformar la solicitud e iniciar el trabajo de la siguiente cadena de preparación de solicitud por otro intérprete . Como resultado, para una sola solicitud, obtenemos de uno a varios iteradores perezosos que forman el mismo resultado, pero de diferentes maneras, de los cuales se selecciona el mejor (de acuerdo con varios criterios definidos por el desarrollador en la configuración).
La estrategia de selección del iterador se especifica en los parámetros de configuración o consulta. Actualmente, se admiten varias estrategias principales:
- primero, cualquiera, último;
- por tipo de prioridad de la base de datos;
- por prioridad de las cadenas que formaron los iteradores;
- por la función de peso de la "ponderación de solicitud";
- de acuerdo con el primer resultado: todos los iteradores se lanzan en paralelo y se espera el primer resultado, como resultado, se utiliza el iterador más rápido y el resto están cerrados.
Como resultado de tal combinación para una solicitud de entrada, obtenemos varias opciones para su ejecución, tanto usando diferentes fuentes como con diferentes estrategias de ejecución: elegir la base de datos principal / objetivo en la que se ejecutará la parte principal de la solicitud y el ensamblaje final de resultados.
Si el DBMS de destino admite la conexión de fuentes externas, entonces es posible crear un circuito inverso en el que el DBMS está conectado a la API del sistema para recibir pequeñas cantidades de datos del sistema, por ejemplo, para filtrar grandes volúmenes "en su lugar". Dicha integración es transparente para el usuario final y el analista: el modelo de cubo no cambia y el sistema realiza automáticamente todas las operaciones.

Para casos más complejos, el sistema implementa un intérprete interno de consultas en memoria en el notable motor de base de datos H2 Embedded, que permite integrar cualquier base de datos compatible de forma inmediata. Literalmente, funciona así: la solicitud se divide en partes por grupos de fuentes, enviados para su ejecución, después de lo cual el ensamblaje y el procesamiento final de los resultados se realizan en la memoria, en H2.
A primera vista, este esquema de integración de datos a nivel de intérprete interno parece "difícil", y esto es cierto si tiene que trabajar con grandes volúmenes de datos de entrada y la necesidad de realizar cálculos después de intersecciones y asociaciones de conjuntos de fuentes externas. De hecho, esta circunstancia está parcialmente nivelada: al mismo tiempo, la solicitud es ejecutada por varios manejadores en diferentes versiones, por lo tanto, el intérprete se usa solo en los casos más extremos, como una solución universal lista para usar. En última instancia, cualquier integración está limitada por los costos de transporte típicos de preparación, transmisión a través de la red y recepción de datos, y esta es una tarea completamente diferente.
Lado técnico
Desde el punto de vista técnico, que probablemente no pueda prescindir, tocando este tema, el sistema también está diseñado de acuerdo con el principio: hacer más, pero simplificar todo lo más posible.
El sistema de procesamiento de datos se implementa sobre el marco cliente-servidor jsBeans como un conjunto de módulos adicionales y proyectos de ensamblaje específicos. jsBeans, a su vez, se implementa en Java, funciona como un servidor de aplicaciones, en general es un conjunto de Rhino, Jetty y Akka, y también incluye tecnología de cliente-servidor desarrollada por nuestro equipo y una rica biblioteca de componentes ensamblados durante varios años de aplicación exitosa.
El cubo de Rubik se implementa completa y completamente en JavaScript en forma de muchos js-bins (archivos * .jsb), algunos de los cuales operan solo en el servidor. La otra parte está en el cliente, y el resto es un componente componente, que funciona como un todo distribuido, cuyas partes interactúan entre sí, transparente para el desarrollador, pero bajo su control. Js-beans puede tener diferentes estrategias de vida, por ejemplo, con o sin una sesión de usuario, y mucho más. El bean es isomorfo; permite que tanto el cliente como el servidor trabajen con él como una instancia virtual de una clase regular. Un contenedor se describe mediante un solo archivo e incluye tres secciones: para campos y métodos que se ejecutan en el cliente, para campos de servidor, así como una sección de campos sincronizados comunes.
Dado que el artículo ya resultó ser detallado para no aburrir a los lectores, es hora de proceder a su finalización, con la intención de describir pronto los detalles y las soluciones arquitectónicas más interesantes en la implementación de JsBeans y nuestros proyectos basados en él: el subsistema de visualización construido, los procesos analíticos, el diseñador ontológico de las áreas temáticas, el lenguaje consultas, importación de datos y algo más ...
Por qué
Esto nunca ha sucedido antes, y aquí de nuevo ...
Inicialmente, había pocos conjuntos de datos primarios. Las áreas temáticas y las tareas se especificaron por completo. Parecería, ¿por qué tal tormento? La tarea parecía simple, todos querían obtener el resultado de inmediato, especialmente cuando la solución rápida yacía en la superficie, y la correcta requería perseverancia y decisiones equilibradas, observando la configuración original. Fuimos en la dirección opuesta, desde soluciones complejas y largas hasta soluciones simples y rápidas, en el camino de generalizar problemas particulares.
La condición principal es que los nuevos paneles de control se deben construir rápidamente, incluso si el nuevo área temática y las necesidades analíticas son muy diferentes de las anteriores. Obviamente no adivinará ni siquiera la mitad de los requisitos futuros, el sistema debe ser flexible en primer lugar. La refinación de la biblioteca de componentes, los algoritmos analíticos, la conexión de nuevos tipos de fuentes es una parte integral de la adaptación del sistema. En otras palabras, el grupo ha funcionado: los analistas crean consultas y paneles, y los programadores se dan cuenta rápidamente de las nuevas necesidades para ellos. Y nosotros, como programadores, inicialmente buscamos simplificar nuestro trabajo en el futuro, tratando de no dañar la usabilidad.
Y el sistema se creó inmediatamente de manera universal y adaptativa: construimos un "constructor por constructor", desarrollando un marco sobre un marco creado previamente con un propósito similar, pero aún más general.
La calificación de las escuelas de Moscú basada en los resultados del Examen Estatal Unificado y las Olimpiadas es un ejemplo de un tablero construido de la manera descrita anteriormente a partir de la descarga del portal de datos abiertos del Gobierno de Moscú.
Cube-Rubik es una plataforma básica para el desarrollo de sistemas de información y análisis. Diseñado como una rama y una continuación lógica de jsBeans. Incluye todos los módulos necesarios para resolver los problemas de recolección, procesamiento, análisis (computacional y orientado a procesos) y visualización.
jsBeans es un marco web isomorfo de pila completa que implementa la tecnología de bean de JavaScript cliente-servidor, desarrollada con una licencia abierta como herramienta universal. Durante el uso, se ha demostrado bien, en la mayoría de los casos, encajando idealmente en las tareas que tenemos ante nosotros.