artigo
performance e diagnóstico: o preço do código lento
GlideRecord sem filtro, getRowCount() e query em loop respondem pela maioria dos incidentes de performance — como reconhecer e medir antes de otimizar.
#o script que passou no seu teste e travou a produção
Você escreve um Business Rule, testa numa instância de dev com 40 registros na tabela, funciona instantaneamente, sobe pro Update Set e segue pra próxima tarefa. Três meses depois a mesma tabela tem 300 mil registros, o mesmo script continua rodando, e agora ele é a explicação para "o sistema ficou lento hoje de manhã" no canal de incidentes.
O detalhe que muda a forma de pensar sobre isso: ServiceNow é multi-tenant dentro da própria instância. Um script pesado num formulário não afeta só quem clicou em salvar — afeta os outros 200 usuários que estão no mesmo processo em paralelo. Performance não é polimento de fim de projeto. É o tipo de coisa que, quando ignorada, vira reunião de RCA com o nome de alguém na ata.
#os três hábitos que parecem inofensivos até o volume chegar
Tem uma característica em comum nos três anti-padrões abaixo: nenhum deles dá erro. O código roda, devolve o resultado certo, passa em code review — porque quem revisa também está olhando pra uma tabela de dev com poucos registros. O problema não é lógico, é de escala.
1. Query sem addQuery(). Um GlideRecord que chama .query() sem nenhum filtro varre a tabela inteira. Em incident ou task, isso pode significar milhões de linhas.
// ❌ varre a tabela inteira
var gr = new GlideRecord("task");
gr.query();
// �
filtro + limite explícito quando você sabe o teto esperado
var gr = new GlideRecord("task");
gr.addQuery("active", true);
gr.addQuery("assigned_to", gs.getUserID());
gr.setLimit(500);
gr.query();2. getRowCount() só para contar. Esse método carrega cada registro na memória, um por um, e só depois entrega o total — é a diferença entre pedir "quantas mesas estão ocupadas" e ter que andar pelo salão inteiro contando cabeça por cabeça antes de responder.
// ❌ carrega tudo pra saber quantos são
var gr = new GlideRecord("incident");
gr.addQuery("state", 1);
gr.query();
var total = gr.getRowCount();
// �
o banco já devolve o número pronto
var ga = new GlideAggregate("incident");
ga.addQuery("state", 1);
ga.addAggregate("COUNT");
ga.query();
ga.next();
var total = parseInt(ga.getAggregate("COUNT"));3. GlideRecord dentro de loop — o padrão N+1. Uma query pra buscar a lista, mais uma query nova a cada iteração do loop. Para 500 registros, isso é 501 idas ao banco. Para 5.000, são 5.001.
// ❌ 1 query na lista + 1 por registro = N+1
var gr = new GlideRecord("incident");
gr.addQuery("state", 1);
gr.query();
while (gr.next()) {
var grCI = new GlideRecord("cmdb_ci");
grCI.get(gr.getValue("cmdb_ci")); // query nova a cada volta
}
// �
junta os IDs primeiro, resolve tudo numa segunda query
var gr = new GlideRecord("incident");
gr.addQuery("state", 1);
gr.query();
var ciIds = [];
while (gr.next()) {
var ciId = gr.getValue("cmdb_ci");
if (ciId && ciIds.indexOf(ciId) === -1) ciIds.push(ciId);
}
var ciMap = {};
if (ciIds.length > 0) {
var grCI = new GlideRecord("cmdb_ci");
grCI.addQuery("sys_id", "IN", ciIds.join(","));
grCI.query();
while (grCI.next()) ciMap[grCI.getUniqueValue()] = grCI.getValue("name");
}#GlideAggregate merece um parágrafo próprio
GlideAggregate não serve só para substituir getRowCount(). É a API certa sempre que a pergunta é "quanto", "quantos" ou "qual o total" — COUNT, SUM, MIN, MAX, AVG, GROUP BY. A diferença prática é que o cálculo acontece no banco, não no Rhino/servidor de aplicação depois de carregar tudo.
// Quantos incidentes ativos por grupo de atribuição
var ga = new GlideAggregate("incident");
ga.addQuery("active", true);
ga.addAggregate("COUNT");
ga.groupBy("assignment_group");
ga.orderByAggregate("COUNT");
ga.setLimit(20);
ga.query();
var porGrupo = {};
while (ga.next()) {
porGrupo[ga.getDisplayValue("assignment_group")] = parseInt(ga.getAggregate("COUNT"));
}Regra prática: se o retorno esperado passa de uma centena de registros, ou a tabela cresce continuamente, GlideAggregate deixa de ser opcional. Na dúvida, use GlideAggregate — o custo de escrever um pouco mais de código é bem menor que o custo de descobrir o limite na produção.
#scheduled script grande? o relógio não perdoa
Scheduled Script não roda pra sempre — o corte é controlado por Transaction Quota Rules configuráveis (System Definition > Transaction Quota Rules), e é comum instâncias cancelarem jobs longos bem antes de qualquer limite teórico que você tenha em mente. Se o seu job processa 100 mil registros a 20ms cada, a conta fecha em 33 minutos — tempo de sobra pra esbarrar em alguma quota configurada, e quando isso acontece o ServiceNow não espera você terminar: corta no meio, com uma fatia de dados processada e outra não.
A saída é processar em lotes menores e controlar o tempo decorrido dentro do próprio script, parando antes do limite e deixando o próximo agendamento continuar de onde parou:
(function syncExternalUsers() {
var CHUNK_SIZE = 200;
var MAX_EXEC_MINUTES = 25; // margem de segurança — ajuste conforme a quota configurada na sua instância
var startTime = new GlideDateTime();
var processed = 0;
var gr = new GlideRecord("x_acme_sync_queue");
gr.addQuery("u_status", "pending");
gr.orderBy("sys_created_on");
gr.setLimit(CHUNK_SIZE);
gr.query();
while (gr.next()) {
var elapsedMin = GlideDateTime.subtract(startTime, new GlideDateTime())
.getNumericValue() / 60000;
if (elapsedMin >= MAX_EXEC_MINUTES) {
gs.warn("syncExternalUsers: parando em " + processed + " registros — resto fica pro próximo agendamento");
break;
}
// processa o registro, atualiza status
processed++;
}
})();Duas peças fazem esse padrão funcionar: setLimit() garante que cada execução pega só um pedaço, e a verificação de tempo dentro do while garante que você nunca chega perto do corte abrupto — o job para de forma limpa, com log explicando o motivo, e não no meio de uma escrita.
#medir antes de mexer
A tentação depois de ler uma lista de anti-padrões é sair caçando getRowCount() no código inteiro da instância. Não faça isso sem medir primeiro — otimização sem medição é aposta, e às vezes você otimiza a parte que não era o gargalo.
| ferramenta | onde fica | o que mostra |
|---|---|---|
| Stats | System Diagnostics > Stats | transações mais lentas, Business Rules mais executados, tempo médio de resposta por tabela |
| Slow Query Log | System Diagnostics > Slow Queries | SQL que passou do threshold configurável (confira o valor vigente na sua instância), com o SQL gerado |
| Transaction Log | System Logs > Transactions | tempo total, memória e número de queries de uma transação específica |
| Instance Scan | System Diagnostics > Instance Scan | varredura automática de Business Rules e configurações problemáticas fora do padrão |
Vale lembrar também o que ficou pendente do artigo sobre modelo de dados: índice de banco acelera leitura e penaliza escrita. Crie índice em campo usado em filtro de lista com frequência alta e tabela que já cresceu (não existe número mágico oficial — na prática, "grande o bastante para o full scan doer" costuma ser a régua); não crie em tabela pequena ou em campo usado só em relatório batch ocasional — o ganho não paga o custo em toda escrita subsequente.
#e agora?
- Na sua instância, abra System Diagnostics > Stats uma vez essa semana, mesmo sem estar investigando nada — é a forma mais barata de pegar uma tabela crescendo rápido antes que ela vire incidente.
- Procure
getRowCount()nos Script Includes e Business Rules do seu escopo. É a troca de menor esforço com maior retorno: geralmente é uma substituição direta porGlideAggregate. - Combine este artigo com Business Rules: timing e arquitetura — lá vimos
changesTo()e saída rápida como regras de performance no nível do Business Rule; aqui vimos o nível da query. Os dois juntos cobrem a maior parte dos incidentes de lentidão que chegam pro time de desenvolvimento.