Visão geral
A edição Enterprise do Firestore oferece suporte a mesclagens de estilo relacional usando subconsultas correlacionadas. Ao contrário de muitos bancos de dados NoSQL que geralmente exigem a desnormalização de dados ou a execução de várias solicitações do lado do cliente, as subconsultas permitem combinar e agregar dados de coleções ou subcoleções relacionadas diretamente no servidor.
As subconsultas são expressões que executam um pipeline aninhado para cada documento processado pela consulta externa. Isso permite padrões complexos de recuperação de dados, como buscar um documento com os itens de subcoleção relacionados ou unir dados logicamente vinculados em coleções raiz diferentes.
Conceitos
Nesta seção, apresentamos os conceitos básicos do uso de subconsultas para realizar mesclagens em operações de pipeline.
Subconsultas como expressões
Uma subconsulta não é um estágio de nível superior. Em vez disso, é uma expressão que
pode ser usada em qualquer estágio que aceite expressões, como
select(...),
add_fields(...),
where(...) ou sort(...).
O Cloud Firestore aceita três tipos de subconsultas:
- Subconsultas de matriz:materializam todo o conjunto de resultados da subconsulta como uma matriz de documentos.
- Subconsultas escalares:são avaliadas como um único valor, como uma contagem, uma média ou um campo específico de um documento relacionado.
subcollection(...)Subconsultas:mesclagens simplificadas para uma relação pai-filho de um para muitos.
Escopo e variáveis
Ao escrever uma mesclagem, a subconsulta aninhada geralmente precisa referenciar campos
do documento "externo" (o pai). Para conectar esses escopos, use o estágio
let(...) (chamado de define(...) em alguns
SDKs) para definir variáveis no escopo pai que podem ser referenciadas na
subconsulta usando a função variable(...).
Sintaxe
As seções a seguir oferecem uma visão geral da sintaxe para realizar mesclagens.
A fase let(...)
O estágio let(...) (chamado de define(...) em alguns
SDKs) é um estágio sem filtragem que traz explicitamente dados do escopo pai
para uma variável nomeada para uso em escopos aninhados subsequentes.
Subconsultas de matriz
Uma subconsulta de matriz é um caso especial de subconsulta de expressão que materializa todo o conjunto de resultados da subconsulta em uma matriz. Se a subconsulta não retornar nenhuma linha, ela será avaliada como uma matriz vazia. Ela nunca retorna uma matriz null. Essas
consultas são úteis quando os resultados completos são necessários no resultado final, como
ao materializar uma coleção aninhada ou correlacionada.
As consultas podem filtrar, classificar e agregar na subconsulta para reduzir a quantidade de dados que precisam ser buscados e retornados, o que ajuda a reduzir o custo da consulta. A ordem da subconsulta é respeitada, o que significa que um estágio sort(...)
na subconsulta controla a ordem dos resultados na matriz final.
Use o wrapper do SDK toArrayExpression() para converter uma consulta em uma matriz.
Subconsultas escalares
As subconsultas escalares são frequentemente usadas em uma fase select(...) ou
where(...), porque permitem filtrar ou resultar o
resultado de uma subconsulta sem materializar a consulta completa diretamente.
Uma subconsulta escalar que produz zero resultados será avaliada como null,
enquanto uma subconsulta que é avaliada como vários elementos resultará em um erro de tempo de
execução.
Quando uma subconsulta escalar produz apenas um campo por resultado, o campo é
elevado para ser o resultado de nível superior da subconsulta. Isso é mais comum
quando a subconsulta termina com um select(field("user_name")) ou
aggregate(countAll().as("total")) em que o esquema da subconsulta é apenas um
único campo. Caso contrário, quando uma subconsulta pode produzir vários campos, eles são
encapsulados em um mapa.
Use o wrapper do SDK toScalarExpression() para converter uma consulta em uma expressão escalar.
subcollection(...) Subconsultas
Embora seja oferecida como uma etapa, a etapa de entrada subcollection(...) permite realizar mesclagens no modelo de dados hierárquico do Cloud Firestore. Em um
modelo hierárquico, as consultas geralmente precisam recuperar um documento com dados
das próprias subcoleções. Embora seja possível fazer isso usando uma
etapa de entrada collection_group(...) seguida
por um filtro na referência pai, o subcollection(...) oferece uma sintaxe muito mais
concisa.
Além da condição de mesclagem implícita, isso funciona de maneira semelhante a uma subconsulta de matriz, retornando um resultado vazio se nenhum documento for correspondente, mesmo que a coleção aninhada não exista.
É basicamente um açúcar sintático: ele usa automaticamente o __name__ do documento no escopo externo como a chave de mesclagem para resolver a relação hierárquica. Isso faz com que seja a maneira preferida de realizar pesquisas em coleções vinculadas em um relacionamento pai-filho.
Exemplos
Dados de exemplo
O código a seguir carrega um conjunto de dados de teste para usar em todos os exemplos seguintes.
Node.js
// Load set of cities.
const cities = db.collection("cities");
await cities.doc("SF").set({
name: "San Francisco",
state: "CA",
country: "USA",
});
await cities.doc("LA").set({
name: "Los Angeles",
state: "CA",
country: "USA"
});
await cities.doc("DC").set({
name: "Washington, D.C.",
state: null,
country: "USA"
});
await cities.doc("TOK").set({
name: "Tokyo",
state: null,
country: "Japan"
});
// Load restaurants in various cities.
const sfRestaurants = db.collection("cities/SF/restaurants");
const laRestaurants = db.collection("cities/LA/restaurants");
const dcRestaurants = db.collection("cities/DC/restaurants");
const rest1 = await sfRestaurants.add({
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi"
});
const rest2 = await sfRestaurants.add({
name: "Bay Area Burger",
type: "burger",
owner_id: "Sarah Jenkins"
});
const rest3 = await sfRestaurants.add({
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
});
const rest4 = await laRestaurants.add({
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
});
const rest5 = await laRestaurants.add({
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano"
});
const rest6 = await dcRestaurants.add({
name: "Capitol Tacos",
type: "mexican",
owner_id: "Maria Garcia"
});
const rest7 = await dcRestaurants.add({
name: "Georgetown Coffee",
type: "cafe",
owner_id: "David Kim"
});
// Load collection of reviews.
const reviews = db.collection("reviews");
await reviews.add({ restaurant: rest1, rating: 5, reviewer_id: "Alice" });
await reviews.add({ restaurant: rest1, rating: 4, reviewer_id: "Bob" });
await reviews.add({ restaurant: rest2, rating: 4, reviewer_id: "Charlie" });
await reviews.add({ restaurant: rest3, rating: 5, reviewer_id: "Diana" });
await reviews.add({ restaurant: rest3, rating: 4, reviewer_id: "Edward" });
await reviews.add({ restaurant: rest3, rating: 4, reviewer_id: "Fiona" });
// rest4 has 0 reviews
await reviews.add({ restaurant: rest5, rating: 3, reviewer_id: "George" });
await reviews.add({ restaurant: rest6, rating: 5, reviewer_id: "Hannah" });
await reviews.add({ restaurant: rest6, rating: 4, reviewer_id: "Ian" });
await reviews.add({ restaurant: rest7, rating: 5, reviewer_id: "Julia" });
Pesquisar um documento em outra coleção
A consulta a seguir no grupo de coleções reviews realiza uma pesquisa no grupo de coleções restaurant usando uma referência de chave primária.
Node.js
let results = await db.pipeline()
.collectionGroup("reviews")
.define(field("restaurant").as("restaurant_name"))
.addFields(db.pipeline()
.collectionGroup("restaurant")
.where(field("__name__").equal(variable("restaurant_name")))
.select("name", "type")
.toScalarExpression()
.as("restaurant"))
.execute();
Resposta
{
rating: 5,
reviewer_id "Alice",
restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
rating: 4,
reviewer_id "Bob",
restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
rating: 4,
reviewer_id "Charlie",
restaurant: { name: "Bay Area Burger", type: "burger" }
},
{
rating: 5,
reviewer_id "Diana",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 4,
reviewer_id "Edward",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 4,
reviewer_id "Fiona",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 3,
reviewer_id "George",
restaurant: { name: "Venice Pizza", type: "pizza" }
},
{
rating: 5,
reviewer_id "Hannah",
restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
rating: 4,
reviewer_id "Ian",
restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
rating: 5,
reviewer_id "Julia",
restaurant: { name: "Georgetown Coffee", type: "cafe" }
}
Combinar várias coleções
A consulta a seguir busca todas as pizzarias do grupo de coleções restaurants e usa uma subconsulta de matriz para buscar e incorporar as avaliações associadas diretamente na resposta.
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.select("rating", "reviewer_id")
.toArrayExpression()
.as("reviews"))
.execute();
Resposta
{
name: "Golden Gate Pizza",
reviews: [
{ rating: 5, reviewer_id "Alice" },
{ rating: 4, reviewer_id "Bob" }
]
},
{
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano",
reviews: [
{ rating: 3, reviewer_id "George" }
]
}
Agregação em várias coleções
A consulta a seguir no grupo de coleções restaurants usa uma subconsulta correlacionada para receber a classificação média de cada restaurante do grupo de coleções reviews.
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.aggregate(average("rating").as("avg_rating"))
.toScalarExpression()
.as("avg_rating"))
.execute();
Resposta
{
name: "Golden Gate Pizza",
avg_rating: 4.5
},
{
name: "Venice Pizza",
avg_rating: 3.0
}
Top-N por grupo (subconsulta com limite)
A consulta a seguir busca todos os documentos do grupo de coleções restaurants
e usa uma subconsulta correlacionada para buscar as duas avaliações mais bem avaliadas
de cada restaurante.
Isso garante que a matriz de avaliações não fique muito grande e atinja o limite de memória da consulta.
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.sort(field("rating").descending())
.limit(2)
.select("rating", "reviewer_id")
.toArrayExpression()
.as("top_reviews"))
.execute();
Resposta
{
name: "Golden Gate Pizza",
top_reviews: [
{ rating: 5, reviewer_id "Alice" },
{ rating: 4, reviewer_id "Bob" }
]
},
{
name: "Bay Area Burger",
top_reviews: [
{ rating: 4, reviewer_id "Charlie" }
]
},
{
name: "Sunset Taco",
top_reviews: [
{ rating: 5, reviewer_id "Diana" },
{ rating: 4, reviewer_id "Edward" }
]
},
{
name: "Hollywood Sushi",
top_reviews: []
},
{
name: "Venice Pizza",
top_reviews: [
{ rating: 3, reviewer_id "George" }
]
},
{
name: "Capitol Tacos",
top_reviews: [
{ rating: 5, reviewer_id "Hannah" },
{ rating: 4, reviewer_id "Ian" }
]
},
{
name: "Georgetown Coffee",
top_reviews: [
{ rating: 5, reviewer_id "Julia" }
]
}
Mesclar subcoleções
A consulta a seguir verifica a coleção cities e usa a etapa subcollection(...) para mesclar implicitamente documentos de uma coleção aninhada e encontrar o número de restaurantes por cidade.
Node.js
let results = await db.pipeline()
.collection("cities")
.addFields(subcollection("restaurants")
.toArrayExpression()
.length()
.as("restaurant_count"))
.execute();
Resposta
{
__name__: cities/SF,
name: "San Francisco",
state: "CA",
country: "USA",
restaurant_count: 3
},
{
__name__: cities/LA,
name: "Los Angeles",
state: "CA",
country: "USA",
restaurant_count: 2
},
{
__name__: cities/DC,
name: "Washington, D.C.",
state: null,
country: "USA",
restaurant_count: 2
},
{
__name__: cities/TOK,
name: "Tokyo",
state: null,
country: "Japan",
restaurant_count: 0
}
Expressar várias condições de mesclagem
A consulta a seguir verifica o grupo de coleções restaurants e realiza uma
mesclagem de vários campos com o grupo de coleções reviews para encontrar proprietários que avaliam
os próprios restaurantes.
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.define(field("owner_id"), field("__name__"))
.where(db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("__name__")))
.where(field("author").equal(variable("owner_id")))
.aggregate(count().as("c"))
.toScalarExpression()
.greaterThan(0))
.execute();
Resposta
{
__name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
}
Anti-Join (NOT EXISTS)
A consulta a seguir verifica o grupo de coleções restaurants e encontra todos os restaurantes que ainda não têm avaliações.
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.define(field("__name__").as("restaurant_name"))
.where(db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.aggregate(count().as("review_count"))
.toScalarExpression()
.equal(0))
.execute();
Resposta
{
__name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
}
Subconsulta como mesclagem
A consulta a seguir nivela a relação entre cada pizzaria e as avaliações dela. Ao colocar a subconsulta dentro de um estágio
unnest(...), o servidor duplica o documento externo
do restaurante para cada avaliação correspondente, produzindo documentos simples e unidos
(semelhantes a um INNER JOIN do SQL).
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.unnest(
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.select("rating", "reviewer_id")
.toArrayExpression()
.as("review"))
.execute();
Resposta
{
__name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi"
review: { rating: 5, reviewer_id "Alice" }
},
{
__name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi",
review: { rating: 4, reviewer_id "Bob" }
},
{
__name__: "cities/LA/restaurants/6CYntvNgbYzgaW652Gq1",
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano",
review: { rating: 3, reviewer_id "George" }
}
Subconsulta não correlacionada como filtro
A consulta a seguir na coleção reviews realiza filtros usando uma subconsulta não correlacionada em si mesma para encontrar avaliações maiores que a classificação média.
Node.js
let results = await db.pipeline()
.collection("reviews")
// Average review rating is 4.3
.where(field("rating").greaterThan(db.pipeline()
.collection("reviews")
.aggregate(average("rating").as("avg"))
.toScalarExpression()))
.select("rating", "reviewer_id")
.execute();
Resposta
{
rating: 5,
reviewer_id "Alice"
},
{
rating: 5,
reviewer_id "Diana"
},
{
rating: 5,
reviewer_id "Hannah"
},
{
rating: 5,
reviewer_id "Julia"
}
Práticas recomendadas
- Gerenciar a memória com
toArrayExpression():tenha cuidado com as subconsultastoArrayExpression(), já que a materialização de um grande número de documentos pode esgotar o limite de memória da consulta (128 MiB). Para reduzir esse problema, useselect(...)na subconsulta para retornar apenas os campos necessários e aplique filtroswhere(...)para limitar o número de documentos retornados. Considere usarlimit(...), se apropriado, para limitar o número de documentos retornados pela subconsulta. - Indexação:verifique se os campos usados na cláusula
where(...)de uma subconsulta estão indexados. As mesclagens de alto desempenho dependem da capacidade de realizar buscas de índice em vez de verificações completas de tabela.
Para mais práticas recomendadas de consulta, consulte nosso guia sobre otimização de consultas.
Limitações
- Escopo
subcollection(...):a etapa de entradasubcollection(...)só é compatível com subconsultas, porque requer o contexto de um documento pai para resolver a relação hierárquica e realizar a mesclagem. - Profundidade de aninhamento:as subconsultas podem ser aninhadas em até 20 camadas.
- Uso da memória:o limite de 128 MiB em dados materializados se aplica a toda a consulta, incluindo todos os documentos mesclados.