Relaciones
Vincent Depassier · 29 de agosto de 2026
PraxQL — Relaciones Anidadas
En vez de hacer varias llamadas para traer datos relacionados, PraxQL te deja expresar relaciones dentro de select. El motor las resuelve por lotes, así que no hay problema N+1.
Cómo funcionan las relaciones
Cuando una columna es de tipo Table, ya nombra la tabla a la que apunta — /schema lo reporta como pointsTo. PraxQL lo usa para deducir el join solo. Declarás la relación en select como un objeto en vez de un string:
{
"refs": {
"Clientes": "aaa-guid",
"Pedidos": "bbb-guid"
},
"query": {
"from": "Clientes",
"select": [
"Nombre",
"Email",
{
"table": "Pedidos",
"select": ["Total", "Fecha", "Estado"]
}
]
}
}{
"data": [
{
"Nombre": "Acme Corp",
"Email": "hola@acme.com",
"Pedidos": [
{ "Total": 1500.00, "Fecha": "2026-03-01", "Estado": "Completado" },
{ "Total": 800.00, "Fecha": "2026-04-15", "Estado": "Pendiente" }
]
}
]
}Los registros relacionados se anidan como arrays dentro de cada fila padre. Nunca se aplanan en filas padre repetidas, así que no tenés que de-duplicar nada.
Todos los campos de un objeto de relación
Campo | Requerido | Descripción |
| sí | Alias de la tabla relacionada. Debe existir en |
| no | Columnas a devolver de esta tabla |
| no | Filtros sobre las filas relacionadas |
| no | Orden de las filas relacionadas |
| no | Máximo de filas relacionadas por fila padre |
| no | Más relaciones anidadas |
| no | Columnas de join explícitas, anulando la detección automática |
| no |
|
Filtrar y limitar una relación
{
"table": "Pedidos",
"select": ["Total", "Fecha", "Estado"],
"where": [
{ "field": "Estado", "op": "neq", "value": "Cancelado" }
],
"orderBy": [{ "field": "Fecha", "dir": "desc" }],
"limit": 5
}Como mucho cinco pedidos no cancelados por cliente, los más nuevos primero. Ojo que acá limit es por fila padre, no un total sobre toda la respuesta.
Anidamiento profundo
Anidá relaciones dentro de relaciones con la clave relations:
{
"refs": {
"Clientes": "aaa-guid",
"Pedidos": "bbb-guid",
"ItemsPedido": "ccc-guid"
},
"query": {
"from": "Clientes",
"select": [
"Nombre",
{
"table": "Pedidos",
"select": ["Total", "Fecha"],
"relations": [
{
"table": "ItemsPedido",
"select": ["Producto", "Cantidad", "PrecioUnitario"]
}
]
}
]
}
}Joins detectados solos
PraxQL deduce la condición de join cuando cualquiera de los dos lados apunta al otro:
la tabla padre tiene una columna
TablecuyopointsToes la tabla hija, ola tabla hija tiene una columna
TablecuyopointsToes la tabla padre.
Para una columna de relación normal nunca escribís on — el esquema ya conoce la conexión.
Joins explícitos
Cuando la detección automática elige la columna equivocada, o la relación no está expresada como columna Table, nombrá vos las columnas:
{
"table": "Facturas",
"on": { "left": "ID", "right": "ClienteRef" },
"joinType": "INNER",
"select": ["NumeroFactura", "Monto"]
}Campo | Descripción |
| Columna en la tabla padre |
| Columna en la tabla hija |
|
|
Límite de profundidad
Acá hay dos números y es fácil confundirlos.
5 es el tope de la plataforma. Nada lo sube.
2 es lo que realmente recibe un scope de tabla recién creado — MaxRelationDepthOverride arranca en 2. O sea que una credencial nueva anida dos niveles, no cinco, hasta que alguien lo amplíe. El override se puede mover hasta 5, nunca más allá.
Pasarse del límite efectivo devuelve 403 SCOPE_VIOLATION. Si una consulta que anda en el Playground falla desde tu app, esto es lo primero que hay que mirar.
La tabla relacionada además tiene que permitir relaciones: AllowRelations viene activo en un scope nuevo, pero se puede apagar.
Por qué no es N+1
PraxQL no emite una consulta por fila padre:
Corre la consulta principal y devuelve las filas padre.
Se juntan sus ids.
Una sola sub-consulta por lote trae todas las filas relacionadas para ese conjunto de ids de una vez.
Los resultados se arman antes de construir la respuesta.
Traer 100 clientes con sus pedidos son dos consultas, no 101. Cada nivel extra de anidamiento agrega una consulta, no una por fila.
Toda tabla debe estar en refs
Toda tabla usada en una relación — incluidas las anidadas profundo — debe estar declarada en el refs de primer nivel con su GUID.
Una tabla referenciada en una relación pero ausente de refs devuelve 400 INVALID_QUERY. Es deliberado: refs es la lista completa de lo que un request puede tocar, así que se puede contrastar con tus scopes antes de hacer ningún trabajo.