Las consultas SOQL lentas son uno de los cuellos de botella de rendimiento más comunes en Salesforce. Ya sea que estés alcanzando los governor limits, provocando timeouts, o simplemente frustrado por páginas lentas, esta guía te da las herramientas para diagnosticar y corregir el rendimiento de tus consultas.
Entendiendo cómo Salesforce ejecuta las consultas
Antes de optimizar, entiende el modelo de ejecución. Salesforce funciona sobre una base de datos relacional (históricamente Oracle) compartida entre orgs, con una arquitectura multi-tenant. Conceptos clave:
- Selectividad: una consulta es "selectiva" si su filtro coincide con un pequeño porcentaje del total de registros. Salesforce requiere selectividad para usar un índice.
- Tipos de índice: los campos estándar (Id, Name, SystemModstamp, etc.) están auto-indexados. Los campos personalizados pueden indexarse bajo solicitud.
- Full table scan: si no existe un filtro selectivo, Salesforce escanea todos los registros — lento y bloqueado a partir de ciertos umbrales.
1. Usa la herramienta Query Plan
La herramienta Query Plan en Developer Console es tu primera arma:
- Abre Developer Console
- Ve a Help → Preferences → Enable Query Plan
- Ejecuta tu consulta → haz clic en la pestaña Query Plan
Interpretando los resultados:
| Coste (columna Cost) | Qué significa |
|---|---|
| < 1 | Filtro selectivo — el optimizador puede usar el índice |
| ≥ 1 | No selectivo — probable recorrido completo de la tabla: ¡optimiza! |
Umbrales de selectividad oficiales (proporción de registros a los que apunta el filtro):
- índice estándar: 30 % del primer millón de registros, luego 15 % por encima, con un tope de 1 millón;
- índice personalizado: 10 % del primer millón, luego 5 % por encima, con un tope de 333.333.
Ejemplo de un plan malo:
Query: SELECT Id FROM Account WHERE Industry = 'Technology'
Plan: TableScan — Cost: 1.35 (≥ 1: no selectivo)
Después de añadir un filtro selectivo:
Query: SELECT Id FROM Account WHERE Industry = 'Technology' AND OwnerId = :currentUserId
Plan: Index on OwnerId — Cost: 0.04 (< 1: selectivo)
2. Filtra siempre sobre campos indexados
Campos estándar indexados (siempre indexados):
IdNameOwnerIdRecordTypeIdCreatedDate/SystemModstamp(y noLastModifiedDate, que no está indexado)- Campos de relación Lookup/Master-Detail
Cómo comprobar si un campo está indexado: Setup → Object Manager → [Objeto] → Fields & Relationships → clic en el campo → "Unique" o "External ID" = indexado.
Buena práctica: incluye siempre al menos un campo indexado en tu cláusula WHERE.
// ❌ No selectiva — full table scan
List<Account> accounts = [
SELECT Id, Name FROM Account WHERE Industry = 'Technology'
];
// ✅ Selectiva — usa el índice de OwnerId
List<Account> accounts = [
SELECT Id, Name FROM Account
WHERE Industry = 'Technology' AND OwnerId = :UserInfo.getUserId()
];3. Evita SOQL dentro de bucles
Esto es una violación de governor limit esperando a ocurrir — y además es lento.
// ❌ SOQL dentro de un bucle — 1 consulta por registro = mal
for (Account acc : accounts) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
// procesa los contactos...
}
// ✅ Consulta masiva — 1 consulta para todos los registros
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) accountIds.add(acc.Id);
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
if (!contactsByAccount.containsKey(c.AccountId)) {
contactsByAccount.put(c.AccountId, new List<Contact>());
}
contactsByAccount.get(c.AccountId).add(c);
}4. Usa consultas de relación para reducir round-trips
En lugar de consultar el padre y los hijos por separado, usa subconsultas:
// ❌ Dos consultas separadas
List<Account> accounts = [SELECT Id, Name FROM Account WHERE OwnerId = :userId];
Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
List<Contact> contacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds];
// ✅ Una consulta con sub-select (de hijo a padre)
List<Account> accounts = [
SELECT Id, Name,
(SELECT Id, FirstName, LastName, Email FROM Contacts WHERE IsActive__c = true)
FROM Account
WHERE OwnerId = :userId
];Límites a tener en cuenta:
- Hasta 5 niveles de relaciones padre-hijo (el padre y luego 4 niveles de hijos): desde la API 58.0 en REST y SOAP, desde la API 61.0 (Summer '24) en Apex; antes, un solo nivel de subconsulta
- Con más de 200 registros hijos por padre, recorre la subconsulta en un bucle
forSOQL: asignarla directamente a una lista lanza «Aggregate query has too many rows for direct assignment» - Máximo 20 subconsultas padre-hijo por consulta
5. Limita tus resultados con LIMIT
Nunca recuperes más registros de los que necesitas:
// ❌ Trae todo
List<Account> all = [SELECT Id, Name FROM Account];
// ✅ Trae solo lo que necesitas
List<Account> recent = [
SELECT Id, Name, CreatedDate
FROM Account
WHERE CreatedDate = LAST_N_DAYS:30
ORDER BY CreatedDate DESC
LIMIT 50
];Usa LIMIT 1 cuando esperes un único resultado:
Account acc = [SELECT Id FROM Account WHERE Name = 'Acme Corp' LIMIT 1];6. Usa FOR UPDATE con cuidado
// Bloquea los registros durante la duración de la transacción
List<Account> accounts = [
SELECT Id, AnnualRevenue FROM Account
WHERE Id IN :accountIds
FOR UPDATE
];Usa FOR UPDATE solo cuando necesites bloqueo a nivel de fila para evitar actualizaciones concurrentes. Evítalo en flujos de solo lectura intensivos — bloquea otras transacciones.
7. Literales de fecha vs. fechas dinámicas
En rendimiento, un literal de fecha y una variable de fecha enlazada son equivalentes: ambos siguen siendo selectivos sobre un campo indexado. Los literales son más legibles y tienen en cuenta la zona horaria del usuario y el año fiscal de la org:
// ✅ Usa literales de fecha
WHERE CreatedDate = LAST_N_DAYS:30
WHERE CloseDate = THIS_QUARTER
WHERE SystemModstamp > LAST_MONTH // indexado, a diferencia de LastModifiedDate
// ✅ También válido — útil cuando el límite se calcula
WHERE CreatedDate > :Date.today().addDays(-30)Literales de fecha comunes:
TODAY,YESTERDAY,TOMORROWTHIS_WEEK,LAST_WEEK,NEXT_WEEKTHIS_MONTH,LAST_MONTH,THIS_QUARTERLAST_N_DAYS:n,NEXT_N_DAYS:nTHIS_YEAR,LAST_YEAR
8. Consultas agregadas y GROUP BY
Las consultas SOQL agregadas pueden reducir drásticamente los datos transferidos a Apex. Ojo: cada fila agregada sigue contando en el límite de 50.000 filas de consulta, salvo COUNT() sin GROUP BY, que cuenta como una sola fila.
// ❌ Recuperar todas las oportunidades para contar en Apex
List<Opportunity> opps = [SELECT Id, StageName FROM Opportunity WHERE AccountId = :accId];
Integer closedCount = 0;
for (Opportunity o : opps) {
if (o.StageName == 'Closed Won') closedCount++;
}
// ✅ Agregar en SOQL
AggregateResult[] results = [
SELECT StageName, COUNT(Id) cnt
FROM Opportunity
WHERE AccountId = :accId
GROUP BY StageName
];
for (AggregateResult r : results) {
System.debug(r.get('StageName') + ': ' + r.get('cnt'));
}9. Evita != y NOT IN en grandes volúmenes de datos
Las condiciones negativas (!=, NOT IN, NOT LIKE) impiden el uso de índices y fuerzan escaneos completos.
// ❌ No puede usar índice
WHERE StageName != 'Closed Won'
// ✅ Replantéalo como condiciones positivas
WHERE StageName IN ('Prospecting', 'Qualification', 'Proposal')10. Solicita índices personalizados para filtros de alto volumen
Si filtras frecuentemente sobre un campo personalizado con millones de registros, solicita un índice personalizado a través de Salesforce Support. No hace falta para los campos marcados como External ID o Unique: se indexan automáticamente.
Candidatos para indexación personalizada:
- Campos filtrados en batch jobs que afectan a más de 100 mil registros
- Campos usados en cláusulas WHERE de SOSL/SOQL de LWC en objetos de alto volumen
Checklist de rendimiento
Antes de desplegar una consulta a producción:
- Ejecutaste Query Plan — coste < 1 en el plan elegido
- Al menos un campo indexado en la cláusula WHERE
- Ningún SOQL dentro de bucles
- LIMIT aplicado donde corresponda
- Subconsultas usadas en lugar de múltiples round-trips
- Consultas agregadas usadas para evitar traer registros innecesarios
- Literales de fecha usados para filtros de fecha
- Condiciones negativas (
!=,NOT IN) reemplazadas cuando sea posible
Conclusión
El rendimiento de SOQL se reduce a un principio: dale al optimizador de consultas algo con qué trabajar. Filtra siempre sobre campos indexados, nunca consultes dentro de bucles y usa la herramienta Query Plan para validar antes de desplegar. En orgs de alto volumen, estos hábitos marcan la diferencia entre un sistema que escala y uno que se derrumba bajo carga.
Recursos útiles: