# NQL, die Abfragesprache für Kennzahlen

> Jede Kennzahl-Kachel in nexxus ist eine NQL-Abfrage. Diese Seite erklärt, wie eine Abfrage aufgebaut ist, welche Quellen und Felder es gibt, wie sich Werte beschriften lassen, was die Antwort enthält und warum niemand über NQL mehr sieht, als er sehen darf.

**NQL** ist die Abfragesprache hinter jeder Kennzahl in nexxus: hinter den Kacheln auf dem [Dashboard](/de/use/dashboard/#eine-kennzahl-hinzufügen), auf den [Auswertungen](/de/use/analytics/) und hinter den Antworten des KI-Assistenten auf Fragen nach Zahlen. Eine Abfrage ist ein kleines JSON-Objekt. Sie nennt eine Quelle, die Kennzahlen, die gezählt oder summiert werden, die Merkmale, nach denen gegliedert wird, und Filter.

```json
{
  "source": "project",
  "measures":   [{"name": "anzahl", "as": "anzahl"}],
  "dimensions": [{"field": "dateCreated", "grain": "month", "as": "monat"}],
  "filters":    [{"field": "isActive", "op": "eq", "value": true}],
  "orderBy":    [{"as": "monat", "dir": "asc"}]
}
```

Diese Abfrage zählt die aktiven Projekte je Monat, in dem sie angelegt wurden, und sortiert nach Monat.

NQL ist absichtlich kein SQL. Eine Abfrage kann keine Tabelle und keine Spalte frei benennen. Sie **wählt aus**, was eine Quelle veröffentlicht hat: Merkmale, Kennzahlen, Operatoren und Zeitraster aus festen Listen. Alles andere lehnt nexxus ab und nennt dabei, was gültig gewesen wäre.

Sie müssen NQL nicht schreiben können. Der KI-Assistent und der Kachel-Dialog schreiben die Abfrage für Sie. Diese Seite hilft, wenn Sie eine Abfrage lesen, von Hand anpassen oder aus einem Skript stellen wollen.

## Wo Sie NQL begegnen

- **Im Kachel-Dialog.** Unter der Vorschau öffnet <Ui>Für Fortgeschrittene: Abfrage als JSON</Ui> die <Ui>NQL-Abfrage</Ui> der Kachel. Sie bearbeiten sie dort direkt; sie <Ui>Wird erst mit „Vorschau“ ausgeführt.</Ui>
- **Beim KI-Assistenten.** Fragen Sie ihn nach Zahlen, schreibt er eine Abfrage und führt sie mit Ihren Rechten aus. Auf einer Auswertung baut er daraus Kacheln, die Sie bestätigen.
- **In den Werkzeugen des KI-Assistenten und in KI-Clients.** Der eingebaute Connector „Auswertungen“ bringt die Werkzeuge `nx_nql_schema` (was Sie auswerten dürfen) und `nx_nql_query` (eine Abfrage ausführen) mit. Sie laufen mit den Rechten der Person der Unterhaltung, auch in einem [verbundenen Client wie Claude Code](/de/mcp/connect/).
- **Über die API.** `GET /api/nql/schema` liefert die Quellen, die Sie lesen dürfen, mit ihren Merkmalen und Kennzahlen; `POST /api/nql/query` führt eine Abfrage aus. Ein Skript authentifiziert sich dafür mit einem [persönlichen API-Token](/de/setup/security/#persönliche-api-tokens) und hat dann dieselben Rechte wie Sie.

## Wer was sieht

Die tragende Regel von NQL: **Niemand bekommt Zahlen, die er sich nicht auch einzeln hätte holen dürfen.** Eine Kachel läuft bei der Person, die sie ansieht, nicht bei der, die sie angelegt hat. Deshalb zeigt dieselbe Kachel verschiedenen Personen verschiedene Zahlen, und eine geteilte Auswertung gibt niemandem Daten.

<Flow
  steps={[
    { icon: 'key-round', title: 'Recht „Auswertungen“', text: 'Ohne core.analytics.read gibt es keine Abfrage.' },
    { icon: 'database', title: 'Leserecht der Quelle', text: 'Etwa project.project.read für Projekte. Manche Quellen nur für Administratoren.', tone: 'cyan' },
    { icon: 'list-checks', title: 'Veröffentlichte Felder', text: 'Nur Merkmale und Kennzahlen, die die Quelle anbietet.', tone: 'lavender' },
    { icon: 'building-2', title: 'Mandant und Zeilen', text: 'Nur Ihr Mandant, nur nicht gelöschte Datensätze, dazu der Zeilenfilter der Quelle.' },
  ]}
  caption="Jede Stufe prüft der Server bei jeder Abfrage, auch wenn die Kachel schon seit Monaten auf einer Auswertung steht."
/>

| Stufe | Was bei Nein passiert |
|---|---|
| Recht **Auswertungen** (`core.analytics.read`) | Die Abfrage scheitert mit 403. Ohne das Recht gibt es weder Kennzahl-Kacheln noch Auswertungen. |
| Leserecht der Quelle | Die Quelle fehlt im Schema der Person, eine Abfrage darauf scheitert mit 403. Die Kachel zeigt <Ui>Keine Berechtigung für diese Daten</Ui>. |
| Veröffentlichte Felder | Die Abfrage scheitert mit 400 und nennt die gültigen Felder. |
| Mandant und Löschstatus | Zeilen anderer Mandanten und gelöschte Datensätze sind schlicht nicht da. |
| Zeilenfilter der Quelle | Zeilen, die die Person nicht sehen darf, sind schlicht nicht da (bei KI-Sitzungen die fremden, bei Abwesenheiten alle außer den eigenen und denen der geleiteten Teams). |

Administratoren bestehen jede Rechteprüfung, aber nicht die Trennung der Mandanten. Ein `.write`-Recht für Auswertungen gibt es nicht, denn NQL schreibt nichts.

## Der Aufbau einer Abfrage

| Feld | Pflicht | Form | Wirkung |
|---|---|---|---|
| `source` | ja | Schlüssel, z. B. `"project"` | Die Quelle. Die Liste steht unter [Die Quellen](#die-quellen). |
| `measures` | eins von beiden | `[{name, as?, percentOfTotal?}]` oder `["anzahl"]` | Die Kennzahlen. `name` ist eine Kennzahl der Quelle, `as` der Spaltenname in der Antwort (Vorgabe: `name`). |
| `dimensions` | eins von beiden | `[{field, grain?, as?, format?, template?, labels?, ids?}]` oder `["status"]` | Die Merkmale, nach denen gegliedert wird. `grain` nur bei Datumsmerkmalen, die Beschriftung unter [Werte beschriften](#werte-beschriften). |
| `filters` | nein | `[{field, op, value}]`, auch mit Gruppen | Zeilen einschränken, **bevor** gezählt wird. `op` ist ohne Angabe `eq`. Gruppen mit ODER und NICHT unter [Filter](#filter). |
| `having` | nein | `[{as, op, value}]`, auch mit Gruppen | Gruppen einschränken, **nachdem** gezählt wurde. `as` ist eine Kennzahl-Spalte. |
| `orderBy` | nein | `[{as, dir}]` | Sortieren nach einer Spalte der Antwort, `dir` ist `asc` (Vorgabe) oder `desc`. |
| `limit` | nein | Zahl | Höchstens so viele Zeilen. Vorgabe 1000, höchstens 5000. |
| `topN` | nein | `{by?, limit?, otherLabel?, within?}` | Die stärksten Gruppen behalten, den Rest zusammenfassen. |
| `totals` | nein | `true` oder `{grandTotal, subtotalsFor}` | Gesamt- und Zwischensummen neben den Zeilen. |
| `split` | nein | Spaltenname eines Merkmals | Die Werte dieses Merkmals werden zu Spalten. |

### Kennzahlen

Eine Kennzahl ist eine fest definierte Rechnung der Quelle: `anzahl` zählt Projekte, `stunden` summiert die Stunden von Zeiteinträgen, `kunden` zählt verschiedene Kunden. Die Abfrage wählt die Kennzahl beim Namen, nicht die Rechnung. Deshalb kann keine Abfrage eine Spalte summieren, die die Quelle nicht dafür freigegeben hat.

Jede Kennzahl hat eine Funktion: `count`, `count_distinct`, `sum`, `avg`, `min` oder `max`. Das Schema nennt sie unter `fn`, dazu `"additive": true` oder `false`. **Additiv** sind `count` und `sum`: zwei Teilsummen ergeben zusammen die Gesamtsumme. Durchschnitte, Höchstwerte und das Zählen verschiedener Werte lassen sich nicht so zusammenfassen. nexxus lehnt sie deshalb dort ab, wo zusammengefasst werden müsste: neben einem Verweis-Merkmal, bei `topN` mit Rest, bei `totals` und bei `percentOfTotal`. Eine Absage ist besser als eine plausible falsche Zahl.

`percentOfTotal: true` an einer Kennzahl ergänzt eine Spalte `<as>Anteil` mit ihrem Anteil am Ganzen in Prozent, mit einer Nachkommastelle. Die Anteile ergeben zusammen genau 100.

### Merkmale und Zeitraster

Ein Merkmal ist ein Feld, nach dem gegliedert und gefiltert werden kann: der Status, die Abrechnungsart, das Anlagedatum. Sein Typ steht im Schema:

| Typ | Beispiel | Besonderheit |
|---|---|---|
| `string` | Projektname, Modell | Text |
| `number` | Auswirkung eines Risikos (1 bis 4) | Zahl |
| `boolean` | Aktiv, Abrechenbar | Die Kacheln zeigen <Ui>Ja</Ui> und <Ui>Nein</Ui> |
| `enum` | Status, Art | Feste Werte; das Schema nennt sie unter `values` und ihre Namen unter `valueLabels` (`OPEN` heißt „Offen“) |
| `date` | Angelegt am, Datum | Nimmt ein Zeitraster `grain`: `day`, `week`, `month`, `quarter` oder `year`. Ein Wert ist der Beginn des Zeitraums |
| Verweis (`"reference": true`) | Kunde eines Projekts, Projekt eines Zeiteintrags | Zeigt auf ein anderes Modul. Die Antwort enthält den Namen, ein Filter braucht die Nummer |

Ein Verweis-Merkmal wird erst nach dem Zählen in Namen übersetzt. Haben zwei Kunden denselben Namen, landen sie in einer Zeile; die Antwort sagt das mit `meta.merged`. Deshalb verträgt ein Verweis nur additive Kennzahlen.

Mit `"ids": true` am Merkmal behält die Antwort stattdessen die Nummer: In den Zeilen stehen Nummern, in `valueLabels` der Spalte ihre Namen, und zwei Kunden gleichen Namens bleiben zwei Zeilen. In dieser Form braucht es ein Filter, denn ein Filter nennt die Nummer.

### Filter

Ein Filter ist eine Liste von Bedingungen, und eine Liste heißt: alle ihre Einträge.

```json
"filters": [
  {"field": "status", "op": "in", "value": ["OPEN", "MITIGATED"]},
  {"field": "dateCreated", "op": "gte", "value": "2026-01-01"},
  {"field": "reviewDate", "op": "isNull"}
]
```

Ein Eintrag kann auch eine Gruppe sein. So kommen ODER und NICHT in eine Abfrage:

| Gruppe | Trifft zu, wenn |
|---|---|
| `{"all": [...]}` | alle Einträge zutreffen |
| `{"any": [...]}` | mindestens ein Eintrag zutrifft |
| `{"not": ...}` | der Eintrag darin nicht zutrifft |

Offene Risiken, die schwer wiegen oder noch keinen Prüftermin haben:

```json
"filters": [
  {"field": "status", "op": "eq", "value": "OPEN"},
  {"any": [
    {"field": "impact", "op": "gte", "value": 3},
    {"not": {"field": "reviewDate", "op": "isNotNull"}}
  ]}
]
```

Gruppen lassen sich bis zu 5 Ebenen tief schachteln (ein `not` zählt nicht als eigene Ebene). Eine Bedingung fasst höchstens 50 Vergleiche, ein Vergleich höchstens 500 Werte. `having` nimmt dieselben Gruppen, mit `as` statt `field`.

| Operator | Bedeutung | `value` |
|---|---|---|
| `eq`, `ne` | gleich, ungleich | ein Wert |
| `gt`, `gte`, `lt`, `lte` | größer, größer gleich, kleiner, kleiner gleich | ein Wert |
| `between`, `notBetween` | innerhalb oder außerhalb zweier Grenzen; die Grenzen zählen als innerhalb | genau zwei Werte |
| `in`, `notIn` | einer der Werte, keiner der Werte | eine Liste |
| `contains`, `notContains` | enthält, enthält nicht | ein Text |
| `startsWith`, `endsWith` | beginnt mit, endet mit | ein Text |
| `isNull`, `isNotNull` | leer, nicht leer | keiner |
| `inLast`, `inNext` | von so lange her bis heute, von heute bis so weit voraus | `{"amount": 7, "unit": "day"}`; `unit` ist `day`, `week`, `month`, `quarter` oder `year` |
| `inPeriod` | in einem Kalenderzeitraum um heute; Wochen beginnen am Montag | eins von `yesterday`, `today`, `tomorrow`, oder `last`, `this` bzw. `next` mit `Week`, `Month`, `Quarter`, `Year`, z. B. `lastMonth` |

Nicht jeder Operator passt zu jedem Merkmal. Jedes Merkmal nimmt `eq`, `ne`, `in`, `notIn`, `isNull` und `isNotNull`. Zahlen, Daten und Text nehmen außerdem `gt`, `gte`, `lt`, `lte`, `between` und `notBetween`. `contains`, `notContains`, `startsWith` und `endsWith` gehören zu Text, `inLast`, `inNext` und `inPeriod` zu Daten. Alles andere endet mit 400.

Werte schreiben Sie so, wie der Typ des Merkmals es verlangt:

| Typ | Wert |
|---|---|
| `date` | `"2026-01-01"` (dieser ganze Tag), ein Zeitpunkt wie `"2026-01-01T00:00:00Z"`, Millisekunden seit 1970 oder `{"relativeDays": -7}` |
| `number` | eine Zahl; Nachkommastellen bleiben erhalten |
| `boolean` | `true` oder `false` |
| `enum` | einer der Rohwerte aus `values`, z. B. `"OPEN"` |
| `string` | ein Text |
| Verweis | die Nummer des Datensatzes, nicht sein Name; bei einem Merkmal, das auf eine Person zeigt, auch `{"currentUser": true}`, die fragende Person |

**Ein Tag meint den ganzen Tag**, in der Zeitzone des Mandanten, auch bei einem Zeitpunkt: `eq "2026-03-31"` nimmt den ganzen Tag, `lte "2026-03-31"` schließt seinen Abend ein, `gt "2026-03-31"` beginnt am 1. April.

**Relative Werte** werden bei jedem Lauf neu berechnet, eine gespeicherte Abfrage stimmt also auch morgen noch. `{"relativeDays": 0}` ist heute, `-7` vor einer Woche, `3` in drei Tagen. `inLast`, `inNext` und `inPeriod` gelten genauso, und immer in ganzen Tagen.

**Ein anderes Merkmal als Wert:** `{"field": "dateCreated"}` vergleicht zwei Merkmale einer Zeile, mit `eq`, `ne`, `gt`, `gte`, `lt` und `lte`. Beide müssen denselben Typ haben, und zwei Verweise müssen auf dieselbe Art von Datensatz zeigen. Trifft ein Tag auf einen Zeitpunkt, zählt der Tag dieses Zeitpunkts in der Zeitzone des Mandanten.

```json
{"field": "followUpDate", "op": "lt", "value": {"field": "dateCreated"}}
```

**Leere Felder:** `ne`, `notIn`, `notContains`, `notBetween` und alles unter `not` finden auch Zeilen, in denen das Merkmal leer ist, denn ein leeres Feld ist nicht „Offen“. Die Größenvergleiche finden keine, deshalb ist `{"not": {"field": "impact", "op": "gt", "value": 3}}` nicht dasselbe wie `lte 3`: Es findet auch Risiken ohne Auswirkung.

**Text** ignoriert Groß- und Kleinschreibung bei `eq`, `ne`, `in`, `notIn` und den Operatoren für Teile eines Textes.

**`filters` oder `having`?** „Projekte aus 2026“ ist ein Filter: Er schränkt die Zeilen ein, bevor gezählt wird. „Kunden mit mehr als fünf Projekten“ ist ein `having`: Es schränkt die Gruppen ein, nachdem gezählt wurde.

```json
{"source": "project", "measures": ["anzahl"], "dimensions": ["kunde"],
 "having": [{"as": "anzahl", "op": "gt", "value": 5}]}
```

### Die stärksten N und der Rest

`topN` behält die stärksten Gruppen und fasst den Rest in einer Zeile zusammen. Ein Diagramm mit vierzig Reihen liest niemand; so bleibt der Rest als ehrlicher Block sichtbar, statt stillschweigend zu fehlen.

```json
"topN": {"by": "stunden", "limit": 5, "otherLabel": "Sonstige", "within": ["monat"]}
```

- `by`: die Kennzahl, nach der gerankt wird. Vorgabe ist die erste Kennzahl.
- `limit`: wie viele Gruppen bleiben. Vorgabe 5.
- `otherLabel`: der Name der Rest-Zeile, Vorgabe „Sonstige“. `null` lässt den Rest weg.
- `within`: Merkmale, innerhalb derer gerankt wird. `["monat"]` heißt „die stärksten fünf je Monat“. Hat die Abfrage außer dem gerankten Merkmal noch ein Datum, muss es hier stehen; sonst würde „Sonstige“ in die Datumsspalte geschrieben, und nexxus lehnt die Abfrage ab.

### Summen und Spalten

`totals` liefert Summen **neben** den Zeilen, nicht zwischen ihnen. Ein Diagramm, das eine Zeile „Gesamt“ zwischen seinen Daten bekäme, würde sie als riesigen Balken zeichnen.

```json
"totals": {"grandTotal": true, "subtotalsFor": ["kategorie"]}
```

`totals: true` ist die Kurzform für die Gesamtsumme. Die Antwort enthält dann `grandTotal` (`{spalte: summe}`) und `subtotals` (je Merkmal die Gruppen mit ihren Summen).

`split` dreht ein Merkmal in Spalten. Aus `monat | status | anzahl` wird `monat | Offen | Behandelt`, die Form, die ein Diagramm mit mehreren Reihen braucht. Höchstens 50 Spalten; bei mehr Werten erst `topN` anwenden.

### Die Reihenfolge

Nach dem Zählen laufen die Stufen immer in dieser Reihenfolge: `having`, `topN`, `percentOfTotal`, `totals`, `orderBy`, Beschriftung, `split`. Eine Abfrage kann sie also nicht in einer anderen Reihenfolge verlangen, und das Ergebnis hängt nicht davon ab, in welcher Reihenfolge die Felder im JSON stehen.

## Werte beschriften

Manche Werte sehen in den Daten technisch aus: der Objekttyp im Änderungsprotokoll ist `app.modules.core.migration.MigrationStatus`, eine Abwesenheitsart heißt intern `SPECIAL_LEAVE`. Drei Angaben an einem Merkmal bestimmen, wie seine Werte in Legenden, Achsen, Tabellen, Filterlisten und im PDF dastehen. Die Daten ändern sie nicht: Die Antwort enthält weiter den Rohwert, und ein Filter nennt weiter den Rohwert.

```json
"dimensions": [
  {"field": "className", "as": "typ", "format": ["class", "words"]},
  {"field": "impact", "as": "auswirkung", "template": "Stufe {value}"},
  {"field": "isActive", "as": "aktiv", "labels": {"true": "Aktiv", "false": "Inaktiv"}}
]
```

| Angabe | Form | Wirkung |
|---|---|---|
| `format` | ein Format oder eine Liste von höchstens vier, der Reihe nach angewandt | Formt den Wert um, siehe die Tabelle darunter |
| `template` | Text mit `{value}`, höchstens 80 Zeichen | Setzt Text um den Wert: aus `3` wird „Stufe 3“. `""` entfernt die Vorlage der Quelle |
| `labels` | `{rohwert: name}`, höchstens 200, je höchstens 80 Zeichen | Feste Namen für einzelne Werte. Bei einem `enum` nur seine Werte, bei Ja/Nein nur `true` und `false` |

| Format | Aus | Wird |
|---|---|---|
| `raw` | | der Wert, wie er gespeichert ist; schaltet die Vorgabe der Quelle ab |
| `class` | `app.modules.core.migration.MigrationStatus` | `MigrationStatus` (der Teil nach dem letzten Punkt) |
| `words` | `MigrationStatus`, `SPECIAL_LEAVE` | `Migration Status`, `Special leave` |
| `upper` | `Offen` | `OFFEN` |
| `lower` | `Offen` | `offen` |
| `capitalize` | `offen` | `Offen` (nur der erste Buchstabe) |

So entsteht ein Name: Steht der Wert in `labels`, gilt dieser Name. Sonst beginnt nexxus mit dem Namen, den die Quelle für den Wert kennt (bei einem `enum` etwa „Offen“ für `OPEN`), oder mit dem Wert selbst, wendet die Formate der Reihe nach an und setzt das Ergebnis in die Vorlage.

Eine Quelle kann ein Format vorgeben. Der Objekttyp des Änderungsprotokolls steht deshalb ohne jede Angabe als `MigrationStatus` da; das Schema zeigt die Vorgabe unter `format`. Die Abfrage ersetzt `format` und `template` der Quelle, `"format": "raw"` schaltet die Vorgabe ab.

Grenzen:

- **Datumsmerkmale** nehmen keine der drei Angaben. Eine Kachel schreibt ein Datum nach seinem Zeitraster („Okt. 26“, „Q3 2026“).
- **Ja/Nein-Merkmale** nehmen nur `labels`.
- Den Rest von `topN` („Sonstige“) beschriftet nichts.
- `split` benennt seine Spalten nach den Namen, der Schlüssel jeder Spalte (`as`) bleibt der Rohwert. Eine Sortierung oder eine Reihenfarbe wandert also nicht mit, wenn Sie die Beschriftung ändern.
- Ergeben zwei Rohwerte denselben Namen, stehen sie in Diagrammen zusammen.

## Die Antwort

```json
{
  "columns": [
    {"as": "typ", "role": "dimension", "type": "string", "label": "Objekttyp", "grain": null,
     "valueLabels": {"app.modules.communication.email.Email": "Email"}},
    {"as": "anzahl", "role": "measure", "type": "number", "label": "Anzahl Änderungen", "grain": null, "valueLabels": null}
  ],
  "rows": [["app.modules.communication.email.Email", 1800], ["Sonstige", 412]],
  "meta": {"rowCount": 2, "truncated": false, "durationMs": 37, "merged": false, "foldedIntoOther": true, "split": null},
  "subtotals": [],
  "grandTotal": null
}
```

- `columns` beschreibt die Spalten einmal, `rows` enthält die Zeilen als Listen in dieser Reihenfolge. Ein Diagramm braucht die Spalten nur einmal, nicht in jeder Zeile.
- `label` ist der Name der Spalte, `valueLabels` die Beschriftung der Werte eines Merkmals: Rohwert und Name, für die Werte in den Zeilen, deren Name vom Rohwert abweicht. Ganze Zahlen stehen als Text ohne Nachkommastellen (`"3"`). Mit `ids` an einem Verweis steht dort der Name jeder Nummer in den Zeilen. Sonst `null`.
- **Datumswerte kommen immer als Millisekunden seit 1970**, gleich welches Zeitraster.
- `meta.truncated: true` heißt: Die Zeilengrenze hat Zeilen abgeschnitten. Wer die Antwort weiterverwendet, sollte das sagen, statt Vollständigkeit vorzutäuschen.
- `meta.merged`: Verweis-Werte gleichen Namens wurden zusammengefasst. `meta.foldedIntoOther`: `topN` hat einen Rest gebildet. `meta.split`: das Merkmal, das zu Spalten wurde.

## Fehler

| Status | Wann | Beispiel |
|---|---|---|
| 403 | Das Recht für Auswertungen fehlt, oder das Leserecht der Quelle; die Meldung nennt den Schlüssel | |
| 400 | Unbekannte Quelle, unbekanntes Merkmal, unbekannte Kennzahl, unbekannter Operator oder unbekanntes Zeitraster, ein ungültiger Enum-Wert, ein Verweis auf eine Spalte, die die Abfrage nicht hat, eine nicht additive Kennzahl, wo addiert werden müsste, eine Beschriftung, die das Merkmal nicht nimmt | „Unknown dimension 'kundenname' for source 'project'. Available: billable, billingModel, dateCreated, designation, isActive, isInternal, kunde, projektgruppe“ |

Die Meldungen sind für Menschen und für Sprachmodelle geschrieben: Sie zählen auf, was gültig gewesen wäre. Der KI-Assistent korrigiert damit seine eigene Abfrage, bevor Sie eine Kachel zu sehen bekommen.

## Die Quellen

Welche Quellen eine Person sieht, hängt von ihren Rechten ab. Die folgende Liste ist die eines Administrators. Merkmale vom Typ `date` bieten alle Zeitraster an. Die Namen der Quellen, Merkmale und Kennzahlen stehen in jeder Sprache auf Deutsch, so wie der Server sie liefert. Die erste Spalte nennt den Schlüssel der Quelle, ihren Namen und das Leserecht, das sie verlangt.

| Quelle | Merkmale | Kennzahlen |
|---|---|---|
| `project`<br/>Projekte<br/>`project.project.read` | `designation` Projekt, `kunde` Kunde (Verweis), `projektgruppe` Projektgruppe (Verweis), `isActive` Aktiv, `isInternal` Intern, `billable` Abrechenbar, `billingModel` Abrechnungsart, `dateCreated` Angelegt am | `anzahl` Anzahl Projekte, `kunden` Kunden (verschiedene) |
| `timesheetEntry`<br/>Zeiteinträge<br/>`timesheet.entry.read` | `entryDate` Datum, `projekt` Projekt (Verweis), `source` Herkunft, `billable` Abrechenbar | `eintraege` Einträge, `stunden` Stunden |
| `vacationRequest`<br/>Abwesenheiten<br/>`absence.vacationRequest.read` | `mitarbeiter` Mitarbeiter (Verweis), `type` Art, `status` Status, `fromDate` Beginn | `antraege` Anträge, `tage` Tage |
| `partner`<br/>Kunden und Lieferanten<br/>`partner.partner.read` | `name` Name, `partnerType` Art, `isActive` Aktiv, `isInternal` Intern, `currency` Währung, `dateCreated` Angelegt am | `anzahl` Anzahl |
| `ismsRisk`<br/>ISMS-Risiken<br/>`isms.risk.read` | `status` Status, `likelihood` Eintrittswahrscheinlichkeit, `impact` Auswirkung, `reviewDate` Nächste Prüfung, `dateCreated` Angelegt am | `anzahl` Anzahl Risiken, `wahrscheinlichkeitSchnitt` und `auswirkungSchnitt` (Durchschnitte) |
| `ismsMeasure`<br/>ISMS-Maßnahmen<br/>`isms.measure.read` | `category` TOM-Kategorie, `measureType` Art, `status` Status, `recurrence` Turnus, `approvalRequired` Freigabe nötig, `reviewDate` Erste Fälligkeit, `dateCreated` Angelegt am | `anzahl` Anzahl Maßnahmen |
| `form`<br/>Formulare<br/>`formBuilder.read` | `name` Formular, `type` Typ, `status` Status, `version` Version, `dateCreated` Angelegt am | `anzahl` Anzahl Formulare, `hoechsteVersion` Höchste Version |
| `user`<br/>Benutzerkonten<br/>`authentication.user.read` | `username` Benutzername, `accountType` Kontotyp, `invitationPending` Einladung offen, `lastLoginAt` Letzte Anmeldung, `dateCreated` Angelegt am | `anzahl` Anzahl Konten |
| `aiSession`<br/>KI-Sitzungen<br/>`ai.session.use` | `dateCreated` Begonnen am, `model` Modell, `status` Status, `origin` Herkunft, `createdById` Benutzer-ID | `sitzungen` Sitzungen, `nachrichten` Nachrichten, `nachrichtenSchnitt` Ø Nachrichten je Sitzung |
| `aiScheduleRun`<br/>Schedule-Läufe<br/>`aiAssistant.schedule.read` | `status` Status, `trigger` Auslöser, `scheduleId` Schedule-ID, `model` Modell, `scheduledFor` Solltermin, `dateCreated` Angelegt am | `laeufe` Läufe, `dauerMs` Ø Dauer (ms), `werkzeugaufrufe` Werkzeugaufrufe, `werkzeugfehler` Werkzeugfehler |
| `auditLog`<br/>Protokoll (Änderungen)<br/>*nur Administratoren* | `className` Objekttyp (Format `class`), `eventName` Vorgang, `actor` Person, `dateCreated` Zeitpunkt | `anzahl` Anzahl Änderungen, `personen` Aktive Personen (verschiedene) |

Einige Quellen sind bewusst knapp: Die Risiken kommen ohne Titel und Beschreibung, die Partner ohne Kontakt- und Bankdaten, das Änderungsprotokoll ohne alte und neue Werte, die Benutzerkonten ohne persönliche Angaben. Was eine Quelle nicht veröffentlicht, kann niemand auswerten.

Zwei Quellen schränken die Zeilen je Person ein: **KI-Sitzungen** zeigen jeder Person nur ihre eigenen, Administratoren alle; **Abwesenheiten** zeigen die eigenen und die der Teams, die die Person leitet, wie die Liste der Abwesenheiten.

### Quellen aus Paketen

Jedes Fachobjekt eines aktiven Paket-Moduls ist außerdem eine eigene Quelle mit dem Schlüssel `<paket>:<fachobjekt>`. Die Liste oben enthält sie nicht, weil sie davon abhängen, was Ihr Mandant installiert hat. Das Paket muss dafür nichts tun.

| Was | Regel |
|---|---|
| Name | Die Mehrzahl des Fachobjekts, dahinter das Paket in Klammern, zum Beispiel <Ui>Kunden (Vorgänge)</Ui>. |
| Recht | Das Leserecht des Fachobjekts. Ein Datensatz hat keine eigenen Rechte: Wer das Fachobjekt lesen darf, sieht alle seine Zeilen. |
| Merkmale | Die indizierten Felder des Fachobjekts und jeder Verweis, dazu `id`, `createdAt`, `updatedAt` und `createdBy`. Ein Feld ohne Index fehlt auch hier. |
| Kennzahlen | `anzahl`, dazu je Zahlenfeld `summe_<feld>`, `schnitt_<feld>`, `min_<feld>` und `max_<feld>`. |

Drei Dinge verhalten sich anders als bei den Quellen oben:

- **Felder mit mehreren Werten:** Ein Feld mit `multiple` führt einen Datensatz unter jedem seiner Werte. Eine Summe über solche Gruppen zählt ihn einmal je Wert.
- **Text:** Vergleiche achten nicht auf Groß- und Kleinschreibung und reichen bis zum 500. Zeichen eines Felds.
- **Ausgeschaltet:** Solange ein Paket deaktiviert ist, fehlen seine Quellen. Kacheln darauf zeigen <Ui>Dieser Bereich ist ausgeschaltet</Ui>, Benachrichtigungsregeln stehen auf <Ui>kann nicht prüfen</Ui>. Nach dem Aktivieren arbeiten beide wieder wie vorher.

## Grenzen

- **Eine Quelle je Abfrage.** NQL verbindet keine Quellen. Werte aus anderen Modulen kommen nur als Verweis-Merkmal, etwa der Kunde eines Projekts.
- **Keine zweite Y-Achse.** Zwei Größen mit verschiedener Skala sind zwei Kacheln.
- **Zeilen:** Vorgabe 1000, höchstens 5000; vor `topN` und Zusammenfassungen höchstens 5000 Gruppen aus der Datenbank. `split` höchstens 50 Spalten.

## Für Entwickler: eine Quelle anbieten

Die Fachobjekte eines Pakets werden von selbst zu Quellen, siehe [Quellen aus Paketen](#quellen-aus-paketen). Alle anderen Quellen entstehen im Code von nexxus.

Im Regelfall macht eine Annotation eine Entität auswertbar, und zwar genau so weit, wie sie es aufzählt:

```kotlin
@NqlExposed(
  label = "Protokoll (Änderungen)",
  permission = "core.auditLog.read",
  adminOnly = true,
  description = "Änderungen im System: Anzahl nach Objekttyp, Vorgang, Person und Zeit.",
  dimensions = [
    NqlDim(field = "className", label = "Objekttyp", format = ["class"]),
    NqlDim(field = "eventName", label = "Vorgang"),
    NqlDim(field = "dateCreated", label = "Zeitpunkt"),
  ],
  measures = [
    NqlMeas(fn = "count", name = "anzahl", label = "Anzahl Änderungen"),
  ])
@Entity
open class AuditLog { ... }
```

- **Positivliste, absichtlich.** Abgefragt werden kann nur, was unter `dimensions` und `measures` steht. Sonst wäre jedes neue Feld sofort auswertbar, beim nächsten `passwordHash` ein Datenleck durch Unterlassung.
- **`permission` ist Pflicht** und derselbe Schlüssel, den die Liste des Moduls verlangt. `adminOnly = true` öffnet eine Quelle nur Administratoren.
- **Der Typ** wird aus dem Feld gelesen. Werte bekommen Namen über `valueLabels = ["OPEN=Offen", ...]`; trägt ein Enum eine Eigenschaft `label`, gilt diese. `format` und `template` geben die Beschriftung vor, solange eine Abfrage nichts anderes sagt.
- **Der Schlüssel** der Quelle ist der Klassenname mit kleinem Anfangsbuchstaben, solange `key` nichts anderes sagt.

Braucht eine Quelle einen Zeilenfilter unterhalb des Mandanten (nur eigene Datensätze) oder ein Merkmal aus einem anderen Modul, schreiben Sie sie als `@Component`, das `NqlSource` implementiert. Merkmale aus anderen Modulen gruppieren nach der Nummer und werden danach über einen `NqlRefResolver` beschriftet, mit einer Abfrage je Ergebnis:

```kotlin
NqlDimension.ref("kunde", "partnerId", NqlEntityLabelResolver(entityManager, Partner::class.java) { it.name }).label("Kunde")
```

Mandant und Löschstatus verantwortet keine Quelle selbst. Die setzt nexxus für jede Abfrage. Fehlt der Mandant, liefert eine Abfrage nichts statt alles.

## Wie es weitergeht

<CardGrid>
  <LinkCard title="Auswertungen" description="Auswertungen anlegen, filtern, teilen und als PDF herunterladen." href="/de/use/analytics/" />
  <LinkCard title="Dashboard bauen" description="Eine Kennzahl auf der Startseite einrichten." href="/de/use/dashboard/#eine-kennzahl-hinzufügen" />
  <LinkCard title="Berechtigungen" description="Welches Recht welche Quelle öffnet." href="/de/reference/permissions/" />
</CardGrid>