Skip to content

Parent-Child Dimension

Download this document

Parent-Child Dimensions handle hierarchical structure data such as organizational structures, product categories, regions, etc.

1. What is a Parent-Child Dimension

A parent-child dimension is a self-referencing hierarchical structure where each member can have one parent and multiple children.

Typical Use Cases:

  • Organizational Structure: Company → Department → Team → Group
  • Product Categories: Major Category → Mid Category → Sub Category
  • Geographic Regions: Country → Province → City → District
  • Menu Permissions: System → Module → Page → Function

2. Closure Table Pattern

Foggy Dataset Model uses Closure Table to store hierarchical relationships, pre-storing all ancestor-descendant relationships for efficient queries.

Advantages:

  • Query ancestors/descendants at any level with a single simple query
  • No recursive queries needed, better performance
  • Supports hierarchies of arbitrary depth

3. Data Table Structure

3.1 Dimension Table

Stores basic information of dimension members:

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 Closure Table

Stores all ancestor-descendant relationships:

sql
CREATE TABLE team_closure (
    parent_id VARCHAR(64) NOT NULL,  -- Ancestor ID
    team_id VARCHAR(64) NOT NULL,    -- Descendant ID
    distance INT DEFAULT 0,          -- Distance (0 means self)
    PRIMARY KEY (parent_id, team_id)
);

-- Recommended indexes
CREATE INDEX idx_team_closure_parent ON team_closure (parent_id);
CREATE INDEX idx_team_closure_child ON team_closure (team_id);

3.3 Sample Data

The following sample data is from the foggy-dataset-demo project for demonstration and testing.

Organizational Structure

Head Office (T001)
├── Technology Dept (T002)
│   ├── R&D Group (T003)
│   │   ├── Frontend Team (T006)
│   │   └── Backend Team (T007)
│   └── QA Group (T004)
└── Sales Dept (T005)
    ├── East Region (T008)
    └── North Region (T009)

Team Dimension Table (dim_team)

team_idteam_nameparent_idteam_levelmanager_name
T001Head OfficeNULL1Manager Zhang
T002Technology DeptT0012Manager Li
T003R&D GroupT0023Lead Wang
T004QA GroupT0023Lead Zhao
T005Sales DeptT0012Manager Qian
T006Frontend TeamT0034Lead Sun
T007Backend TeamT0034Lead Zhou
T008East RegionT0053Manager Wu
T009North RegionT0053Manager Zheng

Closure Table (team_closure)

parent_idteam_iddistanceDescription
T001T0010Self
T001T0021Head Office → Technology Dept
T001T0032Head Office → Technology Dept → R&D Group
T001T0042Head Office → Technology Dept → QA Group
T001T0051Head Office → Sales Dept
T001T0063Head Office → Technology Dept → R&D Group → Frontend
T001T0073Head Office → Technology Dept → R&D Group → Backend
T001T0082Head Office → Sales Dept → East Region
T001T0092Head Office → Sales Dept → North Region
T002T0020Self
T002T0031Technology Dept → R&D Group
T002T0041Technology Dept → QA Group
T002T0062Technology Dept → R&D Group → Frontend
T002T0072Technology Dept → R&D Group → Backend
T003T0030Self
T003T0061R&D Group → Frontend Team
T003T0071R&D Group → Backend Team
T004T0040Self
T005T0050Self
T005T0081Sales Dept → East Region
T005T0091Sales Dept → North Region
T006T0060Self
T007T0070Self
T008T0080Self
T009T0090Self

Total 25 records: 9 self-records + 16 ancestor-descendant relationships

Sales Fact Table (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

Total 18 records: 9 teams × 2 days

Summary Data Reference

TeamOwn SalesIncluding Subordinates (Hierarchical Sum)
T001 Head Office110,000610,000 (Entire company)
T002 Technology Dept65,000130,000 (Including R&D, QA, Frontend/Backend)
T003 R&D Group22,00048,000 (Including Frontend/Backend teams)
T005 Sales Dept220,000408,000 (Including East, North regions)

4. TM Model Configuration

javascript
export const model = {
    name: 'FactTeamSalesModel',
    caption: 'Team Sales Fact Table',
    tableName: 'fact_team_sales',
    idColumn: 'sales_id',

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

            // === Parent-Child Dimension Config ===
            closureTableName: 'team_closure',  // Closure table name (required)
            parentKey: 'parent_id',            // Ancestor column in closure table (required)
            childKey: 'team_id',               // Descendant column in closure table (required)

            properties: [
                { column: 'team_id', caption: 'Team ID' },
                { column: 'team_name', caption: 'Team Name' },
                { column: 'parent_id', caption: 'Parent Team' },
                { column: 'level', caption: 'Level', alias: 'teamLevel' }
            ]
        }
    ],

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

4.1 Configuration Fields

FieldTypeRequiredDescription
closureTableNamestringYesClosure table name
closureTableSchemastringNoClosure table schema
parentKeystringYesAncestor column in closure table
childKeystringYesDescendant column in closure table

5. Two Access Perspectives

Parent-child dimensions provide two access perspectives with clear semantics and consistent behavior:

PerspectiveColumn FormatBehaviorUse Case
Default Perspectiveteam$id, team$captionSame as regular dimensions, exact matchExact query, detail display
Hierarchy Perspectiveteam$hierarchy$id, team$hierarchy$captionUses closure table, matches node and all descendantsHierarchical aggregation, descendant range filtering

Design Principles:

  • Default = Regular Dimension: Behaves exactly like non-parent-child dimensions, no "magic"
  • Hierarchy = Explicit Request: Closure table only activated when user explicitly uses $hierarchy$

5.1 Default Perspective (Regular Dimension Behavior)

Using columns like team$id, team$caption behaves exactly like regular dimensions.

Example: Query only T001's own sales data

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

Generated 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

Returned Data (only T001's own sales):

team$captionsalesAmount
Head Office50,000
Head Office60,000

5.2 Hierarchy Perspective (Using Closure Table)

Using columns like team$hierarchy$id, team$hierarchy$caption activates closure table for hierarchical operations.

Scenario 1: Hierarchical Aggregation (Aggregate to Ancestor Node)

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

Generated 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

Returned Data:

team$hierarchy$captiontotalSalesAmount
Head Office648,000

Explanation: Sales data of T001 and all its descendants (T002-T009) aggregated as "Head Office".

Scenario 2: Descendant Details (Show Each Descendant Separately)

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

Generated 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

Returned Data (9 records, one per descendant):

team$captionsalesAmount
Head Office110,000
Technology Dept65,000
R&D Group22,000
QA Group17,000
Sales Dept220,000
Frontend Team11,000
Backend Team15,000
East Region100,000
North Region88,000

Explanation: Use team$hierarchy$id to filter descendant range, but group by default team$caption to show details of each team.


5.3 Perspective Comparison Summary

Assuming T001 (Head Office) has 9 teams (including itself), each with sales data:

Query TypeslicegroupByRecords ReturnedDescription
Exact Matchteam$id = T001team$caption1 recordOnly T001 itself
Hierarchy Aggregationteam$hierarchy$id = T001team$hierarchy$caption1 recordAggregate to T001
Descendant Detailsteam$hierarchy$id = T001team$caption9 recordsEach descendant shown separately

5.4 Hierarchy Operators

Besides the $hierarchy$ perspective, fine-grained hierarchical queries are supported via op operators without using $hierarchy$ column names:

opMeaningSQL ConditionIncludes Self
childrenOfDirect childrendistance = 1No
descendantsOfAll descendantsdistance > 0No
selfAndDescendantsOfSelf and all descendantsNo restrictionYes

Supports maxDepth parameter to limit query depth.


5.4.1 childrenOf - Query Direct Children

Query direct children of a specified node (distance = 1).

Request:

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

Generated 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

Returned Data (T001's direct sub-departments):

team$captionsalesAmount
Technology Dept65,000
Sales Dept220,000

5.4.2 descendantsOf - Query All Descendants

Query all descendants of a specified node, excluding itself (distance > 0).

Request:

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

Generated 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

Returned Data (All T001's descendants, excluding T001):

team$captionsalesAmount
Technology Dept65,000
R&D Group22,000
QA Group17,000
Sales Dept220,000
Frontend Team11,000
Backend Team15,000
East Region100,000
North Region88,000

5.4.3 selfAndDescendantsOf - Query Self and All Descendants

Query specified node and all its descendants (equivalent to $hierarchy$ perspective).

Request:

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

Generated 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

Returned Data (T001 and all descendants):

team$captionsalesAmount
Head Office110,000
Technology Dept65,000
R&D Group22,000
QA Group17,000
Sales Dept220,000
Frontend Team11,000
Backend Team15,000
East Region100,000
North Region88,000

5.4.4 maxDepth - Limit Query Depth

Use maxDepth parameter to limit the depth of hierarchical queries.

Example 1: Query T001's descendants within 2 levels

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

Generated 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

Returned Data (T001's descendants within 2 levels, i.e., children and grandchildren):

team$captionsalesAmount
Technology Dept65,000
R&D Group22,000
QA Group17,000
Sales Dept220,000
East Region100,000
North Region88,000

Note: Excludes T006 (Frontend Team) and T007 (Backend Team) as they are 3 levels away from T001.


Example 2: childrenOf + maxDepth (Extend children range)

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

Generated 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

Returned Data (T002's children within 2 levels):

team$captionsalesAmount
R&D Group22,000
QA Group17,000
Frontend Team11,000
Backend Team15,000

5.4.5 Multi-Value Query

Hierarchy operators support passing multiple values to query descendants of multiple nodes.

Request:

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

Generated 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

Returned Data (Direct children of T002 and T005):

team$captionsalesAmount
R&D Group22,000
QA Group17,000
East Region100,000
North Region88,000

5.4.6 Operator Comparison Table

Query NeedRecommended ApproachDescription
Exact match on a nodeteam$id = 'T001'Default perspective
Aggregate to a nodeteam$hierarchy$id + team$hierarchy$captionHierarchy perspective
Descendant detailsteam$hierarchy$id + team$captionMixed perspective
Direct childrenop: childrenOfHierarchy operator
All descendants (excluding self)op: descendantsOfHierarchy operator
Depth-limited queryop + maxDepthHierarchy operator

6. Closure Table Maintenance

6.1 Add Node

sql
-- Add new team T010 (under R&D Group T003)
INSERT INTO dim_team VALUES ('T010', 'Backend Team', 'T003', 4, 'ACTIVE');

-- Insert self relationship
INSERT INTO team_closure (parent_id, team_id, distance)
VALUES ('T010', 'T010', 0);

-- Insert all ancestor-to-new-node relationships
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 Node

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

7. Differences from Regular Dimensions

FeatureRegular DimensionParent-Child Dimension
Hierarchy SupportFixed hierarchy (e.g., year-month-day)Arbitrary depth dynamic hierarchy
Association MethodDirect foreign keySupports closure table association
Query BehaviorExact matchDefault exact match, $hierarchy$ enables hierarchy operations
Data StructureSingle tableDimension table + Closure table
Available Columnsdim$id, dim$captionAdditionally supports $hierarchy$ perspective
Maintenance ComplexityLowMedium

8. Best Practices

  1. Index Optimization: Create indexes on parent_id and team_id columns in closure table
  2. Data Consistency: Use transactions to ensure consistency between dimension and closure tables
  3. Hierarchy Depth: Recommend controlling hierarchy depth; excessive depth impacts performance
  4. distance Field: Although not required, helps query specific hierarchy levels
  5. Perspective Selection:
    • Exact match on a node → Use default perspective team$id
    • Need to aggregate to a node → Use team$hierarchy$id + team$hierarchy$caption
    • Need to view descendant details → Use team$hierarchy$id + team$caption

Next Steps