Geode implements the Graph Query Language (GQL) as standardized in ISO/IEC 39075:2024, providing a powerful, declarative language for querying graph databases. GQL builds on decades of query language evolution, combining the best aspects of SQL’s familiarity with specialized graph traversal capabilities.
What is GQL?
GQL (Graph Query Language) is an international standard for querying property graphs, officially published as ISO/IEC 39075:2024. It provides:
- Declarative Syntax: Describe what data you want, not how to retrieve it
- Pattern Matching: Express complex graph patterns intuitively
- Property Graph Model: Native support for labeled nodes and relationships with properties
- SQL Compatibility: Familiar syntax and semantics where applicable
- Composability: Build complex queries from simple building blocks
Geode follows the 100% GQL compliance; see the conformance profile for scope and diagnostics.
Query Structure
Basic Query Anatomy
// Simple query structure
MATCH (u:User {verified: true})
WHERE u.age >= 18
RETURN u.name, u.email
ORDER BY u.name
LIMIT 10
Every GQL query consists of clauses that form a pipeline:
- MATCH: Find patterns in the graph
- WHERE: Filter matched results
- RETURN: Project output columns
- ORDER BY: Sort results
- LIMIT/OFFSET: Paginate results
Reading Clauses
MATCH - Pattern Matching
// Node patterns
MATCH (n) // Any node
MATCH (u:User) // Nodes with User label
MATCH (p:Product {active: true}) // Nodes with label and property
// Relationship patterns
MATCH (a)-[r]->(b) // Any directed relationship
MATCH (a)-[:FOLLOWS]->(b) // Specific relationship type
MATCH (a)<-[r:CREATED]-(b) // Reverse direction
MATCH (a)-[r]-(b) // Undirected (either direction)
// Multi-hop patterns
MATCH (a)-[:KNOWS*1..3]->(b) // Variable-length path (1 to 3 hops)
MATCH (a)-[:KNOWS*]->(b) // Unlimited hops
MATCH (a)-[:KNOWS*..5]->(b) // Up to 5 hops
// Complex patterns
MATCH (u:User)-[:POSTED]->(p:Post)<-[:LIKED]-(f:User)
WHERE u <> f
RETURN u.name AS author, COUNT(DISTINCT f) AS unique_likes
OPTIONAL MATCH - Left Outer Join
// Return users even if they have no posts
MATCH (u:User)
OPTIONAL MATCH (u)-[:POSTED]->(p:Post)
RETURN u.name, COUNT(p) AS post_count
WHERE - Filtering
// Property conditions
MATCH (u:User)
WHERE u.age >= 18
AND u.verified = true
AND u.status IN ['active', 'premium']
RETURN u
// Pattern predicates
MATCH (u:User)
WHERE (u)-[:POSTED]->(:Post) // Users who posted
AND NOT (u)-[:BANNED]->() // Not banned
RETURN u
// String matching
MATCH (p:Product)
WHERE p.name STARTS WITH 'Pro'
OR p.name ENDS WITH 'Edition'
OR p.name CONTAINS 'Premium'
RETURN p
// Regular expressions
MATCH (u:User)
WHERE u.email =~ '.*@example\\.com$'
RETURN u
// Null checks
MATCH (u:User)
WHERE u.phone IS NOT NULL
AND u.email_verified IS NULL
RETURN u
// Collection predicates
MATCH (u:User)
WHERE 'python' IN u.skills
AND ALL(tag IN u.tags WHERE LENGTH(tag) > 2)
AND ANY(score IN u.scores WHERE score > 90)
RETURN u
Writing Clauses
CREATE - Insert Data
// Create single node
CREATE (u:User {
name: 'Alice',
email: 'alice@example.com',
created_at: CURRENT_TIMESTAMP
})
RETURN u
// Create multiple nodes
CREATE (u1:User {name: 'Alice'}),
(u2:User {name: 'Bob'}),
(u3:User {name: 'Charlie'})
// Create with relationships
CREATE (u:User {name: 'Alice'})-[:FOLLOWS]->(f:User {name: 'Bob'})
// Create from matched patterns
MATCH (u:User {name: 'Alice'})
CREATE (u)-[:POSTED]->(p:Post {
title: 'Hello World',
content: 'My first post',
created_at: NOW()
})
RETURN p
MERGE - Upsert Pattern
// Create if not exists
MERGE (u:User {email: 'alice@example.com'})
ON CREATE SET u.created_at = NOW()
ON MATCH SET u.last_seen = NOW()
RETURN u
// Merge relationships
MATCH (u1:User {name: 'Alice'}), (u2:User {name: 'Bob'})
MERGE (u1)-[f:FOLLOWS]->(u2)
ON CREATE SET f.since = NOW()
RETURN f
// Complex merge
MERGE (u:User {email: 'alice@example.com'})
ON CREATE SET
u.created_at = NOW(),
u.verified = false,
u.login_count = 1
ON MATCH SET
u.last_login = NOW(),
u.login_count = u.login_count + 1
RETURN u
SET - Update Properties
// Set single property
MATCH (u:User {id: 123})
SET u.verified = true
RETURN u
// Set multiple properties
MATCH (u:User {id: 123})
SET u.name = 'Alice Smith',
u.updated_at = NOW()
RETURN u
// Set from map
MATCH (u:User {id: 123})
SET u += {verified: true, role: 'admin'}
RETURN u
// Set labels
MATCH (u:User {id: 123})
SET u:Premium:Verified
RETURN u
// Conditional updates
MATCH (p:Post)
WHERE p.likes > 100
SET p.featured = true,
p.featured_at = NOW()
RETURN COUNT(p) AS featured_count
DELETE - Remove Data
// Delete nodes (must have no relationships)
MATCH (u:User {deleted: true})
DELETE u
// Delete relationships
MATCH (u1:User)-[f:FOLLOWS]->(u2:User)
WHERE u1.id = 123
DELETE f
// Delete nodes and relationships
MATCH (u:User {id: 123})
DETACH DELETE u // Deletes node and all its relationships
// Conditional delete
MATCH (p:Post)
WHERE p.created_at < DATE() - DURATION('P1Y')
AND p.archived = true
DETACH DELETE p
REMOVE - Remove Properties/Labels
// Remove property
MATCH (u:User {id: 123})
REMOVE u.deprecated_field
RETURN u
// Remove label
MATCH (u:User {id: 123})
REMOVE u:Premium
RETURN u
// Remove multiple properties
MATCH (u:User)
WHERE u.migrated = true
REMOVE u.old_field1, u.old_field2, u.migration_flag
Query Composition
WITH - Pipeline Chaining
// Aggregate then filter
MATCH (u:User)-[:POSTED]->(p:Post)
WITH u, COUNT(p) AS post_count
WHERE post_count > 10
RETURN u.name, post_count
ORDER BY post_count DESC
// Multiple aggregations
MATCH (u:User)-[:POSTED]->(p:Post)
WITH u, COUNT(p) AS posts, AVG(p.likes) AS avg_likes
WHERE posts > 5 AND avg_likes > 10
RETURN u.name, posts, avg_likes
// Subquery pattern
MATCH (u:User)
WITH u, [(u)-[:POSTED]->(p:Post) | p.likes] AS post_likes
RETURN u.name, SUM(post_likes) AS total_likes
UNION - Combine Results
// Union all (includes duplicates)
MATCH (u:User {verified: true})
RETURN u.name AS name
UNION ALL
MATCH (a:Admin)
RETURN a.name AS name
// Union distinct (removes duplicates)
MATCH (u:User)
WHERE u.role = 'author'
RETURN u.id, u.name
UNION
MATCH (u:User)-[:POSTED]->(p:Post)
RETURN u.id, u.name
UNWIND - Expand Collections
// Expand list to rows
UNWIND [1, 2, 3, 4, 5] AS number
RETURN number, number * number AS square
// Process collection property
MATCH (u:User)
UNWIND u.tags AS tag
RETURN tag, COUNT(u) AS user_count
ORDER BY user_count DESC
// Create from list
WITH [{name: 'Alice', age: 30}, {name: 'Bob', age: 25}] AS users
UNWIND users AS user_data
CREATE (u:User)
SET u = user_data
RETURN u
Advanced Query Patterns
Aggregation
// Basic aggregates
MATCH (u:User)
RETURN COUNT(u) AS total_users,
AVG(u.age) AS avg_age,
MIN(u.created_at) AS first_user,
MAX(u.created_at) AS latest_user
// Group by
MATCH (u:User)-[:POSTED]->(p:Post)
RETURN u.name,
COUNT(p) AS posts,
SUM(p.likes) AS total_likes,
AVG(p.likes) AS avg_likes
ORDER BY total_likes DESC
// Collect aggregation
MATCH (u:User)-[:POSTED]->(p:Post)
RETURN u.name, COLLECT(p.title) AS post_titles
// Distinct aggregation
MATCH (u:User)-[:POSTED]->(p:Post)-[:HAS_TAG]->(t:Tag)
RETURN u.name, COUNT(DISTINCT t) AS unique_tags
Path Queries
// Shortest path
MATCH path = SHORTEST_PATH((a:User {name: 'Alice'})-[:KNOWS*]-(b:User {name: 'Bob'}))
RETURN path, LENGTH(path) AS hops
// All paths with length constraint
MATCH path = (a:User {name: 'Alice'})-[:KNOWS*1..4]-(b:User {name: 'Bob'})
RETURN path, LENGTH(path) AS hops
ORDER BY hops
// Path with conditions
MATCH path = (a:User)-[:KNOWS*]-(b:User)
WHERE ALL(node IN NODES(path) WHERE node.active = true)
AND LENGTH(path) <= 5
RETURN path
Subqueries
// Correlated subquery (EXISTS)
MATCH (u:User)
WHERE EXISTS {
MATCH (u)-[:POSTED]->(p:Post)
WHERE p.likes > 100
}
RETURN u
// Scalar subquery
MATCH (u:User)
RETURN u.name,
(SELECT COUNT(p) FROM (u)-[:POSTED]->(p:Post)) AS post_count
// List subquery
MATCH (u:User)
RETURN u.name,
[(u)-[:FOLLOWS]->(f:User) | f.name] AS following_names
Conditional Logic
// CASE expressions
MATCH (u:User)
RETURN u.name,
CASE
WHEN u.posts > 100 THEN 'power_user'
WHEN u.posts > 10 THEN 'regular_user'
ELSE 'new_user'
END AS user_tier
// CASE with searched conditions
MATCH (p:Post)
RETURN p.title,
CASE
WHEN p.likes > 1000 AND p.shares > 100 THEN 'viral'
WHEN p.likes > 100 THEN 'popular'
WHEN p.likes > 10 THEN 'normal'
ELSE 'new'
END AS status
// Simple CASE
MATCH (u:User)
RETURN u.name,
CASE u.role
WHEN 'admin' THEN 'Administrator'
WHEN 'moderator' THEN 'Moderator'
ELSE 'User'
END AS role_label
Query Optimization
Use Indexes
// Create indexes for frequently queried properties
CREATE INDEX FOR (u:User) ON (u.email)
CREATE INDEX FOR (p:Post) ON (p.created_at)
CREATE INDEX FOR (t:Tag) ON (t.name)
// Composite index
CREATE INDEX FOR (u:User) ON (u.status, u.role)
Filter Early
// Good: Filter before expanding pattern
MATCH (u:User)
WHERE u.verified = true
MATCH (u)-[:POSTED]->(p:Post)
RETURN u, p
// Avoid: Late filtering causes unnecessary work
MATCH (u:User)-[:POSTED]->(p:Post)
WHERE u.verified = true
RETURN u, p
Limit Relationship Traversals
// Good: Bounded traversal
MATCH (u:User)-[:KNOWS*1..3]->(friend)
RETURN friend.name
LIMIT 100
// Avoid: Unbounded traversal
MATCH (u:User)-[:KNOWS*]->(friend)
RETURN friend.name
Use Query Profiling
// Explain query plan
EXPLAIN
MATCH (u:User)-[:POSTED]->(p:Post)
WHERE u.verified = true
RETURN u.name, COUNT(p) AS posts
// Profile actual execution
PROFILE
MATCH (u:User)-[:POSTED]->(p:Post)
WHERE u.verified = true
RETURN u.name, COUNT(p) AS posts
Client Library Integration
Python
from geode_client import Client
client = Client(host="localhost", port=3141)
async with client.connection() as conn:
# Simple query
result, _ = await conn.query("""
MATCH (u:User)
WHERE u.age >= $min_age
RETURN u.name, u.age
ORDER BY u.age DESC
LIMIT 10
""", {"min_age": 18})
for row in result.rows:
print(f"{row['name']}: {row['age']}")
# Parameterized write
await conn.query("""
CREATE (u:User {
name: $name,
email: $email,
created_at: CURRENT_TIMESTAMP
})
""", {
"name": "Alice",
"email": "[email protected]"
})
Go
import "database/sql"
import _ "geodedb.com/geode"
db, _ := sql.Open("geode", "quic://localhost:3141")
defer db.Close()
// Query with parameters
rows, err := db.Query(`
MATCH (u:User)
WHERE u.age >= $1
RETURN u.name, u.age
ORDER BY u.age DESC
LIMIT 10
`, 18)
defer rows.Close()
for rows.Next() {
var name string
var age int
rows.Scan(&name, &age)
fmt.Printf("%s: %d\n", name, age)
}
// Insert with parameters
_, err = db.Exec(`
CREATE (u:User {
name: $1,
email: $2,
created_at: CURRENT_TIMESTAMP
})
`, "Alice", "[email protected]")
Rust
use geode_client::Client;
let client = Client::connect("localhost:3141").await?;
// Query with parameters
let result = client.query(
r#"
MATCH (u:User)
WHERE u.age >= $min_age
RETURN u.name, u.age
ORDER BY u.age DESC
LIMIT 10
"#,
&[("min_age", &18)],
).await?;
for row in result.rows() {
println!("{}: {}", row["name"], row["age"]);
}
// Insert with parameters
client.execute(
r#"
CREATE (u:User {
name: $name,
email: $email,
created_at: CURRENT_TIMESTAMP
})
"#,
&[
("name", &"Alice"),
("email", &"[email protected]"),
],
).await?;
Best Practices
- Use Parameters: Always use parameters ($1, $name, etc.) instead of string concatenation to prevent injection attacks
- Index Strategically: Create indexes on properties used in WHERE clauses and relationship endpoints
- Bound Traversals: Always limit variable-length path traversals (use *1..5, not *)
- Filter Early: Apply WHERE clauses as early as possible in the query pipeline
- Limit Results: Use LIMIT on queries that might return large result sets
- Profile Queries: Use EXPLAIN/PROFILE to understand query execution plans
- Batch Writes: Combine multiple CREATE/MERGE operations in a single transaction
- Avoid Cartesian Products: Be careful with multiple MATCH clauses without relationships
Common Patterns
Recommendation Engine
// Find recommended products based on similar users
MATCH (u:User {id: $user_id})-[:PURCHASED]->(p:Product)
<-[:PURCHASED]-(similar:User)-[:PURCHASED]->(recommendation:Product)
WHERE NOT (u)-[:PURCHASED]->(recommendation)
AND recommendation.active = true
RETURN recommendation.name,
COUNT(DISTINCT similar) AS similar_user_count,
AVG(recommendation.rating) AS avg_rating
ORDER BY similar_user_count DESC, avg_rating DESC
LIMIT 10
Social Graph Analysis
// Find mutual friends
MATCH (me:User {id: $my_id})-[:KNOWS]-(mutual)-[:KNOWS]-(friend:User {id: $friend_id})
WHERE mutual <> me AND mutual <> friend
RETURN mutual.name, mutual.location
ORDER BY mutual.name
Hierarchical Queries
// Organization chart depth-first traversal
MATCH path = (root:Employee {id: $manager_id})-[:MANAGES*]->(report:Employee)
RETURN report.name,
LENGTH(path) AS depth,
[node IN NODES(path) | node.name] AS chain
ORDER BY depth, report.name
Troubleshooting
Query Too Slow
- Check if indexes exist on filtered properties
- Use PROFILE to identify bottlenecks
- Ensure relationship traversals are bounded
- Consider denormalizing frequently accessed data
Out of Memory
- Add LIMIT clauses to constrain result sets
- Use streaming in client libraries
- Break large operations into batches
- Consider variable-length path limits
Unexpected Results
- Verify NULL handling (use IS NULL/IS NOT NULL)
- Check relationship directions
- Ensure proper use of OPTIONAL MATCH
- Validate parameter types and values
Related Topics
- Pattern Matching: Deep dive into graph pattern syntax
- MATCH Clause: Comprehensive MATCH documentation
- EXPLAIN: Query execution plan analysis
- Functions: Built-in GQL functions
- Operators: GQL operators and expressions
Further Reading
- Pattern Matching - Graph pattern matching techniques
- MATCH Clause - Complete MATCH syntax reference
- EXPLAIN - Query optimization with EXPLAIN
- Performance - Query performance tuning
- GQL Reference - Complete GQL language reference
Geode’s full implementation of ISO/IEC 39075:2024 GQL provides a powerful, standardized query language for all your graph database needs, from simple lookups to complex graph analytics.