低代码平台的灵魂不是拖拽界面,而是底层的数据架构。本文拆解动态表单的存储模型、查询优化和扩展性设计,给正在做或准备做低代码平台的技术人一个参考。
做低代码平台开发这几年,踩过的最大的坑就是数据存储。
表面上看,低代码就是让用户拖拖拽拽搭个表单,能有多复杂?等你真正实现了才知道,"动态"两个字背后是一整套数据架构的挑战。

一、动态表单存储的三种主流方案
低代码平台的核心问题:用户定义的表单结构是动态的,你怎么存?
业界主要有三种方案,各有利弊。
方案一: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 存储,保持灵活性
-
支持字段索引提升,高频查询字段自动优化
-
内置数据版本管理,字段变更不中断业务
-
提供可视化数据建模工具,降低架构设计门槛

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