Apex Academy
Módulo 3·04·34 min

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 Admin esLa related list filtrada de la página del registro, y el filtro cruzado de un informe

✓ En esta sub-lección

  1. 01Encontrar el nombre de la relación hija en Object Manager y escribirlo bien en el FROM de la subconsulta.
  2. 02Filtrar, ordenar y limitar DENTRO de la subconsulta, sabiendo que cada padre recibe su propio resultado.
  3. 03Combinar en una misma consulta traer los hijos y filtrar los padres por sus hijos.
  4. 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

Está en Object Manager → el objeto HIJO → Fields & Relationships → el campo que apunta al padre → «Child Relationship Name». Es el nombre que elegiste en el asistente al crear la relación, junto a la etiqueta de la related list. Con __r detrás, es lo que va en el FROM.
Objeto hijoCampo que apunta al padreEn el FROM de la subconsulta
ContactAccountIdContacts
OpportunityAccountIdOpportunities
CaseAccountIdCases
OpportunityLineItemOpportunityIdOpportunityLineItems
Renewal_Line__c (personalizado)Contract__cRenewal_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».

Apex
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 UNA
El WHERE de dentro elige qué hijos vuelven. El WHERE de fuera elige qué padres. Son dos filtros distintos.

Recorrerla: 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.

Apex
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.

Apex
// 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

Esto me costó entenderlo: en un informe con Report Type «Cuentas con Oportunidades» ves las oportunidades Y solo salen las cuentas que tienen alguna. En SOQL son dos piezas separadas: la subconsulta del SELECT trae los hijos pero no quita padres; el IN del WHERE quita padres pero no trae hijos. Para reproducir ese informe hacen falta las dos a la vez.
Apex
// 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)
];
La condición «IsClosed = false» aparece dos veces, y no sobra ninguna: una elige qué cuentas entran, la otra qué casos viajan con cada cuenta.

Los límites que hay que saberse

ReglaLímiteQué significa en la práctica
Hacia abajo (subconsulta)1 nivelNo se anida una subconsulta dentro de otra: cuentas → oportunidades sí; cuentas → oportunidades → productos, no.
Hacia arriba (punto)5 nivelesContact.Account.Owner.Manager.Name es válido.
IN / NOT IN con subconsulta2 por consulta«Con casos abiertos Y sin oportunidades» cabe; una tercera condición así, no.
Filas devueltas50.000 por transacciónLos 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.

CUENTAS EN LA ORG
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.

Los mismos datos, dos maneras de pedirlos. Las filas son idénticas; lo que cambia es cuántas veces preguntas. Esto es el Módulo 4 asomando la cabeza.

La nutria: ¿Y por qué no un Flow? Porque «las que nunca compraron» no se puede preguntar

«Cuentas con negocio abierto» sale en Flow con dos Get Records y un Loop que copia los AccountId para el operador In. Pero «las que nunca han tenido una oportunidad» obliga a traer al flow todas las oportunidades de la historia solo para saber qué cuentas tienen alguna, y en una org con años de datos ese Get Records revienta en «Too many query rows: 50001». Con NOT IN (SELECT AccountId FROM Opportunity), el cruce lo hace la base de datos y a tu código solo llegan las cuentas.