The SELECT operation, once executed, retrieves documents from a collection and uses clauses like WHERE to specify conditions for filtering the documents to return.
DQL now supports projections and aggregates in addition to SELECT * operations. You can select specific fields, perform calculations, and use aggregate functions like COUNT, SUM, AVG, MIN, and MAX.
The following table provides an overview of the different clauses you can use to define specific conditions and calculations within your DQL SELECT statements to provide more granular control over your queries:
Clause
Description
FROM
The required clause specifying the collection containing the documents for retrieval. (See FROM)
WHERE
Applies filtering conditions to restrict the documents included in the result set. (See WHERE)
GROUP BY
Groups documents based on one or more expressions for aggregate calculations. (See GROUP BY)
HAVING
Filters grouped results based on aggregate conditions. (See HAVING)
ORDER BY
Specifies the sorting order of the result set based on one or more expressions. (See ORDER BY)
LIMIT
Restricts the number of documents included in the result set. (See LIMIT)
OFFSET
Skips a specific number of documents before returning the result set. (See OFFSET)
The optional USE IDS clause allows you to efficiently retrieve specific documents by their IDs without performing a collection scan. This is more performant than using WHERE _id IN (...) filters.Syntax:
DQL
-- Inline IDsSELECT * FROM collection_name USE IDS (id1, id2, id3)-- Using a parameter arraySELECT * FROM collection_name USE IDS LIST :id_array
Examples:
DQL
-- Retrieve specific documents by IDSELECT * FROM cars USE IDS ('123', '456', '789')-- Using a parameter array (recommended for dynamic queries)SELECT * FROM cars USE IDS LIST :car_ids
let carIds = ["123", "456", "789"]let results = await ditto.store.execute( query: "SELECT * FROM cars USE IDS LIST :car_ids", arguments: ["car_ids": carIds])
val carIds = listOf("123", "456", "789")val results = ditto.store.execute( "SELECT * FROM cars USE IDS LIST :car_ids", mapOf("car_ids" to carIds))
const carIds = ["123", "456", "789"];const results = await ditto.store.execute( "SELECT * FROM cars USE IDS LIST :car_ids", { car_ids: carIds });
List<String> carIds = Arrays.asList("123", "456", "789");ditto.store.execute( "SELECT * FROM cars USE IDS LIST :car_ids", Collections.singletonMap("car_ids", carIds));
var carIds = new List<string> { "123", "456", "789" };await ditto.Store.ExecuteAsync( "SELECT * FROM cars USE IDS LIST :car_ids", new Dictionary<string, object> { { "car_ids", carIds } });
std::vector<std::string> carIds = {"123", "456", "789"};std::map<std::string, std::vector<std::string>> args;args["car_ids"] = carIds;ditto.get_store().execute( "SELECT * FROM cars USE IDS LIST :car_ids", args).get();
let car_ids = vec!["123", "456", "789"];ditto.store().execute_v2(( "SELECT * FROM cars USE IDS LIST :car_ids", serde_json::json!({ "car_ids": car_ids }))).await?;
final carIds = ["123", "456", "789"];await ditto.store.execute( "SELECT * FROM cars USE IDS LIST :car_ids", arguments: {"car_ids": carIds});
Performance Tip: Where possible, simple equality filters on _id (like WHERE _id = '123') are automatically optimized internally similar to USE IDS. However, for explicit control and guaranteed performance, USE IDS is recommended. See also the #auto_use_ids directive in Directives.
You can assign an alias to a collection in the FROM clause to create shorter, more readable queries. The AS keyword is optional:
DQL
-- With AS keywordSELECT * FROM cars AS c WHERE c.color = 'blue'-- Without AS keywordSELECT * FROM cars c WHERE c.color = 'blue'
Collection aliases are particularly useful in:
Complex queries: Shortening long collection names for readability
Qualified field references: Explicitly referencing fields from a specific collection
Query directives: Specifying collection-level directives (see Directives)
DQL
-- Using collection alias with qualified field referencesSELECT c.make, c.model, c.yearFROM cars cWHERE c.color = 'blue'ORDER BY c.year DESC-- Using collection alias with directives/*+ {"c": {"#index": "ix_color"}} */SELECT * FROM cars c WHERE c.color = 'blue'
Collection aliases follow the same identifier rules as projection aliases and can also use backticks for special characters or reserved words.
For example, here is a basic SELECT statement querying documents from the cars collection based on a given address:
DQL
SELECT * FROM carsWHERE location.address = '123 Main St, San Francisco, CA 98105'
To demonstrate a more complex query, here is a SELECT statement that queries using multiple expressions and logical operators to further refine the criteria for document retrieval:
DQL
SELECT * FROM carsWHERE color = 'blue' AND features.trim = 'Standard' OR features.mileage > 10000
With the ORDER BY clause, if you’d like, you can integrate calculations or expressions in your SELECT statement. Then sort the resulting documents to return in either ascending (ASC) or descending (DESC) alphabetical order:
DQL
SELECT *FROM your_collection_nameORDER BY expression_1, expression_2, ... [ASC|DESC]
For example, here is a simple SELECT statement that uses the ORDER BY clause to query and sort documents from the cars collection in descending (DESC) alphabetical order based on the field value set for the color property:
DQL
SELECT *FROM cars ORDER BY color DESC
In this syntax:
your_collection_name is the name of the collection from which you want to retrieve the data.
expression_1, expression_2, ... are the expressions evaluated to sort the result. Expressions are resolved in order.
[ASC|DESC] is an optional parameter that specifies the sort order. If omitted, the default sort order is ascending (ASC). To sort in descending order, you can specify DESC.
ExampleIn this example, the result set from the query will be sorted in descending order based on the values in the field:
DQL
-- Sort by a given field_nameSELECT *FROM your_collection_nameORDER BY field_name DESC
For instance, here "blue" cars return first and other cars sort by the natural order in the collection:
Unless explicitly defined as DESC in your query, Ditto defaults to sorting in ascending (ASC). So, if you want to sort in ascending order, you do not have to express that in your query.
The LIMIT clause is used to restrict the number of documents returned by a query, allowing you to specify a maximum limit on the number of documents to be included in the result set:
Projections allow you to specify exactly which fields or calculated values to return from your query, rather than returning entire documents. This reduces data transfer and processing overhead.
To exclude specific fields from a projection, use the MISSING keyword. This can be combined with * to select all fields except those explicitly excluded:
DQL
-- Given a document: { "_id": "abc", "make": "Toyota", "model": "Camry", "color": "blue", "vin": "1234" }-- Exclude "vin" and "color", keeping everything else:SELECT cars.*, MISSING vin, MISSING color FROM cars-- Result: { "_id": "abc", "make": "Toyota", "model": "Camry" }
SELECT make AS manufacturer, model AS car_model FROM cars
Default aliases are assigned if not specified:
Simple field references use the field name as the alias
Expressions get aliases like ($1), ($2), etc.
Aliases bind to the element immediately prior in the statement. SELECT COUNT(*) AS car_count FROM cars names the output field, but SELECT COUNT(*) FROM cars AS car_count aliases the cars collection — COUNT(*) falls back to its default alias.
Uniqueness: Each alias in a SELECT list must be unique. Duplicate aliases will raise an error.
Identifier Rules: Aliases must be valid identifiers following field naming conventions.
Special Characters: Use backticks (grave accents) to quote aliases containing special characters or reserved words.
DQL
-- Using backticks for special charactersSELECT price AS `final-price`, model AS `car-model` FROM cars-- Using backticks for reserved wordsSELECT status AS `order`, type AS `select` FROM orders
Projection aliases defined in the SELECT list can be referenced by the ORDER BY clause to sort by an aliased expression:
DQL
SELECT make, price * 0.9 AS sale_price FROM cars ORDER BY sale_price DESC
GROUP BY and HAVING do not accept projection aliases. They must reference the underlying source expression or aggregate function directly: use GROUP BY color (the source field) rather than GROUP BY c (a projection alias), and HAVING COUNT(*) > 5 (the aggregate) rather than HAVING car_count > 5 (the alias).
-- Count all documentsSELECT COUNT(*) FROM cars-- Count non-null values in a fieldSELECT COUNT(color) FROM cars-- Count distinct valuesSELECT COUNT(DISTINCT color) FROM cars
Group results and calculate aggregates for each group:
DQL
SELECT color, COUNT(*) AS car_count, AVG(price) AS avg_priceFROM carsGROUP BY color
When using aggregates with GROUP BY, non-aggregate projections must be part of the GROUP BY clause:
DQL
-- This works: 'make' is in GROUP BYSELECT make, COUNT(*) AS model_count FROM cars GROUP BY make-- This would fail: 'model' is not in GROUP BY-- SELECT make, model, COUNT(*) FROM cars GROUP BY make
SELECT color, COUNT(*) AS car_countFROM carsGROUP BY colorHAVING COUNT(*) > 5
Aggregate functions form a “dam” in the execution pipeline - all documents must be processed before results can be returned. This differs from non-aggregate queries which can stream results.
-- Always return 0 instead of MISSING when no documents matchSELECT IFMISSING(SUM(price), 0) AS total_sales FROM orders WHERE status = 'completed'-- Use default value for missing fields during aggregationSELECT SUM(IFMISSING(price, 0)) AS total_with_defaults FROM products-- Count all non-missing, non-null values regardless of truthinessSELECT COUNT(NOT ISMISSINGORNULL(rating)) AS rated_items FROM reviews
Conditional Aggregation with CASE:
DQL
-- Sum order values by status within a single querySELECT COUNT(*) AS total_orders, SUM(CASE WHEN status = 'completed' THEN final_price ELSE 0 END) AS completed_value, SUM(CASE WHEN status = 'pending' THEN price * discount ELSE 0 END) AS pending_value, SUM(CASE WHEN status = 'cancelled' THEN price ELSE 0 END) AS cancelled_valueFROM orders-- Calculate weighted averagesSELECT category, AVG(CASE WHEN priority = 'high' THEN price * 1.5 ELSE price END) AS weighted_avg_priceFROM productsGROUP BY category-- Sum with conditional multipliersSELECT SUM(CASE WHEN quantity > 100 THEN price * quantity * 0.9 WHEN quantity > 50 THEN price * quantity * 0.95 ELSE price * quantity END) AS total_revenueFROM order_items
Complex Business Logic:
DQL
-- Calculate profit margin by categorySELECT category, (SUM(sale_price) - SUM(cost_price)) / SUM(sale_price) * 100 AS profit_margin_pct, COUNT(*) AS items_soldFROM salesWHERE sale_date >= '2024-01-01'GROUP BY categoryHAVING (SUM(sale_price) - SUM(cost_price)) > 1000-- Customer lifetime value with fallback for missing dataSELECT customer_id, COUNT(*) AS total_orders, SUM(IFMISSING(order_total, 0)) AS lifetime_value, AVG(IFMISSING(order_total, 0)) AS avg_order_value, MAX(order_date) AS last_order_dateFROM ordersGROUP BY customer_idHAVING SUM(IFMISSING(order_total, 0)) > 500-- Inventory health metricsSELECT warehouse_id, COUNT(*) AS total_items, SUM(CASE WHEN stock_level < reorder_point THEN 1 ELSE 0 END) AS items_needing_reorder, SUM(CASE WHEN stock_level = 0 THEN 1 ELSE 0 END) AS out_of_stock_items, AVG(IFMISSING(stock_level, 0)) AS avg_stock_levelFROM inventoryGROUP BY warehouse_id
Filtering Within Aggregates:
DQL
-- Count only items matching specific conditionsSELECT department, COUNT(*) AS total_employees, COUNT(CASE WHEN salary > 100000 THEN 1 END) AS high_earners, AVG(CASE WHEN years_experience > 5 THEN salary END) AS avg_senior_salaryFROM employeesGROUP BY department-- Calculate percentages within groupsSELECT region, COUNT(*) AS total_sales, SUM(CASE WHEN product_type = 'premium' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) AS premium_pctFROM salesGROUP BY region
Handling NULL and MISSING Gracefully:
DQL
-- Coalesce with multiple fallbacksSELECT product_id, COUNT(*) AS review_count, AVG(COALESCE(rating, average_category_rating, 3.0)) AS effective_ratingFROM reviewsGROUP BY product_id-- Conditional counting with null handlingSELECT status, COUNT(*) AS total, COUNT(completed_date) AS completed_with_date, COUNT(*) - COUNT(completed_date) AS missing_datesFROM tasksGROUP BY status-- Sum with NULL protectionSELECT order_id, SUM(IFNULL(item_price * quantity, 0)) AS order_total, COUNT(*) AS item_count, SUM(IFNULL(discount_amount, 0)) AS total_discountFROM order_itemsGROUP BY order_id
Multi-level Aggregations:
DQL
-- Nested calculations with aggregatesSELECT category, COUNT(*) AS product_count, SUM(price * inventory_count) AS total_inventory_value, SUM(price * inventory_count) / COUNT(*) AS avg_value_per_product, MAX(price) - MIN(price) AS price_rangeFROM productsGROUP BY categoryHAVING COUNT(*) >= 5-- Time-based aggregations with conditionalsSELECT DATE_FORMAT(created_at,'YYYY-MM-DD') AS order_date, COUNT(*) AS daily_orders, SUM(IFMISSING(total, 0)) AS daily_revenue, AVG(CASE WHEN total > 0 THEN total END) AS avg_order_value_excluding_zeroFROM ordersWHERE created_at >= DATE_CAST('2024-01-01','YYYY-MM-DD')GROUP BY DATE_FORMAT(created_at,'YYYY-MM-DD')ORDER BY order_date DESC
Working with Arrays and Complex Fields:
DQL
-- Aggregate with array transformationsSELECT seller_id, COUNT(*) AS total_products, AVG(array_length(tags)) AS avg_tags_per_product, SUM(CASE WHEN array_length(tags) > 5 THEN 1 ELSE 0 END) AS well_tagged_productsFROM productsGROUP BY seller_id-- Conditional aggregation on nested fieldsSELECT category, COUNT(*) AS total_items, AVG(IFMISSING(details.weight, 0)) AS avg_weight, SUM(CASE WHEN details.fragile = true THEN 1 ELSE 0 END) AS fragile_countFROM inventoryGROUP BY category