Skip to content

父子维度

下载本文档

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

1. 什么是父子维度

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

典型应用场景

  • 组织架构:公司 → 部门 → 团队 → 小组
  • 商品分类:大类 → 中类 → 小类
  • 地理区域:国家 → 省份 → 城市 → 区县
  • 菜单权限:系统 → 模块 → 页面 → 功能

2. 闭包表模式

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

优势

  • 查询任意层级的祖先/后代只需一次简单查询
  • 无需递归查询,性能更好
  • 支持任意深度的层级结构

3. 数据表结构

3.1 维度表

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

sql
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 闭包表

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

sql
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_idteam_nameparent_idteam_levelmanager_name
T001总公司NULL1张总
T002技术部T0012李经理
T003研发组T0023王组长
T004测试组T0023赵组长
T005销售部T0012钱经理
T006前端小组T0034孙组长
T007后端小组T0034周组长
T008华东区T0053吴经理
T009华北区T0053郑经理

闭包表 (team_closure)

parent_idteam_iddistance说明
T001T0010自身
T001T0021总公司 → 技术部
T001T0032总公司 → 技术部 → 研发组
T001T0042总公司 → 技术部 → 测试组
T001T0051总公司 → 销售部
T001T0063总公司 → 技术部 → 研发组 → 前端小组
T001T0073总公司 → 技术部 → 研发组 → 后端小组
T001T0082总公司 → 销售部 → 华东区
T001T0092总公司 → 销售部 → 华北区
T002T0020自身
T002T0031技术部 → 研发组
T002T0041技术部 → 测试组
T002T0062技术部 → 研发组 → 前端小组
T002T0072技术部 → 研发组 → 后端小组
T003T0030自身
T003T0061研发组 → 前端小组
T003T0071研发组 → 后端小组
T004T0040自身
T005T0050自身
T005T0081销售部 → 华东区
T005T0091销售部 → 华北区
T006T0060自身
T007T0070自身
T008T0080自身
T009T0090自身

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

销售事实表 (fact_team_sales)

team_iddate_keysales_amountsales_count
T0012024010150,0005
T0012024010260,0006
T0022024010130,0003
T0022024010235,0004
T0032024010110,0002
T0032024010212,0002
T004202401018,0001
T004202401029,0001
T00520240101100,00020
T00520240102120,00025
T006202401015,0001
T006202401026,0001
T007202401017,0001
T007202401028,0002
T0082024010145,00010
T0082024010255,00012
T0092024010140,0008
T0092024010248,00010

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

汇总数据参考

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

4. TM 模型配置

javascript
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 配置字段

字段类型必填说明
closureTableNamestring闭包表名称
closureTableSchemastring闭包表 Schema
parentKeystring闭包表中的祖先列
childKeystring闭包表中的后代列

5. 两种访问视角

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

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

设计原则

  • 默认 = 普通维度:行为与非父子维完全一致,无"魔法"
  • 层级 = 显式请求:用户明确使用 $hierarchy$ 才启用闭包表

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

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

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

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

生成的 SQL

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$captionsalesAmount
总公司50,000
总公司60,000

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

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

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

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

生成的 SQL

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$captiontotalSalesAmount
总公司648,000

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

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

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

生成的 SQL

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$captionsalesAmount
总公司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 个团队(包括自身),各团队都有销售数据:

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

5.4 层级操作符

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

op含义SQL 条件包含自身
childrenOf直接子节点distance = 1
descendantsOf所有后代distance > 0
selfAndDescendantsOf自身及所有后代无限制

支持 maxDepth 参数限制查询深度。


5.4.1 childrenOf - 查询直接子节点

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

请求

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

生成的 SQL

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$captionsalesAmount
技术部65,000
销售部220,000

5.4.2 descendantsOf - 查询所有后代

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

请求

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

生成的 SQL

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$captionsalesAmount
技术部65,000
研发组22,000
测试组17,000
销售部220,000
前端小组11,000
后端小组15,000
华东区100,000
华北区88,000

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

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

请求

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

生成的 SQL

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$captionsalesAmount
总公司110,000
技术部65,000
研发组22,000
测试组17,000
销售部220,000
前端小组11,000
后端小组15,000
华东区100,000
华北区88,000

5.4.4 maxDepth - 限制查询深度

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

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

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

生成的 SQL

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$captionsalesAmount
技术部65,000
研发组22,000
测试组17,000
销售部220,000
华东区100,000
华北区88,000

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


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

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

生成的 SQL

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$captionsalesAmount
研发组22,000
测试组17,000
前端小组11,000
后端小组15,000

5.4.5 多值查询

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

请求

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

生成的 SQL

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$captionsalesAmount
研发组22,000
测试组17,000
华东区100,000
华北区88,000

5.4.6 操作符对比表

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

6. 闭包表维护

6.1 新增节点

sql
-- 添加新团队 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 删除节点

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

下一步