一、工具:AI – Cline、VS code和MCP
1.1 首先在VS code上安装插件Cline ,然后安装MCP服务,连接doris。
1.2 在doris中建立ods表、dwd、dws和ads表。你也可以将所有表的字段写好,让cline去帮建表。 我的建立好了。总共13张表。
1.3 写个脚本生成数据,然后导入到doris中。
模拟数据脚本
importpymysqlimportpandasaspdimportnumpyasnpfromfakerimportFakerimportrandomfromdatetimeimportdatetime,timedelta# ==================== Doris 连接配置 ====================DORIS_CONFIG={'host':'你自己的ip','port':9030,'user':'root','password':'你root密码','database':'gmall','charset':'utf8mb4'}# ==================== 数据生成参数 ====================NUM_USERS=500NUM_PRODUCTS=200NUM_ORDERS=20000NUM_DAYS=30START_DATE=datetime(2026,7,1)defgenerate_data():fake=Faker('zh_CN')Faker.seed(42)random.seed(42)# 生成用户users=[]foriinrange(1,NUM_USERS+1):users.append({'user_id':i,'username':fake.user_name(),'name':fake.name(),'gender':random.choice(['M','F']),'age':random.randint(18,65),'province':fake.province(),'city':fake.city(),'register_date':(START_DATE+timedelta(days=random.randint(0,NUM_DAYS))).date()})df_user=pd.DataFrame(users)# 生成商品categories=['手机数码','家用电器','服装鞋帽','食品饮料','图书文具','运动户外','美妆护肤','母婴用品']products=[]foriinrange(1,NUM_PRODUCTS+1):price=round(random.uniform(9.9,999.9),2)products.append({'product_id':i,'product_name':fake.catch_phrase(),'category':random.choice(categories),'price':price,'cost':round(price*random.uniform(0.4,0.7),2),'stock':random.randint(10,1000)})df_product=pd.DataFrame(products)# 生成订单order_status=['待支付','已支付','已发货','已完成']orders=[]order_items=[]payments=[]order_id=1for_inrange(NUM_ORDERS):user=random.choice(users)order_date=START_DATE+timedelta(days=random.randint(0,NUM_DAYS),hours=random.randint(0,23),minutes=random.randint(0,59))status=random.choices(order_status,weights=[0.1,0.2,0.25,0.45])[0]num_items=random.randint(1,5)total_amount=0items=[]for_inrange(num_items):product=random.choice(products)qty=random.randint(1,3)amount=round(product['price']*qty,2)total_amount+=amount items.append({'order_id':order_id,'product_id':product['product_id'],'qty':qty,'price':product['price'],'amount':amount})orders.append({'order_id':order_id,'user_id':user['user_id'],'order_date':order_date.strftime('%Y-%m-%d %H:%M:%S'),'total_amount':round(total_amount,2),'status':status,'pay_date':(order_date+timedelta(minutes=random.randint(5,120))).strftime('%Y-%m-%d %H:%M:%S')ifstatusin['已支付','已发货','已完成']elseNone,'ship_date':(order_date+timedelta(hours=random.randint(2,48))).strftime('%Y-%m-%d %H:%M:%S')ifstatusin['已发货','已完成']elseNone,'finish_date':(order_date+timedelta(days=random.randint(3,10))).strftime('%Y-%m-%d %H:%M:%S')ifstatus=='已完成'elseNone})order_items.extend(items)ifstatusin['已支付','已发货','已完成']:payments.append({'payment_id':order_id,'order_id':order_id,'user_id':user['user_id'],'amount':round(total_amount,2),'pay_method':random.choice(['微信支付','支付宝','银行卡']),'pay_time':(order_date+timedelta(minutes=random.randint(5,120))).strftime('%Y-%m-%d %H:%M:%S')})order_id+=1df_order=pd.DataFrame(orders)df_order_item=pd.DataFrame(order_items)df_payment=pd.DataFrame(payments)# 将空字符串替换为 None(避免 MySQL 插入空字符串问题)# 但我们的数据已经使用 None,无需处理returndf_user,df_product,df_order,df_order_item,df_paymentdefinsert_data(df,table_name,conn):ifdf.empty:return0# 将 DataFrame 转为 list of tuples,并将 NaN 替换为 Nonedata=df.replace({np.nan:None}).to_dict('records')ifnotdata:return0columns=list(data[0].keys())placeholders=','.join(['%s']*len(columns))sql=f"INSERT INTO{table_name}({','.join(columns)}) VALUES ({placeholders})"cursor=conn.cursor()# 构建 values 列表values=[]forrowindata:values.append(tuple(row[col]forcolincolumns))cursor.executemany(sql,values)conn.commit()cursor.close()returnlen(values)defmain():print("生成数据...")df_user,df_product,df_order,df_order_item,df_payment=generate_data()print(f"用户{len(df_user)},商品{len(df_product)},订单{len(df_order)},明细{len(df_order_item)},支付{len(df_payment)}")# 连接 Dorisconn=pymysql.connect(**DORIS_CONFIG)print("开始导入数据...")inserted=0inserted+=insert_data(df_user,'dim_user',conn)inserted+=insert_data(df_product,'dim_product',conn)inserted+=insert_data(df_order,'ods_order',conn)inserted+=insert_data(df_order_item,'ods_order_item',conn)inserted+=insert_data(df_payment,'ods_payment',conn)conn.close()print(f"✅ 成功插入{inserted}行数据到 Doris")if__name__=="__main__":main()导入之后可以稍微看一下,没什么大问题基本就ok。
二、基础数据准备好之后,我们接下来就是重点之重了。因为现在只有ods表和dim表有数据,我们要让AI先对我们源数据进行清洗,然后再让它们利用AI进行ETL写入到dwd表和dws表和ads表中。
上面是提示词,ads层写错了,不过没关系,AI会自动纠正。
它会将我的需求拆分成步骤,然后按照每一步来进行。
我们用AI,就是让它辅助我们生成代码,然后去监控排查我们的数据质量和BUG,或者分析错误日志,找到问题所在,大部分都是辅助作用。并不是让它去帮我们执行ETL代码哈。这个要分清楚,我们的代码始终还是在hadoop里面hive里面或者数据开发平台上去执行。就像这次的,我的ETL脚本是在doris执行的,它只是创建了脚本。
左边可以看到他的进度情况,右边可以看到它创建的脚本和数据质量的验证。
等一会后就结束了。我们来看一下结果怎么样。
烧了113.8KB的token,还行。目前我还没支付一分钱,还是0的状态(参照这张图的前一张,0.000。还可以继续苟着使用)
ads层:一张经营日报表,就最近三天的日报情况,总共32条数据。 另一张是商品的Top5排行榜,6000+条数据。
然后是左下角的那里,是它做了一些结果汇总工作,订单原表ods,2w+条数据,重复101条,去重之后差不多2w。订单明细的,6W条数据,重复456条。
它做的这个结果,数据到底准不准?我还没验证,我明天找个时间验证一下。今天就先到这吧。大家感兴趣的点个赞哈!
后面有时间再讲一讲关于数据治理的案例,这个也挺有意思的!
三、补充一下数据治理
📊 gmall.ods_order 数据质量检查报告
📊 执行摘要
核心发现:ods_order表数据质量优秀,9 个字段的 19,999 条记录在核心业务字段(order_id/user_id/order_date/total_amount/status)上 0 空值、0 异常值,金额与日期逻辑完全自洽。部分日期空值(pay_date/ship_date/finish_date)与订单业务状态严格对应,属于业务流转的正常现象,无需清洗。
风险等级评估:低(无高风险问题,仅存在细微可优化的治理项)
优先级建议:持续监控(无需立即行动,个别项计划修复)
🔍 详细分析
1. 表结构与基础信息
| 项目 | 值 |
|---|---|
| 完整表名 | internal.gmall.ods_order |
| 总行数 | 19,999 |
| 列数 | 9 |
| 分区 | 1 个(ods_order) |
| 数据大小 | 837.67 KB |
| 工具 | get_db_table_list / get_table_schema / get_table_basic_info |
2. 完整性(空值)检查
核心字段空值统计(0 空值,100% 完整):
| 字段 | 类型 | 空值数 | 空值率 |
|---|---|---|---|
| order_id | bigint | 0 | 0% |
| user_id | bigint | 0 | 0% |
| order_date | datetime | 0 | 0% |
| total_amount | decimal(12,2) | 0 | 0% |
| status | text | 0 | 0% |
| etl_time | datetime | 0 | 0% |
业务日期字段空值(与订单状态精确对应):
| 字段 | 空值数 | 空值率 | 对应业务状态 |
|---|---|---|---|
| pay_date | 2,012 | 10.06% | = 待支付 2,012 单 |
| ship_date | 6,064 | 30.32% | = 已支付+待支付 4,052+2,012 |
| finish_date | 11,051 | 55.26% | = 未完成订单数 |
✅关键验证:空值数完全匹配订单状态流转(待支付无支付日期 → 未发货无发货日期 → 未完成无完成日期),证明这是业务合理的空值,而非数据缺失。
3. 准确性(异常值)检查
金额字段(total_amount):
| 指标 | 值 | 判断 |
|---|---|---|
| 最小值 | 10.40 | ✅ 正常(>0) |
| 最大值 | 11,582.04 | ✅ 正常 |
| 平均值 | 2,933.39 | ✅ 合理 |
| 0/负数记录 | 0 | ✅ 无异常 |
| >100,000 记录 | 0 | ✅ 无极端异常 |
| 去重金额数 | 16,354 | ✅ 分布正常 |
日期逻辑一致性(交叉验证):
| 检查项 | 异常记录数 |
|---|---|
| order_date > pay_date(下单晚于支付) | 0 |
| pay_date > ship_date(支付晚于发货) | 0 |
| ship_date > finish_date(发货晚于完成) | 0 |
| order_date > ship_date(下单晚于发货) | 0 |
✅全部 0 条矛盾记录,日期先后顺序完全符合订单生命周期。
日期范围合理性:
| 字段 | 范围 |
|---|---|
| order_date | 2026-07-01 ~ 2026-07-31(当月业务) |
| pay_date | 2026-07-01 ~ 2026-08-01(含跨国月底支付) |
| etl_time | 2026-08-22 20:27 ~ 20:32(ETL 分批加载) |
💡 专业建议
短期优化(可立即执行)
- 避免“循环双表”错误:访问表统一使用
internal.gmall.ods_order三段式命名,避免gmall.gmall.ods_order的重复库名错误(本次分析在此有所纠正)。 - 补充字段注释:当前 9 列
comment全部为空,建议为各字段补充业务注释(如 status 枚举、日期含义),提升数据可读性。需在 DBA 评估后执行:ALTERTABLEinternal.gmall.ods_orderMODIFYCOLUMNstatusTEXTCOMMENT'订单状态:待支付/已支付/已发货/已完成';
中长期治理规划
- 标准化空值语义:
pay_date/ship_date/finish_date目前靠业务逻辑空值表达“未发生”,建议在文档化约定,或引入业务状态码字段,便于下游消费时显式过滤。 - 历史数据归档:当前仅 1 个分区且无动态分区,建议按
order_date按月动态分区,为后续增量 ETL 和性能优化打基础(需评估后执行)。
最佳实践**
- 该表作为 ODS(贴源)层,质量已达到较高的“随原始数据入库”标准,建议在 DWD 层对日期空值进行统一清洗与状态标注。
📈 监控建议
| 指标 | 建议阈值 | 触发动作 |
|---|---|---|
| 核心字段空值率 | >0% 立即告警 | 阻断下游 ETL 任务 |
| total_amount ≤ 0 记录 | >0 告警 | 人工核查来源数据 |
| pay_date 空值与待支付数不匹配 | 偏移 >1% 告警 | 触发审查 |
| 日期逻辑矛盾(pay>ship 等) | >0 条告警 | 定位 ETL bug |
| finish_date 空值率 | >60% 提示 | 观察生命周期是否正常 |
📋 行动计划
- 数据开发:补充字段注释 DDL(评估后执行)
- 数据建模:规划动态分区策略(按日期周期)
- 监控配置:按上述阈值配置质量告警规则
- 预期效果:实现 data quality 自动化护航,降低下游报表因空值导致的计算偏差风险
总结:ods_order表数据质量良好,属“状态机驱动的合理空值”而非质量问题,核心字段 0 空值、金额和日期全无异常,可放心供下游使用。
6.2 从上面的报告,我们也能看出,Cline自己在调用一些工具(Skills)时,或者查询一些数据时,需要填写一些参数,它会将参数写错。这可能是一个还需要继续完善的地方。
我问它:
你为何分析过程中会出现避免 `gmall.gmall.ods_order` 的重复库名的错误,请告诉我原因它给我的回答:
关于出现gmall.gmall.ods_order重复库名的原因说明
这个错误源于我在调用元数据探查工具时,对参数catalog_name和db_name的语义理解混用了,并非数据库本身的问题。
技术原因
Doris 的命名空间是三级结构:catalog_name(目录) → database_name(数据库) → table_name(表),SQL 中的三段式写法为internal.gmall.ods_order。
internal:Doris 内置的默认Catalog(目录),用于存放和管理 Doris 内部表;gmall:只是其中的一个Database(数据库);ods_order:才是表名。
错误是怎么产生的
在分析过程中,我调用get_table_schema/get_table_basic_info等元数据工具时,错误地把gmall填进了catalog_name参数:
我传入:catalog_name = "gmall", db_name = "gmall", table_name = "ods_order" 工具拼接:gmall.gmall.ods_order ← catalog=gmall, db=gmall, table=ods_order而实际系统里并不存在名为gmall的 catalog,gmall只是数据库名。所以工具报错Table does not exist/Table gmall.gmall.ods_order not found。
正确用法
catalog_name = "internal" ← 固定用 Doris 内置目录 db_name = "gmall" ← 真正的数据库名 table_name = "ods_order" ← 表名 拼接结果:internal.gmall.ods_order(存在)对分析结果的影响
- 这些错误只发生在元数据探查阶段的几次调用上,属于参数传错;
- 后续实际做质量检查时,
exec_query中我统一使用了正确的internal.gmall.ods_order,因此最终的空值/异常值分析结果是完全准确、可信的; - 该问题不影响数据质量检查结论,但作为运维经验:调用 Doris 工具时,catalog 统一填
internal,库名只填真正的数据库名,即可避免此类重复命名错误。
我又问它是工具本身的参数有问题还是它自己的问题?
回答:这个错误是我自己调用时记错/混用了参数,不是工具的错误
我严谨地复盘了整个调用链,证据很清楚:
结论:错误在我,不在工具
关键证据对比:
| 调用方式 | 入参 | 结果 |
|---|---|---|
get_table_schema | db_name="gmall",table_name="ods_order"(不传 catalog_name) | ✅ 成功,返回 9 字段 |
get_table_basic_info | db_name="gmall",table_name="ods_order" | ✅ 成功,返回gmall.ods_order19999 行 |
exec_query | SQL 写成internal.gmall.ods_order | ✅ 成功 |
get_table_schema | catalog_name="gmall",db_name="gmall" | ❌ 报gmall.gmall.ods_order不存在 |
get_table_basic_info | catalog_name="gmall",db_name="gmall" | ❌ 报Table gmall.gmall.ods_order not found |
关键点:只要我不传catalog_name(工具默认用当前 catalog =internal),或者正确只传catalog_name="internal",工具就能成功。而凡是传了catalog_name="gmall"的,工具都会按照三段式拼接规则拼出gmall.gmall.ods_order,然后如实报错"表不存在"。
具体来说错在哪
- Doris 命名空间是三层:
catalog(目录) → database(库) → table(表) internal才是 catalog(Doris 内置默认目录),gmall只是一个 database- 我在调用元数据工具时,把数据库名
gmall误填到了 catalog 参数上,好比把"省"填到了"国家"字段里,工具按规则拼出了不存在的gmall.gmall.ods_order - 相反,
exec_query里我一旦写对internal.gmall.ods_order,查询立即成功——这正是正确三段式命名的验证
与工具问题的区分
顺带说明:过程中确实有一个工具 bug——analyze_columns因为Decimal类型无法被 JSON 序列化而报错(Object of type Decimal is not JSON serializable),这个是工具自身的缺陷(MCP server 代码没有把 Decimal 转换为可序列化类型),但与命名错误无直接关系,两者不同源。
一句话总结
gmall.gmall.ods_order是我在调用时把数据库名 gmall 误当目录名传入导致的,属于我自己记错/用错参数,工具没问题;analyze_columns的 Decimal 错误才是工具 bug。两者我已分辨清楚,后续调用已统一使用正确命名(internal作为 catalog,gmall作为 db),空值/异常值分析结果不受影响。
</attempt_completion>