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
- Ein Datumsfeld wird mit einem einfachen Text verglichen:
'2026-01-01'stattDATE '2026-01-01'. - Ein Datumsfeld wird umgewandelt, obwohl es schon ein Datum ist:
from_iso8601_date(...),date_parse(...)oderCAST(... AS VARCHAR). - 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
urlaubsantraegeoder bei Apps aus dem Marketplaceapp_invoicesundapp_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 < @periodEndDatum: 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 mitTYPE_MISMATCH.DATE '@periodStart'ergibt kein gültiges Datum, weil@periodStartein Ausdruck und kein Datumstext ist.@currentUserEmailohne 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.startoder.endwird zuNULL.- 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)liefert1,234.56(englisches Format).replace(replace(replace(format('%,.2f', x), ',', ' '), '.', ','), ' ', '.')tauscht die Trennzeichen und liefert1.234,56.date_format(x, '%d.%m.%Y')liefert ein Datum als05.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,@currentUserSubin Anführungszeichen;@periodStart,@periodEndund$(…)nie.- Summen und Spalten, die leer sein können, mit
COALESCEabsichern. - Aliase so schreiben, wie sie in Zuordnung, Zusammenfassungszeile oder
{{…}}verwendet werden.
Verwandte Seiten: Filter-Container, Tabelle, KPI, Planungstafel.