Querying ref_ Fields on Extended Tables
When to use them, when to avoid them, and what they cost
ref_ queries are one of those ServiceNow features that are useful, slightly obscure, and easy to misuse.
They show up when you start from a base table such as task, but need to filter on a field that exists only on a child table such as incident.caller_id.
Used well, they help you express the query you actually need. Used carelessly, they make the script harder to read and can add unnecessary cost.
1. What ref_ actually means
Suppose you are querying task, but the filter you need exists only on incident.
This is where ref_ comes in:
var gr = new GlideRecord("task");
gr.addQuery("ref_incident.caller_id", userSysId);
gr.query();
ref_incident.caller_id means:
- start from the parent table
task - look at rows whose concrete class is
incident - apply the condition to the
caller_idfield from that child table
This is not the same as normal dot-walking on a reference field.
Normal dot-walking follows a reference, for example:
gr.addQuery("caller_id.department", deptSysId);
ref_ is different. It is about table extension, not about a reference relationship.
2. When ref_ is the right tool
ref_ makes sense when the query truly has to start from the parent table.
Typical cases:
- you are working with a generic
taskquery - the API or configuration starts from the parent table
- you need a combined result set that may include several task-based classes
- the filter depends on a field that exists only on one child table
Reasonable example:
var gr = new GlideRecord("task");
gr.addActiveQuery();
gr.addQuery("sys_class_name", "incident");
gr.addQuery("ref_incident.caller_id", userSysId);
gr.query();
That is much better than loading a broad set of tasks and figuring it out later in the loop.
3. The common wrong approach
The most expensive mistake is querying the parent table too broadly and then applying the child-table logic in script.
Bad:
var gr = new GlideRecord("task");
gr.addActiveQuery();
gr.query();
while (gr.next()) {
if (gr.getRecordClassName() == "incident" &&
gr.ref_incident.caller_id == userSysId) {
// process record
}
}
Why this is bad:
- the database returns more rows than you need
- the class filter happens too late
- the child-field condition happens too late
- the loop does work that the query should have done
If the requirement can be expressed in the query, put it in the query.
Better:
var gr = new GlideRecord("task");
gr.addActiveQuery();
gr.addQuery("sys_class_name", "incident");
gr.addQuery("ref_incident.caller_id", userSysId);
gr.query();
4. The better question: do you need the parent table at all?
Just because ref_ works does not mean it is the best option.
If the business question is really “find incidents for this caller,” query incident directly.
Best in that case:
var gr = new GlideRecord("incident");
gr.addActiveQuery();
gr.addQuery("caller_id", userSysId);
gr.query();
This is usually better because:
- the intent is clearer
- the query is narrower
- you avoid
ref_entirely - the platform does not need to start from a broader base table
This is the main rule I keep in mind:
Use ref_ only when you must query from the parent table. If you already know the child table you want, query the child table.
5. Performance implications of ref_
The performance story is not “never use ref_.”
The real issue is that ref_ is usually paired with a parent table that is broader than the actual data you want.
For example, task can represent many classes. If your real target is only incident, then a task query plus ref_incident... often has more work to do than a direct incident query.
That does not automatically make the query bad, but it means you should be deliberate.
Good habits:
- add the class restriction explicitly
- add other selective conditions early
- avoid broad parent-table scans
- use
setLimit()when you only need a small batch - do not use
ref_in a hot path if a direct child-table query would do the same job more simply
Safer pattern:
var gr = new GlideRecord("task");
gr.addActiveQuery();
gr.addQuery("sys_class_name", "incident");
gr.addQuery("ref_incident.caller_id", userSysId);
gr.orderByDesc("sys_created_on");
gr.setLimit(100);
gr.query();
The key is not the ref_ syntax itself. The key is how much unnecessary data you ask the platform to consider before it can apply that condition.
6. ref_ also shows up in encoded queries
The same idea can appear in encoded query form:
var gr = new GlideRecord("task");
gr.addEncodedQuery("sys_class_name=incident^ref_incident.caller_id=" + userSysId);
gr.query();
This is useful when:
- you are copying a filter from a list
- a module or URL uses
sysparm_query - a configuration stores a field path as text
But the same guidance still applies: if you only need incident, a direct incident query is still cleaner.
7. Do not confuse ref_ with normal field access
One reason ref_ feels strange is that it looks like a field name, but it is really a notation for reaching into a child table from the parent context.
That is why this thinking is usually safer:
ref_is a query path- it is not a sign that the parent table truly owns that field
- it is a workaround for table hierarchy, not a replacement for choosing the right table
If your script needs to do a lot of work with incident-specific fields, that is another hint that you should probably be querying incident, not task.
8. A practical rule of thumb
When I see a ref_ requirement, I usually ask these questions in order:
- Can I query the child table directly?
- If not, do I really need the parent table result set?
- If yes, have I restricted the class and other filters enough?
- Am I filtering in the query instead of in the loop?
That sequence catches most bad ref_ usage.
Final thought
ref_ is a useful tool, especially when you are forced to start from a base table such as task but need child-table fields.
The mistake is not using it. The mistake is using it when a direct child-table query would be simpler, clearer, and cheaper.
Pick the narrowest table that matches the problem. When you truly need the parent table, use ref_ intentionally and keep the query selective.