Segundo plano
Las operaciones de canalización son una nueva interfaz de consultas para Cloud Firestore. Esta interfaz proporciona funciones de consulta avanzadas que incluyen expresiones complejas. La edición Enterprise de Firestore admite uniones de estilo relacional a través de subconsultas correlacionadas. A diferencia de muchas bases de datos NoSQL que a menudo requieren desnormalizar los datos o realizar varias solicitudes del lado del cliente, las subconsultas te permiten combinar y agregar datos de colecciones o subcolecciones relacionadas directamente en el servidor.
Las subconsultas son expresiones que ejecutan una canalización anidada para cada documento que procesa la consulta externa. Esto permite patrones complejos de recuperación de datos, como recuperar un documento junto con los elementos de su subcolección relacionada o unir datos vinculados lógicamente en colecciones raíz dispares.
Conceptos
En esta sección, se presentan los conceptos básicos para usar subconsultas y realizar uniones en las operaciones de Pipeline.
Subconsultas como expresiones
Una subconsulta no es una etapa de nivel superior, sino una expresión que se puede usar en cualquier etapa que acepte expresiones, como select(...), add_fields(...), where(...) o sort(...).
Cloud Firestore admite tres tipos de subconsultas:
- Subconsultas de array: Materializan todo el conjunto de resultados de la subconsulta como un array de documentos.
- Subconsultas escalares: Se evalúan en un solo valor, como un recuento, un promedio o un campo específico de un documento relacionado.
subcollection(...)Subconsultas: Uniones simplificadas para una relación principal-secundaria de uno a varios.
Alcance y variables
Cuando se escribe una unión, la subconsulta anidada a menudo necesita hacer referencia a campos del documento "externo" (el principal). Para conectar estos alcances, usa la etapa let(...) (a la que se hace referencia como define(...) en algunos SDKs) para definir variables en el alcance principal a las que se puede hacer referencia en la subconsulta con la función variable(...).
Sintaxis
En las siguientes secciones, se proporciona una descripción general de la sintaxis para realizar uniones.
La etapa let(...)
La etapa let(...) (a la que se hace referencia como define(...) en algunos SDKs) es una etapa sin filtrado que incorpora de forma explícita datos del alcance principal a una variable con nombre para su uso en alcances anidados posteriores.
Web
async function defineStageData() {
await setDoc(doc(collection(db, "Authors"), "author_123"), {
"id": "author_123",
"name": "Jane Austen"
});
}
Swift
func defineStageData() async throws { try await db.collection("authors").document("author_123").setData([ "id": "author_123", "name": "Jane Austen" ]) }
Kotlin
fun defineStageData() { val author = hashMapOf( "id" to "author_123", "name" to "Jane Austen", ) db.collection("Authors").document("author_123").set(author) }
Java
public void defineStageData() { Map<String, Object> author = new HashMap<>(); author.put("id", "author_123"); author.put("name", "Jane Austen"); db.collection("Authors").document("author_123").set(author); }
Subconsultas de arreglos
Una subconsulta de Array es un caso especial de subconsulta de expresión que materializa todo el conjunto de resultados de la subconsulta en un array. Si la subconsulta muestra cero filas, se evalúa como un array vacío. Nunca devuelve un array null. Estas consultas son útiles cuando se requieren los resultados completos en el resultado final, como cuando se materializa una colección anidada o correlacionada.
Las consultas pueden filtrar, ordenar y agregar datos en la subconsulta para reducir también la cantidad de datos que se deben recuperar y devolver, lo que ayuda a reducir el costo de la consulta. Se respeta el orden de la subconsulta, lo que significa que una etapa sort(...) en la subconsulta controla el orden de los resultados en el array final.
Usa el wrapper del SDK de toArrayExpression() para convertir una consulta en un array.
Web
async function toArrayExpressionStageData() {
await setDoc(doc(collection(db, "Projects"), "project_1"), {
"id": "project_1",
"name": "Alpha Build"
});
await addDoc(collection(db, "Tasks"), {
"project_id": "project_1",
"title": "System Architecture"
});
await addDoc(collection(db, "Tasks"), {
"project_id": "project_1",
"title": "Database Schema Design"
});
}
Respuesta
{
id: "project_1",
name: "Alpha Build",
taskTitles: [
"System Architecture", "Database Schema Design"
]
}
Swift
async function toArrayExpressionStageData() { await setDoc(doc(collection(db, "Projects"), "project_1"), { "id": "project_1", "name": "Alpha Build" }); await addDoc(collection(db, "Tasks"), { "project_id": "project_1", "title": "System Architecture" }); await addDoc(collection(db, "Tasks"), { "project_id": "project_1", "title": "Database Schema Design" }); }
Respuesta
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Kotlin
fun toArrayExpressionData() { val project = hashMapOf( "id" to "project_1", "name" to "Alpha Build", ) db.collection("Projects").document("project_1").set(project) val task1 = hashMapOf( "project_id" to "project_1", "title" to "System Architecture", ) db.collection("Tasks").add(task1) val task2 = hashMapOf( "project_id" to "project_1", "title" to "Database Schema Design", ) db.collection("Tasks").add(task2) }
Respuesta
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Java
public void toArrayExpressionData() { Map<String, Object> project = new HashMap<>(); project.put("id", "project_1"); project.put("name", "Alpha Build"); db.collection("Projects").document("project_1").set(project); Map<String, Object> task1 = new HashMap<>(); task1.put("project_id", "project_1"); task1.put("title", "System Architecture"); db.collection("Tasks").add(task1); Map<String, Object> task2 = new HashMap<>(); task2.put("project_id", "project_1"); task2.put("title", "Database Schema Design"); db.collection("Tasks").add(task2); }
Respuesta
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Subconsultas escalares
Las subconsultas escalares suelen usarse en una etapa select(...) o where(...) como filtro de permiso o para generar el resultado de una subconsulta sin materializar la consulta completa directamente.
Una subconsulta escalar que produce cero resultados se evaluará como null, mientras que una subconsulta que se evalúa como varios elementos generará un error de tiempo de ejecución.
Cuando una subconsulta escalar produce solo un campo por resultado, el campo se eleva para ser el resultado de nivel superior de la subconsulta. Esto se observa con mayor frecuencia cuando la subconsulta finaliza con un select(field("user_name")) o aggregate(countAll().as("total")) en el que el esquema de la subconsulta es solo un campo. De lo contrario, cuando una subconsulta puede producir varios campos, se envuelven en un mapa.
Usa el wrapper del SDK de toScalarExpression() para convertir una consulta en una expresión escalar.
Web
async function toScalarExpressionStageData() {
await setDoc(doc(collection(db, "Authors"), "author_202"), {
"id": "author_202",
"name": "Charles Dickens"
});
await addDoc(collection(db, "Books"), {
"author_id": "author_202",
"title": "Great Expectations",
"rating": 4.8
});
await addDoc(collection(db, "Books"), {
"author_id": "author_202",
"title": "Oliver Twist",
"rating": 4.5
});
}
Respuesta
{
"id": "author_202",
"name": "Charles Dickens",
"averageBookRating": 4.65
}
Swift
try await db.collection("authors").document("author_202").setData([ "id": "author_202", "name": "Charles Dickens" ]) try await db.collection("books").document().setData([ "author_id": "author_202", "title": "Great Expectations", "rating": 4.8 ]) try await db.collection("books").document().setData([ "author_id": "author_202", "title": "Oliver Twist", "rating": 4.5 ])
Respuesta
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
Kotlin
fun toScalarExpressionData() { val author = hashMapOf( "id" to "author_202", "name" to "Charles Dickens", ) db.collection("Authors").document("author_202").set(author) val book1 = hashMapOf( "author_id" to "author_202", "title" to "Great Expectations", "rating" to 4.8, ) db.collection("Books").add(book1) val book2 = hashMapOf( "author_id" to "author_202", "title" to "Oliver Twist", "rating" to 4.5, ) db.collection("Books").add(book2) }
Respuesta
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
Java
public void toScalarExpressionData() { Map<String, Object> author = new HashMap<>(); author.put("id", "author_202"); author.put("name", "Charles Dickens"); db.collection("Authors").document("author_202").set(author); Map<String, Object> book1 = new HashMap<>(); book1.put("author_id", "author_202"); book1.put("title", "Great Expectations"); book1.put("rating", 4.8); db.collection("Books").add(book1); Map<String, Object> book2 = new HashMap<>(); book2.put("author_id", "author_202"); book2.put("title", "Oliver Twist"); book2.put("rating", 4.5); db.collection("Books").add(book2); }
Respuesta
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
subcollection(...) Subconsultas
Si bien se ofrece como una etapa, la etapa de entrada subcollection(...) permite realizar uniones en el modelo de datos jerárquico de Cloud Firestore. En un modelo jerárquico, las consultas a menudo necesitan recuperar un documento junto con los datos de sus propias subcolecciones. Si bien puedes lograr esto con una etapa de entrada collection_group(...) seguida de un filtro en la referencia principal, subcollection(...) proporciona una sintaxis mucho más concisa.
Además de la condición de unión implícita, esta función actúa de manera similar a una subconsulta de array, ya que devuelve un resultado vacío si no se encuentran documentos coincidentes, incluso si no existe la colección anidada.
Es fundamentalmente azúcar sintáctico: usa automáticamente el __name__ del documento en el alcance externo como la clave de unión para resolver la relación jerárquica. Esto la convierte en la forma preferida de realizar búsquedas en colecciones vinculadas en una relación principal-secundaria.
Prácticas recomendadas
- Administra la memoria con
toArrayExpression(): Ten cuidado con las subconsultastoArrayExpression(), ya que materializar una gran cantidad de documentos puede agotar el límite de memoria de la consulta (128 MiB). Para mitigar este problema, usaselect(...)dentro de la subconsulta para devolver solo los campos necesarios y aplica filtroswhere(...)para limitar la cantidad de documentos que se devuelven. Considera usarlimit(...)si es apropiado para limitar la cantidad de documentos que devuelve la subconsulta. - Indexación: Asegúrate de que los campos que se usan en la cláusula
where(...)de una subconsulta estén indexados. Las uniones eficientes se basan en la capacidad de realizar búsquedas de índices en lugar de análisis completos de la tabla.
Para conocer más prácticas recomendadas sobre las consultas, consulta nuestra guía sobre la optimización de consultas.
Limitaciones
- Alcance de
subcollection(...): La etapa de entradasubcollection(...)solo se admite dentro de las subconsultas, ya que requiere el contexto de un documento principal para resolver la relación jerárquica y realizar la unión. - Profundidad de anidamiento: Las subconsultas se pueden anidar hasta 20 niveles de profundidad.
- Uso de memoria: El límite de 128 MiB en los datos materializados se aplica a toda la consulta, incluidos todos los documentos unidos.