Zum Inhalt

SQL in Widgets

Tabelle, KPI, Diagramm, Heatmap, Text und Planungstafel holen ihre Daten über eine SQL-Abfrage aus dem Data Lake. Auch der Filter-Container (Typ „Datensatz per SQL“), der Timer (Zuordnungen per SQL) und die Zeiteinträge (zusätzlicher Filter) arbeiten mit SQL. Diese Seite fasst zusammen, was in allen diesen Abfragen gilt – damit eine Abfrage beim ersten Testlauf funktioniert und die richtigen Zahlen liefert.

Die drei häufigsten Fehler

  1. Ein Datumsfeld wird mit einem einfachen Text verglichen: '2026-01-01' statt DATE '2026-01-01'.
  2. Ein Datumsfeld wird umgewandelt, obwohl es schon ein Datum ist: from_iso8601_date(...), date_parse(...) oder CAST(... AS VARCHAR).
  3. Ein Platzhalter steht mit falschen Anführungszeichen in der Abfrage: '@periodStart' oder '$(kunde)'.

Das gilt auch für Vorschläge des SQL-Assistenten oder eines KI-Assistenten: Vor dem Übernehmen lohnt ein Blick auf diese drei Punkte.


Tabellen und Spalten

  • Jedes Datenschema ist eine Tabelle. Der Tabellenname ist der technische Name des Datenschemas, zum Beispiel urlaubsantraege oder bei Apps aus dem Marketplace app_invoices und app_contacts.
  • Abgeleitete Datenschemas: Hat ein Datenschema abgeleitete Datenschemas, gibt es zusätzlich die Tabelle <name>_only. Sie enthält nur die Datensätze des Datenschemas selbst, ohne die der abgeleiteten (siehe Vererbung und Hierarchien).
  • Spalten heißen wie die technischen Feldnamen, in Kleinbuchstaben. Dazu kommen in jeder Tabelle:
Spalte Inhalt
id Id des Datensatzes
data_schema_id Id des Datenschemas
document_name Dokumentname des Datensatzes
created_at, updated_at Zeitpunkt der Anlage und der letzten Änderung (Zeitstempel)
created_by, updated_by wer den Datensatz angelegt und zuletzt geändert hat

Namen der Ergebnisspalten: Eine Ergebnisspalte heißt genau so, wie ihr Alias geschrieben ist. Die Spaltenzuordnung in Diagramm, Heatmap und Planungstafel, die Zusammenfassungszeile der Tabelle und die Platzhalter {{name}} des Text-Widgets brauchen genau diese Schreibweise. Aliase werden am besten klein geschrieben, ohne Umlaute und Leerzeichen, zum Beispiel AS brutto.

Wörter wie start, end oder row eignen sich nicht als Alias, weil sie in SQL eine eigene Bedeutung haben. Die Beispiele in dieser Dokumentation verwenden deshalb start_day, end_day und row_key.


Spaltentypen

Die Spalten haben echte Typen, je nach Feldtyp im Datenschema. Ein Datumsfeld ist bereits ein Datum, ein Zahlenfeld bereits eine Zahl – umgewandelt werden muss nichts.

Feld im Datenschema Typ in SQL Beispiel
Datum DATE app_invoice_date >= DATE '2026-01-01'
Datum mit Uhrzeit, created_at, updated_at TIMESTAMP created_at >= TIMESTAMP '2026-01-01 00:00:00'
Zahl DOUBLE app_gross_total > 1000
Text, Auswahl Text status = 'Genehmigt'
Referenz Text: die Id des verknüpften Datensatzes LEFT JOIN app_contacts c ON c.id = i.app_recipient
Listen und Objekte ARRAY bzw. ROW mit UNNEST(...) auflösen oder mit CAST(x AS JSON) als Text ausgeben

Nur ein Datum, das in einem Textfeld gespeichert ist, braucht from_iso8601_date(...) oder date_parse(...).


Datum und Zeitstempel vergleichen

Ziel Richtig Falsch Was beim falschen Weg passiert
Datumsfeld ab einem festen Tag app_invoice_date >= DATE '2026-01-01' app_invoice_date >= '2026-01-01' Fehler TYPE_MISMATCH: Datum und Text lassen sich nicht vergleichen.
Datumsfeld verwenden app_invoice_date direkt from_iso8601_date(app_invoice_date), date_parse(app_invoice_date, '%Y-%m-%d') Fehler FUNCTION_NOT_FOUND: Diese Funktionen erwarten Text, kein Datum.
Zwei Datumswerte vergleichen app_due_date < current_date CAST(app_due_date AS VARCHAR) < CAST(current_date AS VARCHAR) Unnötiger Umweg über Text; Datumswerte direkt vergleichen.
Datumsfeld relativ zu heute app_invoice_date >= current_date - INTERVAL '30' DAY
Datumsfeld seit Jahresbeginn app_invoice_date >= date_trunc('year', current_date)
Zeitstempel ab einem festen Tag created_at >= TIMESTAMP '2026-01-01 00:00:00' created_at >= '2026-01-01' Fehler TYPE_MISMATCH.
Zeitstempel an einem Tag date(created_at) = current_date created_at = current_date Kein Fehler, aber es werden nur Einträge von genau 00:00 Uhr gefunden.

Ein Datumsfeld lässt sich auch direkt mit einem Zeitstempel-Ausdruck vergleichen, etwa app_invoice_date >= date_trunc('year', current_timestamp). Für den gewählten Zeitraum eines Widgets ist date(@periodStart) die übersichtlichste Schreibweise (siehe Platzhalter).

Wo die Fehlermeldung erscheint: Bricht eine Abfrage ab, nennt die Datenbank die Ursache, zum Beispiel TYPE_MISMATCH oder FUNCTION_NOT_FOUND. Im Dashboard ist davon derzeit oft wenig zu sehen: Der Testlauf in den Dialogen meldet häufig nur „Query failed“, die KPI zeigt nur ein Fehlersymbol, das Text-Widget den Text mit den nicht ersetzten {{…}}. Bei einem Fehler deshalb zuerst die Datumsvergleiche und Platzhalter dieser Seite prüfen.

Eine vollständige Abfrage mit festen Grenzen:

SELECT app_invoice_no AS rechnungsnr,
       app_invoice_date AS rechnungsdatum,
       app_gross_total AS brutto
FROM app_invoices
WHERE app_invoice_date >= DATE '2026-01-01'
  AND app_invoice_date < DATE '2027-01-01'
ORDER BY app_invoice_date

Platzhalter

Platzhalter werden vor dem Ausführen als reiner Text an genau ihrer Stelle ersetzt. Ob daraus gültiges SQL entsteht, entscheidet deshalb die Schreibweise drumherum. Groß- und Kleinschreibung des Platzhalters spielt keine Rolle.

Platzhalter Wird ersetzt durch Richtig geschrieben
@currentUserEmail E-Mail-Adresse der Person, die das Dashboard ansieht. Wird ohne Anführungszeichen eingesetzt, also selbst in Anführungszeichen schreiben WHERE antragsteller = '@currentUserEmail'
@currentUserSub Benutzer-Kennung dieser Person. Wird ohne Anführungszeichen eingesetzt, also selbst in Anführungszeichen schreiben WHERE user_sub = '@currentUserSub'
@currentDate heutiges Datum, z. B. 2026-11-02, nach UTC. Wird ohne Anführungszeichen eingesetzt WHERE app_due_date < DATE '@currentDate' oder einfach current_date
@currentYear aktuelles Jahr, z. B. 2026 WHERE year(app_invoice_date) = @currentYear
@currentMonth aktueller Monat, immer zweistellig, z. B. 03 WHERE date_format(app_invoice_date, '%Y-%m') = '@currentYear-@currentMonth'
@currentTimeStamp aktueller Zeitpunkt als Text, z. B. 2026-11-02T09:15:00.000Z WHERE created_at < from_iso8601_timestamp('@currentTimeStamp') oder einfach current_timestamp
@periodStart, @periodEnd Beginn und Ende des gewählten Zeitraums als fertiger Zeitstempel-Ausdruck; @periodEnd ist der erste Zeitpunkt nach dem Zeitraum Zeitstempel: created_at >= @periodStart AND created_at < @periodEnd
Datum: app_invoice_date >= date(@periodStart) AND app_invoice_date < date(@periodEnd)

Typische Fehler mit Platzhaltern:

  • '@periodStart' in Anführungszeichen wird zu einem Text – der Vergleich mit einem Datum scheitert mit TYPE_MISMATCH.
  • DATE '@periodStart' ergibt kein gültiges Datum, weil @periodStart ein Ausdruck und kein Datumstext ist.
  • @currentUserEmail ohne Anführungszeichen ergibt ungültiges SQL, weil die E-Mail-Adresse als Text in Anführungszeichen stehen muss.

@currentDate richtet sich nach UTC: Kurz nach Mitternacht deutscher Zeit liefert es noch den Vortag. Für „dieser Monat“ ist app_invoice_date >= date_trunc('month', current_date) meist einfacher als @currentMonth.

Woher der Zeitraum kommt

Widget @periodStart und @periodEnd kommen aus
Tabelle, Diagramm, Heatmap dem Zeitraum eines umgebenden Filter-Containers; sonst dem eigenen Zeitfilter des Widgets; ohne beides dem aktuellen Monat
KPI, Text nur dem Zeitraum eines umgebenden Filter-Containers; sonst immer dem aktuellen Monat – diese Widgets haben keinen eigenen Zeitfilter
Planungstafel dem Zeitraumfilter der Tafel oder dem Zeitraum eines umgebenden Filter-Containers
Zeiteinträge (Zusätzlicher Filter), Timer (SQL-Abfrage für Zuordnungen) derzeit immer dem aktuellen Monat, nicht dem gewählten Zeitraum

Den eigenen Zeitfilter bieten Tabelle, Diagramm und Heatmap im Dialog erst an, wenn die Abfrage beide Platzhalter enthält. „Alle“ reicht vom Jahr 1 bis zum Jahr 9999 und schließt damit auch Einträge in der Zukunft ein. Bei der freien Kalenderauswahl zählt der gewählte letzte Tag mit; @periodEnd ist dann der Tag danach.


Werte aus dem Filter-Container

Liegt ein Widget vollständig im Bereich eines Filter-Containers, stehen dessen Filter als $(schlüssel) zur Verfügung. Anders als die @-Platzhalter wird $(…) durch einen fertigen SQL-Wert mitsamt Anführungszeichen ersetzt. Eigene Anführungszeichen werden deshalb nie geschrieben.

Filtertyp Platzhalter Wird zu
Datensatz, Datensatz per SQL $(kunde) oder $(kunde.id) Id des gewählten Datensatzes, in Anführungszeichen
Datensatz, Datensatz per SQL $(kunde.document_name) Dokumentname, in Anführungszeichen
Datensatz $(kunde.feldname) ein Feld des gewählten Datensatzes: Text in Anführungszeichen, Zahl ohne
Zeitraum $(zeitraum.start), $(zeitraum.end) Beginn und Ende (exklusiv) als Zeitstempel-Ausdruck – wie @periodStart und @periodEnd
Auswahlliste $(status) der gewählte Wert, in Anführungszeichen
Ja/Nein $(nur_offene) TRUE oder FALSE
„Alle“ gewählt jeder Platzhalter dieses Filters der Text '__ALL__'
leer, ohne Option „Alle“ jeder Platzhalter dieses Filters NULL – ein Vergleich spalte = NULL findet nie etwas

Damit „Alle“ wirklich alles zeigt, wird die Bedingung nach diesem Muster geschrieben:

SELECT app_invoice_no AS rechnungsnr,
       app_invoice_date AS rechnungsdatum,
       app_gross_total AS brutto
FROM app_invoices
WHERE ($(kunde) = '__ALL__' OR app_recipient = $(kunde.id))
  AND app_invoice_date >= date($(zeitraum.start))
  AND app_invoice_date < date($(zeitraum.end))
ORDER BY app_invoice_date DESC

Ein Ja/Nein-Filter wird so eingebaut: WHERE ($(nur_offene) = FALSE OR app_open_amount > 0).

Typische Fehler:

  • app_recipient = '$(kunde.id)' – die zusätzlichen Anführungszeichen ergeben ungültiges SQL.
  • $(zeitraum) ohne .start oder .end wird zu NULL.
  • Ein $(…) in einem Widget, das nicht im Bereich eines Filter-Containers liegt, bleibt als Text stehen – die Abfrage schlägt fehl. Timer und Zeiteinträge setzen $(…) grundsätzlich nicht ein.

Leere Werte (NULL)

Leere Werte wirken sich je nach Widget unterschiedlich aus. Mit COALESCE wird ein Ersatzwert gesetzt:

Situation Folge ohne COALESCE Lösung
KPI mit SUM(...), aber keine passenden Datensätze Die Summe ist leer, die KPI zeigt „Keine Daten“. COALESCE(SUM(app_gross_total), 0)
Tabelle, deren erste Zeile in einer Spalte leer ist Die Spalte fehlt in der ganzen Tabelle. COALESCE(c.document_name, '-')
Text-Widget mit leerem Wert Der Platzhalter wird zu einem leeren Text. COALESCE(...) mit sinnvollem Ersatz
Diagramm mit leerem Wert Der Wert wird als 0 gezeichnet, nicht als Lücke. in SQL bewusst entscheiden, welcher Wert gelten soll
Teilen durch eine Anzahl, die 0 sein kann Fehler oder unbrauchbarer Wert NULLIF(COUNT(*), 0) als Teiler

Zahlen und Datum deutsch formatieren

Widgets zeigen Werte so, wie die Abfrage sie liefert, etwa 1234.5 oder 2026-11-05. Ausnahme: Die KPI formatiert mit der Einheit „€“ selbst (1.234,56 €). Für Text-Widgets wird das deutsche Format in SQL gebaut:

  • format('%,.2f', x) liefert 1,234.56 (englisches Format).
  • replace(replace(replace(format('%,.2f', x), ',', ' '), '.', ','), ' ', '.') tauscht die Trennzeichen und liefert 1.234,56.
  • date_format(x, '%d.%m.%Y') liefert ein Datum als 05.11.2026.

Beispiel für ein Text-Widget mit dem Inhalt „Offene Forderungen: {{offen}} € aus {{anzahl}} Rechnungen (Stand {{stand}})“:

SELECT COUNT(*) AS anzahl,
       replace(replace(replace(format('%,.2f', COALESCE(SUM(app_open_amount), 0)), ',', ' '), '.', ','), ' ', '.') AS offen,
       date_format(current_timestamp, '%d.%m.%Y') AS stand
FROM app_invoices
WHERE app_open_amount > 0

Formatierte Werte sind Text. In einer Tabelle werden sie deshalb als Text sortiert (05.11.2026 vor 12.01.2026), und die Zusammenfassungszeile kann sie nicht summieren. In Tabellen bleiben Zahlen und Datumswerte deshalb unformatiert, wenn sortiert oder summiert werden soll.


Berechtigungen

Jede Abfrage läuft mit den Rechten der Person, die das Dashboard ansieht. Gezählt und angezeigt wird nur, was diese Person lesen darf. Dieselbe KPI kann deshalb für zwei Personen unterschiedliche Zahlen zeigen – das ist gewollt. Die Abfragen lesen nur, sie ändern keine Daten.


Vor dem Speichern prüfen

  • Abfrage im Dialog mit Testen, Test ausführen bzw. Testlauf ausprobieren.
  • Datums- und Zeitstempelfelder ohne Umwandlung vergleichen, feste Werte mit DATE '…' bzw. TIMESTAMP '…'.
  • @currentUserEmail, @currentUserSub in Anführungszeichen; @periodStart, @periodEnd und $(…) nie.
  • Summen und Spalten, die leer sein können, mit COALESCE absichern.
  • Aliase so schreiben, wie sie in Zuordnung, Zusammenfassungszeile oder {{…}} verwendet werden.

Verwandte Seiten: Filter-Container, Tabelle, KPI, Planungstafel.