NoSQL Query Languages: MongoDB Query API, CQL, and Gremlin Compared

NoSQL Query Languages: MongoDB Query API, CQL, and Gremlin Compared

SQL had a decades-long head start as the universal language for talking to databases, so it’s no surprise that when NoSQL databases emerged, each major category ended up inventing its own way of asking questions of the data — shaped entirely by the underlying data model it needed to serve. There was never going to be one “NoSQL query language” the way there’s SQL, because a document store, a wide-column store, and a graph database don’t just store data differently, they need fundamentally different ways of expressing queries against that data.

This article compares three of the most important query languages in the NoSQL world: MongoDB’s Query API (and its Aggregation Framework), Cassandra Query Language (CQL), and Gremlin, the graph traversal language used by Neo4j (via openCypher/Gremlin support), JanusGraph, Amazon Neptune, and others.

Why NoSQL Query Languages Differ So Much

SQL was designed for the relational model — data organized into tables with fixed schemas, related through foreign keys, and queried by describing what result you want (a declarative approach), leaving the database engine to figure out how to get it, often by planning and executing joins across multiple tables.

NoSQL databases abandoned the relational model specifically to solve problems SQL and relational engines struggle with — massive horizontal scale, flexible schemas, and specialized data shapes like documents, wide columns, or graphs. Because the underlying data models are so different from each other, the query languages built to interact with them had to diverge just as sharply.

  • MongoDB’s Query API is designed around documents — JSON-like structures with nested fields and arrays — so its query language is expressed as JSON-like query objects and pipelines.
  • CQL is designed around Cassandra’s wide-column, partition-based model, and deliberately mimics SQL syntax to ease the learning curve, even though the underlying semantics (no joins, partition-aware queries only) are very different.
  • Gremlin is designed around graph traversal — describing a path through nodes and relationships — so it reads more like a functional programming pipeline than either SQL or MongoDB’s query objects.

MongoDB Query API and Aggregation Framework

MongoDB’s query language isn’t a separate textual language like SQL — it’s expressed natively as JSON-like documents passed to driver methods, which makes it feel very natural to developers already working with JSON in their application code.

Basic Queries

// Find all orders with status "shipped"
db.orders.find({ status: "shipped" })

// Find orders over $100, sorted by date descending
db.orders.find({ total: { $gt: 100 } }).sort({ created_at: -1 })

// Find a specific order by ID
db.orders.findOne({ _id: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1") })

// Query nested fields
db.orders.find({ "customer.city": "Lahore" })

// Query array fields
db.orders.find({ "items.product_id": "p001" })

The query object itself describes the filter conditions declaratively — MongoDB’s query planner decides how to execute it, using indexes where available, similar in spirit to how a SQL engine plans a query, just expressed in a different syntax.

CRUD Operations

// Insert
db.orders.insertOne({ customer_id: "cust123", total: 149.99, status: "pending" })

// Update
db.orders.updateOne(
  { _id: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1") },
  { $set: { status: "shipped" }, $inc: { shipment_count: 1 } }
)

// Delete
db.orders.deleteOne({ _id: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1") })

Notice the use of update operators like $set and $inc — these allow atomic, partial updates to specific fields without needing to overwrite the entire document, which matters a lot for performance and correctness under concurrent writes.

The Aggregation Framework

Where MongoDB’s query language really shows its depth is the Aggregation Framework — a pipeline-based system for transforming, filtering, grouping, and reshaping data, roughly analogous to SQL’s GROUP BY, JOIN, and window functions combined, but expressed as a sequence of stages that data flows through.

db.orders.aggregate([
  { $match: { status: "shipped" } },
  { $group: {
      _id: "$customer_id",
      totalSpent: { $sum: "$total" },
      orderCount: { $sum: 1 }
  }},
  { $sort: { totalSpent: -1 } },
  { $limit: 10 },
  { $lookup: {
      from: "customers",
      localField: "_id",
      foreignField: "customer_id",
      as: "customer_info"
  }}
])

This pipeline filters shipped orders, groups them by customer to calculate total spend and order count, sorts by total spend, limits to the top 10, and then performs a $lookup — MongoDB’s equivalent of a left outer join — to pull in customer details. The Aggregation Framework is what gives MongoDB query capabilities that go well beyond a typical key-value or simple document lookup, closing much of the gap with SQL’s analytical power while still operating on MongoDB’s native document model.

CQL: Cassandra Query Language

CQL was deliberately designed to look and feel like SQL, which lowers the learning curve for developers coming from a relational background — but underneath that familiar syntax, the semantics are shaped entirely by Cassandra’s distributed, partition-based architecture.

Basic Queries

-- Create a table
CREATE TABLE orders_by_customer (
    customer_id UUID,
    order_date TIMESTAMP,
    order_id UUID,
    total DECIMAL,
    status TEXT,
    PRIMARY KEY (customer_id, order_date, order_id)
) WITH CLUSTERING ORDER BY (order_date DESC);

-- Select rows for a specific partition
SELECT * FROM orders_by_customer WHERE customer_id = 123e4567-e89b-12d3-a456-426614174000;

-- Select with a clustering column range
SELECT * FROM orders_by_customer
WHERE customer_id = 123e4567-e89b-12d3-a456-426614174000
AND order_date > '2026-01-01';

-- Insert
INSERT INTO orders_by_customer (customer_id, order_date, order_id, total, status)
VALUES (uuid(), toTimestamp(now()), uuid(), 149.99, 'pending');

-- Update
UPDATE orders_by_customer SET status = 'shipped'
WHERE customer_id = 123e4567-e89b-12d3-a456-426614174000
AND order_date = '2026-08-10 14:22:00'
AND order_id = 987fcdeb-51a2-43d1-9f4e-123456789abc;

The Critical Constraint: No Arbitrary WHERE Clauses

This is the single biggest thing that trips up developers coming from SQL. In CQL, you generally cannot filter on arbitrary columns the way you can in SQL — every query must include the full partition key, because CQL is designed to route the query directly to the specific node(s) that own that partition, avoiding the kind of expensive, cluster-wide scan that an unrestricted WHERE clause would require.

-- This works: filtering by the full partition key
SELECT * FROM orders_by_customer WHERE customer_id = 123e4567-...;

-- This does NOT work by default: filtering by a non-key column
SELECT * FROM orders_by_customer WHERE status = 'shipped';
-- Error: Cannot execute this query as it might involve data filtering
-- and thus may have unpredictable performance.

You can force this kind of query with ALLOW FILTERING, but Cassandra explicitly warns against it in production because it can trigger a full cluster scan — exactly the kind of unpredictable, unbounded-latency operation Cassandra’s architecture is designed to avoid. The right fix, in true NoSQL fashion, is to create a separate table (orders_by_status, for example) specifically designed around that query pattern, duplicating the data as needed.

No Joins, No Subqueries

CQL doesn’t support joins or subqueries at all. If you need data that would traditionally require a join in SQL, you either denormalize it into a single table (as discussed extensively in NoSQL data modeling) or perform multiple separate queries and combine the results in your application code.

Secondary Indexes and Materialized Views

CQL does support secondary indexes (CREATE INDEX) for querying non-partition-key columns, but they come with real performance caveats at scale, especially on high-cardinality columns, since they can require querying multiple nodes. Materialized views offer an alternative — Cassandra automatically maintains a separate table structured for a different query pattern, updated whenever the base table changes — though they add operational complexity and have their own consistency caveats.

Gremlin: Graph Traversal Language

Gremlin is the query language of the Apache TinkerPop graph computing framework, and it’s supported (natively or via compatibility layers) by a wide range of graph databases, including Amazon Neptune, JanusGraph, and, to varying degrees, Neo4j (which more commonly uses its own Cypher language but also supports Gremlin in some configurations).

Unlike SQL or CQL’s declarative “describe what you want” style, Gremlin is closer to a functional, imperative pipeline — you describe a step-by-step traversal through the graph, and each step transforms the set of things currently being considered.

Basic Traversals

// Find a specific person
g.V().has('name', 'Alice')

// Find all of Alice's friends
g.V().has('name', 'Alice').out('friendsWith')

// Find friends-of-friends
g.V().has('name', 'Alice').out('friendsWith').out('friendsWith').dedup()

// Filter and project specific properties
g.V().has('name', 'Alice')
  .out('friendsWith')
  .has('age', gt(25))
  .values('name')

// Find the shortest path between two people
g.V().has('name', 'Alice')
  .repeat(out('friendsWith').simplePath())
  .until(has('name', 'Zara'))
  .path()
  .limit(1)

Each .out(), .has(), or .values() call is a “step” in the traversal pipeline — you start with a set of vertices, and each subsequent step filters, transforms, or moves that set along edges to a new set of vertices. This chained, pipeline-based style is closer to functional programming patterns (like JavaScript’s .map().filter() chains) than to SQL’s declarative structure.

Creating and Modifying Data

// Add a vertex
g.addV('person').property('name', 'Alice').property('age', 29)

// Add an edge between two vertices
g.V().has('name', 'Alice').as('a')
  .V().has('name', 'Bob').as('b')
  .addE('friendsWith').from('a').to('b').property('since', 2019)

// Update a property
g.V().has('name', 'Alice').property('age', 30)

// Delete a vertex (and its edges)
g.V().has('name', 'Alice').drop()

Aggregation and Grouping in Gremlin

// Count Alice's friends
g.V().has('name', 'Alice').out('friendsWith').count()

// Group people by the company they work at
g.V().hasLabel('person')
  .group()
  .by(out('worksAt').values('name'))
  .by(values('name').fold())

This last example groups all person vertices by the company they work at, collecting the names of people at each company — conceptually similar to a SQL GROUP BY, but expressed as steps in a traversal pipeline rather than a declarative clause.

A Note on Cypher vs. Gremlin

It’s worth addressing directly: Neo4j’s native query language is actually Cypher, not Gremlin, even though Neo4j does support Gremlin through certain compatibility layers and plugins. Gremlin is more commonly the native language of TinkerPop-based systems like JanusGraph and Amazon Neptune. Since both are graph query languages built for traversal, though, comparing Gremlin’s traversal-pipeline paradigm against MongoDB’s and CQL’s very different paradigms still captures the essential contrast this article is making, and much of what’s said about Gremlin applies conceptually to Cypher as well.

For reference, here’s the same “friends of friends” query in Cypher, to see how its declarative, pattern-matching style differs from Gremlin’s step-by-step pipeline approach:

MATCH (alice:Person {name: 'Alice'})-[:FRIENDS_WITH]->(:Person)-[:FRIENDS_WITH]->(fof)
WHERE fof <> alice
RETURN DISTINCT fof.name

Where Gremlin describes a traversal as an explicit sequence of steps (out(), has(), where()), Cypher describes the shape of the pattern you’re looking for using its ASCII-art-like syntax, and lets the query engine figure out the most efficient way to find matches — a more declarative approach, closer in spirit to how SQL and MongoDB’s query objects work, but applied to graph patterns instead of tables or documents. Many developers find Cypher noticeably more approachable than Gremlin for this reason, which is part of why it’s become something of a de facto standard — openCypher, an open-source implementation of Cypher, is now supported by multiple graph database vendors beyond Neo4j itself.

Side-by-Side Comparison

AspectMongoDB Query APICQLGremlin
StyleJSON-like query objects/pipelinesSQL-like declarative syntaxFunctional traversal pipeline
Underlying modelDocumentsWide columns, partitionedGraph (nodes and edges)
Joins$lookup in aggregation (limited)Not supportedNative via traversal (out()/in())
Filtering flexibilityHigh — flexible filters on any field with proper indexesRestricted — must include partition keyHigh — can filter at any traversal step
Learning curve for SQL usersModerate (different syntax paradigm)Low (deliberately SQL-like)High (different paradigm entirely)
Best suited forFlexible document queries, nested dataPartition-aware, high-throughput queriesMulti-hop relationship traversal
Aggregation capabilityVery strong (Aggregation Framework)Basic (COUNT, SUM, AVG on partition)Strong (group, count, path analytics)

Practical Example: The Same Problem, Three Ways

Let’s model the same simple question — “find people who work at the same company as Alice” — across all three languages, assuming roughly comparable underlying data.

MongoDB:

const alice = db.people.findOne({ name: "Alice" });
db.people.find({ company_id: alice.company_id, name: { $ne: "Alice" } });

CQL (assuming a table specifically designed for this query pattern):

SELECT * FROM people_by_company
WHERE company_id = (SELECT company_id FROM people WHERE name = 'Alice')
-- Note: CQL doesn't actually support subqueries like this;
-- in practice, this requires two separate queries in application code.

Gremlin:

g.V().has('name', 'Alice')
  .out('worksAt').as('company')
  .in('worksAt')
  .where(neq('a')).as('a')
  .values('name')

This comparison highlights something important: MongoDB can express this fairly naturally in a single query (with a small application-side step to get Alice’s company ID first). CQL genuinely cannot express this as a single query at all — it requires two round trips, because CQL has no concept of subqueries or joins. Gremlin expresses it as a natural graph traversal in a single query, because “walk to Alice’s company, then walk back out to everyone else there” is exactly the kind of multi-hop relationship query graphs are built for.

Choosing the Right Query Language (and Database)

This comparison isn’t really about which language is “best” — it’s about recognizing that the query language is inseparable from the data model it was built for, and choosing your database (and by extension, its query language) based on your actual access patterns.

  • Choose MongoDB and its Query API when your data is naturally document-shaped, your query patterns are moderately flexible, and you want strong aggregation capabilities without needing full graph traversal or extreme write-scale partitioning.
  • Choose Cassandra and CQL when you need massive write throughput and horizontal scale across data centers, and you’re willing to design (and duplicate data across) tables specifically for each query pattern in exchange for predictable, low-latency performance at any scale.
  • Choose a graph database and Gremlin (or Cypher) when your core problem is genuinely about relationships and multi-hop connections — anything where “find things connected to this thing, several steps removed” is a primary access pattern.

Advantages and Limitations Summary

MongoDB Query API

  • Advantages: Intuitive for JSON-native developers, powerful aggregation pipeline, flexible ad-hoc querying with proper indexing.
  • Limitations: Joins ($lookup) are less performant than relational joins and best used sparingly; deeply nested aggregation pipelines can become hard to read and debug.

CQL

  • Advantages: Familiar SQL-like syntax lowers the learning curve; enforces query patterns that scale predictably by design.
  • Limitations: No joins or subqueries at all; restrictive WHERE clause rules force query-first table design and often significant data duplication.

Gremlin

  • Advantages: Naturally expresses complex multi-hop relationship queries that would be painful or impossible in SQL or CQL; highly composable traversal steps.
  • Limitations: Steeper learning curve due to its functional, pipeline-based paradigm; can be harder to optimize and reason about performance for very deep or unbounded traversals without careful query design.

Best Practices Across All Three

  1. Match your query language’s strengths to your actual access patterns — don’t force a graph traversal problem into CQL, or a massive write-throughput problem into a graph database.
  2. Always check the query execution plan — use explain() in MongoDB, TRACE ON in CQL (via cqlsh), and Gremlin’s profiling steps to understand what your query is actually doing under the hood before shipping it to production.
  3. Avoid anti-patterns specific to each language — unbounded ALLOW FILTERING in CQL, unindexed $lookup operations in MongoDB, and unbounded traversal depth without limits in Gremlin can all silently degrade performance at scale.
  4. Index deliberately. Every one of these languages performs dramatically better with the right indexes in place, and dramatically worse without them — indexing strategy is not optional in any of them.
  5. Write queries that match how your schema was actually designed. Since all three of these databases expect query-driven modeling, your queries and your schema should have been designed together, not independently.

Conclusion

MongoDB’s Query API, CQL, and Gremlin aren’t just different syntaxes for asking similar questions — they’re purpose-built languages that reflect three fundamentally different ways of organizing data: documents, partitioned wide columns, and graphs. Understanding why each language works the way it does — CQL’s insistence on partition-key filtering, MongoDB’s pipeline-based aggregation, Gremlin’s step-by-step traversal model — is far more valuable than memorizing syntax, because it’s that underlying data model, not the query language on top of it, that ultimately determines whether a given database is the right choice for your application in the first place.

Total
0
Shares

Leave a Reply

Previous Post
Introduction to MongoDB: Installation, CRUD Operations, and Aggregation Framework

Introduction to MongoDB: Installation, CRUD Operations, and Aggregation Framework

Next Post
NoSQL Data Modeling Techniques: Denormalization, Aggregation, and Embedded Documents

NoSQL Data Modeling Techniques: Denormalization, Aggregation, and Embedded Documents

Related Posts