返回正文目录

父子维度

父子维度(Parent-Child Dimension)用于处理层级结构数据,如组织架构、商品分类、地区等。

1. 什么是父子维度

父子维度是一种自引用的层级结构,每个成员可以有一个父成员和多个子成员。

典型应用场景:

2. 闭包表模式

Foggy Dataset Model 使用闭包表(Closure Table)存储层级关系,预存所有祖先-后代关系,实现高效查询。

优势:


3. 数据表结构

3.1 维度表

存储维度成员的基本信息:

CREATE TABLE dim_team (
    team_id VARCHAR(64) PRIMARY KEY,
    team_name VARCHAR(100) NOT NULL,
    parent_id VARCHAR(64),
    level INT,
    status VARCHAR(20) DEFAULT 'ACTIVE'
);

3.2 闭包表

存储所有祖先-后代关系:

CREATE TABLE team_closure (
    parent_id VARCHAR(64) NOT NULL,  -- 祖先 ID
    team_id VARCHAR(64) NOT NULL,    -- 后代 ID
    distance INT DEFAULT 0,          -- 距离(0 表示自身)
    PRIMARY KEY (parent_id, team_id)
);

-- 建议索引
CREATE INDEX idx_team_closure_parent ON team_closure (parent_id);
CREATE INDEX idx_team_closure_child ON team_closure (team_id);

3.3 示例数据

以下示例数据来自 foggy-dataset-demo 项目,用于演示和测试。

组织架构

总公司 (T001)
├── 技术部 (T002)
│   ├── 研发组 (T003)
│   │   ├── 前端小组 (T006)
│   │   └── 后端小组 (T007)
│   └── 测试组 (T004)
└── 销售部 (T005)
    ├── 华东区 (T008)
    └── 华北区 (T009)

团队维度表 (dim_team)

team_id team_name parent_id team_level manager_name
T001 总公司 NULL 1 张总
T002 技术部 T001 2 李经理
T003 研发组 T002 3 王组长
T004 测试组 T002 3 赵组长
T005 销售部 T001 2 钱经理
T006 前端小组 T003 4 孙组长
T007 后端小组 T003 4 周组长
T008 华东区 T005 3 吴经理
T009 华北区 T005 3 郑经理

闭包表 (team_closure)

parent_id team_id distance 说明
T001 T001 0 自身
T001 T002 1 总公司 → 技术部
T001 T003 2 总公司 → 技术部 → 研发组
T001 T004 2 总公司 → 技术部 → 测试组
T001 T005 1 总公司 → 销售部
T001 T006 3 总公司 → 技术部 → 研发组 → 前端小组
T001 T007 3 总公司 → 技术部 → 研发组 → 后端小组
T001 T008 2 总公司 → 销售部 → 华东区
T001 T009 2 总公司 → 销售部 → 华北区
T002 T002 0 自身
T002 T003 1 技术部 → 研发组
T002 T004 1 技术部 → 测试组
T002 T006 2 技术部 → 研发组 → 前端小组
T002 T007 2 技术部 → 研发组 → 后端小组
T003 T003 0 自身
T003 T006 1 研发组 → 前端小组
T003 T007 1 研发组 → 后端小组
T004 T004 0 自身
T005 T005 0 自身
T005 T008 1 销售部 → 华东区
T005 T009 1 销售部 → 华北区
T006 T006 0 自身
T007 T007 0 自身
T008 T008 0 自身
T009 T009 0 自身

共 25 条记录:9 条自身记录 + 16 条祖先-后代关系

销售事实表 (fact_team_sales)

team_id date_key sales_amount sales_count
T001 20240101 50,000 5
T001 20240102 60,000 6
T002 20240101 30,000 3
T002 20240102 35,000 4
T003 20240101 10,000 2
T003 20240102 12,000 2
T004 20240101 8,000 1
T004 20240102 9,000 1
T005 20240101 100,000 20
T005 20240102 120,000 25
T006 20240101 5,000 1
T006 20240102 6,000 1
T007 20240101 7,000 1
T007 20240102 8,000 2
T008 20240101 45,000 10
T008 20240102 55,000 12
T009 20240101 40,000 8
T009 20240102 48,000 10

共 18 条记录:9 个团队 × 2 天

汇总数据参考

团队 自身销售额 含下属销售额(层级汇总)
T001 总公司 110,000 610,000(全公司)
T002 技术部 65,000 130,000(含研发组、测试组、前端/后端小组)
T003 研发组 22,000 48,000(含前端/后端小组)
T005 销售部 220,000 408,000(含华东区、华北区)

4. TM 模型配置

export const model = {
    name: 'FactTeamSalesModel',
    caption: '团队销售事实表',
    tableName: 'fact_team_sales',
    idColumn: 'sales_id',

    dimensions: [
        {
            name: 'team',
            tableName: 'dim_team',
            foreignKey: 'team_id',
            primaryKey: 'team_id',
            captionColumn: 'team_name',
            caption: '团队',

            // === 父子维度配置 ===
            closureTableName: 'team_closure',  // 闭包表名(必填)
            parentKey: 'parent_id',            // 闭包表祖先列(必填)
            childKey: 'team_id',               // 闭包表后代列(必填)

            properties: [
                { column: 'team_id', caption: '团队ID' },
                { column: 'team_name', caption: '团队名称' },
                { column: 'parent_id', caption: '上级团队' },
                { column: 'level', caption: '层级', alias: 'teamLevel' }
            ]
        }
    ],

    properties: [...],
    measures: [...]
};

4.1 配置字段

字段 类型 必填 说明
closureTableName string 是 闭包表名称
closureTableSchema string 否 闭包表 Schema
parentKey string 是 闭包表中的祖先列
childKey string 是 闭包表中的后代列

5. 两种访问视角

父子维度提供两种访问视角,语义清晰,行为一致:

视角 列名格式 行为 用途
默认视角 team$id, team$caption 与普通维度相同,精确匹配 精确查询、明细展示
层级视角 team$hierarchy$id, team$hierarchy$caption 使用闭包表,匹配节点及所有后代 层级汇总、后代范围筛选

设计原则:


5.1 默认视角(普通维度行为)

使用 team$id、team$caption 等列,行为与普通维度完全相同。

示例:只查 T001 自身的销售数据

{
    "param": {
        "columns": ["team$caption", "salesAmount"],
        "slice": [
            { "field": "team$id", "op": "=", "value": "T001" }
        ]
    }
}

生成的 SQL:

SELECT d1.team_name, t0.sales_amount
FROM fact_team_sales t0
LEFT JOIN dim_team d1 ON t0.team_id = d1.team_id
WHERE d1.team_id = 'T001'
GROUP BY d1.team_name

返回数据(只包含 T001 自身的销售):

team$caption salesAmount
总公司 50,000
总公司 60,000

5.2 层级视角(使用闭包表)

使用 team$hierarchy$id、team$hierarchy$caption 等列,启用闭包表进行层级操作。

场景 1:层级汇总(汇总到祖先节点)

{
    "param": {
        "columns": ["team$hierarchy$caption", "salesAmount"],
        "slice": [
            { "field": "team$hierarchy$id", "op": "=", "value": "T001" }
        ],
        "groupBy": [
            { "field": "team$hierarchy$caption" }
        ]
    }
}

生成的 SQL:

SELECT d4.team_name AS "team$hierarchy$caption",
       SUM(t0.sales_amount) AS "salesAmount"
FROM fact_team_sales t0
LEFT JOIN team_closure d2 ON t0.team_id = d2.team_id
LEFT JOIN dim_team d4 ON d2.parent_id = d4.team_id
WHERE d2.parent_id = 'T001'
GROUP BY d4.team_name

返回数据:

team$hierarchy$caption totalSalesAmount
总公司 648,000

说明:T001 及其所有后代(T002-T009)的销售数据汇总显示为"总公司"。

场景 2:后代明细(分别显示各后代)

{
    "param": {
        "columns": ["team$caption", "salesAmount"],
        "slice": [
            { "field": "team$hierarchy$id", "op": "=", "value": "T001" }
        ],
        "groupBy": [
            { "field": "team$caption" }
        ]
    }
}

生成的 SQL:

SELECT d1.team_name AS "team$caption",
       SUM(t0.sales_amount) AS "salesAmount"
FROM fact_team_sales t0
LEFT JOIN dim_team d1 ON t0.team_id = d1.team_id
LEFT JOIN team_closure d2 ON t0.team_id = d2.team_id
WHERE d2.parent_id = 'T001'
GROUP BY d1.team_name

返回数据(9 条记录,每个后代一条):

team$caption salesAmount
总公司 110,000
技术部 65,000
研发组 22,000
测试组 17,000
销售部 220,000
前端小组 11,000
后端小组 15,000
华东区 100,000
华北区 88,000

说明:使用 team$hierarchy$id 过滤后代范围,但用默认的 team$caption 分组显示各团队明细。


5.3 视角对比总结

假设 T001(总公司)有 9 个团队(包括自身),各团队都有销售数据:

查询方式 slice groupBy 返回记录数 说明
精确匹配 team$id = T001 team$caption 1 条 只查 T001 自身
层级汇总 team$hierarchy$id = T001 team$hierarchy$caption 1 条 汇总到 T001
后代明细 team$hierarchy$id = T001 team$caption 9 条 各后代分别显示

5.4 层级操作符

除了 $hierarchy$ 视角,还支持通过 op 操作符进行细粒度层级查询,无需使用 $hierarchy$ 列名:

op 方向 含义 SQL 条件 包含自身
childrenOf 后代 ↓ 直接子节点 distance = 1 否
descendantsOf 后代 ↓ 所有后代 distance > 0 否
selfAndDescendantsOf 后代 ↓ 自身及所有后代 无限制 是
selfAndAncestorsOf 祖先 ↑ 自身及所有祖先 无限制 是
ancestorsOf 祖先 ↑ 所有祖先 distance > 0 否

所有操作符均支持 maxDepth 参数限制查询深度。


5.4.1 childrenOf - 查询直接子节点

查询指定节点的直接子节点(distance = 1)。

请求:

{
    "param": {
        "columns": ["team$caption", "salesAmount"],
        "slice": [
            { "field": "team$id", "op": "childrenOf", "value": "T001" }
        ],
       "groupBy": [
          { "field": "team$caption" }
       ]
    }
}

生成的 SQL:

SELECT d1.team_name AS "team$caption",
       SUM(t0.sales_amount) AS "salesAmount"
FROM fact_team_sales t0
LEFT JOIN dim_team d1 ON t0.team_id = d1.team_id
LEFT JOIN team_closure d2 ON t0.team_id = d2.team_id
WHERE d2.parent_id = 'T001' AND d2.distance = 1
GROUP BY d1.team_name

返回数据(T001 的直接子部门):

team$caption salesAmount
技术部 65,000
销售部 220,000

5.4.2 descendantsOf - 查询所有后代

查询指定节点的所有后代,不包含自身(distance > 0)。

请求:

{
    "param": {
        "columns": ["team$caption", "salesAmount"],
        "slice": [
            { "field": "team$id", "op": "descendantsOf", "value": "T001" }
        ],
       "groupBy": [
          { "field": "team$caption" }
       ]
    }
}

生成的 SQL:

SELECT d1.team_name AS "team$caption",
       SUM(t0.sales_amount) AS "salesAmount"
FROM fact_team_sales t0
LEFT JOIN dim_team d1 ON t0.team_id = d1.team_id
LEFT JOIN team_closure d2 ON t0.team_id = d2.team_id
WHERE d2.parent_id = 'T001' AND d2.distance > 0
GROUP BY d1.team_name

返回数据(T001 的所有后代,不含 T001):

team$caption salesAmount
技术部 65,000
研发组 22,000
测试组 17,000
销售部 220,000
前端小组 11,000
后端小组 15,000
华东区 100,000
华北区 88,000

5.4.3 selfAndDescendantsOf - 查询自身及所有后代

查询指定节点及其所有后代(等效于 $hierarchy$ 视角)。

请求:

{
    "param": {
        "columns": ["team$caption", "salesAmount"],
        "slice": [
            { "field": "team$id", "op": "selfAndDescendantsOf", "value": "T001" }
        ],
       "groupBy": [
          { "field": "team$caption" }
       ]
    }
}

生成的 SQL:

SELECT d1.team_name AS "team$caption",
       SUM(t0.sales_amount) AS "salesAmount"
FROM fact_team_sales t0
LEFT JOIN dim_team d1 ON t0.team_id = d1.team_id
LEFT JOIN team_closure d2 ON t0.team_id = d2.team_id
WHERE d2.parent_id = 'T001'
GROUP BY d1.team_name

返回数据(T001 及所有后代):

team$caption salesAmount
总公司 110,000
技术部 65,000
研发组 22,000
测试组 17,000
销售部 220,000
前端小组 11,000
后端小组 15,000
华东区 100,000
华北区 88,000

5.4.4 selfAndAncestorsOf - 查询自身及所有祖先

查询指定节点及其所有祖先(反向 JOIN 闭包表)。典型场景:Odoo parent_of 权限规则。

请求:

{
    "param": {
        "columns": ["team$caption", "salesAmount"],
        "slice": [
            { "field": "team$id", "op": "selfAndAncestorsOf", "value": "T003" }
        ],
       "groupBy": [
          { "field": "team$caption" }
       ]
    }
}

生成的 SQL(注意 JOIN 方向反转):

SELECT d1.team_name AS "team$caption",
       SUM(t0.sales_amount) AS "salesAmount"
FROM fact_team_sales t0
LEFT JOIN dim_team d1 ON t0.team_id = d1.team_id
LEFT JOIN team_closure d2 ON t0.team_id = d2.parent_id
WHERE d2.team_id = 'T003'
GROUP BY d1.team_name

返回数据(T003 研发组 + 其所有祖先):

team$caption salesAmount
总公司 110,000
技术部 65,000
研发组 22,000

5.4.5 ancestorsOf - 查询所有祖先(不含自身)

查询指定节点的所有祖先,不包含自身(distance > 0)。

请求:

{
    "param": {
        "columns": ["team$caption", "salesAmount"],
        "slice": [
            { "field": "team$id", "op": "ancestorsOf", "value": "T003" }
        ],
       "groupBy": [
          { "field": "team$caption" }
       ]
    }
}

生成的 SQL:

SELECT d1.team_name AS "team$caption",
       SUM(t0.sales_amount) AS "salesAmount"
FROM fact_team_sales t0
LEFT JOIN dim_team d1 ON t0.team_id = d1.team_id
LEFT JOIN team_closure d2 ON t0.team_id = d2.parent_id
WHERE d2.team_id = 'T003' AND d2.distance > 0
GROUP BY d1.team_name

返回数据(T003 的祖先,不含 T003 自身):

team$caption salesAmount
总公司 110,000
技术部 65,000

5.4.6 maxDepth - 限制查询深度

使用 maxDepth 参数限制层级查询的深度。

示例 1:查询 T001 的 2 级以内后代

{
    "param": {
        "columns": ["team$caption", "salesAmount"],
        "slice": [
            { "field": "team$id", "op": "descendantsOf", "value": "T001", "maxDepth": 2 }
        ],
       "groupBy": [
          { "field": "team$caption" }
       ]
    }
}

生成的 SQL:

SELECT d1.team_name AS "team$caption",
       SUM(t0.sales_amount) AS "salesAmount"
FROM fact_team_sales t0
LEFT JOIN dim_team d1 ON t0.team_id = d1.team_id
LEFT JOIN team_closure d2 ON t0.team_id = d2.team_id
WHERE d2.parent_id = 'T001' AND d2.distance BETWEEN 1 AND 2
GROUP BY d1.team_name

返回数据(T001 的 2 级以内后代,即子和孙):

team$caption salesAmount
技术部 65,000
研发组 22,000
测试组 17,000
销售部 220,000
华东区 100,000
华北区 88,000

注:不包含 T006(前端小组)和 T007(后端小组),因为它们距离 T001 是 3 级。


示例 2:childrenOf + maxDepth(扩展子节点范围)

{
    "param": {
        "columns": ["team$caption", "salesAmount"],
        "slice": [
            { "field": "team$id", "op": "childrenOf", "value": "T002", "maxDepth": 2 }
        ],
       "groupBy": [
          { "field": "team$caption" }
       ]
    }
}

生成的 SQL:

SELECT d1.team_name AS "team$caption",
       SUM(t0.sales_amount) AS "salesAmount"
FROM fact_team_sales t0
LEFT JOIN dim_team d1 ON t0.team_id = d1.team_id
LEFT JOIN team_closure d2 ON t0.team_id = d2.team_id
WHERE d2.parent_id = 'T002' AND d2.distance BETWEEN 1 AND 2
GROUP BY d1.team_name

返回数据(T002 的 2 级以内子节点):

team$caption salesAmount
研发组 22,000
测试组 17,000
前端小组 11,000
后端小组 15,000

5.4.7 多值查询

层级操作符支持传入多个值,查询多个节点的后代。

请求:

{
    "param": {
        "columns": ["team$caption", "salesAmount"],
        "slice": [
            { "field": "team$id", "op": "childrenOf", "value": ["T002", "T005"] }
        ],
       "groupBy": [
          { "field": "team$caption" }
       ]
    }
}

生成的 SQL:

SELECT d1.team_name AS "team$caption",
       SUM(t0.sales_amount) AS "salesAmount"
FROM fact_team_sales t0
LEFT JOIN dim_team d1 ON t0.team_id = d1.team_id
LEFT JOIN team_closure d2 ON t0.team_id = d2.team_id
WHERE d2.parent_id IN ('T002', 'T005') AND d2.distance = 1
GROUP BY d1.team_name

返回数据(T002 和 T005 的直接子节点):

team$caption salesAmount
研发组 22,000
测试组 17,000
华东区 100,000
华北区 88,000

5.4.8 操作符对比表

查询需求 推荐方式 说明
精确匹配某节点 team$id = 'T001' 默认视角
汇总到某节点 team$hierarchy$id + team$hierarchy$caption 层级视角
各后代明细 team$hierarchy$id + team$caption 混合视角
直接子节点 op: childrenOf 层级操作符
所有后代(不含自身) op: descendantsOf 层级操作符
自身及所有祖先 op: selfAndAncestorsOf 祖先方向操作符
所有祖先(不含自身) op: ancestorsOf 祖先方向操作符
限定深度查询 op + maxDepth 所有操作符均支持

6. 闭包表维护

6.1 新增节点

-- 添加新团队 T010(隶属于研发组 T003)
INSERT INTO dim_team VALUES ('T010', '后端小组', 'T003', 4, 'ACTIVE');

-- 插入自身关系
INSERT INTO team_closure (parent_id, team_id, distance)
VALUES ('T010', 'T010', 0);

-- 插入所有祖先到新节点的关系
INSERT INTO team_closure (parent_id, team_id, distance)
SELECT parent_id, 'T010', distance + 1
FROM team_closure
WHERE team_id = 'T003';

6.2 删除节点

DELETE FROM team_closure WHERE team_id = 'T010' OR parent_id = 'T010';
DELETE FROM dim_team WHERE team_id = 'T010';

7. 与普通维度的区别

特性 普通维度 父子维度
层级支持 固定层级(如年-月-日) 任意深度动态层级
关联方式 直接外键关联 支持闭包表关联
查询行为 精确匹配 默认精确匹配,$hierarchy$ 启用层级操作
数据结构 单表 维度表 + 闭包表
可用列 dim$id, dim$caption 额外支持 $hierarchy$ 视角
维护复杂度 低 中等

8. 最佳实践

  1. 索引优化:在闭包表的 parent_id 和 team_id 列建立索引
  2. 数据一致性:使用事务确保维度表和闭包表的一致性
  3. 层级深度:建议控制层级深度,过深会影响性能
  4. distance 字段:虽非必需,但有助于查询特定层级的数据
  5. 视角选择:
    • 精确匹配某节点 → 使用默认视角 team$id
    • 需要汇总到某节点 → 使用 team$hierarchy$id + team$hierarchy$caption
    • 需要查看后代明细 → 使用 team$hierarchy$id + team$caption

下一步