Production recipes and query optimization patterns for JCR-SQL2 in Jackrabbit Oak repository.
AEM JCR-SQL2 Recipe Book
If you remember only one thing from this guide, it is that mastering JCR-SQL2 is the difference between an AEM engineer who guesses why a feature is broken and one who diagnoses repository state in seconds. In enterprise AEM deployments, the Jackrabbit Oak repository holds millions of nodes—pages, assets, configurations, and user data. Without a precise querying mechanism, you are effectively flying blind. Most teams get this wrong because they rely on expensive, iterative Java code to traverse the JCR tree instead of leveraging the underlying Oak query engine to fetch exactly what they need in milliseconds. JCR-SQL2 is the declarative standard for interacting with the AEM repository, and knowing how to write optimized, index-backed queries is a non-negotiable skill for any Staff-level AEM engineer.
In this exhaustive guide, we cover the real-world application of JCR-SQL2 in production AEM environments. We will explore the fundamental anatomy of a JCR-SQL2 statement, the critical differences in query execution between AEM 6.5 and AEM as a Cloud Service (AEMaaCS), and how to leverage the EXPLAIN plan for performance tuning. The core of this guide is a massive recipe library containing over 20 copy-pasteable, highly optimized queries for everyday AEM tasks—ranging from finding specific component resource types and querying DAM assets, to executing complex inner joins across cq:Page and jcr:content nodes.
To fully grasp the underlying Oak indexing strategies and repository concepts discussed here, I highly recommend reviewing our core architecture materials. This post serves as the companion piece to the JCR & Oak Repository Complete Guide. You should also cross-reference the AEM Query Builder API Complete Reference for the HTTP-based abstraction over JCR-SQL2, the AEM Architecture Complete Guide for topological context, and the AEM Backend Development Complete Guide for integrating these queries into your OSGi services.
The Anatomy of a JCR-SQL2 Query
Before diving into the recipes, we must establish the structural semantics of JCR-SQL2. Unlike traditional relational SQL, JCR-SQL2 queries a hierarchical graph. The tables are Node Types, the columns are Properties, and the relationships are parent-child paths.
Node Types over Paths
Most developers make the mistake of querying nt:base (every node in the repository) and then filtering by path. This forces the Oak engine to scan massive subtrees. Always query the most specific node type possible. In AEM, this usually means cq:PageContent, dam:AssetContent, cq:Component, or rep:User.
AEM 6.5 vs. AEM as a Cloud Service
When writing queries, you must account for the platform.
- AEM 6.5: You have direct access to CRXDE Lite (
/crx/de/index.jsp) and can use the Query tool to test JCR-SQL2 directly against your local or dev servers. Custom Lucene indexes are deployed via package manager and sit under/oak:index. - AEM as a Cloud Service: CRXDE Lite is read-only and often locked down in higher environments. To run JCR-SQL2 queries in AEMaaCS, you must use the Developer Console (accessible via Cloud Manager). Navigate to the "Queries" tab to execute JCR-SQL2 or Explain statements. Furthermore, indexes in AEMaaCS are strictly managed via the
ui.configproject and the Blue/Green deployment pipeline.
Executing JCR-SQL2
Via CRXDE Lite (AEM 6.5 / Local)
- Open
http://localhost:4502/crx/de/index.jsp. - Click Tools > Query in the top menu.
- Select Type:
JCR-SQL2. - Paste your query and click Execute.
Via Developer Console (AEMaaCS)
- Log into Adobe Cloud Manager.
- Select your Program and Environment.
- Click the Developer Console icon.
- Navigate to the Queries tab.
- Select JCR-SQL2 from the dropdown, paste the query, and run.
Via Java API (OSGi Services)
In backend development, you execute JCR-SQL2 using the JCR QueryManager. Never hardcode queries; use OSGi configurations or constants, and always use parameterized values to prevent injection, though JCR-SQL2 doesn't have a standard bind variable syntax natively supported across all implementations without specific Sling abstractions.
The JCR-SQL2 Recipe Library
Here is the definitive collection of JCR-SQL2 queries. Every Staff engineer should keep these bookmarked.
1. The Basics: Finding Nodes by Path and Type
The most common requirement is finding all nodes of a certain type under a specific path.
Recipe: Find all Page Content nodes under a specific site path.
Why this works: [cq:PageContent] restricts the search to page properties nodes (the actual content). ISDESCENDANTNODE is a highly optimized JCR function that uses path-based indexing.
2. Filtering by Template
When authors request a list of all "Article" pages, you query by cq:template.
Recipe: Find all pages using a specific Editable Template.
3. Finding Specific Component Instances
If you are deprecating an old component and need to know every page where it is authored, you query for the sling:resourceType. Because components are unstructured nodes under jcr:content, we query nt:unstructured.
Recipe: Find all instances of the "Carousel" component.
4. Full-Text Search (Contains)
The CONTAINS() function triggers the underlying Lucene (or Elastic in AEMaaCS) full-text index. This is powerful but can be expensive if not scoped.
Recipe: Search the entire WKND site for the word "adventure".
Recipe: Search specifically within the jcr:title property.
5. Querying Digital Assets (DAM)
The DAM is structured differently. The root is dam:Asset, and the metadata sits at dam:AssetContent under jcr:content/metadata.
Recipe: Find all PDF documents in a specific DAM folder.
Recipe: Find assets larger than 5 Megabytes.
Note: We use CAST() to ensure the binary size is evaluated as a Long integer.
6. Working with Dates
Date queries require standardizing the timestamp format. AEM uses the CAST('YYYY-MM-DDTHH:MM:SS.000Z' AS DATE) syntax.
Recipe: Find pages modified after a specific date.
Recipe: Find assets expiring in the next 30 days.
7. Identifying Null or Missing Properties
Sometimes you need to find nodes that lack a mandatory property, such as pages without an SEO description.
Recipe: Find pages missing a jcr:description.
Recipe: Find pages where jcr:description exists (IS NOT NULL).
8. Utilizing LIKE and Wildcards
The LIKE operator allows for pattern matching. % matches zero or more characters, and _ matches a single character.
Recipe: Find pages where the title starts with "Summer".
9. Finding Nodes by Mixin Types
Mixins add behavioral traits to nodes. For example, mix:versionable means the node history is tracked, and cq:LiveSync indicates the node is part of a Live Copy.
Recipe: Find all Live Copy root pages (MSM).
Recipe: Find all nodes that are taggable but not strictly pages.
10. Querying Users and Groups (Security)
Security data lives under /home. Users are rep:User and groups are rep:Group.
Recipe: Find a user by their email address.
Recipe: Find all groups that have "approvers" in the name.
11. Complex INNER JOINs
This is where JCR-SQL2 outshines XPath. You can join a parent node to a child node to filter on properties of both simultaneously.
Recipe: Find all cq:Page nodes (the page wrapper) where the underlying cq:PageContent has a specific tag.
Why this works: We select the cq:Page node, but we join it with its child cq:PageContent node, allowing us to filter on the content's tags while returning the actual page node.
Recipe: Find Assets based on Rendition properties.
12. Using LocalName() and Name()
Sometimes you need to query based on the exact name of the node in the JCR tree.
Recipe: Find all nodes named precisely jcr:content.
13. Advanced Multi-condition Query (The "Active Campaign" query)
Combining everything into a production-realistic query.
Recipe: Find all English Article pages that are currently valid (published, not expired), tagged with "summer", and have an SEO description.
14. Finding Orphan Nodes
Orphan nodes (nodes that exist but are disconnected from standard application logic, like a component without a parent paragraph system) are tricky. Usually, you look for specific structures.
Recipe: Find components directly under jcr:content instead of in a root parsys.
(This targets unstructured nodes directly attached to PageContent, bypassing the layout container).
Query Diagnostics: The EXPLAIN Statement
Never run a query in production without prefixing it with EXPLAIN. The Explain statement tells you exactly which Oak index the query engine will use, or worse, if it will resort to a full traversal.
Syntax:
Understanding the Output: When you run this in CRXDE or the Developer Console, look for the plan column.
- Good Plan:
[cq:PageContent] as p /* lucene:cqPageLucene(/oak:index/cqPageLucene) ...This means it successfully hit a Lucene index. - Bad Plan:
[cq:PageContent] as p /* traverse "/content/wknd" ...This is a traversal warning. If the path has more than 100,000 nodes, AEM will throw aTraversalExceptionand crash the query.
If you see a traversal, you MUST create or update an Oak index under /oak:index to cover the properties you are querying (e.g., adding cq:template to the property index).
JCR-SQL2 Cheat Sheet
Best Practices
- Never use
[nt:base]: Always restrict your query to the most specific node type (cq:PageContent,dam:Asset,rep:User). Queryingnt:basebypasses specialized indexes and forces massive scans. - Always scope with
ISDESCENDANTNODE: Never search the entire repository. Always limit the query to/content/mysite,/content/dam/myfolder, or/home. - Set limits in Java: JCR-SQL2 does not have a
LIMITkeyword natively in the string syntax. You MUST callquery.setLimit(int)in your Java code to prevent OutOfMemory errors on large result sets. - Use QueryBuilder for HTTP, JCR-SQL2 for Backend: If you are writing a frontend application making AJAX calls, use the AEM QueryBuilder API. If you are writing internal Java OSGi services, use JCR-SQL2 via the
QueryManagerfor maximum performance and type safety.
Do's & Don'ts
- DO use the Developer Console in AEM as a Cloud Service to test queries before committing code.
- DO use
EXPLAIN MEASUREto verify that your query uses an index. - DON'T use JCR-SQL2 to iterate over every node just to read a single property; if you know the exact path, use
session.getNode(path)—it is orders of magnitude faster. - DON'T perform string manipulation inside JCR-SQL2. The query language is limited; fetch the nodes and do complex data extraction in Java.
- DON'T forget that AEM applies ACLs (Access Control Lists) to query results. A query executed by a
content-authorwill only return nodes they have permission to read, whereas the same query run by an administrative service user will return everything.
Mastering these recipes will significantly reduce your debugging time and ensure your backend AEM code is performant, scalable, and robust. Keep this recipe book handy, and always check your Explain plans!