Subconsultas a fondo
La sub-lección anterior presentó las subconsultas. Esta las lleva al límite: su propio filtro y orden, cómo recorrerlas, cómo combinarlas con IN y NOT IN, y las reglas exactas que la plataforma no te deja saltar.
✓ En esta sub-lección
- 01Encontrar el nombre de la relación hija en Object Manager y escribirlo bien en el FROM de la subconsulta.
- 02Filtrar, ordenar y limitar DENTRO de la subconsulta, sabiendo que cada padre recibe su propio resultado.
- 03Combinar en una misma consulta traer los hijos y filtrar los padres por sus hijos.
- 04Conocer los límites reales: un nivel hacia abajo, cinco hacia arriba, dos filtros por hijos, y cómo cuenta todo para los governor limits.
La nutria: ¿Te acuerdas? · Repaso de la lección 3
¿Cómo pides el nombre del propietario de cada cuenta en la misma consulta?
Abre la related list de Oportunidades de una cuenta, ordénala por importe y quédate con las primeras: eso, para todas las cuentas a la vez, es una con su propio ORDER BY y LIMIT. Y el filtro cruzado de un informe («Cuentas con Oportunidades») es la otra mitad de la sub-lección.
El nombre de la relación: dónde se mira
El FROM de una subconsulta no lleva el nombre del objeto hijo, sino el de la relación hija. Para los objetos estándar es el plural en inglés (Contacts, Opportunities, Cases). Para los personalizados lo decidió quien creó el campo de búsqueda, y lleva __r al final. No hay que adivinarlo: está escrito en Setup.
La nutria: Lo configuraste tú al crear el campo
| Objeto hijo | Campo que apunta al padre | En el FROM de la subconsulta |
|---|---|---|
| Contact | AccountId | Contacts |
| Opportunity | AccountId | Opportunities |
| Case | AccountId | Cases |
| OpportunityLineItem | OpportunityId | OpportunityLineItems |
| Renewal_Line__c (personalizado) | Contract__c | Renewal_Lines__r (el que pusieras al crearlo) |
Su propio WHERE, ORDER BY y LIMIT
Dentro de los paréntesis de una subconsulta caben las mismas cláusulas que ya conoces. Y aquí está la idea que más cuesta al principio: se aplican a los hijos DE CADA PADRE por separado. LIMIT 3 dentro de la subconsulta no significa «tres oportunidades en total», significa «como mucho tres oportunidades por cuenta».
List<Account> accounts = [
SELECT Name,
(SELECT Name, Amount
FROM Opportunities
WHERE IsClosed = false
ORDER BY Amount DESC
LIMIT 3)
FROM Account
WHERE Industry = 'Technology'
];
// 10 cuentas → hasta 30 oportunidades: las 3 mayores DE CADA UNARecorrerla: un bucle dentro de otro
Cada cuenta trae sus hijos en una propiedad con el nombre de la relación (a.Opportunities, a.Contacts): una List normal que recorres con el for de siempre. Si no hay hijos llega vacía, no null, y el bucle no da ninguna vuelta.
for (Account a : accounts) {
System.debug(a.Name + ' tiene ' + a.Opportunities.size() + ' abiertas');
for (Opportunity o : a.Opportunities) {
System.debug(' ' + o.Name + ' · ' + o.Amount);
}
}Filtrar los padres por sus hijos: IN y NOT IN
Cuando lo que quieres es decidir QUÉ padres entran según sus hijos, la subconsulta va en el WHERE, detrás de IN o NOT IN, y devuelve una lista de Ids. Es exactamente el filtro cruzado de los informes: «Cuentas CON casos abiertos» es IN; «Cuentas SIN oportunidades» es NOT IN.
// Filtro cruzado «CON»: cuentas que tienen algún caso abierto
List<Account> withOpenCases = [
SELECT Name FROM Account
WHERE Id IN (SELECT AccountId FROM Case WHERE IsClosed = false)
];
// Filtro cruzado «SIN»: cuentas que nunca han tenido una oportunidad
List<Account> neverSold = [
SELECT Name FROM Account
WHERE Id NOT IN (SELECT AccountId FROM Opportunity)
];- A la izquierda del IN va el Id o un campo que apunta a otro registro (AccountId, OwnerId), y sin puntos: Account.Id no vale.
- La subconsulta selecciona UN solo campo, que también apunta a un registro, y tampoco puede llevar puntos.
- Los dos lados tienen que hablar del mismo objeto: Id de Account a la izquierda, AccountId a la derecha.
La nutria: Traer hijos y filtrar por hijos no se sustituyen
// Solo cuentas con casos abiertos, y cada una con SUS casos abiertos
List<Account> support = [
SELECT Name,
(SELECT CaseNumber, Subject, Priority
FROM Cases
WHERE IsClosed = false
ORDER BY Priority)
FROM Account
WHERE Id IN (SELECT AccountId FROM Case WHERE IsClosed = false)
];Los límites que hay que saberse
| Regla | Límite | Qué significa en la práctica |
|---|---|---|
| Hacia abajo (subconsulta) | 1 nivel | No se anida una subconsulta dentro de otra: cuentas → oportunidades sí; cuentas → oportunidades → productos, no. |
| Hacia arriba (punto) | 5 niveles | Contact.Account.Owner.Manager.Name es válido. |
| IN / NOT IN con subconsulta | 2 por consulta | «Con casos abiertos Y sin oportunidades» cabe; una tercera condición así, no. |
| Filas devueltas | 50.000 por transacción | Los hijos cuentan: 200 cuentas con 10 contactos cada una son 2.200 filas, no 200. |
Y el límite que más importa, el de consultas: una consulta con subconsultas es UNA sentencia SOQL de las 100 que permite una transacción, traiga los hijos que traiga. Por eso la subconsulta no es solo cómoda: es la forma de no preguntar a la base de datos una vez por cada registro. Cambia de pestaña en el diagrama y sube a 200 cuentas.
List<Account> accs = [SELECT Name,
(SELECT LastName FROM Contacts)
FROM Account];
for (Account a : accs) {
for (Contact c : a.Contacts) { … }
}CONSULTAS SOQL1 / 100
FILAS RECUPERADAS250 / 50.000
✓ Dentro de los límites. Una sola sentencia SOQL trae las 50 cuentas con sus contactos colgando. Da igual que sean 5 o 200: sigue siendo 1 de 100. (Letra pequeña: la relación hija se apunta en un cupo aparte, tres veces mayor, así que en la práctica no es lo que te para). Las filas sí cuentan todas —cuentas y contactos— para el tope de 50.000.
La nutria: ¿Y por qué no un Flow? Porque «las que nunca compraron» no se puede preguntar