Zum Hauptinhalt springen

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​

FeldtypOperatoren
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
Enumnur _in
DateTime, Date, Time_eq, _in, _gt, _lt, _gte, _lte, _is_null
Array_contains, _contained_in, _has_key, _is_null
object-EigenschaftKein Operatorsatz — filtern Sie die inneren Felder, siehe Verschachtelte Objekte

Zwei davon sind schmaler, als man erwarten könnte, und zwar mit Absicht:

  • ID hat kein _is_null. Ein Datensatz hat immer eine id. Die Ordnungsvergleiche gibt es hingegen: Sie existieren, damit id einen 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 _eq auf 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.

urgent-open-requests.graphql
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.

filter-ilike.graphql
{ 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:

OperatorBedeutung
_containsDas Array enthält den angegebenen JSON-Wert.
_contained_inJedes Element des Arrays kommt im angegebenen JSON-Wert vor.
_has_keyDer angegebene Schlüssel ist vorhanden (bei Arrays von Objekten).

Der Operand ist ein JSON-String, keine GraphQL-Liste, und auf 3.000 Zeichen begrenzt:

certifications-contains.graphql
{ 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: FilterDer angegebene Filter darf nicht zutreffen.
neglected-or-critical.graphql
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:

powerful-chillers.graphql
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:

equipment-with-urgent-requests.graphql
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:

filter-both-levels.graphql
{
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:

order-by-category-name.graphql
{
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_at aufsteigend, falls Sie überhaupt keine Sortierung angegeben haben — eine unsortierte Liste ist damit älteste-zuerst und nicht willkürlich;
  • dann immer id aufsteigend, 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.