4.1 Querying Relational Data with a!queryEntity
Key Takeaways
a!queryEntity() executes type-safe SQL queries against relational database tables mapped through Data Store Entities, abstracting underlying JDBC connections and credentials.
Setting fetchTotalCount to false prevents an expensive secondary database COUNT(*) query, which is a critical performance optimization for high-volume enterprise tables.
Using a!querySelection() with a!queryColumn() projects only required attributes, minimizing database I/O, network transmission latency, and application server memory consumption.
a!queryFilter provides individual conditions, and a!queryLogicalExpression combines filters and nested logical expressions.
a!queryAggregation() delegates grouping and SQL aggregate calculations (COUNT, SUM, AVG, MIN, MAX) to the database engine rather than processing large datasets in Appian memory.
4.1 Querying Relational Data with a!queryEntity
The Architecture and Anatomy of a!queryEntity
In the Appian platform, enterprise applications interact with relational database tables using the a!queryEntity() expression function. Rather than writing raw SQL statements inside application code, developers execute type-safe queries against Data Store Entities (DSEs). This architectural abstraction ensures that database connection credentials, dialect idiosyncrasies, and connection pools remain securely managed at the infrastructure layer, while developers work with structured Appian data models.
The function signature for a!queryEntity() comprises three core parameters:
a!queryEntity(
entity: cons!ORDER_ENTITY,
query: a!query(
selection: a!querySelection(
columns: {
a!queryColumn(field: "orderId"),
a!queryColumn(field: "orderNumber"),
a!queryColumn(field: "orderTotal")
}
),
filter: a!queryFilter(
field: "orderStatus",
operator: "=",
value: "PENDING"
),
pagingInfo: a!pagingInfo(
startIndex: 1,
batchSize: 25,
sort: a!sortInfo(
field: "orderDate",
ascending: false
)
)
),
fetchTotalCount: false
)
Core Parameters Explained
entity(DataStoreEntity, Required): A pointer to the target Data Store Entity. In standard enterprise architecture, developers reference entities through dedicated constants (e.g.,cons!CUSTOMER_DSEorcons!INVOICE_ENTITY) rather than hardcoding entity references.query(Query, Required): Ana!query()configuration object containing the structural definitions for column selection, logical filtering, aggregations, and pagination rules.fetchTotalCount(Boolean, Optional): Determines whether Appian retrieves the total number of records matching the filter criteria across the entire database table. Defaults tofalsein modern versions of Appian.
The Return Structure: Understanding the DataSubset Object
When a!queryEntity() evaluates, it returns an Appian system object known as a DataSubset. Understanding the fields of this dictionary structure is essential for displaying data in read-only grids and extracting field values:
| DataSubset Property | Data Type | Description & Usage |
|---|---|---|
.data | List of Variant / CDT | The actual records retrieved from the database table. If no projection is configured, it returns an array of the underlying CDT. If a!querySelection() or a!queryAggregation() is used, it returns a list of dictionaries with field aliases. |
.totalCount | Integer | The total number of rows matching the query filters in the database. When fetchTotalCount: true, this contains the exact row count. When fetchTotalCount: false, it returns -1 for a finite batch when the count is not requested. With batchSize -1, the full result size is already known. |
.startIndex | Integer | The 1-based index of the first record returned in the current batch. |
.batchSize | Integer | The number of rows requested in the a!pagingInfo() configuration. |
.sort | List of SortInfo | The sort fields and direction instructions applied to the database query. |
.identifiers | List of Variant | The primary key values corresponding to each returned row in .data. |
To access queried values in an interface or expression rule, developers chain directly into the .data property, such as local!orderDataSubset.data.orderNumber.
Column Projection: a!querySelection() vs. Entire Entity Schemas
By default, if the selection parameter is omitted from a!query(), Appian executes a SELECT * query against the database table, mapping all relational columns back into the complete Custom Data Type (CDT) schema. In production systems with wide tables (e.g., 30 to 60 columns or fields containing large text and notes), retrieving entire schemas introduces severe architectural bottlenecks:
- Database I/O Strain: The database engine must read broad disk blocks and transfer unneeded columns across the network.
- Appian Memory Bloat: Retrieved values use application memory; values also saved into process state add to the process footprint. When multiple concurrent users view dashboards, loading full CDTs can lead to high memory consumption and garbage collection pauses.
Best Practice: Explicit Projections with a!querySelection()
Developers should explicitly project only the fields required for the immediate interface or business rule using a!querySelection() and a!queryColumn():
selection: a!querySelection(
columns: {
a!queryColumn(field: "customerId"),
a!queryColumn(field: "companyName"),
a!queryColumn(field: "accountManagerEmail")
}
)
When a projection is applied, Appian issues a targeted SQL query (e.g., SELECT customer_id, company_name, account_manager_email FROM ...). The resulting .data property contains lightweight dictionary objects containing only the selected keys, reducing unneeded data transfer and memory use; measure the actual benefit for the selected fields.
Filtering Relational Data: a!queryFilter() and a!queryLogicalExpression()
Appian provides two distinct helper functions for filtering database records: a!queryFilter() for individual conditional statements, and a!queryLogicalExpression() for combining multiple conditions with boolean logic.
Anatomy of a!queryFilter()
An individual filter requires four primary parameters:
field(Text): The exact CDT field name (case-sensitive) mapped to the relational column.operator(Text): The comparison operator, such as"=","<>","<",">","<=",">=","between","in","not in","starts with","ends with","includes","is null", or"is not null".value(Any Type): The target value to match. For multi-value operators like"in"or"between", this must be an array.applyWhen(Boolean): A conditional toggle that determines whether the filter is executed. WhenapplyWhenevaluates tofalse, Appian completely omits the filter from the generated SQL query.
Tip
Always use applyWhen: a!isNotNullOrEmpty(ri!searchTerm) on user-facing search fields. This eliminates the need for complex if() branching when configuring dynamic search queries and prevents invalid SQL queries when search inputs are blank.
Combining Filters with a!queryLogicalExpression()
When queries require multiple conditions, developers use a!queryLogicalExpression() to define boolean grouping:
logicalExpression: a!queryLogicalExpression(
operator: "AND",
filters: a!queryFilter(
field: "department",
operator: "=",
value: "Engineering"
),
logicalExpressions: a!queryLogicalExpression(
operator: "OR",
filters: {
a!queryFilter(field: "employmentStatus", operator: "=", value: "ACTIVE"),
a!queryFilter(field: "onboardingStage", operator: "=", value: "PROBATION")
}
)
)
This translates directly into the SQL WHERE clause: WHERE (department = 'Engineering' AND (employment_status = 'ACTIVE' OR onboarding_stage = 'PROBATION')).
Pagination and Sorting: a!pagingInfo() and a!sortInfo()
Every call to a!queryEntity() requires an a!pagingInfo() configuration to instruct the database how many rows to retrieve, where to begin reading, and how to sort results.
| Parameter | Rules & Constraints | Operational Behavior |
|---|---|---|
startIndex | Must be an integer ≥ 1. | Appian pagination is 1-indexed. Specifying 0 or a negative number results in an immediate expression evaluation error. |
batchSize | Integer (e.g., 10, 25, 50, -1, 0). | Defines the maximum number of rows returned. Setting batchSize: -1 requests all matching records. Setting batchSize: 0 returns zero data rows but returns totalCount if fetchTotalCount: true. |
sort | List of a!sortInfo(). | Defines multi-column ordering. Each sort item specifies a field (Text) and ascending (Boolean). When ascending: true, data sorts from low-to-high or A-to-Z. |
Bound the Result Set
Use a positive, bounded batch size for ordinary interface queries. QueryEntity can request all results using batchSize -1, but memory and timeout limits still apply. QueryRecordType has a different contract and does not accept -1. Do not transfer paging assumptions between the two APIs.
The fetchTotalCount Parameter: Performance Trade-offs
When a finite result batch is requested with fetchTotalCount: true, Appian may need an additional count query against the database engine:
- The primary data retrieval query:
SELECT ... FROM table WHERE ... ORDER BY ... LIMIT 25 OFFSET 0 - An exhaustive counting query:
SELECT COUNT(*) FROM table WHERE ...
On large relational tables containing hundreds of thousands or millions of records, SELECT COUNT(*) requires the database engine to scan table partitions or index trees, which often takes significantly longer than fetching the 25 specific rows.
Architectural Guidance
- Set
fetchTotalCount: falsewhen populating dropdown components, card lists, feeding downstream batch processing, or when using modern "Load More" infinite-scrolling interface designs. - Set
fetchTotalCount: trueonly when displaying a classic read-only grid where users must see total page numbers (e.g., "Showing page 1 of 42").
Aggregating Data: a!queryAggregation() and Grouping
When an interface requires summary statistics—such as total revenue by region or headcount by department—developers should never retrieve raw records into Appian memory to compute sums with sum() or count(). Instead, a!queryAggregation() instructs the database engine to perform SQL grouping and calculation natively.
Inside a!queryAggregation(), developers supply an array of a!queryAggregationColumn() objects configured with:
isGrouping: true: Specifies that the column forms part of the SQLGROUP BYclause.aggregationFunction: Specifies the SQL aggregate operation:"COUNT","SUM","AVG","MIN", or"MAX".alias(Required): Defines the property name in the returned dictionary for the computed value.
a!queryEntity(
entity: cons!EMPLOYEE_ENTITY,
query: a!query(
aggregation: a!queryAggregation(
columns: {
a!queryAggregationColumn(
field: "department",
isGrouping: true
),
a!queryAggregationColumn(
field: "employeeId",
aggregationFunction: "COUNT",
alias: "totalHeadcount"
),
a!queryAggregationColumn(
field: "salary",
aggregationFunction: "AVG",
alias: "averageSalary"
)
}
),
pagingInfo: a!pagingInfo(startIndex: 1, batchSize: -1)
),
fetchTotalCount: false
)
This generates: SELECT department, COUNT(employee_id) AS totalHeadcount, AVG(salary) AS averageSalary FROM employee GROUP BY department. The returned .data property contains dictionaries with the keys department, totalHeadcount, and averageSalary.
Warning
In an a!query() definition, selection and aggregation are mutually exclusive. You cannot specify both selection: a!querySelection(...) and aggregation: a!queryAggregation(...) within the same query call.
Real-World Scenario: Financial Risk Dashboard Latency
A financial institution built a risk analysis dashboard to display high-risk commercial loans. During initial development with 100 sample records, the dashboard loaded instantly. However, after migrating to user acceptance testing (UAT) with 650,000 real-world loan records, loading the dashboard took over 9.5 seconds, creating unacceptable latency for the example application.
Upon inspecting the expression rule, the lead developer identified three critical anti-patterns:
- The query omitted
a!querySelection(), loading all 48 columns of theCommercialLoanCDT, including large JSON audit strings and loan history remarks. - The query specified
fetchTotalCount: true, forcing a full-tableCOUNT(*)across 650,000 unindexed rows on every tab switch. - The paging configuration used
batchSize: -1, loading thousands of rows into Appian application memory.
The developer refactored the query to use a!querySelection() with only 5 required summary columns, set fetchTotalCount: false, and capped batchSize at 20 rows with a dedicated sort index. Compare before-and-after query timings and result correctness using the same representative dataset; this example does not establish a measured latency.
Exam Watchouts & Common Traps
- 1-Based Indexing:
startIndexmust be1or greater. Providing0is a classic exam trick and causes an evaluation failure. - Different Query Contracts: Use bounded QueryEntity batches, and remember that QueryRecordType does not accept batchSize -1.
- Field Name Casing: The
fieldparameter ina!queryColumn(),a!queryFilter(), anda!sortInfo()must match the CDT field name (e.g.,"createdDateTime"), NOT the physical database column name (e.g.,"CREATED_DATE_TIME"). - Aggregations Require Aliases: Any
a!queryAggregationColumn()using anaggregationFunctionmust have a definedalias. Omitting the alias results in an evaluation error because Appian cannot name the returned dictionary key. - Mutual Exclusivity: An
a!query()call cannot contain both aselectionand anaggregation. Chooseselectionfor row-level projection andaggregationfor grouped metrics.
Source checked 2026-10-18: Appian queryEntity.
An Appian developer is optimizing an interface expression rule that queries a database table containing 2.5 million order records. The query retrieves only the 10 most recent orders to display in a summary card. Which configuration change will most effectively reduce database query execution time and avoid an unnecessary full-table count?
Set fetchTotalCount: false in a!queryEntity so the database does not execute a secondary COUNT(*) query
Set startIndex: 0 in a!pagingInfo to bypass index lookup operations
Change the batchSize from 10 to -1 to retrieve all records into memory at once
Replace a!querySelection with a full CDT query schema return
A developer needs the first 25 matching data-store rows. Which paging configuration uses Appian's one-based start index?
startIndex: 1, batchSize: 25
startIndex: 0, batchSize: 25
startIndex: -1, batchSize: 25
startIndex: 0, batchSize: -1
A developer is building a search filter for a customer support portal. When the search box is empty, the interface should return all active customers without filtering by name. What is the recommended and most robust way to implement this in a!queryFilter?
Set value: "%" so SQL wildcards match all possible strings
Use the applyWhen parameter configured with a!isNotNullOrEmpty(ri!searchTerm) so the filter is ignored when empty
Wrap the entire a!queryEntity call in an if() statement with two separate duplicate queries
Set the filter operator to "between" with null boundary values
Sections you finish are checked off in the contents.