低代码平台的数据架构设计:动态表单存储与查询优化

低代码平台的灵魂不是拖拽界面,而是底层的数据架构。本文拆解动态表单的存储模型、查询优化和扩展性设计,给正在做或准备做低代码平台的技术人一个参考。

做低代码平台开发这几年,踩过的最大的坑就是数据存储。

表面上看,低代码就是让用户拖拖拽拽搭个表单,能有多复杂?等你真正实现了才知道,"动态"两个字背后是一整套数据架构的挑战。

一、动态表单存储的三种主流方案

低代码平台的核心问题:用户定义的表单结构是动态的,你怎么存?

业界主要有三种方案,各有利弊。

方案一:EAV 模型(Entity-Attribute-Value)

最经典的方案,每个字段值存成一行:

复制代码
CREATE TABLE entity_values (
    id BIGINT PRIMARY KEY,
    entity_id BIGINT,       -- 数据行ID
    field_id BIGINT,        -- 字段定义ID
    value_text TEXT,        -- 文本值
    value_number DECIMAL,   -- 数值
    value_date TIMESTAMP,   -- 日期
    INDEX idx_entity (entity_id),
    INDEX idx_field_value (field_id, value_text(255))
);

优点

  • 结构极其灵活,新增字段不需要改表

  • 字段级别权限控制容易实现

缺点

  • 查询性能灾难------一行数据散落在N条记录里,JOIN 到怀疑人生

  • 数据量大时索引膨胀严重

  • 类型安全弱,一个字段可能被存成文本也可能被存成数字

复制代码
# EAV 查询示例:获取某个表单的所有数据
def query_eav(form_id, page=1, page_size=20):
    # 需要先查出所有字段定义
    fields = db.query("SELECT * FROM field_defs WHERE form_id = ?", form_id)
    
    # 然后 pivot 查询把行转列
    entity_ids = db.query("""
        SELECT DISTINCT entity_id FROM entity_values 
        WHERE field_id IN (SELECT id FROM field_defs WHERE form_id = ?)
        ORDER BY entity_id 
        LIMIT ? OFFSET ?
    """, form_id, page_size, (page-1)*page_size)
    
    result = []
    for eid in entity_ids:
        row = {'id': eid}
        values = db.query("""
            SELECT field_id, value_text, value_number, value_date 
            FROM entity_values WHERE entity_id = ?
        """, eid)
        for v in values:
            field = fields[v.field_id]
            row[field.name] = v.value_text or v.value_number or v.value_date
        result.append(row)
    return result

这个查询复杂度......数据量上来之后基本不可用。

方案二:JSON 文档存储

把整行数据存成一个 JSON 字段:

复制代码
CREATE TABLE form_data (
    id BIGINT PRIMARY KEY,
    form_id BIGINT,
    data JSONB,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    INDEX idx_form (form_id),
    GIN INDEX idx_data (data)  -- JSON 索引
);
​
-- 示例数据
-- {"name": "张三", "age": 30, "department": "研发部", "status": "在职"}

优点

  • 存取简单,一行就是一条完整记录

  • 不需要预定义表结构

  • 现代数据库(PostgreSQL、MySQL 5.7+)对 JSONB 有良好的索引支持

缺点

  • 复杂查询性能不如列式存储

  • 单字段更新需要读-改-写整个 JSON

  • 存储空间相对较大

复制代码
# JSON 方案查询
def query_json(form_id, filters=None, page=1, page_size=20):
    query = "SELECT * FROM form_data WHERE form_id = ?"
    params = [form_id]
    
    if filters:
        for key, value in filters.items():
            # PostgreSQL JSONB 查询
            query += f" AND data->>'{key}' = ?"
            params.append(str(value))
    
    query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
    params.extend([page_size, (page-1)*page_size])
    
    return db.query(query, params)

性能比 EAV 好很多,但大数据量下仍然有瓶颈。

方案三:动态 DDL(运行时建表)

最激进的方案------每个表单直接创建一张真实的数据库表:

复制代码
def create_form_table(form_id, fields):
    """根据表单定义动态创建数据表"""
    table_name = f"form_{form_id}"
    
    columns = ["id BIGINT PRIMARY KEY AUTO_INCREMENT"]
    columns.append("created_by BIGINT")
    columns.append("created_at TIMESTAMP DEFAULT NOW()")
    columns.append("updated_at TIMESTAMP DEFAULT NOW()")
    
    for field in fields:
        col_type = map_field_type(field.type)
        col_def = f"`{field.name}` {col_type}"
        if field.required:
            col_def += " NOT NULL"
        if field.indexed:
            col_def += ", INDEX"
        columns.append(col_def)
    
    sql = f"CREATE TABLE `{table_name}` ({', '.join(columns)})"
    db.execute(sql)
​
def map_field_type(field_type):
    type_map = {
        'text': 'VARCHAR(500)',
        'number': 'DECIMAL(15,4)',
        'date': 'DATE',
        'datetime': 'DATETIME',
        'select': 'VARCHAR(100)',
        'boolean': 'TINYINT(1)',
        'long_text': 'TEXT',
    }
    return type_map.get(field_type, 'VARCHAR(500)')

优点

  • 查询性能最优,就是普通的关系型查询

  • 数据库原生索引、约束全部可用

  • SQL 直接写,调试方便

缺点

  • 需要 DBA 级别的权限来动态建表

  • 字段变更(加字段、改类型)需要 ALTER TABLE,大表会很慢

  • 表单数量多时,数据库表数量爆炸

  • 跨表单查询困难

二、我们的选择:混合方案

实际做下来,单一方案都不够好。我们采用了混合策略:

复制代码
┌─────────────────────────────────────────┐
│              应用层 API                   │
├─────────────────────────────────────────┤
│           路由层 (Query Router)           │
│    根据查询类型选择不同的存储引擎          │
├──────────┬──────────┬───────────────────┤
│ 关系型DB  │ 文档型DB  │  搜索引擎          │
│ (主存储)  │ (扩展字段) │  (全文检索/复杂查询)│
│ PostgreSQL│ MongoDB   │  Elasticsearch    │
└──────────┴──────────┴───────────────────┘

核心思路:

  • 主存储用 PostgreSQL 的 JSONB,兼顾灵活性和查询性能

  • 高频查询字段抽成独立列,用元数据表管理哪些字段被"提升"为列

  • 全文检索和复杂查询走 Elasticsearch,定期从主存储同步

复制代码
class HybridStorage:
    def __init__(self):
        self.pg = PostgreSQL()
        self.es = Elasticsearch()
    
    def create_form(self, form_id, fields):
        """创建表单时,自动决定哪些字段需要提升为列"""
        promoted = [f for f in fields if f.indexed or f.sorted]
        
        # 创建主表(JSONB 存储所有数据 + 提升列用于高频查询)
        columns = [
            "id BIGSERIAL PRIMARY KEY",
            "data JSONB NOT NULL",
            "created_at TIMESTAMP DEFAULT NOW()"
        ]
        for f in promoted:
            columns.append(f'"{f.name}" {self.map_type(f.type)}')
        
        self.pg.execute(f"""
            CREATE TABLE form_{form_id} ({', '.join(columns)})
        """)
        
        # 创建触发器,自动同步提升列
        for f in promoted:
            self.pg.execute(f"""
                CREATE OR REPLACE FUNCTION sync_{f.name}() 
                RETURNS TRIGGER AS $$
                BEGIN
                    NEW."{f.name}" := NEW.data->>'{f.name}';
                    RETURN NEW;
                END;
                $$ LANGUAGE plpgsql;
            """)
    
    def query(self, form_id, filters, sort=None, page=1, size=20):
        """智能路由查询"""
        # 简单过滤走 PG
        if self._is_simple_query(filters):
            return self._pg_query(form_id, filters, sort, page, size)
        # 复杂搜索走 ES
        else:
            return self._es_query(form_id, filters, sort, page, size)

三、字段变更的优雅处理

动态表单最怕的就是改字段。加个字段、改个类型、删个字段,在大数据量下都是噩梦。

我们的方案是 Schema Registry + 版本化

复制代码
class SchemaRegistry:
    """管理表单的 Schema 版本"""
    
    def add_field(self, form_id, field):
        """新增字段:只更新 Schema,不改表结构"""
        schema = self.get_latest_schema(form_id)
        new_version = schema.version + 1
        
        # 记录新版本的 schema
        schema.fields.append(field)
        schema.version = new_version
        self.save_schema(schema)
        
        # 如果字段被标记为"提升列",异步 ALTER TABLE
        if field.promoted:
            self.enqueue_alter_table(form_id, field, action='ADD')
        
        return new_version
    
    def modify_field(self, form_id, field_name, changes):
        """修改字段:兼容旧数据"""
        schema = self.get_latest_schema(form_id)
        field = schema.get_field(field_name)
        
        # 类型变更需要数据迁移
        if 'type' in changes and changes['type'] != field.type:
            migration = DataMigration(
                form_id=form_id,
                field=field_name,
                from_type=field.type,
                to_type=changes['type']
            )
            self.enqueue_migration(migration)
        
        # 更新 schema
        field.update(changes)
        schema.version += 1
        self.save_schema(schema)
    
    def read_data(self, form_id, record_id):
        """读取数据时,根据记录的 schema_version 做适配"""
        record = self.pg.get(f"form_{form_id}", record_id)
        record_schema_version = record.get('_schema_version', 1)
        current_schema = self.get_latest_schema(form_id)
        
        # 如果 schema 版本不一致,做运行时适配
        if record_schema_version < current_schema.version:
            record = self._adapt_record(record, record_schema_version, current_schema)
        
        return record

这样做的核心好处是:

  • 加字段不需要立刻改表,只需要更新 Schema 注册表

  • 读取时自动适配,老数据用老 Schema 解读,新数据用新 Schema

  • ALTER TABLE 可以异步执行,不阻塞业务

四、性能优化实践

当表单数据量超过百万级时,查询性能是最大挑战。几个关键优化:

1. JSONB 路径索引

复制代码
-- 为高频查询字段创建 GIN 索引
CREATE INDEX idx_form_data_status ON form_123 
USING GIN ((data->'status'));
​
-- 或使用 BTree 索引(等值查询更快)
CREATE INDEX idx_form_data_dept ON form_123 
((data->>'department'));

2. 分区表

复制代码
-- 按时间分区,大表单必备
CREATE TABLE form_123 (
    id BIGSERIAL,
    data JSONB,
    created_at TIMESTAMP
) PARTITION BY RANGE (created_at);
​
CREATE TABLE form_123_2024q1 PARTITION OF form_123
    FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

3. 物化视图加速聚合

复制代码
-- 常用统计视图预计算
CREATE MATERIALIZED VIEW form_123_stats AS
SELECT 
    data->>'department' as department,
    data->>'status' as status,
    COUNT(*) as count,
    AVG((data->>'amount')::numeric) as avg_amount
FROM form_123
GROUP BY data->>'department', data->>'status';
​
-- 定期刷新
REFRESH MATERIALIZED VIEW CONCURRENTLY form_123_stats;

五、JVS 低代码平台的实践

在实际项目中,JVS 低代码平台的数据引擎采用的就是类似的混合存储架构。它在动态表单场景下的处理方式:

  • 基础字段用 JSONB 存储,保持灵活性

  • 支持字段索引提升,高频查询字段自动优化

  • 内置数据版本管理,字段变更不中断业务

  • 提供可视化数据建模工具,降低架构设计门槛

对于中大型项目来说,底层数据架构的选型直接决定了平台的上限。表面上的拖拽体验只是冰山一角,水面下的存储设计才是核心壁垒。

相关推荐
W658034192 小时前
Meta Muse Glimmer 30B开源深度拆解:Apache 2.0+投机解码,24GB显卡跑本地Agent
ai·开源·大模型·apache·deepseek
Logintern098 小时前
PostgreSQL 的 ORDER BY 多列排序
数据库·postgresql
许彰午9 小时前
27-AuthService登录链路
java·低代码·架构
a1117769 小时前
基于 ROS 2 的 Franka Panda 机械臂视觉分拣系统
人工智能·开源
冬奇Lab10 小时前
一天一个开源项目(第209篇):holaOS - Agent 原生的本地工作台
人工智能·开源·agent
夏文强11 小时前
DeepSeek Harness 权限与审批:给 Agent 上一把 human-in-the-loop 的安全阀
人工智能·开源·大模型·agent·deepseek
微软技术分享12 小时前
Ubuntu 本地部署Ollama+OpenWebUI教程
数据库·ubuntu·postgresql
风云12 小时前
MiniLog 1.0 GA 发布:零分配、零依赖的 .NET 高性能日志框架
开源·c#·.net·日志·高性能·minilog
oort12313 小时前
OortCloud Token Plan
大数据·人工智能·开源