使用测量

本文档介绍了如何在图表上定义和查询度量。 您可以使用度量来确保在联接中正确执行聚合。

概览

度量是在节点或边表的 PROPERTIES 子句中定义的聚合属性。测量由测量表达式定义,该表达式通过在以下项之一周围使用 MEASURE 关键字创建:

  • 一个聚合表达式
  • 将聚合函数与标量运算相结合的表达式
  • 派生测量,即在同一元素表上定义的、封装在 AGG 函数中的测量组合

度量支持以下聚合函数:

  • SUM
  • AVG
  • COUNT
  • MIN
  • MAX
  • AGG(针对派生测量)

除 AGG 之外的所有聚合函数都支持 DISTINCT 关键字。如需详细了解支持的测量表达式,请参阅测量类型。

度量会参考其所定义的节点表或边表的 KEY 来定义其聚合。这意味着,当您查询度量时,即使底层表以导致行重复的方式联接,聚合也会正确执行。

您无法在 GQL 查询中引用由度量定义的属性。而是通过调用 GRAPH_EXPAND TVF 来查询图的关系表示,从而访问度量。默认情况下,GRAPH_EXPAND 会返回图数据的展平表表示形式,其中每个属性对应一列。您还可以指定 flatten_results => FALSE,以返回包含嵌套 STRUCT 列的分层表。如果您的图包含多个根节点表,您可以指定 start_node 参数来选择要遍历的根表。

此函数不接受所有类型的图。如需详细了解哪些图表构成有效输入,请参阅输入限制。如需运行 GQL 查询,您必须拥有使用企业版或企业 Plus 版的预留。如果您使用按需价格,则可以调用 GRAPH_EXPAND 函数对图运行 SQL 查询。

您可以对 GRAPH_EXPAND TVF 的输出调用 AGG 函数,以根据度量中定义的聚合来聚合属性。

定义测量

您可以在节点或边表定义的 PROPERTIES 子句中定义度量,方法是在度量表达式周围使用 MEASURE() 关键字。

以下示例会创建一个名为 university 的数据集,以及学院、院系和课程表:

CREATE SCHEMA IF NOT EXISTS university;

CREATE OR REPLACE TABLE university.College (
  college_id INT64 PRIMARY KEY NOT ENFORCED,
  college_name STRING
);

CREATE OR REPLACE TABLE university.Department (
  dept_id INT64 PRIMARY KEY NOT ENFORCED,
  dept_name STRING,
  college_id INT64,
  budget FLOAT64,
  FOREIGN KEY (college_id) REFERENCES university.College(college_id) NOT ENFORCED
);

CREATE OR REPLACE TABLE university.Course (
  course_id INT64 PRIMARY KEY NOT ENFORCED,
  course_name STRING,
  dept_id INT64,
  credits INT64,
  FOREIGN KEY (dept_id) REFERENCES university.Department(dept_id) NOT ENFORCED
);

INSERT INTO university.College (college_id, college_name)
VALUES (101, 'College of Engineering'),
       (102, 'College of Arts');

INSERT INTO university.Department (dept_id, dept_name, college_id, budget)
VALUES (1001, 'Computer Science', 101, 500000),
       (1002, 'Mechanical Engineering', 101, 400000),
       (1003, 'Fine Arts', 102, 200000),
       (1004, 'Research', 101, 50000);

INSERT INTO university.Course (course_id, course_name, dept_id, credits)
VALUES (1, 'Intro to CS', 1001, 3),
       (2, 'Data Structures', 1001, 4),
       (3, 'Thermodynamics', 1002, 3),
       (4, 'Oil Painting', 1003, 2);

以下语句会创建一个名为 SchoolGraph 的图表,该图表在 Department 和 Course 节点中定义属性的度量。 您必须为由测量定义的属性提供别名。

CREATE OR REPLACE PROPERTY GRAPH university.SchoolGraph
  NODE TABLES (
    university.College
      KEY(college_id)
      PROPERTIES(college_id, college_name),
    university.Department
      KEY(dept_id)
      PROPERTIES(dept_id, dept_name, college_id,
        budget OPTIONS(description="Department budget in USD"),
        MEASURE(SUM(budget)) AS total_budget,
        MEASURE(SUM(budget) * 1.10) AS budget_with_overhead OPTIONS(
          description="Total department budget including 10% overhead",
          extensions=[("unit", "USD"), ("tier", "planning")]
        )),
    university.Course
      KEY(course_id)
      PROPERTIES(
        course_id,
        course_name,
        credits,
        dept_id,
        MEASURE(AVG(credits)) AS avg_credits,
        MEASURE(SUM(credits)) AS total_credits,
        MEASURE(COUNT(course_id)) AS course_count,
        MEASURE(COUNT(DISTINCT CASE WHEN credits >= 3 THEN course_id ELSE NULL END)) AS major_course_count,
        MEASURE(SAFE_DIVIDE(AGG(total_credits), NULLIF(AGG(course_count), 0))) AS computed_avg_credits)
  )
  EDGE TABLES (
    university.Department AS CollegeDept
      SOURCE KEY (college_id) REFERENCES College (college_id)
      DESTINATION KEY (dept_id) REFERENCES Department (dept_id),
    university.Course AS DeptCourse
      SOURCE KEY (dept_id) REFERENCES Department (dept_id)
      DESTINATION KEY (course_id) REFERENCES Course (course_id)
  );

SchoolGraph 图的可视化。

total_budget 测量定义为 MEASURE(SUM(budget))。该测量会将汇总锁定到 KEY,即 dept_id。

派生度量

派生测量是通过使用 AGG() 函数引用在同一元素表上定义的其他测量的测量。借助此功能,您可以构建可重复使用的测量层次结构。

例如,在 university.Course 中,computed_avg_credits 测量会计算测量 total_credits 和 course_count 之间的比率:

MEASURE(SAFE_DIVIDE(AGG(total_credits), NULLIF(AGG(course_count), 0))) AS computed_avg_credits

派生度量会保留底层元素表键的粒度锁定属性。度量依赖项会在图编译时解析。度量之间的循环引用会返回错误。

了解重复统计

联接表时,每当数据中存在一对多关系时,数据就会重复。例如,如果您联接 Course、Department 和 College 表,则具有多门课程的院系会在输出的多行中显示:

SELECT
  college_name,
  dept_name,
  course_name,
  budget
FROM university.Course
LEFT JOIN university.Department
  ON Course.dept_id = Department.dept_id
LEFT JOIN university.College
  ON Department.college_id = College.college_id;

/*------------------------+------------------------+-----------------+----------+
 | college_name           | dept_name              | course_name     | budget   |
 +------------------------+------------------------+-----------------+----------+
 | College of Engineering | Computer Science       | Intro to CS     | 500000.0 |
 | College of Engineering | Computer Science       | Data Structures | 500000.0 |
 | College of Engineering | Mechanical Engineering | Thermodynamics  | 400000.0 |
 | College of Arts        | Fine Arts              | Oil Painting    | 200000.0 |
 +------------------------+------------------------+-----------------+----------*/

如果您尝试使用 SUM(budget) 计算每个学院的总预算,则计算机科学系的预算会被计算两次。您可以通过直接查询 Department 表来避免此问题,但如果您想通过构成联接数据的不同表计算多个聚合,则此方法不起作用。以下部分介绍了度量如何解决此问题。

使用度量查询图表

您可以使用 GQL 查询包含度量的图,但查询无法直接引用由度量定义的任何属性。例如,以下查询引用了已定义测量属性的节点,但未使用或返回任何测量字段:

GRAPH university.SchoolGraph
MATCH (c:College)->(d:Department)->(course:Course)
RETURN c.college_name, d.dept_name, course.course_name;

/*------------------------+------------------------+-----------------+
 | college_name           | dept_name              | course_name     |
 +------------------------+------------------------+-----------------+
 | College of Engineering | Computer Science       | Intro to CS     |
 | College of Engineering | Computer Science       | Data Structures |
 | College of Engineering | Mechanical Engineering | Thermodynamics  |
 | College of Arts        | Fine Arts              | Oil Painting    |
 +------------------------+------------------------+-----------------*/

如需使用度量,您可以使用 GRAPH_EXPAND 表值函数 (TVF) 将图作为关系表进行查询:

GRAPH_EXPAND(
  graph_name [, start_node => 'NodeName'] [, flatten_results => TRUE|FALSE]
)

该函数接受以下参数:

  • graph_name:包含图表名称的 STRING。
  • start_node:可选 STRING,用于指定遍历的起始节点表。如果图表有多个根节点表,则此参数是必需的。
  • flatten_results:可选 BOOL(默认值为 TRUE):
    • TRUE:将所有节点和边缘属性展平为具有 <element>_<property> 前缀的顶级列。
    • FALSE:将根节点属性作为无前缀的顶级列返回,并将联接的相邻表作为 STRUCT 列返回。

使用展平输出

当 flatten_results 设置为 TRUE(默认模式)时,输出表中的列通过串联节点或边缘表标签和属性名称来命名,例如 Course_course_name 或 Department_total_budget。以下查询显示了 GRAPH_EXPAND TVF 的示例输出:

SELECT
  College_college_name,
  Department_dept_name,
  Department_budget,
  Course_course_name
FROM
  GRAPH_EXPAND("university.SchoolGraph");

/*------------------------+------------------------+-------------------+--------------------+
 | College_college_name   | Department_dept_name   | Department_budget | Course_course_name |
 +------------------------+------------------------+-------------------+--------------------+
 | College of Engineering | Computer Science       | 500000.0          | Intro to CS        |
 | College of Engineering | Computer Science       | 500000.0          | Data Structures    |
 | College of Engineering | Mechanical Engineering | 400000.0          | Thermodynamics     |
 | College of Arts        | Fine Arts              | 200000.0          | Oil Painting       |
 +------------------------+------------------------+-------------------+--------------------*/

GRAPH_EXPAND 函数通过对节点表和边表应用一系列 LEFT JOIN 操作来生成扁平化表。如果您未指定起始节点,则输入图在架构关系图中必须具有单个根节点表。同样,根节点表必须与它共享边的每个其他节点表具有多对一关系。无法通过一系列联接从根节点表访问的数据不会显示在输出中。在前面的示例中,Course 是根节点表。Research 部门不包含任何课程,因此从输出中省略。

您无法直接为由度量定义的属性选择列。 而是必须将其封装在 AGG() 函数中。 此函数可确保对度量定义的聚合针对每个键仅执行一次。

以下查询会同时计算每个学院的总预算和课程总数:

SELECT
  College_college_name,
  AGG(Department_total_budget) AS college_budget,
  AGG(Course_course_count) AS total_courses
FROM
  GRAPH_EXPAND("university.SchoolGraph")
GROUP BY
  College_college_name;

/*------------------------+----------------+---------------+
 | College_college_name   | college_budget | total_courses |
 +------------------------+----------------+---------------+
 | College of Engineering | 900000.0       | 3             |
 | College of Arts        | 200000.0       | 1             |
 +------------------------+----------------+---------------*/

使用分层输出

指定 flatten_results => FALSE 时,GRAPH_EXPAND 会返回分层关系,而不是带前缀的列:

  • 起始根节点的属性显示为不带前缀的顶级列(例如 course_name 或 credits)。
  • 联接的近邻表(可能是节点表或边表)的属性会显示为以元素表命名的嵌套 STRUCT 字段。例如,如果起始节点为 Course,则您将使用 DeptCourse.Department.CollegeDept.College.college_nameto access thecollege_name` 属性。
  • 嵌套结构体中的测量值使用 AGG() 中的点表示法进行查询,例如 AGG(DeptCourse.Department.total_budget)。
  • 如果某个元素表的属性与相邻元素表的属性同名,则您无法使用 flatten_results => FALSE 查询图。您必须改为展平结果。
SELECT
  DeptCourse.Department.dept_name AS department_name,
  DeptCourse.Department.CollegeDept.College.college_name,
  AGG(DeptCourse.Department.total_budget) AS total_budget,
  AGG(course_count) AS total_courses
FROM GRAPH_EXPAND(
       "university.SchoolGraph",
       start_node => "Course",
       flatten_results => FALSE)
GROUP BY department_name, college_name;

使用多个根节点表

图表通常包含多个根节点表(也称为事实表),这些表共享维度表。如需查询具有多个根节点表的图,请在 GRAPH_EXPAND 中使用 start_node 实参。该函数会遍历从起始节点可到达的子图。

例如,您可以向 university 数据集添加一个 Instructor 表,该表也链接到 Department:

CREATE OR REPLACE TABLE university.Instructor (
  instructor_id INT64 PRIMARY KEY NOT ENFORCED,
  instructor_name STRING,
  dept_id INT64,
  salary FLOAT64,
  FOREIGN KEY (dept_id) REFERENCES university.Department(dept_id) NOT ENFORCED
);

INSERT INTO university.Instructor (instructor_id, instructor_name, dept_id, salary)
VALUES (501, 'Dr. Dana', 1001, 120000),
       (502, 'Dr. Lee', 1001, 110000),
       (503, 'Dr. Taylor', 1002, 115000),
       (504, 'Prof. Izumi', 1003, 90000);

您可以创建一个名为 university.SchoolGraphMultiRoot 的新图,其中包含 Instructor 节点表以及从 Instructor 到 Department 的边:

CREATE OR REPLACE PROPERTY GRAPH university.SchoolGraphMultiRoot
  NODE TABLES (
    university.College
      KEY(college_id)
      PROPERTIES(college_id, college_name),
    university.Department
      KEY(dept_id)
      PROPERTIES(dept_id, dept_name, college_id, budget,
        MEASURE(SUM(budget)) AS total_budget),
    university.Course
      KEY(course_id)
      PROPERTIES(
        course_id, course_name, credits, dept_id,
        MEASURE(COUNT(course_id)) AS course_count),
    university.Instructor
      KEY(instructor_id)
      PROPERTIES(
        instructor_id, instructor_name, dept_id, salary,
        MEASURE(COUNT(instructor_id)) AS instructor_count,
        MEASURE(SUM(salary)) AS total_salary)
  )
  EDGE TABLES (
    university.Department AS CollegeDept
      SOURCE KEY (college_id) REFERENCES College (college_id)
      DESTINATION KEY (dept_id) REFERENCES Department (dept_id),
    university.Course AS DeptCourse
      SOURCE KEY (dept_id) REFERENCES Department (dept_id)
      DESTINATION KEY (course_id) REFERENCES Course (course_id),
    university.Instructor AS DeptInstructor
      SOURCE KEY (dept_id) REFERENCES Department (dept_id)
      DESTINATION KEY (instructor_id) REFERENCES Instructor (instructor_id)
  );

在此图表中,Course 和 Instructor 均作为根节点表,链接到 Department 和 College。您可以通过传递 start_node 来定位任一子图:

-- Expand the Course subgraph (Course -> Department -> College)
SELECT
  College_college_name,
  AGG(Course_course_count) AS total_courses
FROM GRAPH_EXPAND("university.SchoolGraphMultiRoot", start_node => "Course")
GROUP BY College_college_name;

-- Expand the Instructor subgraph (Instructor -> Department -> College)
SELECT
  College_college_name,
  AGG(Instructor_total_salary) AS total_faculty_salary
FROM GRAPH_EXPAND("university.SchoolGraphMultiRoot", start_node => "Instructor")
GROUP BY College_college_name;

如需分析多个事实表中的指标,请使用 GRAPH_EXPAND 查询每个子图,并在标准 SQL 中通过其共享维度联接这些子图:

SELECT
  C.College_college_name,
  AGG(C.Course_course_count) AS total_courses,
  AGG(I.Instructor_total_salary) AS total_faculty_salary
FROM GRAPH_EXPAND("university.SchoolGraphMultiRoot", start_node => "Course") AS C
INNER JOIN GRAPH_EXPAND("university.SchoolGraphMultiRoot", start_node => "Instructor") AS I
  ON C.Department_dept_id = I.Department_dept_id
GROUP BY C.College_college_name;

AGG(C.Course_course_count) 和 AGG(I.Instructor_total_salary) 都会在各自的粒度上准确计算其聚合,而不会受到联接的干扰。

最佳做法

设计包含度量的图表时,请遵循以下最佳实践:

在单个表中定义节点和边

我们建议您为节点表定义及其连接边表定义使用同一个表。重复使用该表可确保节点表和边表之间存在一对一 (1:1) 关系,从而防止扇出,并确保 GRAPH_EXPAND 函数不会因边存在歧义而忽略它。

例如,如果您有一个输入表 Enrollment,其中包含指向 Student 和 Course 表的外键,请使用 Enrollment 输入表定义 Enrollment 节点及其传出边缘。

避免在边缘表上定义不必要的属性

使用同一物理表定义节点表和边表时,请勿在边表上使用 PROPERTIES 子句,除非边上有要捕获的新信息。由于节点表和边表使用相同的物理表,因此所有这些属性都已是节点表的一部分。

在边缘表上定义冗余属性可能会导致 GRAPH_EXPAND 函数将边缘表视为与共享节点表不同的实体。为了优化性能,如果边缘上没有要捕获的新属性,请在边缘表定义中指定 NO PROPERTIES。

将多对多关系建模为节点表

当表 A 中的一行可以对应于表 B 中的多行,并且表 B 中的一行可以对应于表 A 中的多行时,就会发生多对多 (M:N) 关系。例如,一名学生可以注册多门课程,一门课程可以有多名学生。您可以使用中间联接表将多对多关系拆分为两个一对多关系。例如,您可以定义一个 Enrollment 表,其中包含指向 Student 和 Course 表的外键。如果您在 CREATE PROPERTY GRAPH 语句中将中间联接表建模为连接两个维度节点表的边表(如以下示例所示),则 GRAPH_EXPAND 函数会忽略这些边,因为它们的关系不明确:不是明确的多对一或一对一关系。

CREATE PROPERTY GRAPH university.SchoolGraph
  NODE TABLES (
    university.Student,
    university.Course
  )
  EDGE TABLES (
    -- Modeling the junction table as a standalone edge causes ambiguity
    university.Enrollment AS StudentEnrollment
      SOURCE KEY (student_id) REFERENCES Student (student_id)
      DESTINATION KEY (course_id) REFERENCES Course (course_id)
  );

如果图中的所有边缘都被忽略,则对 GRAPH_EXPAND 或 BQ.SHOW_GRAPH_EXPAND_SCHEMA 的调用会失败并报错。

如需正确为度量建立 M:N 关系模型,请将中间联接表提升为节点表。在此结构中,联接节点表充当单个根节点表(入度为零的节点表)。然后,定义两个多对一 (N:1) 边表,这两个表共享联接表,以便将 N:1 连接到每个维度节点表:

CREATE PROPERTY GRAPH university.SchoolGraph
  NODE TABLES (
    university.Student KEY (student_id),
    university.Course KEY (course_id),
    -- Promote the junction table to a node table (acting as the single root node)
    university.Enrollment KEY (enrollment_id)
  )
  EDGE TABLES (
    -- Share (reuse) the Enrollment table to define N:1 edges to each dimension
    university.Enrollment AS EnrollmentToStudent
      SOURCE KEY (enrollment_id) REFERENCES Enrollment (enrollment_id)
      DESTINATION KEY (student_id) REFERENCES Student (student_id),
    university.Enrollment AS EnrollmentToCourse
      SOURCE KEY (enrollment_id) REFERENCES Enrollment (enrollment_id)
      DESTINATION KEY (course_id) REFERENCES Course (course_id)
  );

在此结构中,Enrollment 作为唯一的根节点表(入度为零),以 N:1 的关系连接到 Student 和 Course 维度表(入度为一)。GRAPH_EXPAND 函数可以通过从 Enrollment 向外遍历到 Student 和 Course 来展平此图。如果您在其中任何一个表上定义了度量,则可以对度量列调用 AGG 函数,以正确计算聚合,而不会重复计算。

选择度量如何处理不匹配的行

如果某行在连接的元素表中没有匹配项,则该表的列在 GRAPH_EXPAND 输出中为 NULL。例如,如果某个课程没有 dept_id 值,则该行的 Department 列为 NULL。Department 节点上的测量是否包含这些行取决于您在何处编写测量聚合的标量表达式。

如果您在聚合函数中编写表达式,则系统会针对展开后的输出中的每一行(包括不匹配的行)对表达式求值。当表达式为其中一行返回一个值而不是 NULL 时,该值会影响结果。

如果您将表达式定义为属性,并从测量中引用该属性,则系统会针对节点表的行对表达式求值,并且不匹配的行不会对结果产生影响。

以下示例在 university.Department 上以两种方式定义了同一表达式:

university.Department
  KEY(dept_id)
  PROPERTIES(dept_id, dept_name, college_id, budget,
    COALESCE(budget, 0) AS budget_or_zero,
    -- Includes courses that have no department, as a budget of 0.
    MEASURE(AVG(COALESCE(budget, 0))) AS avg_budget_inline,
    -- Averages only departments that exist.
    MEASURE(AVG(budget_or_zero)) AS avg_budget_property)

这两项措施都将 NULL 预算替换为 0。内嵌表单还会替换为没有部门的课程生成的 NULL 值,以便将该课程作为预算为 0 的部门进行平均。选择与您要衡量的 内容相匹配的表单。

查看 GRAPH_EXPAND 架构

如需在不调用函数的情况下查看 GRAPH_EXPAND 函数返回的表的架构,请使用 BQ.SHOW_GRAPH_EXPAND_SCHEMA 系统过程:

DECLARE schema STRING DEFAULT '';
CALL BQ.SHOW_GRAPH_EXPAND_SCHEMA('university.SchoolGraph', schema);
SELECT schema;

此过程会使用每列的名称、类型和模式填充 schema 变量。它还会指明该列引用的属性是否为度量,并列出您为其定义的任何说明或同义词。

后续步骤