← All guides

10 Ways to Optimize GlideRecord Queries in ServiceNow

Improve ServiceNow GlideRecord performance with indexed filters, encoded queries, row limits, GlideAggregate, efficient loops, and practical diagnostics.

GlideRecord is central to ServiceNow server-side development, but a query that feels instant with test data can become a production bottleneck when a table contains millions of rows. Slow queries do more than delay one script: they hold worker threads, increase database load, lengthen transactions, and can affect every user on the instance.

The best optimization is usually not a clever line of JavaScript. It is asking the database a smaller, more selective question. These ten practices help developers and administrators improve GlideRecord performance without sacrificing correctness.

1. Query Only the Records You Need

Never call query() without conditions on a large table unless a controlled maintenance task genuinely requires every record. Start with the narrowest business requirement and translate it into database filters.

var incident = new GlideRecord('incident');
incident.addQuery('active', true);
incident.addQuery('assignment_group', groupSysId);
incident.addQuery('sys_updated_on', '>=', gs.daysAgoStart(7));
incident.query();

A condition such as active=true may still match hundreds of thousands of rows. Combine it with selective criteria such as a group, state, date range, or known identifier. Avoid fetching a broad result set and discarding most rows with JavaScript if statements inside the loop.

2. Filter on Indexed, Selective Fields

Indexes help the database locate matching rows without scanning an entire table. Common system fields such as sys_id, number, and some reference or date fields are indexed, but the exact indexes vary by table and instance.

An indexed field is not automatically fast. A condition matching most of the table has low selectivity and may still require substantial work. Prefer filters that sharply reduce the candidate set, and verify indexes through the table definition rather than assuming they exist.

Do not add custom indexes as a first reaction. Each index consumes space and adds overhead to inserts and updates. A justified index supports a recurring, high-value query pattern—not one poorly scoped script.

3. Avoid Invalid Fields in Conditions

GlideRecord can log a warning and return unexpectedly broad results when a query references a field that does not exist. This is both a correctness and performance risk, particularly when field names are assembled dynamically.

Use isValidField() when a field comes from configuration or input. During development, check system logs for invalid query messages and test scripts with realistic data volumes. A typo should fail safely rather than turn a targeted update into a table-wide operation.

4. Use setLimit() When You Need Only a Few Rows

If a process needs one matching record or a small batch, tell the database:

var task = new GlideRecord('task');
task.addQuery('correlation_id', externalId);
task.setLimit(1);
task.query();
if (task.next()) {
  // Process the match.
}

setLimit() is especially useful for existence checks, duplicate detection, and scheduled jobs that intentionally process bounded batches. For deterministic batch processing, add an appropriate orderBy() and persist a reliable checkpoint rather than repeatedly selecting an arbitrary first set.

5. Use GlideAggregate for Counts and Totals

Do not retrieve thousands of records merely to increment a JavaScript counter. GlideAggregate performs aggregation in the database and returns a much smaller result.

var count = new GlideAggregate('incident');
count.addQuery('active', true);
count.addQuery('assignment_group', groupSysId);
count.addAggregate('COUNT');
count.query();
if (count.next()) {
  gs.info('Active incidents: ' + count.getAggregate('COUNT'));
}

Use it for COUNT, SUM, MIN, MAX, AVG, and grouped reporting. This reduces record transfer, object creation, and server-side loop work.

6. Keep Queries Outside Loops

The classic N+1 query problem occurs when a script runs another GlideRecord query for every row in its outer result. One initial query plus 1,000 inner queries creates avoidable database traffic.

Where possible, collect identifiers and query related data in a consolidated operation, use reference information already available, or redesign the process around a relationship table. Cache stable lookups in an object during the transaction. If each row truly requires separate work, move it to controlled asynchronous batches rather than a long interactive transaction.

7. Use Dot-Walking Carefully

Dot-walked conditions are convenient, but queries across references can introduce joins and more complex execution plans. They are not inherently wrong; the risk depends on table size, selectivity, and index coverage.

For frequently executed logic, compare a dot-walked query with alternatives such as resolving the reference IDs first and filtering directly on the reference field. Do not denormalize data blindly, but recognize when a simple-looking condition causes expensive database work.

8. Avoid Unnecessary Sorting

orderBy() and orderByDesc() can require the database to sort a large result set, especially when the ordering is not supported by a suitable index. Sort only when the business outcome depends on order.

This matters with setLimit(): asking for the ten most recently updated records is a legitimate ordered query, while sorting thousands of rows that will all receive the same update provides no value. Keep the selected columns, filters, order, and limit aligned with the exact task.

9. Keep Record Processing Lightweight

Query time is only part of transaction time. Within a while (gr.next()) loop, avoid repeated logging, synchronous integrations, complex calculations, and individual updates that trigger extensive business logic.

Do not use setWorkflow(false) or autoSysFields(false) as generic performance switches; they change platform behavior and auditability. Use them only in controlled scenarios with documented consequences. For large corrections, process bounded batches, monitor impact, and provide a restart strategy.

Also avoid getRowCount() as the default way to count a large query. When only the count matters, use GlideAggregate.

10. Measure Before and After

Optimization should be evidence-driven. Reproduce the slow transaction in a sub-production environment with representative data, review transaction logs and slow-query diagnostics, and capture the query, row count, and duration.

ServiceNow administrators can use tools such as Debug SQL, session debugging, transaction logs, and database performance views according to their access and release. Enable verbose diagnostics briefly and carefully because they can generate substantial output.

Look beyond one execution. A 200-millisecond query run once a day may be harmless; the same query run on every form load can be costly. Record frequency, concurrency, table growth, and user-facing impact. After changing filters or indexes, measure again and confirm the returned data is still correct.

Final Thoughts

Fast GlideRecord code starts with precise data access. Apply selective conditions, use appropriate indexes, limit results, aggregate in the database, avoid queries inside loops, and remove unnecessary sorting and per-record work.

Most importantly, test with production-like volume and measure the complete transaction. A query optimization is successful only when it reduces resource use while preserving the business result.

More practical ServiceNow notes.

Back to the guides