跳到主要内容

关系

概述

Relationship 是 Wren Engine 中的一个关键概念。它代表了两个模型之间的关键关联,说明了系统复杂的连接和交互。在建模定义语言 (MDL) 中,您可以使用它们的关系构建模型之间的连接。它具有一些好处

  • Wren Engine 允许用户通过 MDL 中的关系从一个模型访问另一个模型。
  • 这对于大型语言模型 (LLM) 了解数据集的拓扑结构是一个很好的上下文。它可以更准确地关联它们。

定义关系

为了描述一个关系,Wren Engine 提供了一些用于描述关系的属性。通常,我们可以将关系描述为一个 JSON 对象

{
"name" : "CustomerOrders",
"models" : [ "Customer", "Orders" ],
"joinType" : "ONE_TO_MANY",
"condition" : "Customer.custkey = Orders.custkey"
}

一个关系由以下部分组成

  • name:关系的名称。
  • models:此关系中关联的模型。Wren Engine 在一个关系中只关联 2 个模型。
    • joinType:关系的类型。通常,两个模型之间有 4 种类型的关系
      • 一对一 (1-1)
      • 一对多 (1-M)
      • 多对一 (M-1)
      • 多对多 (M-M)
      只有 1-11-MM-1 对 Wren Engine 有意义。稍后我们将讨论它。
  • condition:两个模型之间的连接条件。Wren Engine 在 SQL 生成期间用作连接条件。

关系的连接类型

每个关系都是双向的。方向可以是 TO_ONETO_MANY。以下是一些定义

  • TO_ONE 关系:这意味着根据连接条件,左表记录在右表中只有一个对应的记录。
  • TO_MANY 关系:这意味着根据连接条件,左表记录在右表中可以有多个记录。

有了这些组成部分,我们可以用四种类型来描述两个模型之间的关系:一对一一对多多对一多对多

如何使用关系

在传统数据库中,如果用户想要关联两个表来获取一些结果,他们必须构建表之间的连接条件。通过定义的关系,Wren Engine 允许用户从一个模型访问另一个模型。这对于构建常见指标或从模型内部访问另一个模型中的相关属性非常有用。

让我们来谈谈特殊列:`关系字段` 和 `计算字段`

关系字段

假设我们如上定义了 `CustomerOrders`。给定一个包含关系的两模型示例定义

{
"name": "Customer",
"refSql": "select * from tpch.customer",
"columns": [
{
"name": "custkey",
"type": "integer",
"expression": "c_orderkey"
},
{
"name": "name",
"type": "varchar",
"expression": "c_name"
}
],
"primaryKey": "custkey"
},
{
"name": "Orders",
"refSql": "select * from tpch.orders",
"columns": [
{
"name": "orderkey",
"type": "integer",
"expression": "o_orderkey"
},
{
"name": "custkey",
"type": "integer",
"expression": "o_custkey"
},
{
"name": "totalprice",
"type": "integer",
"expression": "o_totalprice"
},
{
"name": "customer",
"type": "Customer",
"relationship": "CustomerOrders"
}
],
"primaryKey": "orderkey"
}

关系字段是一个列,其必需属性与 基本相同。但是,它们之间有两个区别

  • type:类型是现有模型的名称。它指定将哪个引用返回到此列。
  • relationship:它指定此关系字段将基于哪个关系来构建连接条件。

让我们检查示例定义。模型 `Orders` 有一个名为 `customer` 的列,该列将根据关系 `CustomerOrders` 返回模型 `Customer` 的引用。

模型的返回引用如何使用?让我们继续学习计算字段。

计算字段

如上所述,关系方向有两种类型:`TO_ONE` 或 `TO_MANY`。它们在访问数据时具有不同的行为。让我们先看看更简单的一种,`TO_ONE`。请看下面的示例定义

{
"name": "Orders",
"refSql": "select * from tpch.orders",
"columns": [
{
"name": "orderkey",
"type": "integer",
"expression": "o_orderkey"
},
{
"name": "custkey",
"type": "integer",
"expression": "o_custkey"
},
{
"name": "orderdate",
"type": "date",
"expression": "o_orderdate",
},
{
"name": "totalprice",
"type": "integer",
"expression": "o_totalprice"
},
{
"name": "customer",
"type": "Customer",
"relationship": "CustomerOrders"
},
{
"name": "order_year",
"type": "date",
"isCalculated": true,
"expression": "date_trunc('year', orderdate)"
},
{
"name": "customer_name",
"type": "varhcar",
"isCalculated": true,
"expression": "customer.name"
}
],
"primaryKey": "orderkey"
}

计算字段的常见定义

计算字段的必需属性与普通列相同。唯一的区别是 `isCalculated`。我们应该通过将此属性设置为 true 来标记字段为计算字段。普通字段和计算字段的主要区别在于它们的表达式范围。

普通列的范围是数据源表的列。然而,计算字段的范围是模型的普通列。您可以查看 `order_year` 列来了解此概念。我们在此不深入探讨此问题。让我们先关注关系。

使用 TO_ONE 关系

如上所述,我们可以在计算字段的表达式中访问此模型的列。关系字段也属于它们。参见列 `customer_name`。其表达式为 `customer.name`,这意味着从引用 `customer` 中获取 `name`。我们如何访问 `customer`?Wren Engine 将从关系 `CustomerOrders` 中获取此信息。

定义后,我们可以像这样查询模型 `Orders`

SELECT customer_name FROM Orders

使用 TO_MANY 关系

每个关系都可以从任何方向使用。在上一部分中,我们尝试从 `Orders` 访问 `Customer` 的数据。根据 `OrdersCustomer`,Orders 到 Customer 是一个 `多对一` 关系。Orders 中的每条记录只能映射到 Customer 中的一条记录。反过来使用它怎么样?让我们看一个例子

{
"name": "Customer",
"refSql": "select * from tpch.customer",
"columns": [
{
"name": "custkey",
"type": "integer",
"expression": "c_orderkey"
},
{
"name": "name",
"type": "varchar",
"expression": "c_name"
},
{
"name": "orders",
"type": "Orders",
"relationship": "CustomerOrders"
},
{
"name": "consumption",
"type": "integer",
"isCalculated": true,
"expression": "sum(orders.totalprice)"
}
],
"primaryKey": "custkey"
},

在这种情况下,我们定义了一个名为 `consumption` 的计算字段,用于汇总 Orders 中所有相关的 `totalprice` 值。您可能会注意到表达式是一个聚合表达式。这是使用 `TO_MANY` 关系的一个限制。由于 `TO_MANY` 映射的结果会增加数据量,Wren Engine 只允许 `TO_MANY` 关系与聚合一起使用。

不带聚合使用 TO_MANY

为了处理 TO_MANY 结果的聚合,Wren Engine 将把左模型的主键作为分组维度。在本例中,左模型是 `Customer`。列 `custkey` 将是 `sum(order_item.totalprice)` 的维度。

定义后,我们可以像这样查询 `Customer`

SELECT name, consumption FROM Customer

关于多对多关系

Wren Engine 致力于保留每个模型的原始特性。行数不会改变,过滤条件也不会改变。为了实现这一目的,我们只允许 `TO_MANY` 关系与聚合一起使用,并按主键分组。

但是,我们无法处理 `MANY_TO_MANY`。当使用 `MANY_TO_MANY` 关系时,其拥有模型的行数会增加。这就是 Wren Engine 不支持 `MANY_TO_MANY` 的原因。

关系如何工作

模型通过 CTE 实现。Wren Engine 在部署 MDL 时,将在 CTE 中生成计算字段和必需的 JOIN 条件。让我们看看在 `Orders` 中选择 `customer_name` 时生成的 SQL

wrenai=> select orderkey, customer_name from Orders;

WITH
"Customer" AS (
SELECT
"Customer"."custkey" "custkey"
, "Customer"."name" "name"
FROM
(
SELECT
c_custkey "custkey"
, c_name "name"
FROM
"memory"."tpch"."customer" "Customer"
) "Customer"
)
, "Orders" AS (
SELECT
"Orders"."orderkey" "orderkey"
, "Orders"."custkey" "custkey"
, "Orders"."totalprice" "totalprice"
, "Orders"."orderdate" "orderdate"
, "Orders_relationsub"."customer_name" "customer_name"
FROM
((
SELECT
o_orderkey "orderkey"
, o_custkey "custkey"
, o_totalprice "totalprice"
, o_orderdate "orderdate"
FROM
"memory"."tpch"."orders" "Orders"
) "Orders"
LEFT JOIN (
SELECT
"Orders"."orderkey"
, "Customer"."name" "customer_name"
FROM
((
SELECT
o_orderkey "orderkey"
, o_custkey "custkey"
, o_totalprice "totalprice"
, o_orderdate "orderdate"
FROM
"memory"."tpch"."orders" "Orders"
) "Orders"
LEFT JOIN "Customer" ON ("Orders"."custkey" = "Customer"."custkey"))
) "Orders_relationsub" ON ("Orders"."orderkey" = "Orders_relationsub"."orderkey"))
)
SELECT
orderkey
, customer_name
FROM
Orders

在 `Orders` CTE 中,我们可以发现它包含一个 JOIN,用于从 `Customer` 访问列 `name`。用户可以像使用普通列一样使用 `customer_name`。

那么 `TO_MANY` 呢?让我们看看 `Customer` 中 `consumption` 生成的 SQL。

WITH
"Orders" AS (
SELECT
"Orders"."orderkey" "orderkey"
, "Orders"."custkey" "custkey"
, "Orders"."orderstatus" "orderstatus"
, "Orders"."totalprice" "totalprice"
, "Orders"."orderdate" "orderdate"
FROM
(
SELECT
o_orderkey "orderkey"
, o_custkey "custkey"
, o_orderstatus "orderstatus"
, o_totalprice "totalprice"
, o_orderdate "orderdate"
FROM
"memory"."tpch"."orders" "Orders"
) "Orders"
)
, "Customer" AS (
SELECT
"Customer"."custkey" "custkey"
, "Customer"."name" "name"
, "consumption"."consumption" "consumption"
FROM
((
SELECT
c_custkey "custkey"
, c_name "name"
FROM
"memory"."tpch"."customer" "Customer"
) "Customer"
LEFT JOIN (
SELECT
"Customer"."custkey"
, sum("Orders"."totalprice") "consumption"
FROM
((
SELECT
c_custkey "custkey"
, c_name "name"
FROM
"memory"."tpch"."customer" "Customer"
) "Customer"
LEFT JOIN "Orders" ON ("Orders"."custkey" = "Customer"."custkey"))
GROUP BY 1
) "consumption" ON ("Customer"."custkey" = "consumption"."custkey"))
)
SELECT
name
, consumption
FROM
Customer

很容易发现 `Customer` CTE 中的 JOIN 部分与 `Orders` 中 `customer_name` 的部分不同。这是一个聚合查询,它汇总了 `Orders` 中的 `totalprice`,并按 `Customer` 的主键 `custkey` 分组。

实现很简单;但是,它有一个限制:循环关系。

正如我们讨论的,模型实现和关系都由 CTE 管理。Wren Engine 动态生成 SQL。然而,CTE 需要按顺序排列;它只能访问在其之前出现的 CTE。

考虑一下这种情况

SELECT o.customer_name, c.consumption
FROM Customer c JOIN Orders o ON o.custkey = c.custkey

它在 SQL 中同时使用了 `customer_name` 和 `consumption`。Wren Engine 如何对 CTE 进行排序?理解这一点将使我们能够确定 CTE 的拓扑结构是一个有向无环图 (DAG)。如果您收到关于循环关系的错误消息,请检查所选项目是否创建了依赖循环。