Lambda Filtern wie im SQL

  • Themenstarter Themenstarter Gelöschtes Mitglied 68249
  • Beginndatum Beginndatum
G

Gelöschtes Mitglied 68249

Gast
Hallo,

ich habe ein kleines Denkproblem mit einem Filter für eine Liste.
Früher haben wir das alles mit einem riesigen SQL Statement erledigt, aber je mehr wir das erweitern, desto schlimmer sieht es aus und desto mehr Fehler haben wir im Ergebnis.
Deswegen möchte ich das in Zukunft im Java machen. Dazu habe ich die Statements, die die Daten sammeln in Listen verpackt und möchte die jetzt so gegeneinander filtern, dass dasselbe Ergebnis herauskommt, wie beim SQL.

Hier erstmal das SQL:
SQL:
SELECT DISTINCT
    w.hostname [host],
    LEFT(w.auftragsnummerzeile, len(w.auftragsnummerzeile) - 4) [auftrag],
    w.idp_status [status],
    'Kein Fakturadatensatz zum Host' [fehler]
FROM
    allewaa w
WHERE
    w.sft in ('LISTE_VON_SFT')
    AND w.faktdatumstr = (SELECT MAX(faktdatumstr) FROM allewaa AS aw WHERE aw.hostname = w.hostname)
    AND w.auftragsnummerzeile = (SELECT MAX(auftragsnummerzeile) FROM allewaa AS mf WHERE mf.hostname = w.hostname)
    AND w.art <> 'Abbau'
    AND w.hostname NOT IN (SELECT hostname FROM allefaktura)

Zur Erklärung,
  • auftragsnummerzeile == Eine Kombination aus Auftragsnummer und der Zeile im Auftrag, vierstellig, damit man diese sortieren kann, um diese sortieren zu können
  • faktdatumstr == das Fakturadatum im Format yyyyMMdd, auch damit man es nach Größe sortieren kann
Java:
private static void findMissingFakturaRecord(List<WaaObject> waaObjectList, List<FakturaObject> fakturaObjectList) {
    List<String> relevantSft = Arrays.asList("LISTE_VON_SFT");

    for(WaaObject waaObject : waaObjectList) {
        String waaSft = waaObject.getSft();
        if(!relevantSft.contains(waaSft)) continue;

        if("abbau".equalsIgnoreCase(waaObject.getArt())) continue;

        String waaHost = waaObject.getHostname();

        List<FakturaObject> fakturaForHost = fakturaObjectList.stream()
            .filter(f -> f.getHostname().equals(waaHost))
            .collect(Collectors.toList());
        Optional<WaaObject> maxWaaOpt = waaForHost.stream().max(Comparator.comparing(WaaObject::getFaktdatumstr));
        if(maxWaaOpt.isEmpty()) {
            String orderNo = waaObject.getAuftragsnummerzeile().substring(0,waaObject.getAuftragsnummerzeile().length() - 4);
            FakturaError e = new FakturaError(waaHost, orderNo, waaObject.getStatus(),"", ErrorCode.NO_FAKTURA);
            fakturaErrorList.add(e);
        }
    }
}

In dem Fall ist waaObjectList == allewaa und fakturaObjectList == allefaktura.
Also Ergebnis soll sein, dass für den WAA-Auftrag mit der höchsten Nummer und dem höchsten Datum zu einem Hostnamen, der einen relevanten SFT hat und kein Abbau ist, geprüft wird, ob es einen Faktura-Datensatz dazu gibt mit demselben Hostnamen.

Eigentlich könnte man direkt weiter gehen und prüfen, ob es einen Fakturadatensatz zu jedem WAA-Datensatz gibt. Gematcht über den Hostnamen und über waaObject.faktdatumstr mit FakturaObject.fakturaVon.
 
Das gezeigte SQL ist doch eher sehr klein!? -> aber sehr schlecht umgesetzt. Weiterhin ist gutes SQL idR. viel schneller als Java. Wenn bereits im SQL gefiltert wird, dann müssen auch viel weniger Daten übers Netzwerk gehen und es wird weniger Speicher verbraucht. Wieso bei SQL Fehler im Ergebnis sein sollen, dass erschließt sich mir auch nicht. Kannst du das näher erläutern?

Der gezeigte Java-Code ist sehr unübersichtlich und schlecht wartbar. Ich bin kein Java-Anfänger, aber hier bekomme ich Augenkrebs und würde das nie so machen.
Da würde ich SQL vorziehen. SQL kann auch in Prozeduren abgelegt werden. Ich sehe da folgenden Vorteil. Das SQL kann im Fehlerfall ohne neue Programmversion direkt geändert werden. Bei Updates der DB kann das dann durch einen DB-Admin oder SQL-Profi optimiert werden, ohne Kenntnisse vom Programm selbst haben zu müssen.

Eigentlich könnte man direkt weiter gehen und prüfen, ob es einen Fakturadatensatz zu jedem WAA-Datensatz gibt. Gematcht über den Hostnamen und über waaObject.faktdatumstr mit FakturaObject.fakturaVon.
Das sollte per SQL und Left-Join problemlos umsetzbar sein.
 
Zuletzt bearbeitet:
Das SQL kann im Fehlerfall ohne neue Programmversion direkt geändert werden.
Bei Crowdstrike wurde auch nur eine Konfiguration geändert, da kann gar nichts passieren.

Im Ernst, ich war mal auf einer C++ Veranstaltung und die waren stolz, dass sie mit Templates eine ORM-Schicht hinbekommen hatten.

Ich habe lieber nichts von Hibernate und so erzählt.

Aber sie haben noch ihr Geheimnis erzählt: "Unser SQL steckt in Konfigurationsdateien".

Nun ja, wer die Gefahr liebt, umgeht die Qualitätssicherung.
 
Früher haben wir das alles mit einem riesigen SQL Statement erledigt, aber je mehr wir das erweitern, desto schlimmer sieht es aus und desto mehr Fehler haben wir im Ergebnis.
Eventuell sollten dort erst mal die Fehler gesucht werden.

Allerdings kann ich eines verstehen, SQL ist sozusagen eine black box, man kann es nicht einfach in Teile aufteilen, die man einzeln testet (zumindest nach meinen Wissenstand).

Bei dot net (.Net) gab es mal LINQ, ich weiß nicht, inwieweit es dies inzwischen in Java gibt. Danach könnte man mal suchen.

Vielleicht gibt es was bei JOOQ (Lukas Eder).

2015 habe ich mal was mit JPQL (JPA Query Language) gemacht und war davon begeistert, für mich war es SQL, aber richtig gemacht.
 
Früher haben wir das alles mit einem riesigen SQL Statement erledigt, aber je mehr wir das erweitern, desto schlimmer sieht es aus und desto mehr Fehler haben wir im Ergebnis.
Deswegen möchte ich das in Zukunft im Java machen.
Damit muessen alle Daten zuerst an Java uebertragen werden um sie dort zu filtern, ist dir das bewusst? Je nachdem wie ihr die Index gelegt habt und welche Datenmenge ihr habt, ist das SQL halt einfach schneller.

if(!relevantSft.contains(waaSft)) continue;
Das willst du eventuell als Set machen zwecks schnellerem Zugriff.

Allerdings kann ich eines verstehen, SQL ist sozusagen eine black box, man kann es nicht einfach in Teile aufteilen, die man einzeln testet (zumindest nach meinen Wissenstand).
Ehrlich gesagt habe ich es noch nie gemacht, aber eine Test-Datenbank mit entsprechenden Test-Daten aufzusetzen, und dann zu pruefen ob der Ergebnis-Satz dem entspricht was man erwartet klingt jetzt auch nicht so aufwaendig. Und es gibt da wohl auch Ansaetze dazu.

Tatsaechlich habe ich mal einen Prototypen erstellt fuer die Migration einer Oracle Forms Applikation. Die gesamte Geschaeftslogik war in der Oracle-Datenbank, nichts war in Oracle Forms. Das war mit Abstand das genialste was ich je gesehen hatte, weil du hast in der Oberflaeche einen Button eingefuegt und von der Action einfach nur die entsprechende Datenbank-Funktion aufgerufen. Und sehen wir's ein, die Datenbanken leben immer laenger als jegliche Oberflaeche oder Mittelschicht.
 
Bei Crowdstrike wurde auch nur eine Konfiguration geändert, da kann gar nichts passieren.

Im Ernst, ich war mal auf einer C++ Veranstaltung und die waren stolz, dass sie mit Templates eine ORM-Schicht hinbekommen hatten.

Ich habe lieber nichts von Hibernate und so erzählt.

Aber sie haben noch ihr Geheimnis erzählt: "Unser SQL steckt in Konfigurationsdateien".

Nun ja, wer die Gefahr liebt, umgeht die Qualitätssicherung.
Wer hat denn behauptet, dass SQL in Prozeduren nicht getestet wird?
z.B.: https://github.com/utPLSQL/utPLSQL
Für kleinere Anwendungen gibt es mind. 4 Stages: Entwicklung,Test, Vorproduktion u. Produktion. Für richtige Anwendungen sind 8-10 Stages normal. Trotzdem würde ich nie über Crowdstrike lästern - Wer im Glashaus sitzt, soll nicht mit Steinen werfen!
 
Das gezeigte SQL ist doch eher sehr klein!?
Ja, das liegt aber daran, dass ich den gesamten WITH-Block vorher weggelassen habe. Die Daten werden vorher im With-Block schon gefilter und zusammengestellt und gejoint und dann wird auf der en Grundlage ein Select mit ganz vielen Unions gemacht und da ist die Wartung einfach Mist.
Beim With-Block kann man nicht alles einzeln testen, weil manche Statements auf vorherigen Statements aufbauen und unten bei den Unions muss ich immer alles auskommentieren, was ich gerade nicht mittesten will.
In Java kann ich einfach das eine Statement oben nehmen und baue mir dazu den Code, der es so filtert, wie ich das will, damit das Ergebnis rauskommt, dass ich gerne will. Ich kann in Java die vorherigen Listen einfach direkt aufgrund eines einzelnen Datensatzes prüfen, ohne die Statements an zu passen, also es hat sehr viele Vorteile.
Das willst du eventuell als Set machen zwecks schnellerem Zugriff.
Schneller ist besser, was ist der Unterschied, bzw. wo kann ich nachlesen, was die Geschwindigkeit da aus macht? Und du meinst bestimmt die ArrayList in ein Set umwandeln?
 
Nur ganz am Rande: Wenn man große, komplexe Abfragen hat, dann kann es ggf. hilfreich sein, diese aufzuteilen und mit INSER INTO in Zusammenhang mit temporären Dateien zu arbeiten. Das kann auch die Performance deutlich steigern. Das nur kurz am Rande um eine weitere Alternative zu nennen.

Aber das sollte natürlich nicht darauf hinauslaufen, dass man unter dem Strich mehr Daten abfragt, als man wirklich braucht, weil man das als eine Art Baukasten betrachtet.

Und je nach Datenbank hat man ggf. auch die Möglichkeit, persistente Views einzurichten:
  • Oracle / PostgreSQL: Materialized View
  • SQL Server: Indexed View
  • MySQL: Kennt die nicht aber es gibt Drittanbieter wie Flexviews

Das nur ein paar Dinge, die mir hier so durch den Kopf gehen bei dem Thema.
 
Ein paar Gedanken meinerseits.

Grundsätzlich halte ich es für vollkommen legitim sich zu entscheiden, manche Dinge nicht in SQL sondern in Java zu machen oder anders rum. Wichtig halte ich es aber da, es nicht 1:1 zu übernehmen. Sondern in Java kann der Code ganz anders aussehen - ggf. können sogar die Statements ganz anders aussehen, weil man es anders aufbaut. Also nicht zu sagen "Der Teil ist zu komplex für , den mach ich Java" und dann den einfach aus dem SQL Statement rauswerfen und nach Java zu verlagern.

Da läuft man nämlich schnell in die Gefahr, dass man einen spürbaren Performance-Impact hat. Eine Datenbank ist - sauberes Schema und Indexe vorausgesetzt - echt gut daran Querys zu optimieren dafür zu sorgen, dass nur (relativ) wenige Datensätze gelesen werden müssen. Schmeißt man ein paar Bedingungen/Filter weg, weil man das in Java "besser" kann, kann aufeinmal die Datenmenge, die die Datenbank liest um mehrere Größenordnungen ansteigen. Kann - nicht muss.

Wenn man es daher aufsplittet, dann wirklich noch mal komplett die fachliche Anforderung durchdenken was man eigentlich prüfen will. Auf fachlicher - nicht technischer - Ebene. Dann kann man nochmal frei überlegen, wie man es aufteilt.

Und vielleicht ist gar nicht die Frage SQL oder Java die entscheidende, sondern die entscheidende Frage ist, ist das Datenmodell sinnvoll? (Auch wenn ich weiß, das das zu ändern oft schwer ist).

Paar Dinge die mir auffallen:

auftragsnummerzeile == Eine Kombination aus Auftragsnummer und der Zeile im Auftrag, vierstellig, damit man diese sortieren kann, um diese sortieren zu können
Kombinierte Felder in der DB ist meist ein Anti-Pattern und nicht in Normalform. Sinnvoller ist es sowas in der Regel in zwei getrennten Feldern zu haben.

faktdatumstr == das Fakturadatum im Format yyyyMMdd, auch damit man es nach Größe sortieren kann
Das verstehe ich nicht? Sortieren kann ich auch ein Standard-Datumsfeld - und das vermutlich sogar performanter als den String.

AND w.faktdatumstr = (SELECT MAX(faktdatumstr) FROM allewaa AS aw WHERE aw.hostname = w.hostname)
AND w.auftragsnummerzeile = (SELECT MAX(auftragsnummerzeile) FROM allewaa AS mf WHERE mf.hostname = w.hostname)
Das sieht extrem schräg aus. Ich möchte Zeilen haben, wo zwei Spalten dem Maximum eines Selects entsprechen? Ist überhaupt garantiert, dass es so eine Zeile gibt? Oder kann nicht eine Zeile dem Maximum faktdatumstr entsprechend und eine andere Zeile dem Maximum aus auftragsnummerzeile?

Das riecht von außen danach, als wäre das Datenmodell hier nicht ganz sauber und es würde ein sinnvoller Schlüssel zum sortieren fehlen - oder sogar eine zusätzliche Tabelle. So sieht das falsch aus. Wenn in allewaa der neuste Datensatz zu einem Hostnamen den Typ "Abbau" , dann wird nie eine Zeile selektiert.

Also Ergebnis soll sein, dass für den WAA-Auftrag mit der höchsten Nummer und dem höchsten Datum zu einem Hostnamen, der einen relevanten SFT hat und kein Abbau ist, geprüft wird, ob es einen Faktura-Datensatz dazu gibt mit demselben Hostnamen.
Das ist nicht das Select.

Das Select ist, wenn es einen Datensatz gibt, der sowohl die höchste Nummer als auch das höchste Datum hat für den Hostnamen und genau dieser Datensaz kein Abbau ist, in der Liste der relevanten SFT ist und es keinen Faktura Datensatz gibt, dann wird er selektiert.

Dementsprechend wäre mein erster Kandidat hier das Datenmodell - aber das lässt sich von außen fairerweise immer sehr schwer abschließend beurteilen.
 
Alles sehr gute Gedanken, deswegen zur Aufarbeitung:
Eine Datenbank ist - sauberes Schema und Indexe vorausgesetzt - echt gut daran Querys zu optimieren dafür zu sorgen, dass nur (relativ) wenige Datensätze gelesen werden müssen.
Ja, aber nein. Die Datenbank ist weder sauber noch toll indiziert. Weil die Daten alle in mehr oder weniger drei Tabellen sind.
Tabelle 1: Objektkatalog
Hier ist für jeden "Datensatz" eine Zeile pro Version d.h. Schlüssel und Versionsnummer sind Identifier. Zusätzlich sind hier noch Felder, wie Status und Start und Ende-Datum der Version zu finden und es gibt eine Spalte für die Überkategorie und die Unterkategorie des Objekttyps.

Tabelle 2: Objektdaten
Hier ist für jedes Datum eines Datensatzes eine Zeile, d.h. Schlüssel, Versionsnummer und Feldindex sind Identifier zusätzlich gibt es dann noch die Spalt Wert als VarChar.

Tabelle 3: Objektverweise (hier wird es richtig gruselig):
Hier sind sämtlich Felder, wie schon in Tabelle 2 mit Schlüssel, Version und Feldindex indiziert, zusätzlich kommt aber eine Spalte "Fremdschlüssel" dazu und dazu dann noch "Feld in der Fremdtabelle" und oben drauf noch "Letzter Wert", also das ist der letzte Wert, den die Anwendung quasi zu dem Fremdschlüssel im anderen Datensatz gefunden hat. Sollte der sich im anderen Datensatz ändern, aber dieser wird nicht angefasst, dann bleibt in "letzter Wert" der falsche Wert für immer stehen.

D.h. also für mich, wenn ich alle Windows Aufträge laden möchte, dann lade ich erstmal alle Objektkataloge des Objekttyps Windows Auftrag mit ihrer aktuelle gültigen Version. Dann left-outer-joine ich die Objektdaten dazu mit allen Feldern, die mich für diese aktuelle Suche brauche. Warum Left Outer Join? Weil es sein könnte, das in der Objektklasse irgendwann ein Feld hinzu gekommen ist, dass es ja dann folglich in älteren Datensätzen nicht gibt.

Also ich hoffe anhand der Beschreibung erklärt sich, warum ich das ganze gerne in Java mache. Es ist ein reines Kontroll-Tool, also es wird in keinem Fall zurück geschrieben. D.h. ich habe für jede Objekt-Sorte bei mir eine Klasse mit Typsicherheit und den Feldern, die ich für die Prüfung eben brauche. Lade dann jede Objektklasse in eine Liste von sich selbst und ab dann mache ich die komplette Prüfung in Java.

Fachlich habe ich schon schleifen optimiert, wo ich im SQL mehrfach auf denselben Datenbestand zugreifen musste, weil ich jedes Mal ein anderes Kriterium geprüft habe, mache ich die Prüfung jetzt eben in einer Schleife auf mehrere Fehler.

Kombinierte Felder in der DB ist meist ein Anti-Pattern und nicht in Normalform. Sinnvoller ist es sowas in der Regel in zwei getrennten Feldern zu haben.
Jupp, sind in der Datenbank auch zwei Felder, aber beide eben String, d.h. der Auftrag 8521 ist damit "höher" als der Auftrag 26512 und die Zeilennummern waren mal dreistellig und sind jetzt vierstellig. Für das Select Mache ich die Kombination mit Der Auftragsnummer auf 8 Stellen aufgefüllt und die Zeilennummer auf 4 Stellen und muss dann nur über dieses eine Feld iterieren.
Das verstehe ich nicht? Sortieren kann ich auch ein Standard-Datumsfeld - und das vermutlich sogar performanter als den String.
Ja, siehe Tabelle2 das Datum, über das wir sprechen ist als String gespeichert, also muss ich es sowieso erst parsen. Aber stimmt, da habe ich einfach das SQL kopiert, ich könnte an der Stelle tatsächlich einfach nach Datum sortieren, weil es ja schon ein LocalDate ist.
Das riecht von außen danach, als wäre das Datenmodell hier nicht ganz sauber und es würde ein sinnvoller Schlüssel zum sortieren fehlen - oder sogar eine zusätzliche Tabelle. So sieht das falsch aus. Wenn in allewaa der neuste Datensatz zu einem Hostnamen den Typ "Abbau" , dann wird nie eine Zeile selektiert.
Ja, gut erkannt. Also bei der Prüfung geht es darum, dass bei den "aktuellen" Abrechnungssätzen für diesen Monat, für jeden gültigen WAA Auftrag auch ein Faktura-Datensatz da ist. D.h. wenn der neuste Auftrag zu einem System dessen Abbau ist, dann wird keine Rechnung mehr geschrieben, also ist die Prüfung in dieser Richtung nicht nötig. Ich prüfe auch nochmal in Gegenrichtung, um es zu jeder Rechnungszeile auch einen Auftrag gibt.
Das mit dem Max(Datum) und Max(Auftragsnummer) sollte ja eigentlich der Normalfall sein, also eine Höhere Auftragsnummer zum selben Host sollte ja auch immer ein höheres Datum haben, zu dem fakturiert wird. Mit der Formel fische ich nur eine Handvoll Datensätze heraus, die mal Nachdokumentiert worden sind und das nach ihrem Abbau, d.h. bei denen hat der Aufbau eine höhere Auftragsnummer, als der Abbau, aber die Fakturadaten sind ja in der richtigen Reihenfolge.

Das Select ist, wenn es einen Datensatz gibt, der sowohl die höchste Nummer als auch das höchste Datum hat für den Hostnamen und genau dieser Datensaz kein Abbau ist, in der Liste der relevanten SFT ist und es keinen Faktura Datensatz gibt, dann wird er selektiert.
Jupp, richtig erkannt, da war mein Kopf bei was anderem, ich möchte ja die Fehler finden und nicht die guten, also wenn WAA-Auftrag dann muss es auch einen Faktura-Satz geben, sonst anzeigen, weil falsch.
Dementsprechend wäre mein erster Kandidat hier das Datenmodell - aber das lässt sich von außen fairerweise immer sehr schwer abschließend beurteilen.
Nope, das hast du vollkommen richtig erkannt, das Datenmodell ist aus der Hölle und deswegen drehen sich viele meiner anderen Posts um die Ablösung des Tools in seiner jetzigen Form und dem Übergang in eine neue Form mit einem Datenmodell aus dem 20ten Jahrhundert, wo WAA-Aufträge in einer eigenen Tabelle liegen und nicht mit WindowsOS und Servern in derselben. Wo jedes Datum eine eigene Spalte mit passendem Typ hat und jeder Datensatz mit genau einem Identifier gefunden werden kann.
 

Zurück
Oben