ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从sqlplus到gsql:Shell脚本迁移GaussDB的完整改造指南

从sqlplus到gsql:Shell脚本迁移GaussDB的完整改造指南 上个月接了一个数据库国产化迁移的评估任务业务 SQL 的兼容性问题提前过了语法层面基本没有大阻碍。真正让我头疼的是那几十个在生产环境跑了好多年的 Shell 脚本——清一色的 sqlplus 调用输出格式、退出码判断、SPOOL 文件解析全是按 Oracle 的习惯写的。把它们迁移到 GaussDB 的 gsql 上不是把命令名换掉就完事中间各种隐蔽差异个个都能让脚本在凌晨跑批时悄悄挂掉。这篇文章就围绕“Shell 中执行 SQL 文件”这个场景把 sqlplus 到 gsql 的迁移改造过程完整拆开讲。内容包括存量脚本梳理、两者差异对照、统一封装方案、真实踩坑记录和回归验证方法适合正在做 GaussDB 迁移的 DBA、运维和开发同学参考。1. 迁移之前梳理存量脚本里的 sqlplus 调用形态在动手改任何脚本之前我先把所有线上脚本翻了一遍。这一步看起来枯燥但直接决定了后面改造的工作量。很多人一上来就搜“gsql 怎么替代 sqlplus”然后对着单个脚本改结果改了十几个脚本之后发现每个写法都不一样越改越乱所以我建议第一步永远是盘点。1.1 最常见的六种 sqlplus 调用形态我梳理了生产环境里的存量脚本sqlplus 的调用方式基本逃不出下面六种静默执行单个 SQL 文件sqlplus -S $DB_USER/$DB_PASS$DB_SID nightly_report.sql用 heredoc 把 SQL 直接喂给 sqlplussqlplus -S -L $DB_USER/$DB_PASS$DB_SID EOF SET PAGESIZE 0 SET FEEDBACK OFF SELECT * FROM tab; EXIT; EOF通过 SPOOL 命令在 SQL 内部生成报表文件SPOOL /data/reports/result_20240101.csv SELECT ...; SPOOL OFF带位置参数传给 SQL 文件sqlplus -S $DB_USER/$DB_PASS$DB_SID daily_stat.sql 20240101SQL 文件里用1引用这个参数。在 sqlplus 脚本内部控制退出码WHENEVER SQLERROR EXIT SQL.SQLCODE EXIT SUCCESS连接串里带服务名或主机端口sqlplus -S $DB_USER/$DB_PASS//127.0.0.1:1521/orcl这六种形态在 gsql 里的处理方式完全不同。其中第 1、2 种是壳层的替换第 3、6 种涉及 SQL 文件内容的改造第 4、5 种最容易在迁移后被忽略却恰恰是批处理脚本里最要命的部分。1.2 迁移清单里必须确认的几个前置问题我把整理的清单列在这里建议迁移前逐条过一遍统计脚本数量grep -rln sqlplus /opt/scripts/ | wc -l先知道要动多少文件。检查是否有 login.sql 或 glogin.sql。这个文件是 sqlplus 启动时自动加载的里面可能设置了默认的格式参数迁移到 gsql 后没有自动加载机制这些设置就全丢了。排查 SQL 文件里是否依赖 SPOOL 生成文件以及下游是否有按 SPOOL 输出格式做解析的脚本。确认是否用了1、2这类位置参数以及DEFINE变量。gsql 没有完全对应的机制需要在 Shell 层用-v定义。检查 SQL 文件中的 Oracle 专有函数和语法比如 SYSDATE、ROWNUM、CONNECT BY、DUAL 表、NVL等。GaussDB 对其中一部分做了兼容但远非全部。确认 Shell 脚本对退出码的判断逻辑。sqlplus 的错误返回和 gsql 的错误返回机制差异很大不确认清楚日志里就会出现“报表已生成”但实际文件只有半个表的情况。这一步做完之后我对改造范围基本心里有数了大约三分之二的脚本可以通过封装的函数直接替换剩下的需要逐个改 SQL 文件。2. sqlplus 与 gsql 的本质差异连接、执行语义与输出gsql 不是 sqlplus 的克隆品它是 GaussDB 自带的命令行交互工具设计理念更接近 PostgreSQL 的 psql。很多在 sqlplus 里被视为理所当然的行为在 gsql 里完全没有对应物或者行为完全相反。这一章把最关键的三类差异讲透。2.1 连接信息与命令行参数对照这是最表层的差异但也是最容易被忽略的。sqlplus 把连接信息塞进一个连接串user/passsidgsql 则是一堆独立参数-h指定主机-p指定端口-U指定用户-d指定数据库密码通过-W或环境变量PGPASSWORD传入。功能点Oracle sqlplusGaussDB gsql连接数据库user/passsid-h host -p port -U user -d dbname密码传递写在连接串里-W参数或环境变量PGPASSWORD静默模式-S-q执行 SQL 文件file.sql-f file.sql执行单条 SQL-c sql-c sql非对齐输出需SET组合命令-A仅输出数据需SET HEADING OFF等-t错误即停WHENEVER SQLERROR EXIT-v ON_ERROR_STOP1注意一个细节sqlplus 的密码直接写在连接串里用ps查看进程列表会直接暴露密码gsql 如果用-W传密码也有同样问题。迁移到 gsql 后我建议统一改用环境变量PGPASSWORD这样进程列表里看不到密码Shell 脚本里也可以把密码集中放在配置文件中统一管理。这也是这次迁移中顺手补上的一个安全改进。2.2 执行语义差异回显、停止策略与退出码sqlplus 默认ECHO OFF执行file.sql时不会回显 SQL 语句本身输出相对干净。gsql 则相反执行-f file.sql时会把 SQL 语句原样打到输出里如果不加-q报表文件的开头会混入一堆 SQL 文本下游解析直接崩。更隐蔽的是错误停止策略。sqlplus 脚本里如果写了WHENEVER SQLERROR EXIT中间任何一条 SQL 报错都会立刻终止但 gsql 的默认行为是“报错继续”一条 UPDATE 失败了后面的 INSERT 照样执行最终返回码还是 0。这在数据修复类脚本里是灾难级的隐患。所以给 gsql 加-v ON_ERROR_STOP1应该成为所有 Shell 调用的默认配置没有例外。退出码方面也有差异。sqlplus 支持EXIT SUCCESS、EXIT FAILURE、EXIT SQL.SQLCODE这类显式退出码控制gsql 的\q命令不带退出码参数脚本执行完毕后返回 0。想判断 gsql 是否真的成功依靠的就是ON_ERROR_STOP触发非零返回。换句话说sqlplus 的退出码是“主动声明”的gsql 的退出码是“错误触发”的。Shell 脚本里的判断逻辑要按这个思路重写。2.3 输出格式的差异与好消息sqlplus 的报表输出需要一堆SET命令组合才能控制干净常见组合是SET PAGESIZE 0 SET HEADING OFF SET FEEDBACK OFF SET TRIMSPOOL ON SET LINESIZE 500gsql 把这个过程简化了很多。命令行层面直接加三个参数就能拿到最干净的纯数据输出gsql -q -A -t -f query.sql-q去掉欢迎信息和 SQL 回显-A关闭对齐去掉表格边框的竖线-t只输出查询结果元组不带列头和行数统计。这三个组合是替代 sqlplus 报表输出最核心的手段。如果你的 SQL 文件里还有SPOOL /path/file.csv这类命令在 gsql 里不需要了——直接在 Shell 层用重定向到文件效果一样还省去了SPOOL OFF的配对维护。这是移植过程中最值得做的简化操作。3. Shell 脚本统一改造封装一个双引擎执行函数搞清楚差异之后我没有一头扎进脚本堆里逐个改而是先封装了一个统一的 Shell 函数。这样改造成本最低外层脚本只改一行调用SQL 文件按需做兼容性调整未来的回滚和双跑验证也都有了一个稳定的入口。3.1 改造原则先隔离差异再批量替换我给自己定的原则很简单所有连接参数、输出参数、错误处理逻辑全部收敛到一个函数里业务脚本只负责传入“用哪个引擎、执行哪个文件、输出到哪”。这样做的好处有几点参数统一管理不会出现这个脚本忘了加-q、那个脚本忘了加ON_ERROR_STOP的情况。如果想要切回 Oracle 做双跑验证只需要把函数里的engine参数从gaussdb改成oracle业务脚本完全不用动。后续如果 GaussDB 的 gsql 版本升级导致参数有变化改一个函数比改几十个脚本省事得多。封装本身不复杂核心是把这个函数写得足够稳。3.2 一个可以直接抄走的统一执行函数下面这个函数是我在实际迁移中用的版本你可以直接拿过去改改参数名就能用#!/bin/bash # lib_exec_sql.sh —— 数据库 SQL 文件统一执行函数 # 从配置文件加载连接参数 source /etc/db_migrate/db_env.sh run_sql_file() { local engine$1 # oracle 或 gaussdb local sql_file$2 local out_file$3 local biz_date$4 # 业务日期透传给 SQL 内变量 shift 4 case $engine in oracle) sqlplus -S -L $ORA_USER/$ORA_PASS$ORA_SID $sql_file $biz_date $out_file 21 return $? ;; gaussdb) export PGPASSWORD$GS_PASS gsql -h $GS_HOST -p $GS_PORT -U $GS_USER -d $GS_DB \ -q -A -t \ -v ON_ERROR_STOP1 \ -v biz_date$biz_date \ -f $sql_file $out_file 21 return $? ;; *) echo unknown engine: $engine 2 return 2 ;; esac }调用方式统一为run_sql_file gaussdb /opt/scripts/daily_stat.sql /data/out/stat.csv 20240101几个参数设计的考虑biz_date通过-v biz_date20240101传进 gsqlSQL 文件里用:biz_date引用替代原来 sqlplus 的1。这个变量名你完全可以按自己的习惯调整但建议全项目统一。输出用 $out_file 21统一捕获既拿到结果也拿到错误日志。排查问题的时候错误信息和结果在同一个文件里定位非常方便。函数末尾return $?把 sqlplus/gsql 的退出码原样返回给外层脚本业务脚本里的if [ $? -eq 0 ]判断逻辑基本不用改。3.3 改造实战一份日终报表脚本的前后对比拿一个典型的日终报表脚本举例。改造前的 Oracle 版本如下#!/bin/bash DB_USERscott DB_PASStiger DB_SIDorcl SQL_FILE/opt/scripts/nightly_report.sql OUT_FILE/data/reports/report_$(date %Y%m%d).csv sqlplus -S $DB_USER/$DB_PASS$DB_SID $SQL_FILE $OUT_FILE if [ $? -eq 0 ]; then echo $(date %F %T) report success else echo $(date %F %T) report failed 2 exit 1 fi对应的nightly_report.sql是SET PAGESIZE 0 SET HEADING OFF SET FEEDBACK OFF SET TRIMSPOOL ON SET LINESIZE 500 SPOOL /dev/null SELECT store_id,order_cnt,sales_amount FROM DUAL; SPOOL OFF SELECT store_id || , || order_cnt || , || sales_amount FROM daily_sales WHERE stat_date TO_DATE(1, YYYYMMDD); EXIT SUCCESS迁移到 GaussDB 后Shell 脚本改成#!/bin/bash source /etc/db_migrate/db_env.sh export PGPASSWORD$GS_PASS SQL_FILE/opt/scripts/nightly_report.sql OUT_FILE/data/reports/report_$(date %Y%m%d).csv BIZ_DATE$(date %Y%m%d) gsql -h $GS_HOST -p $GS_PORT -U $GS_USER -d $GS_DB \ -q -A -t \ -v ON_ERROR_STOP1 \ -v biz_date$BIZ_DATE \ -f $SQL_FILE $OUT_FILE 21 if [ $? -eq 0 ]; then echo $(date %F %T) report success else echo $(date %F %T) report failed 2 exit 1 fiSQL 文件改成SELECT store_id,order_cnt,sales_amount; SELECT store_id || , || order_cnt || , || sales_amount FROM daily_sales WHERE stat_date TO_DATE(:biz_date, YYYYMMDD);注意这里删掉了SPOOL /dev/null和SPOOL OFF这一对命令。原来在 sqlplus 里写SPOOL /dev/null是为了把开头的 SQL 回显拦掉现在-q已经把回显消掉了SPOOL 就没有存在意义了。FROM DUAL也顺手删了GaussDB 查询常量可以直接SELECT xxx。如果使用前面封装好的统一函数外层脚本更简洁source /opt/scripts/lib/lib_exec_sql.sh run_sql_file gaussdb /opt/scripts/nightly_report.sql \ /data/reports/report_$(date %Y%m%d).csv $(date %Y%m%d) if [ $? -eq 0 ]; then echo $(date %F %T) report success else echo $(date %F %T) report failed 2 exit 1 fi4. 改造时踩过的四个坑从错误堆栈到输出解析这一章写的都是我在真实迁移中踩过的坑。有的坑是跑批到凌晨两点才暴露的有的坑是下游数据分析团队找上门才发现的。写出来希望你能绕过。4.1 为什么 ON_ERROR_STOP 是最容易被漏掉的参数第一次改造完成后我跑通了一个 UPDATE 类脚本返回码是 0日志显示“执行成功”。但我随手翻了翻输出文件发现里面有一条 SQL ERROR 的记录——也就是说 SQL 文件中间其实报了一个错后面的语句继续执行了最后退出码却是 0。这就是 gsql 默认行为最坑的地方。sqlplus 的WHENEVER SQLERROR EXIT是写在 SQL 文件里的而 gsql 的ON_ERROR_STOP必须在启动时传入两条路完全不同。如果你只是把sqlplus file.sql机械地换成gsql -f file.sql这个坑百分百踩。血的教训是-v ON_ERROR_STOP1必须作为 gsql 启动参数的标配。要么写进封装函数要么写进 Shell 脚本的统一变量。不要指望每个人都能记住这条。另外要注意ON_ERROR_STOP只对 SQL 执行错误有效对 gsql 元命令的报错不一定都生效。所以我在封装函数里不仅加了-v ON_ERROR_STOP1还加了-q -A -t并在函数注释里写明这三件套缺一不可。4.2 输出文件里多了东西回显、Banner 与格式差异迁移后的第一版报表文件打开一看前面多了一大段文本包括 gsql 的版本信息、SQL 语句原文以及结果列头。下游同事拿着这个文件直接去解析第一行就报了格式错误。根源是两个gsql 执行-f时会默认回显 SQL 文本sqlplus 默认不回显不加-q时gsql 在开始阶段会打印 banner 和连接信息。解决方式就是-q -A -t三件套。但还有一个细节如果你用-c执行单条 SQL加上-t会去掉列头如果你用-f执行 SQL 文件文件里的SELECT也会受-t影响。这很好但要提醒你-t去掉列头后如果某个 SQL 文件是专门用来生成表头报告的比如我上面例子里的第一行SELECT store_id,order_cnt,sales_amount仍然能正常输出因为这条结果本身就是数据。另外一个格式差异是 NULL 值的显示。sqlplus 中 NULL 默认显示为空白gsql 中默认也是空白看起来一致。但如果 SQL 里用了NVL或COALESCE注意 GaussDB 中空字符串和 NULL 的语义与 Oracle 有细微差别尤其在做字符串拼接时NULL || abc在 Oracle 里是abc在 GaussDB 里结果是 NULL这种差异非常隐蔽建议在 SQL 改写时统一用COALESCE显式处理。4.3 中文乱码又一个字符集问题报表文件里有中文Oracle 环境下正常切到 GaussDB 后导出文件打开全是乱码。排查过程并不复杂先确认数据库本身的字符集再确认 Shell 终端和客户端的NLS_LANG或client_encoding是否一致。GaussDB 默认 UTF8而生产环境里不少老的 Oracle 库为了兼容历史业务用的是 GBK/ZHS16GBK。sqlplus 能正常显示中文是因为它从NLS_LANG拿了字符集gsql 则更依赖数据库和客户端的编码设置。处理方法export PGCLIENTENCODINGUTF8或者在 gsql 连接时加参数指定客户端编码。如果数据库本身是 GBK而你要导出 UTF8 的文件可以做一次显式转换iconv -f GBK -t UTF8 input.csv output_utf8.csv这个坑不大但容易在联调时被人忽略建议写进迁移自查清单里和端口、权限一起检查。4.4 残留的 Oracle 语法在 gsql 下挣扎Shell 脚本层面改造完SQL 文件里的 Oracle 专有语法也得清理。这块我整理了一批高频出现的兼容性差异Oracle 写法GaussDB 建议写法说明SYSDATECURRENT_TIMESTAMP/now()GaussDB 兼容SYSDATE但不推荐SELECT 1 FROM DUALSELECT 1GaussDB 支持 DUAL但无谓的 DUAL 最好删掉ROWNUM 1LIMIT 1分页/取首行逻辑重写CONNECT BY层级查询WITH RECURSIVE递归 CTE改写工作量较大需要认真测NVL(a, b)COALESCE(a, b)GaussDB 兼容 NVL但 COALESCE 更通用和 NULL 混用显式区分Oracle 中空字符串即 NULLGaussDB 中两者不同()外连接LEFT JOIN老 SQL 常用需人工改写这些语法问题爆发的时间点不固定。SQL 里没有它执行顺利一遇到边界数据比如某列为空触发NVL逻辑结果就不对了。所以我在迁移前加了一个步骤把所有 SQL 文件里的SYSDATE、ROWNUM、CONNECT BY、()、NVL这些关键词全部grep出来提前人工评审而不是等报错再处理。5. 回归验证与灰度切换确保迁移没有跑偏改造完成不代表迁移完成。Shell 脚本的改造风险在于一件事看起来跑通了但输出结果和原来不一致可能是一列数据没查出来也可能是数字格式变了但肉眼看不出来。所以我要做差异化的双跑验证。5.1 双库双跑结果文件的标准化比对我的做法是准备一套相同的测试数据分别灌到 Oracle 测试库和 GaussDB 测试库然后让同一个业务脚本在两个引擎下各跑一遍比较输出文件。但直接 diff 几乎不可能通过因为两个工具的默认输出还是有一些细微差异。我的标准化步骤是去掉文件首尾的空白行和空行sed -i /^\s*$/d file.csv统一行尾符号dos2unix或sed -i s/\r$//去掉行内所有空白字符做纯文本比对tr -d [:space:]第三步最实用。报表里的数字、中文经过tr -d去空格后只要内容一致diff 就是干净的。当然这只能保证“文本一致”要保证“语义一致”还得做数据聚合校验比如对两个库的报表结果分别做COUNT(*)、SUM(金额)比对聚合值。对数据修复类脚本我还额外做了一步执行前导出受影响表的全量快照执行后对比变更行数、变更前后的 SUM。这一步很笨但能最大程度避免“UPDATE 执行成功但影响行数不对”这种问题。5.2 分批灰度切换的顺序与回滚策略就算双跑验证通过我也不建议一次性把所有脚本切换过去。生产环境的数据分布和测试库不一样某些 SQL 在测试库跑得飞快到了生产环境可能因为数据倾斜直接走全表扫。我采用的顺序是第一批切只读查询类脚本比如报表、统计查询。这些脚本即使出问题影响也仅限读操作不会污染数据。第二批切 DML 类脚本比如 UPDATE、DELETE、INSERT 的批处理。这批必须先确认ON_ERROR_STOP生效、事务提交方式正确。第三批才切 DDL 和涉及事务多步操作的脚本。这类脚本影响最大要配合数据库侧的审计日志做观察。每一批都预留回滚按钮封装函数里engine参数由gaussdb改回oracle重新跑一遍输出对比一下就知道是不是 GaussDB 侧的问题。这个双引擎设计让回滚成本几乎为零这也是我在第三章坚持封装统一函数的原因——它不单是为了改造方便更是为了给运维留一条后路。灰度期间我还会盯几个关键指标脚本执行时长、日志中的错误数、输出文件行数是否稳定。如果某天凌晨的批处理执行时间比 Oracle 时代突然翻倍即使结果没问题也要查一下是不是执行计划走了全表扫。经历过这次迁移之后我最大的体会是数据库切换的难点从来不在“连接方式变了”而在那些被默认行为掩盖住的隐性差异。sqlplus 和 gsql 的表面对齐只是第一步把错误处理、输出格式、变量传递、字符集这些细节逐一敲实脚本才能真正在凌晨三点安稳地跑完。上面这套封装和验证方法我已经沉淀到团队公共脚本库里后续再遇到其他数据库的 CLI 工具替换直接复用同样的思路就能少走很多弯路。
返回列表