Filtern und Sortieren
Jede Connection — ob Wurzelfeld oder To-many-Relation — nimmt ein where-Argument zum Filtern und ein
order_by-Argument zum Sortieren. Beide werden aus Ihren Definitionen erzeugt; welche Operatoren auf einem
Feld verfügbar sind, folgt also aus dem Typ dieses Feldes.
Die Beispiele verwenden die Objekte equipment und maintenance_request aus dem
Überblick.
Operatoren nach Feldtyp
| Feldtyp | Operatoren |
|---|---|
ID (string + format: uuid) | _eq, _in, _gt, _lt, _gte, _lte |
String | _eq, _in, _like, _ilike, _is_null |
Float (number) | _eq, _in, _gt, _lt, _gte, _lte, _is_null |
Boolean | _eq, _is_null |
| Enum | nur _in |
DateTime, Date, Time | _eq, _in, _gt, _lt, _gte, _lte, _is_null |
| Array | _contains, _contained_in, _has_key, _is_null |
object-Eigenschaft | Kein Operatorsatz — filtern Sie die inneren Felder, siehe Verschachtelte Objekte |
Zwei davon sind schmaler, als man erwarten könnte, und zwar mit Absicht:
IDhat kein_is_null. Ein Datensatz hat immer eineid. Die Ordnungsvergleiche gibt es hingegen: Sie existieren, damitideinen zusammengesetzten Cursor vervollständigen kann — siehe vollständige Durchläufe.- Enums akzeptieren nur
_in, und der Operand ist eine Liste von Strings. Das Ausgabefeld ist ein GraphQL-Enum, der Filter erwartet aber_in: ["hvac"]— in Anführungszeichen —, nicht das nackte Enum-Literal[hvac]. Für einen einzelnen Wert verwenden Sie eine Liste mit einem Element; ein_eqauf einem Enum-Feld gibt es nicht.
Alle Operatoren auf einem Feld werden mit UND verknüpft. { name: { _like: "Chiller%" }, operational: { _eq: true } }
bedeutet: beide Bedingungen.
query UrgentOpenRequests {
maintenance_request(
where: {
resolved: { _eq: false }
priority: { _in: ["urgent"] }
reported_at: { _gte: "2026-03-01T00:00:00Z" }
}
order_by: [{ reported_at: desc }]
first: 20
) {
totalCount
edges {
node {
id
title
reported_at
}
}
}
}
Textvergleich
_like unterscheidet Groß- und Kleinschreibung, _ilike nicht. Beide verwenden SQL-Mustersyntax: %
steht für eine beliebige Zeichenfolge, _ für genau ein Zeichen.
Ein Muster darf höchstens zwei %-Wildcards enthalten. Ein drittes wird mit validation-failed
abgelehnt — Wildcards an beiden Enden eines langen Musters erzwingen einen Scan, den die Datenbank nicht
über einen Index bedienen kann.
{ equipment(where: { name: { _ilike: "%chiller%" } }, first: 10) { edges { node { name } } } }
Prüfung auf Null
_is_null: true findet Datensätze, in denen das Feld nicht gesetzt ist; _is_null: false findet die, in
denen es gesetzt ist. Das ist etwas anderes als _eq: null, was nicht unterstützt wird.
Array-Felder
Eine Array-Eigenschaft wird als JSON gespeichert und daher über Enthaltensein gefiltert, nicht über Vergleiche:
| Operator | Bedeutung |
|---|---|
_contains | Das Array enthält den angegebenen JSON-Wert. |
_contained_in | Jedes Element des Arrays kommt im angegebenen JSON-Wert vor. |
_has_key | Der angegebene Schlüssel ist vorhanden (bei Arrays von Objekten). |
Der Operand ist ein JSON-String, keine GraphQL-Liste, und auf 3.000 Zeichen begrenzt:
{ equipment(where: { certifications: { _contains: "[\"TUV\"]" } }, first: 10) { edges { node { name } } } }
Bedingungen verknüpfen
_and: [Filter!] | Alle angegebenen Filter müssen zutreffen. |
_or: [Filter!] | Mindestens einer muss zutreffen. |
_not: Filter | Der angegebene Filter darf nicht zutreffen. |
query NeglectedOrCritical {
equipment(
where: {
_or: [
{ operational: { _eq: false } }
{ _and: [{ category: { _in: ["hvac"] } }, { replacement_cost: { _gte: 20000 } }] }
]
_not: { asset_tag: { _like: "TEMP-%" } }
}
first: 25
) {
totalCount
edges { node { name asset_tag } }
}
}
Zwei Randfälle sollten Sie kennen, weil sie einander entgegengesetzt sind:
_and: []— eine leere Liste — findet alles._or: []findet nichts.
Das folgt aus der Logik (eine leere Konjunktion ist wahr, eine leere Disjunktion falsch), fällt aber
unangenehm auf, wenn ein Client die Liste dynamisch aufbaut: Ein _or, dessen Bedingungen alle
herausgefiltert wurden, liefert null Treffer statt ignoriert zu werden. Lassen Sie den Schlüssel weg, wenn
Sie keine Bedingungen haben.
Verschachtelungsgrenze
Ein where-Argument darf höchstens 4 Ebenen tief verschachtelt sein. Jedes _and, _or, _not und
jeder Filter auf ein verschachteltes Objekt zählt als eine Ebene. Bei Überschreitung kommt
graphql/filter-too-deep zurück, mit der übermittelten Tiefe und dem Grenzwert.
Dieses Limit ist unabhängig vom Tiefenlimit für Abfragen, das die Relationsnavigation begrenzt — es sind zwei getrennte Regler mit getrennten Werten. Siehe Fehler und Limits.
Verschachtelte Objekte
Eine Eigenschaft vom Typ object wird als JSON-String zurückgegeben, ist aber nach ihren inneren
Feldern filter- und sortierbar. Der erzeugte Filter-Input spiegelt das verschachtelte Schema:
query PowerfulChillers {
equipment(
where: { specs: { power_kw: { _gte: 40 }, manufacturer: { _eq: "Kelvion" } } }
order_by: [{ specs: { power_kw: desc } }]
first: 10
) {
edges { node { name specs } }
}
}
Die Verschachtelung folgt der Struktur Ihrer Definition, bis zur maximalen Schematiefe von 3.
Nach einem verknüpften Datensatz filtern
Eine Relation steuert dem Filter-Input ein Feld bei, benannt nach dem Relations-Slug und typisiert als Filter der anderen Seite. Es trifft auf einen Elterndatensatz zu, wenn mindestens ein verknüpfter Datensatz zutrifft:
query EquipmentWithUrgentRequests {
equipment(
where: { maintenance_requests: { priority: { _in: ["urgent"] }, resolved: { _eq: false } } }
first: 20
) {
totalCount
edges { node { name } }
}
}
Achten Sie genau auf die Semantik: Das liefert Geräte, die irgendeine unerledigte dringende Anfrage
haben. Es filtert nicht die maintenance_requests-Connection selbst. Um beides zu erreichen — die Geräte
auswählen und die zurückgegebenen Anfragen einschränken — filtern Sie an beiden Stellen:
{
equipment(where: { maintenance_requests: { resolved: { _eq: false } } }, first: 20) {
edges {
node {
name
maintenance_requests(where: { resolved: { _eq: false } }, first: 5) {
edges { node { title } }
}
}
}
}
}
Verborgene Relationsseiten steuern kein Filterfeld bei, so wie sie auch kein Ausgabefeld beisteuern.
Sortieren
order_by nimmt eine Liste, und die Reihenfolge der Liste ist die Sortierpriorität:
{
equipment(order_by: [{ category: asc }, { name: asc }], first: 50) {
edges { node { category name } }
}
}
Die Richtungen sind asc und desc. Felder verschachtelter Objekte werden in derselben Form sortiert, die
auch beim Filtern verwendet wird: order_by: [{ specs: { power_kw: desc } }].
Nach lock_version kann nicht sortiert werden; nach jedem anderen Feld schon, einschließlich der
Systemfelder id, created_at und updated_at.
Ergebnisse stehen immer in einer totalen Ordnung
Das ist wichtiger, als es klingt, denn es macht Paginierung verlässlich. PostgreSQL garantiert ohne ausdrückliche Sortierung keine Zeilenreihenfolge; eine unsortierte Abfrage in Kombination mit einem Seitenfenster könnte also denselben Datensatz auf zwei Seiten liefern und einen anderen nie.
Deshalb erhält jede Abfrage eine totale Ordnung:
- zuerst Ihre
order_by-Felder, in der von Ihnen angegebenen Reihenfolge; - dann
created_ataufsteigend, falls Sie überhaupt keine Sortierung angegeben haben — eine unsortierte Liste ist damit älteste-zuerst und nicht willkürlich; - dann immer
idaufsteigend, was jeden verbleibenden Gleichstand auflöst.
Sie müssen also nie selbst ein Tiebreaker-Feld ergänzen, und zwei identische Abfragen liefern Seiten, die zusammenpassen.