返回博客

OQL:我们设计的 Ontology 查询语言(语法篇)

Tags: #OQL #QueryLanguage #BNF #GraphTraversal #MetricExpansion #智策平台

Coomia发布于 2025年7月15日15 分钟阅读
分享本文Twitter / X

系列:S3 数据基座 · 第 6 篇 | 难度:高级 | 阅读时间:20 分钟

OQL:我们设计的 Ontology 查询语言(语法篇)

Tags: #OQL #QueryLanguage #BNF #GraphTraversal #MetricExpansion #智策平台

#TL;DR

coomia-dip 平台为三表模型设计了专属查询语言——OQL(Ontology Query Language)。OQL 不是 SQL 的简单封装,而是从 Ontology 语义出发,融合实体查询、图遍历、指标展开和时间旅行的领域特定语言。本文完整呈现 OQL 的 BNF 语法定义、核心语句设计(FETCH / TRAVERSE / AGGREGATE / TIMELINE)、指标展开机制和图遍历语法。通过与 SQL、GraphQL、Cypher 的对比,阐明 OQL 在 Ontology 场景下的独特优势。

#1. 为什么需要专属查询语言

#1.1 SQL 的局限性

Code
SQL 查询 Ontology 数据的痛点:

问题 1:类型无关查询需要大量 JSON 函数
  SQL:   SELECT JSON_EXTRACT(properties, '$.name') FROM entity_common
         WHERE entity_type = 'Person'
         AND JSON_EXTRACT(properties, '$.age') > 30
  OQL:   FETCH Person WHERE age > 30

问题 2:图遍历需要多层 JOIN
  SQL:   SELECT ... FROM entity_common ec1
         JOIN entity_edge ee1 ON ...
         JOIN entity_common ec2 ON ...
         JOIN entity_edge ee2 ON ...
         JOIN entity_common ec3 ON ...  -- 3 跳需要 5 个 JOIN
  OQL:   TRAVERSE Person -> WorksAt -> Company -> LocatedIn -> City

问题 3:指标展开需要嵌套子查询
  SQL:   SELECT *, (SELECT COUNT(*) FROM entity_edge
         WHERE source_id = ec.entity_id AND edge_type = 'Manages')
         AS direct_reports FROM entity_common ec ...
  OQL:   FETCH Person WITH METRIC direct_reports

问题 4:时间旅行需要版本化 JOIN
  SQL:   极其复杂的 Iceberg 时间旅行语法
  OQL:   FETCH Person AT TIME '2024-01-01T00:00:00Z'

#1.2 OQL 设计目标

Code
OQL 设计目标矩阵:

┌─────────────────────┬──────────────────────────────────┐
│ 目标                 │ 实现方式                          │
├─────────────────────┼──────────────────────────────────┤
│ Ontology 原生        │ 以 Entity Type 为查询入口         │
│                      │ 属性直接引用,无需 JSON 函数       │
├─────────────────────┼──────────────────────────────────┤
│ 图遍历内建           │ TRAVERSE 语句 + 路径表达式         │
│                      │ 支持 1-N 跳遍历                    │
├─────────────────────┼──────────────────────────────────┤
│ 指标展开             │ WITH METRIC 子句自动展开           │
│                      │ 指标定义与查询分离                  │
├─────────────────────┼──────────────────────────────────┤
│ 时间旅行             │ AT TIME / AT BRANCH 子句           │
│                      │ 透明映射到 Iceberg 快照             │
├─────────────────────┼──────────────────────────────────┤
│ 可翻译到 SQL         │ 最终编译为 Doris SQL 执行           │
│                      │ 保持 OLAP 引擎的全部优化能力        │
└─────────────────────┴──────────────────────────────────┘

#2. BNF 语法定义

#2.1 顶层语法

BNF
(* OQL 顶层语法 - EBNF 表示 *)

oql_statement
    ::= fetch_statement
      | traverse_statement
      | aggregate_statement
      | timeline_statement
      | diff_statement
      | mutate_statement
      ;

(* === FETCH 语句:实体查询 === *)
fetch_statement
    ::= 'FETCH' entity_type_ref
        [ 'WHERE' predicate_expr ]
        [ 'WITH' with_clause ( ',' with_clause )* ]
        [ 'AT' temporal_clause ]
        [ 'IN' world_ref ]
        [ 'ORDER' 'BY' order_expr ( ',' order_expr )* ]
        [ 'LIMIT' integer [ 'OFFSET' integer ] ]
    ;

(* === TRAVERSE 语句:图遍历 === *)
traverse_statement
    ::= 'TRAVERSE' entity_ref
        traverse_path+
        [ 'WHERE' predicate_expr ]
        [ 'WITH' with_clause ( ',' with_clause )* ]
        [ 'DEPTH' integer [ '..' integer ] ]
        [ 'LIMIT' integer ]
    ;

(* === AGGREGATE 语句:聚合分析 === *)
aggregate_statement
    ::= 'AGGREGATE' entity_type_ref
        'BY' group_expr ( ',' group_expr )*
        'COMPUTE' agg_expr ( ',' agg_expr )*
        [ 'WHERE' predicate_expr ]
        [ 'HAVING' predicate_expr ]
        [ 'AT' temporal_clause ]
        [ 'IN' world_ref ]
        [ 'ORDER' 'BY' order_expr ( ',' order_expr )* ]
        [ 'LIMIT' integer ]
    ;

(* === TIMELINE 语句:事件时间线 === *)
timeline_statement
    ::= 'TIMELINE' entity_ref
        [ 'EVENTS' event_type_list ]
        [ 'FROM' datetime_expr 'TO' datetime_expr ]
        [ 'WHERE' predicate_expr ]
        [ 'ORDER' 'BY' 'event_time' ( 'ASC' | 'DESC' ) ]
        [ 'LIMIT' integer ]
    ;

(* === DIFF 语句:分支对比 === *)
diff_statement
    ::= 'DIFF' entity_type_ref
        'BETWEEN' branch_ref 'AND' branch_ref
        [ 'WHERE' predicate_expr ]
    ;

#2.2 表达式语法

BNF
(* === 谓词表达式 === *)
predicate_expr
    ::= comparison_expr
      | predicate_expr 'AND' predicate_expr
      | predicate_expr 'OR' predicate_expr
      | 'NOT' predicate_expr
      | '(' predicate_expr ')'
      | exists_expr
      | contains_expr
      | search_expr
      | similar_expr
    ;

comparison_expr
    ::= property_ref comparator value_expr
    ;

comparator
    ::= '=' | '!=' | '>' | '>=' | '<' | '<='
      | 'IN' | 'NOT' 'IN'
      | 'LIKE' | 'NOT' 'LIKE'
      | 'IS' 'NULL' | 'IS' 'NOT' 'NULL'
      | 'BETWEEN' value_expr 'AND' value_expr
    ;

(* 全文搜索 *)
search_expr
    ::= 'SEARCH' '(' property_ref ',' string_literal ')'
    ;

(* 语义相似度 *)
similar_expr
    ::= 'SIMILAR' '(' string_literal ',' float_literal ')'
      | 'SIMILAR' '(' vector_literal ',' float_literal ')'
    ;

(* 存在性检查 *)
exists_expr
    ::= 'EXISTS' '(' traverse_path [ 'WHERE' predicate_expr ] ')'
    ;

(* 数组包含 *)
contains_expr
    ::= property_ref 'CONTAINS' value_expr
      | property_ref 'CONTAINS' 'ANY' '(' value_list ')'
      | property_ref 'CONTAINS' 'ALL' '(' value_list ')'
    ;

(* === 属性引用 === *)
property_ref
    ::= identifier                     (* 顶层属性 *)
      | identifier '.' identifier      (* 嵌套属性 *)
      | identifier '[' integer ']'     (* 数组索引 *)
    ;

(* === 图遍历路径 === *)
traverse_path
    ::= '->' edge_type_ref [ '(' predicate_expr ')' ] '->' entity_type_ref
      | '<-' edge_type_ref [ '(' predicate_expr ')' ] '<-' entity_type_ref
      | '--' edge_type_ref [ '(' predicate_expr ')' ] '--' entity_type_ref
    ;

(* === 指标和计算属性 === *)
with_clause
    ::= 'METRIC' metric_name_list
      | 'COMPUTED' computed_prop_list
      | 'EDGES' edge_summary_list
    ;

metric_name_list
    ::= metric_name ( ',' metric_name )*
    ;

(* === 聚合函数 === *)
agg_expr
    ::= agg_function '(' property_ref ')' [ 'AS' alias ]
    ;

agg_function
    ::= 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX'
      | 'PERCENTILE' | 'STDDEV' | 'VARIANCE'
      | 'COUNT_DISTINCT' | 'TOPN'
      | 'TIME_BUCKET'
    ;

(* === 时间子句 === *)
temporal_clause
    ::= 'TIME' datetime_expr
      | 'BRANCH' branch_name
      | 'SNAPSHOT' snapshot_id
    ;

(* === World 引用 === *)
world_ref
    ::= 'WORLD' world_name
    ;

#3. 核心语句详解

#3.1 FETCH:实体查询

Code
FETCH 语句是 OQL 最基础的查询,类似 SQL 的 SELECT:

┌───────────────────────────────────────────────────────────┐
│                    FETCH 语句结构                           │
│                                                            │
│  FETCH <EntityType>                                        │
│    WHERE <conditions>          ← 过滤条件                   │
│    WITH METRIC <metrics>       ← 指标展开                   │
│    AT TIME <timestamp>         ← 时间旅行                   │
│    IN WORLD <world>            ← 指定 World                 │
│    ORDER BY <fields>           ← 排序                       │
│    LIMIT <n> OFFSET <m>        ← 分页                       │
└───────────────────────────────────────────────────────────┘

示例集合:

OQL
-- 基础查询:查找所有 30 岁以上的工程师
FETCH Person
WHERE department = 'Engineering' AND age > 30
ORDER BY hire_date DESC
LIMIT 20;

-- 嵌套属性查询
FETCH Person
WHERE address.city = 'Shanghai' AND skills CONTAINS 'Python';

-- 全文搜索 + 语义搜索组合
FETCH Document
WHERE SEARCH(content, '风险评估报告')
  AND SIMILAR('分析信用风险的机器学习模型', 0.8)
ORDER BY _score DESC
LIMIT 10;

-- 带指标展开的查询
FETCH Person
WHERE department = 'Engineering'
WITH METRIC direct_reports, total_projects, avg_performance_score
ORDER BY direct_reports DESC;

-- 时间旅行查询
FETCH Person
WHERE department = 'Engineering'
AT TIME '2023-06-01T00:00:00Z'
IN WORLD 'production';

-- 存在性条件
FETCH Company
WHERE EXISTS(-> Employs -> Person WHERE role = 'CTO')
  AND industry = 'Technology';

#3.2 TRAVERSE:图遍历

Code
TRAVERSE 语句执行图遍历,替代多表 JOIN:

┌───────────────────────────────────────────────────────────┐
│                  TRAVERSE 语句结构                          │
│                                                            │
│  TRAVERSE <start_entity>                                   │
│    -> <EdgeType> -> <EntityType>     ← 正向遍历             │
│    <- <EdgeType> <- <EntityType>     ← 反向遍历             │
│    -- <EdgeType> -- <EntityType>     ← 双向遍历             │
│    WHERE <conditions>                ← 路径条件             │
│    DEPTH 1..3                        ← 遍历深度             │
│    LIMIT <n>                         ← 结果限制             │
└───────────────────────────────────────────────────────────┘

示例集合:

OQL
-- 一跳遍历:某人在哪家公司工作
TRAVERSE Person('person-001')
  -> WorksAt -> Company;

-- 两跳遍历:某公司所有员工的项目
TRAVERSE Company('company-001')
  <- WorksAt <- Person
  -> WorksOn -> Project;

-- 带条件的遍历:只看高级员工
TRAVERSE Company('company-001')
  <- WorksAt(weight > 0.8) <- Person
WHERE level = 'Senior'
  -> Manages -> Person;

-- 可变深度遍历:组织架构树
TRAVERSE Person('ceo-001')
  -> Manages -> Person
DEPTH 1..5;

-- 双向遍历:找关联实体
TRAVERSE Device('device-001')
  -- ConnectedTo -- Device
  -- LocatedAt -- Location
DEPTH 1..3
LIMIT 50;

-- 路径模式匹配:供应链追溯
TRAVERSE Product('prod-001')
  <- SuppliedBy <- Supplier
  <- ManufacturedBy <- Factory
  -> LocatedIn -> Region
WHERE Region.country = 'China';

#3.3 AGGREGATE:聚合分析

OQL
-- 按部门统计人数和平均年龄
AGGREGATE Person
BY department
COMPUTE COUNT(*) AS headcount,
        AVG(age) AS avg_age,
        MAX(salary) AS max_salary
WHERE is_active = true
ORDER BY headcount DESC;

-- 按时间桶统计事件
AGGREGATE Event
BY TIME_BUCKET(event_time, '1 HOUR') AS hour_bucket,
   event_type
COMPUTE COUNT(*) AS event_count,
        COUNT_DISTINCT(entity_id) AS unique_entities
WHERE severity IN ('error', 'critical')
  AND event_time BETWEEN '2024-06-01' AND '2024-06-30'
ORDER BY hour_bucket ASC;

-- 嵌套聚合:TopN
AGGREGATE Transaction
BY source_account
COMPUTE SUM(amount) AS total_amount,
        COUNT(*) AS tx_count,
        TOPN(target_account, 5) AS top_targets
WHERE transaction_date >= '2024-01-01'
HAVING total_amount > 1000000
ORDER BY total_amount DESC
LIMIT 100;

#3.4 TIMELINE:事件时间线

OQL
-- 实体的完整时间线
TIMELINE Person('person-001')
FROM '2024-01-01' TO '2024-06-30'
ORDER BY event_time DESC
LIMIT 100;

-- 只看特定事件类型
TIMELINE Device('device-001')
EVENTS Alert, StatusChange, Measurement
FROM '2024-06-01' TO '2024-06-30'
WHERE severity IN ('error', 'critical')
ORDER BY event_time DESC;

-- 带关联追踪的时间线
TIMELINE Order('order-001')
EVENTS StatusChange
WHERE correlation_id = 'trace-abc-123'
ORDER BY event_time ASC;

#4. 指标展开机制

#4.1 指标定义

YAML
# metrics-registry/person-metrics.yaml
metrics:
  - name: direct_reports
    entity_type: Person
    description: "直接下属人数"
    type: count
    definition:
      edge_type: Manages
      direction: outbound
      count: targets

  - name: total_projects
    entity_type: Person
    description: "参与项目总数"
    type: count
    definition:
      edge_type: WorksOn
      direction: outbound
      count: targets

  - name: avg_performance_score
    entity_type: Person
    description: "平均绩效得分"
    type: aggregate
    definition:
      event_type: PerformanceReview
      field: payload.score
      function: avg
      time_range: last_12_months

  - name: alert_frequency
    entity_type: Device
    description: "告警频率(每天)"
    type: rate
    definition:
      event_type: Alert
      time_range: last_30_days
      unit: per_day

#4.2 展开过程

Code
指标展开的编译过程:

输入 OQL:
  FETCH Person WHERE department = 'Eng'
  WITH METRIC direct_reports, total_projects

编译步骤 1:查找指标定义
  direct_reports → COUNT(edge WHERE Manages outbound)
  total_projects → COUNT(edge WHERE WorksOn outbound)

编译步骤 2:生成子查询
  ┌─────────────────────────────────────────────┐
  │ SELECT ec.*,                                 │
  │   (SELECT COUNT(*)                           │
  │    FROM entity_edge ee                       │
  │    WHERE ee.source_id = ec.entity_id         │
  │      AND ee.edge_type = 'Manages'            │
  │      AND ee.is_deleted = FALSE               │
  │   ) AS direct_reports,                       │
  │   (SELECT COUNT(*)                           │
  │    FROM entity_edge ee                       │
  │    WHERE ee.source_id = ec.entity_id         │
  │      AND ee.edge_type = 'WorksOn'            │
  │      AND ee.is_deleted = FALSE               │
  │   ) AS total_projects                        │
  │ FROM entity_common ec                        │
  │ WHERE ec.entity_type = 'Person'              │
  │   AND JSON_EXTRACT(ec.properties,            │
  │       '$.department') = 'Eng'                │
  └─────────────────────────────────────────────┘

编译步骤 3:优化
  - 子查询 → 左连接聚合(如果记录数多)
  - 检查是否有预计算的物化视图
  - 检查 computed_props 缓存

#5. 与其他查询语言对比

#5.1 对比矩阵

Code
OQL vs 其他查询语言对比:

┌──────────────┬──────────┬──────────┬──────────┬──────────┐
│ 特性          │   OQL    │   SQL    │  Cypher  │ GraphQL  │
├──────────────┼──────────┼──────────┼──────────┼──────────┤
│ 实体类型感知  │  原生    │  手动    │  标签    │  Schema  │
│ 图遍历        │ TRAVERSE │ 多JOIN   │  原生    │  嵌套    │
│ 聚合分析      │ AGGREGATE│  原生    │  弱      │  弱      │
│ 全文搜索      │ SEARCH   │ 扩展     │  插件    │  无      │
│ 向量搜索      │ SIMILAR  │ 扩展     │  无      │  无      │
│ 时间旅行      │ AT TIME  │ 无       │  无      │  无      │
│ 分支对比      │ DIFF     │ 无       │  无      │  无      │
│ 指标展开      │ METRIC   │ 子查询   │  无      │  无      │
│ 事件时间线    │ TIMELINE │ 手动     │  路径    │  无      │
│ 翻译到 SQL    │  是      │  N/A    │  否      │  自定义  │
│ OLAP 优化     │  继承    │  原生    │  弱      │  弱      │
└──────────────┴──────────┴──────────┴──────────┴──────────┘

#5.2 等价查询对比

Code
同一查询在四种语言中的写法:

需求:"找到张三管理的所有人,以及他们参与的项目"

OQL (4 行):
  TRAVERSE Person('zhangsan')
    -> Manages -> Person
    -> WorksOn -> Project;

SQL (15 行):
  SELECT p2.*, proj.*
  FROM entity_common p1
  JOIN entity_edge e1 ON e1.source_id = p1.entity_id
    AND e1.edge_type = 'Manages'
  JOIN entity_common p2 ON p2.entity_id = e1.target_id
  JOIN entity_edge e2 ON e2.source_id = p2.entity_id
    AND e2.edge_type = 'WorksOn'
  JOIN entity_common proj ON proj.entity_id = e2.target_id
  WHERE p1.entity_id = 'zhangsan'
    AND p1.world_id = 'main'
    AND e1.world_id = 'main'
    AND p2.world_id = 'main'
    AND e2.world_id = 'main'
    AND proj.world_id = 'main';

Cypher (6 行):
  MATCH (p1:Person {id: 'zhangsan'})
    -[:Manages]->(p2:Person)
    -[:WorksOn]->(proj:Project)
  RETURN p2, proj;

GraphQL (12 行):
  query {
    person(id: "zhangsan") {
      manages {
        name
        worksOn {
          name
          status
        }
      }
    }
  }

#6. 语法扩展:高级特性

#6.1 管道操作符

OQL
-- 管道操作符 |> 允许链式处理
FETCH Person
WHERE department = 'Engineering'
|> TRAVERSE -> WorksOn -> Project
|> AGGREGATE BY Project.status
   COMPUTE COUNT(*) AS project_count;

-- 等价于嵌套查询,但可读性更好

#6.2 变量绑定

OQL
-- 使用 LET 绑定中间结果
LET engineers = FETCH Person WHERE department = 'Engineering';
LET high_performers = engineers WHERE performance_score > 90;

TRAVERSE high_performers
  -> Manages -> Person
WITH METRIC direct_reports;

#6.3 模式匹配

OQL
-- 模式匹配:找出"三角形"关系
MATCH PATTERN
  (a:Person) -> Knows -> (b:Person),
  (b:Person) -> Knows -> (c:Person),
  (c:Person) -> Knows -> (a:Person)
WHERE a.department != b.department
  AND b.department != c.department
LIMIT 100;

#7. 类型系统

#7.1 值类型

Code
OQL 值类型系统:

基本类型:
├── String          "hello"
├── Integer         42
├── Float           3.14
├── Boolean         true / false
├── DateTime        '2024-06-15T10:30:00Z'
├── Date            '2024-06-15'
├── Duration        '30 DAYS' / '2 HOURS'
└── Null            NULL

复合类型:
├── Array           [1, 2, 3] / ['a', 'b']
├── Map             {key: value}
├── Vector          VECTOR([0.1, 0.2, ...])
└── GeoPoint        GEO(31.2, 121.5)

引用类型:
├── EntityRef       Person('id-001')
├── EdgeRef         Edge('edge-001')
├── WorldRef        WORLD('main')
└── BranchRef       BRANCH('feature-x')

#7.2 函数库

OQL
-- 字符串函数
FETCH Person WHERE LOWER(name) LIKE '%zhang%';
FETCH Person WHERE LENGTH(description) > 100;

-- 数学函数
FETCH Device WHERE ABS(temperature - 25.0) < 2.0;

-- 日期函数
FETCH Person WHERE YEAR(hire_date) = 2023;
FETCH Event WHERE event_time > NOW() - INTERVAL '7 DAYS';

-- 数组函数
FETCH Person WHERE ARRAY_LENGTH(skills) >= 3;
FETCH Person WHERE ARRAY_OVERLAP(skills, ['Python', 'Java']);

-- 地理函数
FETCH Store WHERE GEO_DISTANCE(location, GEO(31.2, 121.5)) < 5000;

#8. 错误处理与提示

#8.1 语法错误提示

Code
OQL 友好的错误提示:

输入: FETCH Perso WHERE age > 30
错误: Unknown entity type 'Perso'. Did you mean 'Person'?
     FETCH Perso WHERE age > 30
           ^^^^^
     Available types: Person, Company, Device, Document

输入: FETCH Person WERE age > 30
错误: Expected 'WHERE', found 'WERE'. Did you mean 'WHERE'?
     FETCH Person WERE age > 30
                  ^^^^

输入: FETCH Person WHERE ages > 30
错误: Unknown property 'ages' on type 'Person'. Did you mean 'age'?
     Available properties: age (Int), name (String), email (String)...

#Key Takeaways

  1. OQL 将 Ontology 语义提升为查询语言的一等公民:实体类型、关系遍历、事件时间线都有专属语法,避免了 SQL 中大量的 JSON 函数和多表 JOIN。

  2. 四大核心语句覆盖所有查询场景:FETCH(实体查询)、TRAVERSE(图遍历)、AGGREGATE(聚合分析)、TIMELINE(事件时间线),加上 DIFF(分支对比)覆盖了 Ontology 数据的全部访问模式。

  3. 指标展开机制实现查询与指标定义的解耦:WITH METRIC 子句自动展开预定义指标,用户无需关心底层的子查询和聚合逻辑。

  4. OQL 最终编译为 SQL 执行:保留了 Doris OLAP 引擎的全部优化能力(物化视图、向量化执行、列式存储),不牺牲性能换取易用性。

  5. 渐进式复杂度设计:简单查询极其简洁(FETCH Person WHERE age > 30),复杂查询通过组合语法元素实现,学习曲线平滑。

#Next Article

下一篇 S3-07《OQL 解析器实现:从文本到 AST》 将深入 OQL 解析器的实现细节,包括词法分析器(Lexer)、递归下降解析器(Parser)、抽象语法树(AST)构建、查询优化器和执行计划生成。

Tags: #OQL #OntologyQueryLanguage #BNF #GraphTraversal #MetricExpansion #FETCH #TRAVERSE #AGGREGATE #TIMELINE #智策平台 #coomia-dip #数据基座