Wenn Sie das Gegenteil tun und das Gleiche bekommen ...
Bei der analytischen (rechnerischen / aggregierten) Datenverarbeitung müssen Sie einen Kompromiss zwischen Reaktionsfähigkeit, Geschwindigkeit und Komfort finden.
Einige Systeme sind gut indiziert und gefunden, andere können Daten schnell berechnen und aggregieren, während andere einfach sind. Irgendwo ist es notwendig, das Vorladen und Indizieren von Daten mit allen damit verbundenen Schwierigkeiten zu organisieren, und irgendwo wird dem Benutzer eine Abstraktion seines Modells von Quell- und aggregierten Daten zusätzlich zu den integrierten oder externen physischen Speichern und Datenbanken bereitgestellt, die direkt während der Berechnungen verwendet werden. In jedem Fall muss der Benutzer, vom Programmierer bis zum Analysten, einen relativ großen Job erledigen, angefangen bei der Vorbereitung der Rohdaten und dem Kompilieren von Abfragen, der Berechnung von Modellen bis hin zur Visualisierung des Ergebnisses auf Widgets, natürlich „Sexy“ - schön, reaktionsschnell und verständlich - ansonsten Alle geleistete Arbeit wird den Bach runtergehen. Und oft, wie es das Glück wollte, bemerken wir bei der Auswahl einer Lösung, wie aus einer einfachen und verständlichen Aufgabe auf den ersten Blick ein gruseliges Monster wird, das mit den verfügbaren Mitteln nicht zu bekämpfen ist, und wir müssen dringend etwas erfinden - ein Fahrrad "mit Blackjack und Huren". ©. Unser Fahrrad ist gefahren, es geht sogar ziemlich gut um die Unebenheiten herum und bewältigt Hindernisse, die man vorher nur erraten konnte.

Im Folgenden wird eine Seite des ursprünglichen internen Geräts des fiktiven "Rubik's Cube" beschrieben - Computerverarbeitung für die interaktive Datenvisualisierung.
Eine einfache Aufgabe sollte einfach gelöst werden, und eine schwierige sollte auch einfach sein, aber länger ...
Wir begannen, ein System mit kleinen Kräften zu schaffen, und gingen von einfach zu komplex über. Bei der Erstellung eines Konstruktors waren wir intern davon überzeugt, dass wir den Zweck des Systems gut verstehen und gleichzeitig mit dem Wunsch kämpfen, nicht zu viel zu tun, und dem entgegengesetzten Wunsch, alles und jedes zu automatisieren und einen Rahmen für alles zu schaffen. Darüber hinaus war eines unserer wunderbaren Frameworks fertig und wurde sogar in der Produktion getestet - jsBeans. Deshalb haben wir mit der Entwicklung des nächsten Datenverarbeitungssystems begonnen, das gewachsen ist und jetzt gleichzeitig ein autarkes Produkt ist - ein Designer und eine Plattform für die Entwicklung einer ganzen Klasse von Datenverarbeitungssystemen. Bedingt werden wir es in dem Artikel "Rubik's Cube" nennen, um auf Werbung zu verzichten, aber unserer Meinung nach interessante Lösungen zu beschreiben.
Würfel, Scheibe, Messung
Die Hauptaufgabe besteht darin, aus nicht zusammenhängenden Datensätzen, einschließlich heterogener externer Datenbanken und Dateien, ein mehrdimensionales Modell aus miteinander verbundenen Elementen der Quelldaten und den Ergebnissen ihrer analytischen Verarbeitung zur Visualisierung auf dynamischen Dashboards und miteinander verbundenen Widgets zu erstellen.
Einfach ausgedrückt als anklickbares Dashboard:

Ein solches mehrdimensionales Datenmodell in unserem System heißt "Cube" und stellt buchstäblich eine abstrakte Sammlung von veränderlichen Datensätzen dar, die als "Slice" bezeichnet werden und durch gemeinsame Ausgabefelder / -spalten oder interne Felder mit der Bezeichnung "Dimensions" miteinander verbunden sind und zum Filtern von und verwendet werden Slices miteinander verbinden.
Ein Slice kann abhängig von den Filterbedingungen als virtuelle Tabelle oder Ansicht ( CTE ) mit Parametern und einem variablen Anforderungshauptteil dargestellt werden. Die Hauptsache ist, dass sich die Ausgabedaten abhängig von den Kontextsuchbedingungen (innerhalb des Widgets) und dem globalen Filter ändern, der durch Auswahl von Werten in den Widgets und Verwendung grundlegender Logikfunktionen (UND / ODER / NICHT) und Kombinationen erstellt wird.
Mit dem globalen Filter können Sie den Zauberwürfel wie im Video drehen:
Wenn das Ausgabefeld eines Slice gleichzeitig eine Messung in einem anderen Slice ist und denselben Namen hat, werden die Werte dieses Felds vom System als „Fakten“ (wenn es sich um OLAP handelt ) wahrgenommen, die in Form eines globalen Filters festgelegt sind, der die ursprünglichen Datensätze während der Berechnungen und Aggregation ändert . Infolgedessen kommt es zu einer dynamischen Interaktion von Widgets, bei der die Werte der angezeigten Indikatoren von den ausgewählten Elementen und Filtern abhängen.
Ein Slice ist ein Datensatz, der "durch Messungen" geändert werden kann - ursprünglich oder als Ergebnis analytischer Berechnungen. gekennzeichnet durch Ausgabefelder / -spalten, eine Liste der unterstützten Messungen und eine Reihe von Parametern mit Standardwerten; beschrieben durch eine relativ elegante Abfrage in einem visuellen Editor, der Filterung, Sortierung, Gruppierung / Aggregation, Schnittpunkte (JOIN), Gewerkschaften (UNION), Rekursion und andere Manipulationen unterstützt.
Slices, die sich gegenseitig als Quellen verwenden, beschreiben die interne Struktur eines Cubes, zum Beispiel:

Slicer-Beispiel im Editor:

Ein Slice unterstützt beide Messungen, die explizit in den Ausgabefeldern angegeben sind, und erbt Messungen von den Abfragequellen. Dies bedeutet, dass die Ausgabe des Slice auch aufgrund von Änderungen in anderen Slices-Quellen geändert werden kann. Mit anderen Worten, die Ergebnisse des Slice können nicht nur nach den Ausgabefeldern, sondern auch nach den internen Feldmessungen der Quellen irgendwo in der Tiefe der Abfrage bis zu den primären Datenbanktabellen gefiltert werden.
Die Abfragestruktur wird zum Zeitpunkt der Ausführung vom System automatisch erweitert und geändert. Dies hängt von den aktuellen globalen Filter- und Eingabeparametern ab und zieht sie je nach Cube-Modell, deklarierten Messungen und Slices tiefer in die Abfrage.
Ein Beispiel für einen einfachen globalen Filter im wahrsten Sinne des Wortes, wenn ein Benutzer Werte für mehrere Widgets festgeschrieben oder ausgewählt hat:

Der globale Filter wird in einer JSON-Anforderung gespeichert:

Die Anfrage kommt bereits in vorbereiteter Form an die Primärquelle (an die Datenbank), nachdem mehrere Hauptphasen durchlaufen wurden:
- Anforderungsassemblierung, einschließlich Auswahl und Einbettung optimaler Slices unter Berücksichtigung des aktuellen globalen Filters (wenn der Filter fehlt oder einfach ist, können Sie einfache / schnelle Slices auswählen; wenn der Filter komplex ist - Slices mit komplexer Struktur und zusätzlichen Messungen);
- Einbetten eines globalen Filters und Hinzufügen von Filtern zu den Hauptteilen von Abfragen und Unterabfragen;
- Einbetten von Makros und Vorlagenabfrageausdrücken;
- Abfrageoptimierung, einschließlich Entfernen nicht verwendeter Felder und Ausdrücke;
- Zusätzliche Operationen mit der Abfrage für die Besonderheiten von Primärdatenbanken (wenn beispielsweise über SQL gesprochen wird und die Datenbank kein WITH enthält, werden benannte Abfragen eingebettet).
Die letzte Phase ist die Übersetzung der Anforderung in das Format der Primärquelle, beispielsweise in SQL:

Wenn die Quellen unterschiedlich sind
In der Regel ist alles einfach und klar, wenn Sie mit einem einzigen Data Warehouse arbeiten müssen. Wenn es jedoch mehrere davon gibt und diese sich grundlegend unterscheiden, müssen Sie für jede bestimmte Aufgabe unterschiedliche Tricks anwenden. Und Sie möchten immer eine universelle Lösung haben, die immer geeignet ist, vorzugsweise sofort einsatzbereit, mit maximal geringfügigen Änderungen. Zu diesem Zweck wird eine andere Abstraktion benötigt: Über Data Warehouses wird zum einen die Koordination von Abfrageformaten und -sprachen realisiert und zum anderen die gegenseitige Abhängigkeit der Daten sichergestellt, zumindest auf der Ebene zusätzlicher Filterbedingungen bei Abfragen an eine Quelle durch Werte aus einer anderen.
Zu diesem Zweck haben wir eine universelle Abfragesprache entwickelt, die sowohl zur Darstellung eines virtuellen Modells von Cube-Daten als auch zur Arbeit mit bedingt beliebigen Speichern geeignet ist, indem die Anforderung in das gewünschte Format und die gewünschte Sprache übersetzt wird. Durch einen glücklichen Zufall hat sich die Abfragesprache, die ursprünglich für die einfache Zuordnung und Filterung von Daten aus verschiedenen Quellen vorgesehen war, leicht zu einer vollwertigen Sprache für die Suche und Verarbeitung von Daten entwickelt, die es ermöglicht, auf mehreren Seiten und mit vielen Unterabfragen Rechenkonstruktionen von der einfachsten bis zur komplexesten zu erstellen.
Quellen können in drei Typen unterteilt werden:
- Datendateien, die auf das System heruntergeladen werden müssen;
- Datenbanken, die die vollständige Datenverarbeitung und andere Vorgänge unterstützen;
- Speicher, die nur die Datenextraktion mit oder ohne Filterung unterstützen, einschließlich verschiedener Arten von externen Diensten.
Beim ersten Typ ist alles eindeutig - das Importmodul ist in das System integriert, das verschiedene Eingabeformate analysiert und die Ergebnisse in das Repository eintaucht. Für den Import wurde auch ein spezieller Konstruktor entwickelt, der separat besprochen werden sollte.
Der zweite Typ sind in sich geschlossene Datenbanken, für die Sie nur die ursprüngliche Anfrage in das gewünschte Format und die gewünschte Sprache der Anfrage, den Dialekt, übersetzen müssen.
Der dritte Typ erfordert mindestens Nachbearbeitungsdaten. Und alle Typen gleichzeitig können auch eine Nachbearbeitung erfordern - Schnittpunkte, Gewerkschaften, Aggregation und endgültige Berechnungen. Dies geschieht, wenn die Datenverarbeitung in einer Datenbank unter Berücksichtigung der Filterergebnisse in einer anderen externen Datenbank durchgeführt werden muss.
Das einfachste Beispiel ist, wenn eine Fuzzy-Suche in einer Datenbank durchgeführt wird und am Ausgang eine Aggregation von Indikatoren abgerufen werden muss, die in einer anderen Datenbank auf einem anderen Server gespeichert sind, wobei die Suchergebnisse berücksichtigt werden.
Um die Arbeit eines solchen Schemas zu implementieren, wird in unserem System ein einfacher Algorithmus implementiert - die anfängliche Anforderung wird gleichzeitig von mehreren Interpreten vorbereitet, von denen jeder entweder die Ausführung der Anforderung ablehnen kann, wenn sie nicht kompatibel ist, oder einen Iterator mit Daten zurückgeben oder die Anforderung konvertieren und die Arbeit der nächsten Anforderungsvorbereitungskette durch einen anderen Interpreter initiieren kann . Als Ergebnis erhalten wir für eine einzelne Anforderung einen bis mehrere faule Iteratoren, die das gleiche Ergebnis erzielen, jedoch auf unterschiedliche Weise, aus denen das beste ausgewählt wird (gemäß verschiedenen Kriterien, die vom Entwickler in der Konfiguration definiert wurden).
Die Iteratorauswahlstrategie wird in den Konfigurations- oder Abfrageparametern angegeben. Derzeit werden mehrere wichtige Strategien unterstützt:
- zuerst, jeder, zuletzt;
- nach Prioritätstyp der Datenbank;
- nach Priorität der Ketten, die die Iteratoren bildeten;
- durch die Gewichtsfunktion der "Anforderungsgewichtung";
- Gemäß dem ersten Ergebnis werden alle Iteratoren parallel gestartet und das erste Ergebnis wird erwartet. Infolgedessen wird der schnellste Iterator verwendet, der Rest wird geschlossen.
Als Ergebnis einer solchen Kombination für eine Eingabeanforderung erhalten wir mehrere Optionen für deren Ausführung, sowohl unter Verwendung unterschiedlicher Quellen als auch mit unterschiedlichen Ausführungsstrategien - Auswahl der Haupt- / Zieldatenbank, in der der Hauptteil der Anforderung ausgeführt wird, und der endgültigen Zusammenstellung der Ergebnisse.
Wenn das Ziel-DBMS die Verbindung externer Quellen unterstützt, kann eine umgekehrte Schaltung erstellt werden, in der das DBMS mit der System-API verbunden ist, um kleine Datenmengen vom System zu empfangen, um beispielsweise große Volumes "an Ort und Stelle" zu filtern. Eine solche Integration ist für den Endbenutzer und den Analysten transparent - das Cube-Modell ändert sich nicht und alle Vorgänge werden automatisch vom System ausgeführt.

In komplexeren Fällen implementiert das System einen internen In-Memory-Abfrageinterpreter für das bemerkenswerte H2 Embedded-Datenbankmodul, mit dem jede unterstützte Datenbank sofort integriert werden kann. Im wahrsten Sinne des Wortes funktioniert es so: Die Anforderung wird durch Gruppen von Quellen in Teile unterteilt, die zur Ausführung gesendet werden. Anschließend werden die Zusammenstellung und die endgültige Verarbeitung der Ergebnisse im Speicher in H2 durchgeführt.
Auf den ersten Blick scheint ein solches Datenintegrationsschema auf der Ebene der internen Interpreter „schwierig“ zu sein. Dies gilt, wenn Sie mit großen Mengen an Eingabedaten arbeiten müssen und Berechnungen nach Schnittpunkten und Zuordnungen von Mengen aus externen Quellen durchführen müssen. Tatsächlich ist dieser Umstand teilweise ausgeglichen - gleichzeitig wird die Anforderung von mehreren Handlern in unterschiedlichen Versionen ausgeführt, daher wird der Interpreter nur in den extremsten Fällen als sofort einsatzbereite universelle Lösung verwendet. Letztendlich ist jede Integration durch die typischen Transportkosten für die Vorbereitung, Übertragung über das Netzwerk und den Empfang von Daten begrenzt, und dies ist eine völlig andere Aufgabe.
Technische Seite
Von der technischen Seite, auf die Sie wahrscheinlich nicht verzichten können, wenn Sie dieses Thema ansprechen, ist das System auch nach dem Prinzip konzipiert - mehr zu tun, aber alles so weit wie möglich zu vereinfachen.
Das Datenverarbeitungssystem wird auf dem jsBeans-Client-Server-Framework als eine Reihe zusätzlicher Module und spezifischer Assemblyprojekte implementiert. jsBeans wiederum ist in Java implementiert, arbeitet als Anwendungsserver, besteht im Großen und Ganzen aus Rhino, Jetty und Akka und umfasst auch die von unserem Team entwickelte Client-Server-Bean-Technologie sowie eine umfangreiche Bibliothek von Komponenten, die über mehrere Jahre erfolgreicher Anwendung zusammengestellt wurden.
Der Rubik's Cube ist vollständig und vollständig in JavaScript in Form vieler js-Bins (* .jsb-Dateien) implementiert, von denen einige nur auf dem Server ausgeführt werden. Der andere Teil befindet sich auf dem Client, und der Rest ist eine Komponentenkomponente, die als verteiltes Ganzes fungiert und deren Teile miteinander interagieren, für den Entwickler transparent, aber unter seiner Kontrolle. Js-Bins können unterschiedliche Lebensstrategien haben, zum Beispiel mit oder ohne Benutzersitzung und vieles mehr. Die Bean ist isomorph und ermöglicht es sowohl dem Client als auch dem Server, als virtuelle Instanz einer regulären Klasse damit zu arbeiten. Der Bin wird durch eine einzelne Datei beschrieben und enthält drei Abschnitte - für Felder und Methoden, die auf dem Client ausgeführt werden, für Serverfelder sowie einen Abschnitt allgemeiner synchronisierter Felder.
Da sich der Artikel bereits als ausführlich herausgestellt hat, um die Leser nicht zu langweilen, ist es an der Zeit, mit der Fertigstellung fortzufahren, mit der Absicht, bald die Details und die interessantesten architektonischen Lösungen bei der Implementierung von JsBeans und unseren darauf basierenden Projekten zu beschreiben - das konstruierte Visualisierungssubsystem, analytische Prozesse, ontologischer Designer von Themenbereichen, Sprache Abfragen, Datenimport und etwas anderes ...
Warum so?
Das ist noch nie passiert und hier wieder ...
Anfangs gab es nur wenige Primärdatensätze. Themenbereiche und Aufgaben wurden vollständig spezifiziert. Es scheint, warum solche Qualen? Die Aufgabe sah einfach aus, jeder wollte das Ergebnis sofort erhalten - besonders wenn die schnelle Lösung auf der Oberfläche lag und die richtige Ausdauer und ausgewogene Entscheidungen erforderte, wobei das ursprüngliche Setup eingehalten wurde. Wir sind in die entgegengesetzte Richtung gegangen, von komplexen und langen Lösungen zu einfachen und schnellen, um bestimmte Probleme zu verallgemeinern.
Die Hauptbedingung ist, dass neue Dashboards schnell erstellt werden müssen, auch wenn sich der neue Themenbereich und die analytischen Anforderungen stark von den vorherigen unterscheiden. Offensichtlich werden Sie nicht einmal die Hälfte der zukünftigen Anforderungen erraten, das System muss in erster Linie biegsam sein. Die Verfeinerung der Komponentenbibliothek, analytische Algorithmen und die Verbindung neuer Arten von Quellen ist ein wesentlicher Bestandteil der Anpassung des Systems. Mit anderen Worten, der Haufen hat funktioniert - Analysten erstellen Abfragen und Dashboards, und Programmierer erkennen schnell neue Anforderungen für sie. Und wir als Programmierer haben zunächst versucht, unsere Arbeit in Zukunft zu vereinfachen, um die Benutzerfreundlichkeit nicht zu beeinträchtigen.
Und das System wurde sofort universell und anpassungsfähig erstellt - wir haben einen „Konstruktor für Konstruktor“ erstellt und ein Framework auf einem Framework entwickelt, das zuvor mit einem ähnlichen, aber noch allgemeineren Zweck erstellt wurde.
Die Bewertung der Moskauer Schulen auf der Grundlage der Ergebnisse des Einheitlichen Staatsexamens und der Olympiaden ist ein Beispiel für ein Dashboard, das auf die oben beschriebene Weise aus dem Entladen offener Daten der Moskauer Regierung aus dem Portal erstellt wurde.
Cube-Rubik ist eine grundlegende Plattform für die Entwicklung von Informations- und Analysesystemen. Entworfen als Zweig und logische Fortsetzung von jsBeans. Es enthält alle notwendigen Module zur Lösung der Probleme der Erfassung, Verarbeitung, Analyse (rechnerisch und prozessorientiert) und Visualisierung.
jsBeans ist ein isomorphes Full-Stack-Webframework, das die Client-Server-JavaScript-Bean-Technologie implementiert und mit einer offenen Lizenz als universelles Tool entwickelt wurde. Während des Gebrauchs hat es sich in den meisten Fällen als gut erwiesen und passt ideal zu den vor uns liegenden Aufgaben.