Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Switch services using the Version drop-down list. Learn more about navigation.
Applies to: ✅ Azure Data Explorer
GQL (Graph Query Language) is an ISO standard (ISO/IEC 39075:2024) for querying graphs by matching patterns of paths, nodes, and edges.
Prerequisites
Create a graph and set a graph reference before running queries. See Create a graph and set a graph reference. Every query begins with
MATCH.Use the sample movies graph G() referenced in the prerequisites. The graph contains Person nodes (Name, Born), Movie nodes (Title, Description, Year), and ACTED_IN and DIRECTED edges (Role), with labels stored in the Label2 column.
For the complete list of supported clauses, see GQL clauses.
For the complete list of supported functions, see GQL scalar functions.
Note
- Some examples use illustrative labels that aren't included in the sample movies graph.
- All graph queries begin with the MATCH clause.
Query basic structure
A GQL query starts with a MATCH clause and follows a pipeline structure, where each clause operates on the results of the previous clause:
MATCHfinds patterns in the graph.WHEREfilters the matched rows.RETURNprojects the output columns, optionally withDISTINCTand aggregations.ORDER BY, thenOFFSETandLIMIT, sort and paginate the result.
Combine several statements with a composite MATCH sequence, a NEXT pipeline (the rows returned by one statement feed the next), or UNION to concatenate results.
MATCH
MATCH describes a pattern to find in the graph. A pattern is a chain of node patterns connected by edge patterns. Bind variables to entities to filter or return them.
Each element pattern (node or edge) can be filtered by label (:Person), by inline properties ({Year: 1995}), and by predicate (WHERE inside the element, such as (n:Person WHERE n.Born > 1950)).
Node patterns
A node pattern includes parentheses, optionally with a variable, a label, and inline properties.
| Pattern | Matches |
|---|---|
() |
any node |
(m) |
any node, bound to variable m |
(:Movie) |
nodes labeled Movie |
(m:Movie) |
Movie nodes, bound to variable m |
(m:Movie {Year: 1995}) |
Movie nodes where Year equals 1995 |
(p:Adventure & !Horror) |
nodes with the Adventure label but not Horror. Combine labels with &, \| and ! (see Labels) |
match (m:Movie) return m.Title as MovieTitle
| MovieTitle |
|---|
| m.Title |
| T1 |
| T2 |
| T3 |
Edge patterns
An edge pattern includes square brackets between two nodes, with dashes and arrows that set direction.
| Pattern | Direction |
|---|---|
(a)-[e]->(b) |
from a to b |
(a)<-[e]-(b) |
from b to a |
(a)-[e]-(b) |
either direction |
(a)->(b), (a)<-(b), (a)-(b) |
direction without a bound edge |
(a)-[e:ACTED_IN \| DIRECTED]->(b) |
from a to b, matching ACTED_IN or DIRECTED edges. Combine labels with &, \|, !, and () (see Labels) |
The shorthand ->, <-, and - may be used in place of -[]->, <-[]-, and -[]-. Edge variables and labels work like nodes: [e], [:ACTED_IN], [e:ACTED_IN], [e {Role: 'Director'}].
match (p:Person)-[:ACTED_IN]->(m:Movie) return p.Name as actor, m.Title as movie
| actor | movie |
|---|---|
| Tom | T1 |
| Tom | T2 |
| Tom | T3 |
| Kevin | T1 |
Count the matched patterns:
match (p:Person)-[:ACTED_IN]->(m:Movie) return count(*) as patternsCount
| patternsCount |
|---|
| 4 |
Chained patterns
Connect multiple patterns to follow relationships across several hops. Use the same variable to refer to the same node more than once.
match (p1:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person)
return p1.Name as firstActor, m.Title as movie, p2.Name as secondActor
| firstActor | movie | secondActor |
|---|---|---|
| Tom | T1 | Kevin |
| Kevin | T1 | Tom |
Multiple sequences (Multi-path or "star" pattern)
GQL supports multiple comma-separated patterns in a single MATCH clause. The patterns must share at least one node variable, so they form a single connected pattern. For example, the following query finds people who acted in one movie and directed another.
match (p:Person)-[:ACTED_IN]->(m:Movie), (p)-[:DIRECTED]->(m2:Movie)
return p.Name as person, m.Title as firstTitle, m2.Title as secondTitle
| person | firstTitle | secondTitle |
|---|---|---|
| Tom | T1 | T2 |
| Tom | T2 | T2 |
| Tom | T3 | T2 |
Variable-length edges
Use a quantifier to follow a relationship across multiple hops. Assign the path to a variable (p = ...) to return the full path, or reference specific edges or nodes at index i with expressions such as p[i].
match p = (n1)-[e]->{2,2}(n2) return p
| Quantifier | Hops |
|---|---|
{m,n} |
between m and n hops |
{m,} |
m or more hops |
{,n} |
up to n hops |
{,} |
any number of hops |
{n} |
exactly n |
* |
zero or more (same as {,}) |
+ |
one or more (same as {1,}) |
Example:
match (e:Employee)->*(m:Manager) return m.Name as managerName
Labels
Labels select nodes and edges by category. Filter with : followed by a label expression. Any logical combination of AND (&), OR (|), and NOT (!), nested with parentheses to any depth, is supported for both node and edge patterns. Label names are case-sensitive; quote names that collide with keywords using backticks.
For the following examples, set the graph label name to Label2 as described in Create a graph and set a graph reference.
| Operator | Meaning |
|---|---|
:A |
has label A |
A & B |
has both |
A \| B |
has either |
!A |
does not have A |
Next query finds all entities with label Female:
match (p :Female) return p.Name as name
| name |
|---|
| Julia |
Next query showcases a group label expression:
match ()-[e: !ACTED_IN & (DIRECTED | A) | E]->(m:Movie) return m.Title as title
| title |
|---|
| T1 |
| T2 |
Reference the same variable with two labels to require both:
match (n:Male), (n:BestActorAward) return n.Name as name
| name |
|---|
| Tom |
The labels() function returns an element's labels as a list. It's useful in WHERE and RETURN clauses. For example, 'DIRECTED' IN labels(e).
match (p:Person)-[e:ACTED_IN]->(m:Movie)
return p.Name as name, labels(e) as EdgeLabels
| name | EdgeLabels |
|---|---|
| Tom | ["ACTED_IN"] |
| Tom | ["ACTED_IN"] |
| Tom | ["ACTED_IN"] |
| Kevin | ["ACTED_IN"] |
Access node properties
Access a property of a node or an edge by name using dot notation: the bound variable, a ., and the property name.
match (m:Movie)
return m.Title as title, m.Year as year
| title | year |
|---|---|
| T1 | 1995 |
| T2 | 2011 |
| T3 | 2020 |
The [] indexer accesses an element of a list or path by its integer position (zero-based), not a property by name. Combine it with dot notation to read a property of a path element. For example, p[0] is the first node of the path p, so p[0].Name returns that node's Name:
match p = (:Person)-[:ACTED_IN]->(:Movie)
return p[0].Name as name
| name |
|---|
| Tom |
| Tom |
| Tom |
| Kevin |
Comparisons
GQL supports the standard comparison operators on scalar values: =, <> (not equal), <, <=, >, and >=.
Next query filters out movies with title 'T1' from matched patterns:
match (p:Person)-[:ACTED_IN]->(m:Movie)
where m.Title <> 'T1'
return p.Name as name, m.Title as title
| name | title |
|---|---|
| Tom | T2 |
| Tom | T3 |
You can't compare two nodes or edges directly with = or <>. To test whether two entities are different, compare their identities with element_id() instead. For example, the following query finds pairs of distinct actors who appeared in the same movie:
match (p1:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person)
where element_id(p1) <> element_id(p2)
return p1.Name as firstName, p2.Name as secondName, m.Title as title
| firstName | secondName | title |
|---|---|---|
| Tom | Kevin | T1 |
| Kevin | Tom | T1 |
Filtering edges in a variable-length path
Inside the edge pattern, label and WHERE filters apply to every edge along the path; only paths whose hops all match are kept.
match ()-[e:DIRECTED]->+() return e[0].Name as personName, e[0].Role as personRole
| personName | personRole |
|---|---|
| Ron | Director |
| Tom | Director |
match ()-[e where 'DIRECTED' in labels(e)]->+() return e[0].Role as role
| role |
|---|
| Director |
| Director |
Outside the pattern, the edge variable e is an array of the traversed edges, so index it (e[0], e[1]) to access individual hops.
The following example finds the distinct roles. The CAST to string is required to deduplicate the values, because the type of Role is unknown in this context.
match ()-[e where 'DIRECTED' in labels(e)]->+() return distinct cast(e[0].Role as string) as role
| role |
|---|
| Director |
The following example finds all patterns with one or two hops and filters every edge by Role. The query returns all paths of length 1, and each path is returned as a JSON string.
match p = (n1)-[e {Role: "Role2"}]->{1,2}(n2)
where path_length(p) = 1
return to_json_string(p) as myPath
| myPath |
|---|
| [{"Name":"Kevin", ... "Description":"A movie about space","Year":1995}] |
Operators
Beyond comparison operators, GQL supports arithmetic, concatenation, and boolean logic:
- Arithmetic (
+,-): Operate on numeric values, and add or subtract atimespan(duration(...)) to or from adatetimeto shift a point in time. - Concatenation (
||): Joins two strings, or two lists, into a single value. - Boolean (
AND,OR,NOT,IN): Combine predicates inWHERE,CASE, and other conditions; group them with parentheses.
match (p:Person)
return p.Name || ' born ' || cast(p.Born as string) as `Label`, p.Born + 1 as NextYear
| Label | NextYear |
|---|---|
| Tom born 1956 | 1957 |
| Kevin born 1958 | 1959 |
| Ron born 1954 | 1955 |
| Julia born 1967 | 1968 |
Note
The Label variable is escaped because label is a reserved keyword. To differentiate between GQL syntax and user variables, escape user variables. Alternatively, add a prefix or suffix _.
Paths
A path is the full route a pattern traverses. Bind it with a path variable using name = pattern; the variable holds an alternating sequence of nodes and edges, which you can return, index (p[0]), or pass to a path function.
match `path` = (n1)-[e]->{1,2}(n2)
where `path`[0].Born = 1958
return `path`
| path |
|---|
| [{"Name": "Kevin" ,..., "Drama", "History"]}] |
Tip
The path alias path is escaped because path is a reserved keyword. To differentiate between GQL syntax and user variables, you can escape user variables. Alternatively, add a prefix or suffix _.
match p1 = (n1)-[e]->{1,2}(n2), p2 = (:Movie & War) return p1, p2
| p1 | p2 |
|---|---|
| [{ "Name": "Tom" ,..., "War" ]}] | [{ ,..., "War" ]}] |
The available path functions are path_length, nodes, and edges (also spelled relationships), which operate on a path variable. For details, see Graph and path functions.
match p = (n1)-[]->{1,3}(n2)
where p[0].Name starts with 'K'
return nodes(p)[0] as firstNode, edges(p) as `edges`, path_length(p) as pathLength
| firstNode | edges | pathLength |
|---|---|---|
| { "Name": "Kevin", ... | [ ..."ACTED_IN"... }] | 1 |
Note
The edges variable is escaped because edges is a reserved keyword. To differentiate between GQL syntax and user variables, you can escape user variables. Alternatively, add a prefix or suffix _.
You can combine these functions to return the nodes, edges, and length of a matched path:
match p = (n0:Person)-[:DIRECTED]->(m:Movie {Title: 'T1'})
return nodes(p), edges(p), path_length(p)
| nodes(p) | edges(p) | path_length(p) |
|---|---|---|
| [{"Name": "Ron",...},{"Title": "T1", ...}] | [{..."DIRECTED"...}] | 1 |
Shortest paths
ANY SHORTEST returns a single shortest path for each source and target pair.
match P = any shortest (n {station:"South-West"})->{1,3}(a {station: "North"})
return P
All shortest paths
ALL SHORTEST returns all shortest paths of equal minimum length.
match P = all shortest (n {station:"South-West"})->{1,3}(a {station: "North"})
return P
Limitations
- A shortest path
MATCHclause can't contain multiple pattern sequences.
MATCH and PATH modes
A MATCH clause can specify how to handle cycles and repeated elements. The structure is:
MATCH [DIFFERENT EDGES | REPEATABLE ELEMENTS] [WALK | TRAIL | ACYCLIC | SIMPLE] PATTERN_1, ..., [WALK | TRAIL | ACYCLIC | SIMPLE] PATTERN_N WHERE ...
- Match mode — one setting per
MATCHclause, eitherDIFFERENT EDGES(No edge matched twice) orREPEATABLE ELEMENTS(As many results as possible). The default isDIFFERENT EDGES. - Path mode — each path in the clause can be prefixed with
WALK(No restrictions),TRAIL(No repeated edges),ACYCLIC(No repeated nodes), orSIMPLE(No repeated interior nodes). The GQL default isWALK.
The following tables show which combinations are supported. Multi-path is a "star" pattern with several paths.
DIFFERENT EDGES (default):
| Path mode | Single path | Multi-path ("star") |
|---|---|---|
| WALK | Supported | Supported (only if all paths are WALK/TRAIL) |
| TRAIL | Supported | Supported (only if all paths are WALK/TRAIL) |
| ACYCLIC | Supported | Not supported |
| SIMPLE | Not supported | Not supported |
REPEATABLE ELEMENTS:
| Path mode | Single path | Multi-path ("star") |
|---|---|---|
| WALK | Supported | Supported (only if all paths are WALK) |
| TRAIL | Supported | Not supported |
| ACYCLIC | Supported | Not supported |
| SIMPLE | Not supported | Not supported |
match different edges trail (p:Person)-[]->{1,3}(m:Movie) return p.Name as name, m.Title as title
With REPEATABLE ELEMENTS, the same node or edge can be revisited within a match:
match repeatable elements walk (p:Person)-[:ACTED_IN]->(m:Movie) return p.Name as name, m.Title as title
Aggregations
Aggregate expressions in RETURN compute over the matched rows. The non-aggregated columns form the grouping key. Supported aggregations are count, sum, avg, min, max, and collect_list; each accepts DISTINCT.
For example, find all actors who acted in a movie:
match (p:Person)-[:ACTED_IN]->(m:Movie)
return m.Title as Title, count(*) as Actors, collect_list(p.Name) as ActorNames
| Title | Actors | ActorNames |
|---|---|---|
| T3 | 1 | [ "Tom"] |
| T2 | 1 | [ "Tom"] |
| T1 | 2 | [ "Tom", "Kevin"] |
Next query calculates count of patterns:
match (p:Person)-[:ACTED_IN]->(m:Movie)
return count(*) as countOfPatterns
| countOfPatterns |
|---|
| 4 |
Combine several aggregations in one RETURN; the non-aggregated columns form the grouping key:
match (p:Person)-[:ACTED_IN]->(m:Movie)
return m.Title as Title, min(p.Born) as Earliest, max(p.Born) as Latest, avg(p.Born) as AvgBorn
| Title | Earliest | Latest | AvgBorn |
|---|---|---|---|
| T3 | 1956 | 1956 | 1956 |
| T2 | 1956 | 1956 | 1956 |
| T1 | 1956 | 1958 | 1957 |
sum adds up a numeric expression across each group:
match (p:Person)-[:ACTED_IN]->(m:Movie)
return m.Title as Title, sum(p.Born) as BornSum
| Title | BornSum |
|---|---|
| T3 | 1956 |
| T2 | 1956 |
| T1 | 3914 |
Aggregate by entity
To group by an entire node, edge, or path instead of a scalar property, use to_json_string(entity). This function serializes the entity and its properties to a JSON string and uses that string as the grouping key.
match (p:Person)-[:ACTED_IN]->(m:Movie)
return to_json_string(m) as Movie, count(*) as Actors
| Movie | Actors |
|---|---|
| {"Title":"T3", ... } | 1 |
| {"Title":"T2", ... } | 1 |
| {"Title":"T1", ... } | 2 |
Alternatively, convert the entity to a string with CAST(entity AS string). This conversion also works as a grouping key:
match (p:Person)-[:ACTED_IN]->(m:Movie)
return cast(m as string) as Movie, count(*) as Actors
| Movie | Actors |
|---|---|
| {"Title":"T3", ... } | 1 |
| {"Title":"T2", ... } | 1 |
| {"Title":"T1", ... } | 2 |
Composite queries
A query can contain more than one MATCH statement. How the statements relate depends on whether they share variables.
Cross join. When two MATCH statements don't share variables, the result is the cartesian product of their rows (every combination).
match (p:Person where p.Name starts with 'J')
match (m:Movie where m.Title starts with 'T3')
return p.Name as name, m.Title as title
| name | title |
|---|---|
| Julia | T3 |
Match sequence. When a later MATCH reuses a variable bound by an earlier one, it continues from those bindings, joining the patterns on the shared variable.
match (p:Person {Name: 'Tom'})
match (p)-[:ACTED_IN]->(m:Movie)
return m.Title as title
| title |
|---|
| T3 |
| T2 |
| T1 |
Tip
Each additional MATCH may introduce another join. Whenever a relationship can be expressed within a single pattern, prefer fewer MATCH statements for better performance. For example, the query above is equivalent to, and faster when written as, a single statement:
match (p:Person {Name: 'Tom'}), (p)-[:ACTED_IN]->(m:Movie)
return m.Title as title
| title |
|---|
| T3 |
| T2 |
| T1 |
Optional match
OPTIONAL MATCH works like a left outer join. It keeps the rows from the preceding statement even when the optional pattern has no match. Unmatched variables are null or empty. The first MATCH in a query can't be OPTIONAL; an OPTIONAL MATCH must follow another statement.
match (p:Person)
optional match (p)-[:DIRECTED]->(m:Movie)
return p.Name as name, m.Title as title
| name | title |
|---|---|
| Ron | T1 |
| Tom | T2 |
| Julia | |
| Kevin |
Supported types
GQL values map to the underlying Kusto scalar types. The following types are supported:
| Kusto type | Description | GQL literal example |
|---|---|---|
string |
Unicode text. | 'Tom' |
bool |
Boolean value. | true, false |
int |
32-bit signed integer. | 42 |
long |
64-bit signed integer. | 9000000000 |
real |
64-bit floating-point number. | 3.14 |
decimal |
128-bit decimal number. | CAST('19.99' AS decimal) |
datetime |
Point in time (UTC). | zoned_datetime('2020-01-01 00:00:00.0') |
timespan |
Duration. | duration({days: 1, hours: 6}) |
dynamic |
List or record (the GQL key-value pair type, a JSON value). |
[1, 2, 3], {a: 1} |
null |
Absence of a value. | null |
CAST(value AS type) accepts these GQL type names (case-insensitive), each mapping to a Kusto type:
| Kusto type | Accepted GQL type names |
|---|---|
string |
string, char, varchar |
bool |
bool, boolean |
int |
int, int32, integer |
long |
int64, integer64, uint, uint32, uint64 |
real |
float, float32, real, double, float64 |
decimal |
decimal |
dynamic |
list, record, any record |
match (p:Person)
return p.Name || ' (' || cast(p.Born as string) || ')' as `Label`
| Label |
|---|
| Tom (1956) |
| Kevin (1958) |
| Ron (1954) |
| Julia (1967) |
Note
The Label variable is escaped because label is a reserved keyword. To differentiate between GQL syntax and user variables, you can escape user variables. Alternatively, add a prefix or suffix _.
Temporal data analysis
GQL provides comprehensive support for temporal data analysis using duration functions. These functions enable you to perform time-based filtering, calculations, and comparisons on graph data with timestamps. For the full list of temporal functions such as duration(), duration_between(), and zoned_datetime(), see GQL date and time functions.
Supported duration units
The duration() function supports a wide range of time units with flexible, case-insensitive syntax and returns a timespan object:
| Time unit | Supported names | Example | Timespan output |
|---|---|---|---|
| Days | days, day |
duration({days: 7}) |
7.00:00:00 |
| Hours | hours, hour |
duration({hours: 24}) |
1.00:00:00 |
| Minutes | minutes, minute |
duration({minutes: 30}) |
00:30:00 |
| Seconds | seconds, second |
duration({seconds: 45}) |
00:00:45 |
| Milliseconds | milliseconds, millisecond |
duration({milliseconds: 500}) |
00:00:00.5000000 |
| Microseconds | microseconds, microsecond |
duration({microseconds: 1000}) |
00:00:00.0010000 |
| Nanoseconds | nanoseconds, nanosecond |
duration({nanoseconds: 1000000}) |
00:00:00.0010000 |
You can combine multiple units in a single duration object: duration({days: 1, minutes: 8, seconds: 7}) returns 1.00:08:07.
Note
duration() accepts only a record of units (for example, duration({days: 14, hours: 16})); ISO 8601 duration strings such as duration('P14DT16H12M') aren't supported. The timestamp() function isn't supported either; use zoned_datetime() to construct a datetime.
Example:
Combine duration functions with timestamp arithmetic for precise temporal filtering. For example, the following query finds alerts generated within a three-minute time window.
match (system:System)-[event:generated]->(alert:Alert)
where event.event_timestamp > zoned_datetime("2012-01-01 08:00:00.0") and event.event_timestamp <= zoned_datetime("2012-01-01 08:00:00.0") + duration({minutes: 3})
return
system.name,
alert.severity,
event.event_timestamp,
duration_between(zoned_datetime("2012-01-01 08:00:00.0"), event.event_timestamp) as time_since_baseline
order by event.event_timestamp
Work with JSON
In GQL, a JSON object is a record value (the key-value pair type, written with {...}), or a list object value (written with [...]) which maps to the Kusto dynamic type. Two JSON functions convert between JSON text and record values:
parse_json_string(s)parses a JSON string into arecordor alist.to_json_string(x)serializes a value, including a node, edge, or path, into a JSON string.
Read JSON. After parsing, access record members with . and list elements with [index]. Because the result is a dynamic type, cast it before you compare or compute with it as needed.
match ()
return
parse_json_string('{"a":{"b":3}}').a.b as Nested,
cast(parse_json_string('[10,20,30]')[1] as int) as Second
limit 1
| Nested | Second |
|---|---|
| 3 | 20 |
Write JSON. Use to_json_string to serialize a node, edge, path, or value.
match (n:Person {Name: 'Julia'})
return to_json_string(n) as `Json`
| Json |
|---|
| {"Name":"Julia","Born":1967,"Label2":["Female","BestActressAward"]} |
Note
The Json variable is escaped because json is a reserved keyword. To differentiate between GQL syntax and user variables, you can escape user variables. Alternatively, add a prefix or suffix _.
Work with nulls
Test for null by using IS NULL and IS NOT NULL.
Find entities where a property is not null:
match (p:Person)
where p.Born is not null
return count(*) as `count`
Find entities where a property is null:
match (p:Person)
where p.Born is null
return p.Name as name
Note
- In Kusto,
stringvalues can't be null. You can instead test the number of characters. - The
countvariable is escaped because count is a reserved keyword. To differentiate between GQL syntax and user variables, escape user variables. Alternatively, add a prefix or suffix_.
Limitations
For the full list of unsupported clauses, functions, aggregates, and predicates, see the limitations here.