一、业务背景
在后端报表开发、数据统计、定时出数场景中,我们经常面临一个痛点:
每新增一张业务报表,就要重新写一套执行脚本、一套参数处理逻辑,代码冗余极高、维护成本大、无法统一日志审计。
理想的通用报表架构应该满足:
1.Java层统一调用入口 ,只传业务参数,不关心SQL细节2.Shell做统一中间层 ,所有报表复用同一套执行框架3.报表逻辑全部解耦到SQL文件 ,新增报表只需要新增/修改PostgreSQL SQL,不用改Java业务代码、不用改Shell脚本 4.支持多参数动态传入 ,每张报表参数各不相同5.可追溯、可排查:真实执行SQL落地日志,支持线上问题回溯
本文讲解企业生产最常用、最稳定的 Shell传递参数给PostgreSQL的两种标准方案,封装一套通用报表自动化执行框架。
二、整体架构流程
完整调用链路:
Java程序 → 调用通用Shell脚本 → 传入业务参数 → Shell注入参数到SQL模板 → psql执行统计SQL → 输出报表结果 + 保存真实执行SQL日志
⚠️重要差异: ClickHouse客户端使用
${var};PostgreSQL psql变量语法为:var;字符串参数-v需要内置单引号。
核心设计思想:
•脚本通用化 :Shell永不改动•业务多样化 :只靠不同SQL文件实现不同报表•参数动态化:每张报表参数不同,由外部Java动态传入
三、Shell给PostgreSQL SQL传参的两种工业级标准方式
方式一:psql客户端原生 -v 变量传参(内存传参、无临时文件)
原理: psql 通过 -v key=value 向SQL上下文注入变量, SQL模板中直接使用 :变量名 引用,不生成文件、不修改原模板文件。
特点:
1.变量在psql内存中生效,不生成磁盘临时文件2.原始SQL模板保持纯净3.执行速度快4.缺点:看不到最终替换之后完整SQL,故障排查能力弱
适用场景:定时报表、高频调度、生产常规跑批
方式二:SED占位符替换 + 临时SQL文件落地(可审计、可回溯)
原理: SQL模板设置占位符,例如@QUERY_DATE@、@CHANNEL@, Shell执行sed -s批量替换占位符,生成可直接运行的临时SQL; 执行前把完整SQL保存到日志,用于排查数据异常;执行结束自动清理临时文件。
特点:
1.完整保存实际运行SQL,对账、数据异常问题可以直接复现2.支持大量业务参数,适配复杂报表3.少量磁盘IO,性能影响可以忽略
适用场景:业务报表、对账统计、线上需要定位问题的任务
四、通用可复用报表框架实现
目录结构
/run_report.sh # 通用shell执行脚本(统一不变)./sql/ report_daily_user.sql report_order.tpl.sql./log/ # 执行日志目录./tmp/ # 临时SQL目录
方案一:psql -v 变量传参(简洁版框架)
SQL模板 sql/report_daily_user.sql
-日活统计报表 psql模板,变量使用 :date :channelSELECT stat_date, channel, count(DISTINCT uid) AS user_numFROM user_statWHERE stat_date = :date AND channel = :channelGROUP BY stat_date, channel;
Shell脚本 run_report.sh(接收Java传入参数)
#!/bin/bashset -e# PostgreSQL psql 通用报表脚本,-v内存变量模式# Java调用示例:sh run_report.sh '2026-08-16' 'app'
#接收Java传入参数QUERY_DATE="$1"CHANNEL="$2"
#数据库连接配置PG_HOST="127.0.0.1"PG_PORT="5432"PG_DB="business_db"PG_USER="postgres"
TS=$(date +%Y%m%d%H%M%S)LOG="./log/report_${TS}.log"
echo "【执行时间】$(date)" >> "${LOG}"echo "【传入参数】QUERY_DATE=${QUERY_DATE}, CHANNEL=${CHANNEL}" >> "${LOG}"
# psql -v传递变量;字符串必须内置单引号,数字不需要psql -h "${PG_HOST}" -p "${PG_PORT}" -U "${PG_USER}" -d "${PG_DB}" \-v date="'${QUERY_DATE}'" \-v channel="'${CHANNEL}'" \-v ON_ERROR_STOP=1 \-f ./sql/report_daily_user.sql >> "${LOG}" 2>&1
echo "【报表执行完成】" >> "${LOG}"
重点:
-v date="'${QUERY_DATE}'"两层引号;外层shell引号,内层单引号给到SQL日期字面量。ON_ERROR_STOP=1:SQL报错立刻终止脚本,避免错误继续往下跑。
方案二:SED占位符替换 + 临时SQL日志审计(生产推荐)
模板文件 sql/report_order.tpl.sql
SELECT stat_date, order_type, count(1) AS order_cntFROM order_statWHERE stat_date = '@QUERY_DATE@' AND order_type = '@ORDER_TYPE@'
Shell脚本 run_report_sed.sh
#!/bin/bashset -e# PostgreSQL psql SED模板替换,生成临时SQL,完整日志审计# Java调用:sh run_report_sed.sh '2026-08-16' '1'
#接收Java传入参数QUERY_DATE="$1"ORDER_TYPE="$2"
#PG连接配置PG_HOST="127.0.0.1"PG_PORT="5432"PG_DB="business_db"PG_USER="postgres"
TS=$(date +%Y%m%d%H%M%S)TMP_SQL="./tmp/sql_${TS}.sql"LOG_FILE="./log/report_${TS}.log"
#sed替换占位符,生成临时执行SQLsed \-e "s|@QUERY_DATE@|'${QUERY_DATE}'|g" \-e "s|@ORDER_TYPE@|'${ORDER_TYPE}'|g" \./sql/report_order.tpl.sql > "${TMP_SQL}"
#把真实执行SQL写入日志,故障定位核心echo "====================真实执行SQL====================" >> "${LOG_FILE}"cat "${TMP_SQL}" >> "${LOG_FILE}"echo "====================================================" >> "${LOG_FILE}"
#psql执行临时sql文件psql -h "${PG_HOST}" -p "${PG_PORT}" -U "${PG_USER}" -d "${PG_DB}" \-v ON_ERROR_STOP=1 \-f "${TMP_SQL}" >> "${LOG_FILE}" 2>&1
#自动清理临时文件rm -f "${TMP_SQL}"
echo "报表执行完成,日志文件:${LOG_FILE}"
五、Java如何调用shell
Java使用ProcessBuilder调用shell,把业务参数作为命令行参数传入脚本。
Java只负责拼装参数,完全不写SQL。新增报表,只增加一份sql模板即可。
六、注意事项
1.psql变量语法**:var** ,和clickhouse ${var}不要搞混。2.sed替换分隔符使用|,避免日期、内容包含/造成sed语法异常。3.禁止直接shell字符串拼接SQL,防止SQL注入风险。4.ON_ERROR_STOP=1必须带上,SQL执行异常脚本及时退出。5.tmp、log目录脚本启动前可以增加判断,不存在自动mkdir创建。
七、总结
1.Java:调度业务,组装参数,调用shell 2.Shell:统一中间执行层,一套脚本支撑全部报表 3.SQL模板:全部业务报表逻辑,新增报表只维护SQL4.psql两种传参:
•-v:内存传入变量,无临时文件,适合高频跑批•sed占位替换:生成临时SQL,完整保存执行语句到日志,对账排查优先
这是PostgreSQL做轻量化报表跑批非常成熟的一套架构。
如果你需要,我可以再补充一份Java调用ProcessBuilder简易示例代码。