The Question Practitioners Ask
When you need a count, which is faster: GlideAggregate with COUNT, or GlideRecord with getRowCount()? The docs say “GlideAggregate is optimized for aggregations.” That’s true but incomplete. There are three scenarios, and in one of them the docs’ guidance is exactly wrong.
We ran this benchmark on a controlled Vancouver instance with a 50M-row incident table, varying filter selectivity. Numbers below.
Scenario 1: Counting With No Filter
Counting every row in a table is the cheapest case. Both approaches hit a cardinality estimate the database maintains, not the rows themselves.
| Method | p50 (ms) | p95 (ms) |
|---|---|---|
GlideAggregate.addAggregate('COUNT', 'sys_id') | 38 | 71 |
GlideRecord.setLimit(1); query(); getRowCount() | 142 | 320 |
GlideRecord.query(); getRowCount() (full result) | OOM | OOM |
GlideAggregate wins decisively. Don’t ever use GlideRecord.query() without setLimit() on a 50M-row table.
Scenario 2: Counting With Selective Filter (≤100K matching rows)
This is where the answer flips. When the filter is selective enough that the database can use an index to return the matching rows quickly, GlideRecord can match or beat GlideAggregate because aggregation has overhead.
| Method | p50 (ms) | p95 (ms) |
|---|---|---|
GlideAggregate.addAggregate('COUNT', 'sys_id') with selective filter | 86 | 210 |
GlideRecord.query() with selective filter, getRowCount() | 64 | 180 |
The crossover happens around 100K matching rows. Below that, GlideRecord wins because the index-scan-and-count path is shorter than the aggregation pipeline. Above 200K, GlideAggregate wins by an increasing margin.
The catch: GlideRecord.getRowCount() after query() loads the matching rows. On 100K rows that’s about 30MB of memory before you do anything with them. If you’re only going to count, that memory was wasted.
Scenario 3: Counting With Non-Selective Filter (>1M matching rows)
| Method | p50 (ms) | p95 (ms) |
|---|---|---|
GlideAggregate.addAggregate('COUNT', 'sys_id') | 1,840 | 4,200 |
GlideRecord.query(); getRowCount() | Failure (memory) | Failure (memory) |
GlideAggregate is the only viable option above ~500K matching rows. GlideRecord will eat the JVM. Don’t even try.
The Practical Decision
// Rule of thumb codified
function countMatching(table, encodedQuery, expectedSelectivity) {
if (expectedSelectivity === 'high' && /* < 100K */) {
var gr = new GlideRecord(table);
gr.addEncodedQuery(encodedQuery);
gr.setLimit(200000); // safety cap
gr.query();
return gr.getRowCount();
} else {
var ga = new GlideAggregate(table);
ga.addEncodedQuery(encodedQuery);
ga.addAggregate('COUNT', 'sys_id');
ga.query();
ga.next();
return ga.getAggregate('COUNT', 'sys_id');
}
}
If you don’t know the selectivity ahead of time, default to GlideAggregate. The penalty of GlideAggregate on a selective query is ~30ms. The penalty of GlideRecord on a non-selective query is an out-of-memory and a paged on-call engineer.
The Three Things Most People Get Wrong
1. Iterating instead of counting. while (gr.next()) { count++; } is the slowest possible count. Every next() round-trips to the database. Don’t do this; we still see it in legacy code.
2. Forgetting that setLimit doesn’t change the getRowCount semantics. gr.setLimit(10); gr.query(); gr.getRowCount() returns 10 if there are at least 10 matching rows, not the total. For counting, you don’t want a limit.
3. Using addAggregate('COUNT', 'number') on a nullable field. COUNT ignores nulls if you specify a column other than sys_id. If the column has nulls, your count is wrong. Always count on sys_id, which is non-null by definition.
When You Want Performance Beyond GlideAggregate
For dashboards that recompute counts every few minutes, neither GlideAggregate nor GlideRecord is the right tool — you want Performance Analytics indicators or a scheduled job writing to a small cache table. A count that runs every 5 minutes on a 50M-row table will saturate the database; a count that reads from a 1-row cache table is free.
The rule: real-time counts use GlideAggregate; dashboard counts use a materialized indicator.
What to Do This Week
Search your scoped apps for getRowCount() calls following an unscoped query(). If the filter is selective, you’re fine. If the filter is broad (especially on incident, change_request, task, or a custom transaction table), refactor to GlideAggregate. The change is two lines and pays back in p99 latency on the first non-selective query.