首页 / 资讯中心 / 文章详情

Oracle汉字转拼音PL/SQL包:字典表、集合化与物化列

Oracle汉字转拼音PL/SQL包:字典表、集合化与物化列 ★ FEATURED ARTICLE
简介面向Oracle数据库开发与运维人员的汉字转拼音PL/SQL包适用于UTF8字符集环境下的数据分析、索引优化与中文文本处理场景。压缩包约156KB仅含1个SQL脚本文件导入后可在库中创建对应Package提供汉字转全拼、提取首字母等函数分别对应全拼与首字母获取并涉及多音字、首选读音及UTF8编码一致性的处理思路。已有500人学习下载对需要快速补齐中文转换能力的项目具有一定参考价值。读者可据此生成拼音检索字段、构建快速查询索引或清洗多语言文本也可参考其PL/SQL组织方式将转换逻辑封装为可复用函数在SQL或PL/SQL中直接调用减少从零编写与调试的时间面对数据量较大的场景还可结合批量处理与算法优化思路进行二次调整提升中文数据加工效率。1. 客户名排序这件小事把开发拖了两周某公司的客户列表要按拼音排序前端最初的方案是把十几万条客户名一次性拉到应用内存里转拼音再排第一页能撑住翻到第三页内存就告警。后来把这段逻辑挪回数据库用一份 oracle 汉字转拼音 package 包在 SQL 层直接出结果应用侧只改了一个字段。这份包是纯 PL/SQL 实现靠一张可维护的拼音字典表逐字查表转换支持 UTF8 字符集能输出全拼和首字母两种形态分隔符、大小写都可配。适合在 Oracle 里做报表排序、模糊检索、姓名脱敏后再匹配以及离线数据清洗的人。它不解决自然语言分词也不做智能多音字识别定位就是稳定的工程级逐字映射。2. 字典表先行码表结构与 UTF8 存储位宽的两个坑在动手写包体之前先得把字典表定下来。这一步比函数本身重要因为后面所有转换质量、性能、扩展能力都压在这张表上。我见过不少团队一上来就写几千行 CASE WHEN 把汉字硬编码进函数体结果新增十几个生僻字就得改包、重新编译、走一次上线流程。换成表之后补录字只是几条 INSERT业务方自己就能维护。这一章先把表结构、字符语义和灌数方式落定后面第 3 章的包体才有地基。2.1 为什么拼音要放进表而不是写进代码硬编码的好处是函数自包含不需要额外对象拷贝即用。坏处也很明显多音字没有回旋余地一个汉字只能有一个读音字典升级等于代码发布跨库同步时函数和字典会脱节。表结构的方案牺牲一点点部署复杂度换来的是可维护性。表设计要解决三个具体问题。第一一个汉字可能对应多个读音比如「行」「重」「长」所以主键不能只用汉字得用「汉字 拼音」组合。第二转换时得知道哪个读音是默认值需要一列标记。第三多个默认值会导致结果随机漂移必须靠约束或优先级列压住。CREATE TABLE T_PINYIN_DICT ( HANZI VARCHAR2(4 CHAR) NOT NULL, -- 单个汉字按字符语义UTF8 下够用 PINYIN VARCHAR2(32 CHAR) NOT NULL, -- 无声调全拼如 zhang PY_INITIAL VARCHAR2(1 CHAR) NOT NULL, -- 首字母如 z PRIORITY NUMBER(3) DEFAULT 100 NOT NULL, -- 数值越小优先级越高 IS_DEFAULT NUMBER(1) DEFAULT 1 NOT NULL, -- 1 参与默认转换0 仅作词组备选 SRC VARCHAR2(16 CHAR) DEFAULT BASE, -- 数据来源标记方便回溯 CONSTRAINT PK_PINYIN_DICT PRIMARY KEY (HANZI, PINYIN) ); CREATE INDEX IDX_PY_DICT_LOOKUP ON T_PINYIN_DICT (HANZI, IS_DEFAULT, PRIORITY);建表语句里有几个参数值得说明。VARCHAR2(4 CHAR)显式声明字符语义一个汉字在 AL32UTF8 下占 3 字节BYTE 语义的VARCHAR2(4)只能存下一个汉字加一字节余量遇上扩展区字符直接报 ORA-12899。PRIORITY列是给多音字兜底的默认值 100需要特殊处理的字给个更小的数包体里ORDER BY PRIORITY取第一条结果就是确定的不会出现同一份数据两次跑出两种拼音的情况。IS_DEFAULT和PRIORITY同时存在看起来冗余实际是两个维度前者决定这个读音是否参与默认转换词组模式下可以把非默认读音临时启用后者决定多个默认读音之间谁优先。用唯一索引把「一个汉字只能有一个默认读音」这条规则钉死比在函数里写 IF 判断可靠得多。CREATE UNIQUE INDEX UK_PY_DICT_DEFAULT ON T_PINYIN_DICT (CASE WHEN IS_DEFAULT 1 THEN HANZI END);这是基于函数的唯一索引只对IS_DEFAULT 1的行生效。一旦有人手工插了第二条默认读音INSERT 直接失败问题在入库那一刻暴露而不是等到某天报表排序结果看起来「有点怪」才被翻出来。2.2 字符集语义LENGTH、LENGTHB 与 SUBSTRB 的区别这一步容易翻车。包体逐字遍历靠的是SUBSTR(str, i, 1)但在写任何字符串处理代码之前得先确认当前库的字符集和长度函数的实际行为。SELECT VALUE FROM NLS_DATABASE_PARAMETERS WHERE PARAMETER NLS_CHARACTERSET; -- 典型返回AL32UTF8 或 ZHS16GBK SELECT LENGTH(汉) AS 字符数, LENGTHB(汉) AS 字节数, SUBSTR(汉字, 2, 1) AS 按字符截, SUBSTRB(汉字, 2, 1) AS 按字节截 FROM DUAL;在 AL32UTF8 下第一行返回 1 和 3SUBSTR得到「字」SUBSTRB从第 2 个字节开始截 1 个字节拿到的是半个字符的残缺字节序列转成可显示字符就是一个问号或者方块。这是 UTF8 环境里最高频的乱码来源而且它不会报错只会让结果「看起来不太对」。我把这条当成硬规则只要入参里可能有中文长度计算一律用LENGTH截取一律用SUBSTR永远不要用LENGTHB和SUBSTRB。例外只有一个——计算字段容量、判断会不会超长时才去关心字节数因为VARCHAR2(n CHAR)在 UTF8 下最多可能占用 4n 字节超过 4000 字节的行限制就会报错。做转换输出列时长度要按字节余量估目标列VARCHAR2(200 CHAR)中文全拼平均一个字 5 个字母200 字大约要 1000 字节安全。如果库是 ZHS16GBK一个汉字 2 字节LENGTH和LENGTHB的比值是 2 而不是 3但结论一样只用字符语义的函数。同一份包要在两种字符集下都能跑就不能在代码里写死任何字节数。2.3 灌数与批量导入的写法字典表的数据通常从现成的码表文件来几千到两万多条。手工一条条 INSERT 不现实用INSERT ALL分批灌是最省事的做法每批 500 到 1000 条避免单条 SQL 过长。INSERT ALL INTO T_PINYIN_DICT (HANZI, PINYIN, PY_INITIAL, PRIORITY, IS_DEFAULT) VALUES (中, zhong, z, 10, 1) INTO T_PINYIN_DICT (HANZI, PINYIN, PY_INITIAL, PRIORITY, IS_DEFAULT) VALUES (中, zhong4, z, 90, 0) INTO T_PINYIN_DICT (HANZI, PINYIN, PY_INITIAL, PRIORITY, IS_DEFAULT) VALUES (行, xing, x, 10, 1) INTO T_PINYIN_DICT (HANZI, PINYIN, PY_INITIAL, PRIORITY, IS_DEFAULT) VALUES (行, hang, h, 20, 0) SELECT 1 FROM DUAL; COMMIT;每条记录对应「字 读音」的组合IS_DEFAULT 1的那条才是默认转换用的。多音字的所有读音都进表先别急着删后面做词组优先或者按上下文改写时用得上。PRIORITY在同一个字的多个读音间拉开差距10 比 90 小默认取 10。数据量再大就上 SQL*Loader控制文件里注意CHARACTERSET AL32UTF8这一行源文件编码和库字符集不一致时导入结果会出现整批问号。灌完之后做一次完整性核对-- 默认读音重复的字结果应为 0 行 SELECT HANZI, COUNT(*) CNT FROM T_PINYIN_DICT WHERE IS_DEFAULT 1 GROUP BY HANZI HAVING COUNT(*) 1; -- 首字母与全拼首字符不一致的记录通常是码表录入错误 SELECT HANZI, PINYIN, PY_INITIAL FROM T_PINYIN_DICT WHERE SUBSTR(PINYIN, 1, 1) LOWER(PY_INITIAL); -- 总字数与默认读音数 SELECT COUNT(DISTINCT HANZI) AS 字数, COUNT(*) AS 记录数 FROM T_PINYIN_DICT;第一条查询如果有返回说明唯一索引没建或者被禁用了必须处理完再往下走。第二条抓的是码表本身的录入错误比如PINYIN写成zhang而PY_INITIAL写成c这类脏数据在包里不会报错只会让首字母检索悄悄漏结果。第三条给个基本盘常用汉字 3500 个左右加上生僻字和扩展区字典表两万条上下算正常范围。3. 包规范与包体三种输出形态的参数设计表备好了接下来把这套查表逻辑封成包。封包而不是写成独立函数主要是为了版本管理和权限控制——业务方只需要GRANT EXECUTE ON PKG_PINYIN不用碰底层表字典表结构怎么调都不影响调用方。接口设计上我倾向少而稳一个主函数负责全拼和首字母两种模式一个便捷函数专门出首字母其余行为全靠参数控制不做函数爆炸。参数多了调用方记不住参数少了又得到处写包装函数三个参数是个平衡点。3.1 包规范用三个入参覆盖绝大多数场景CREATE OR REPLACE PACKAGE PKG_PINYIN AS /* 汉字转拼音工具包 P_MODE : FULL 全拼汉字转拼音非汉字原样保留 FIRST 首字母每个字取拼音首字母 P_SEP : 字间分隔符NULL 表示不分隔 P_UPPER : 1 输出大写0 输出小写 */ FUNCTION F_GET_PY ( P_STR IN VARCHAR2, P_MODE IN VARCHAR2 DEFAULT FULL, P_SEP IN VARCHAR2 DEFAULT NULL, P_UPPER IN NUMBER DEFAULT 0 ) RETURN VARCHAR2 DETERMINISTIC; FUNCTION F_GET_PY_FIRST ( P_STR IN VARCHAR2, P_UPPER IN NUMBER DEFAULT 0 ) RETURN VARCHAR2 DETERMINISTIC; END PKG_PINYIN;DETERMINISTIC这个关键字必须有。它的含义是「相同入参必然返回相同结果」优化器据此可以在同一条 SQL 里对重复入参复用结果也允许基于该函数建函数索引。严格说它依赖字典表内容字典表一改结果就变语义上并不完全成立但在工程实践中这个约定能带来实打实的性能收益代价是字典表变更后要手工重建函数索引。这个边界后面第 6 章会再提。P_MODE用字符串而不是数字是为了调用方读得懂。F_GET_PY(张三, FIRST)比F_GET_PY(张三, 2)清晰得多代价是每次多几次字符串比较在百万级调用下可以忽略。P_SEP默认 NULL也就是不分隔需要zhang san这种形态时传空格即可。3.2 包体核心逐字符遍历与查不到时的回退CREATE OR REPLACE PACKAGE BODY PKG_PINYIN AS FUNCTION F_GET_PY ( P_STR IN VARCHAR2, P_MODE IN VARCHAR2 DEFAULT FULL, P_SEP IN VARCHAR2 DEFAULT NULL, P_UPPER IN NUMBER DEFAULT 0 ) RETURN VARCHAR2 DETERMINISTIC IS V_BUF VARCHAR2(32767 CHAR); -- PL/SQL 内最大 32767SQL 调用受 4000/8191 限制 V_CH VARCHAR2(4 CHAR); -- 单字符UTF8 下 4 CHAR 足够容纳扩展区 V_PY VARCHAR2(32 CHAR); V_LEN PLS_INTEGER; BEGIN IF P_STR IS NULL THEN RETURN NULL; -- 空进空出不返回空串 END IF; V_LEN : LENGTH(P_STR); -- 字符数绝不能用 LENGTHB FOR I IN 1 .. V_LEN LOOP V_CH : SUBSTR(P_STR, I, 1); BEGIN SELECT CASE WHEN P_MODE FIRST THEN PY_INITIAL ELSE PINYIN END INTO V_PY FROM (SELECT PY_INITIAL, PINYIN FROM T_PINYIN_DICT WHERE HANZI V_CH AND IS_DEFAULT 1 ORDER BY PRIORITY) WHERE ROWNUM 1; -- 配合 PRIORITY 保证结果唯一 EXCEPTION WHEN NO_DATA_FOUND THEN V_PY : V_CH; -- 非汉字、数字、字母、标点原样保留 END; IF I 1 AND P_SEP IS NOT NULL THEN V_BUF : V_BUF || P_SEP; END IF; V_BUF : V_BUF || V_PY; END LOOP; RETURN CASE WHEN P_UPPER 1 THEN NLS_UPPER(V_BUF) ELSE NLS_LOWER(V_BUF) END; END F_GET_PY; FUNCTION F_GET_PY_FIRST ( P_STR IN VARCHAR2, P_UPPER IN NUMBER DEFAULT 0 ) RETURN VARCHAR2 DETERMINISTIC IS BEGIN RETURN F_GET_PY(P_STR, FIRST, NULL, P_UPPER); END F_GET_PY_FIRST; END PKG_PINYIN;逐行拆开看。V_BUF声明成32767 CHAR是 PL/SQL 的上限但通过 SQL 调用时返回值受 SQL 层限制12c 之前是 4000 字节之后是 4000 字符或 32767 字节取决于MAX_STRING_SIZE。这意味着超过大约一千个汉字的入参会被截断而且不报错。真要处理长文本得分段调用再拼接或者干脆走集合化方案第 4 章会讲。FOR I IN 1 .. V_LEN里的V_LEN必须在循环前算好。如果写成FOR I IN 1 .. LENGTH(P_STR)每一轮迭代都会重新计算长度虽然结果是常量但多了一次函数调用开销在十万行批量场景下这个差异会被放大。内层BEGIN ... EXCEPTION WHEN NO_DATA_FOUND是关键。字典表查不到时SELECT INTO会抛NO_DATA_FOUND这里捕获后把原字符赋给V_PY于是英文、数字、标点、生僻字都会被原样保留。这个设计让函数对混合字符串是安全的不会因为一个未收录的字把整条记录变成 NULL。如果业务上希望未收录的字被剔除把V_PY : V_CH改成V_PY : NULL并调整拼接逻辑即可。ORDER BY PRIORITY加ROWNUM 1的组合保证了即使唯一索引因为某些原因没生效结果也是确定的第一优先级读音不会每次执行返回不同值。NLS_UPPER和NLS_LOWER而不是UPPER、LOWER是为多语言环境准备的。对纯英文和拼音来说两者行为一致但用 NLS 版本更稳妥。3.3 参数组合与输出对照下表的输出可以直接拿去和调用方对口径避免「我以为你要的是这个」这类返工。入参 P_STRP_MODEP_SEPP_UPPER返回张三FULLNULL0zhangsan张三FULL空格1ZHANG SAN张三FIRSTNULL0zs张三A1FULLNULL0zhangsana1张三A1FIRST-0z-s-a-1重庆FULLNULL0zhongqing重庆FIRSTNULL1ZQ(NULL)任意任意任意NULL空串任意任意任意NULL最后两行值得单独说。P_STR为 NULL 时直接返回 NULL这是正确的三值逻辑空串在 Oracle 里等价于 NULL所以也返回 NULL调用方如果期望拿到空串需要在应用层做转换。第六行暴露了逐字映射的天然短板重庆应该读chongqing但字典表默认读音是zhong单字映射拿不到语境。这是这套方案的边界不是 bug需要靠词组表或者预处理解决第 5 章会给出实际可用的绕法。4. 集合化改写十万行数据下的性能分水岭函数跑单条记录响应在毫秒级感觉不出问题。一旦出现在SELECT ... FROM 大表 WHERE 条件里或者被写进报表的每一行性能就断崖式下跌。这一章把慢的根因说清楚再给一个能直接跑在十万行量级上的替代方案。判断标准很朴素如果转换只用于展示单条详情页包函数够用不必折腾如果要参与排序、分组、导出、批量查重老老实实走集合化。4.1 逐字循环在批量场景下为什么撑不住假设一张 10 万行的客户表平均每个名字 3 个汉字一次全量转换要执行 30 万次SELECT INTO。每次查询都是一次 PL/SQL 到 SQL 引擎的上下文切换加上字典表的索引探测单次按 20 微秒估30 万次就是 6 秒以上实际在共享池压力大、字典表缓存命中率低的库上跑出一分钟也不稀奇。更麻烦的是函数出现在WHERE子句里时比如WHERE F_GET_PY(CUST_NAME) LIKE zhang%优化器无法下推任何条件只能全表扫描后逐行调用。数据量翻倍耗时跟着翻倍索引完全用不上。真要优化方向只有一个把逐行调用改成集合运算让字典表的连接和聚合都在 SQL 引擎里一次完成。4.2 递归拆分加表连接一次算完思路是把每个字符串拆成单字符的中间结果集和字典表做一次 LEFT JOIN再用LISTAGG按位置拼回去。整条 SQL 里没有任何 PL/SQL 调用。CREATE OR REPLACE VIEW V_CUST_PY AS WITH SRC AS ( SELECT CUST_ID, CUST_NAME FROM T_CUSTOMER ), SPLIT AS ( -- 用 CONNECT BY 把每个名字拆成单字符行POS 记录原始位置 SELECT S.CUST_ID, LEVEL AS POS, SUBSTR(S.CUST_NAME, LEVEL, 1) AS CH FROM SRC S CONNECT BY LEVEL LENGTH(S.CUST_NAME) AND PRIOR S.CUST_ID S.CUST_ID AND PRIOR SYS_GUID() IS NOT NULL -- 阻止跨行递归固定写法 ), MAP AS ( SELECT P.CUST_ID, P.POS, NVL(D.PINYIN, P.CH) AS PY FROM SPLIT P LEFT JOIN T_PINYIN_DICT D ON D.HANZI P.CH AND D.IS_DEFAULT 1 ) SELECT CUST_ID, LISTAGG(PY, ) WITHIN GROUP (ORDER BY POS) AS PY_FULL, LISTAGG(SUBSTR(PY, 1, 1), ) WITHIN GROUP (ORDER BY POS) AS PY_FIRST FROM MAP GROUP BY CUST_ID;SPLIT那一段是整个方案的核心。CONNECT BY LEVEL LENGTH(名字)生成和字符数等长的行SUBSTR(..., LEVEL, 1)按位置取字符。PRIOR S.CUST_ID S.CUST_ID保证递归在单行内进行PRIOR SYS_GUID() IS NOT NULL是一个永远为真的附加条件用来防止 Oracle 把递归扩展到多行之间——少了这一句结果集会呈指数膨胀十万行能跑出上千万行中间结果。这个写法看起来像玄学实际是递归查询里必须加的保护。MAP里的LEFT JOIN而不是INNER JOIN保证未收录字符原样保留。NVL(D.PINYIN, P.CH)和包函数里的WHEN NO_DATA_FOUND THEN V_PY : V_CH是完全等价的语义只是从异常处理变成了空值处理。LISTAGG在 12.2 之前的版本有 4000 字节的上限超了直接报 ORA-01489。名字字段一般不超但如果是长文本转换要么用LISTAGG(... ON OVERFLOW TRUNCATE)12.2要么换XMLAGGSELECT CUST_ID, RTRIM(EXTRACTVALUE( XMLTYPE(XMLELEMENT(R, XMLAGG(XMLELEMENT(C, PY) ORDER BY POS))), /R/C/text() ), ,) AS PY_FULL FROM MAP GROUP BY CUST_ID;XMLAGG的写法在旧版本上兼容性更好但生成的中间 XML 占用内存更多十万行级别还行百万级以上建议直接用 12.2 之后的LISTAGG ON OVERFLOW。这套改写在某公司的十万行客户表上从原来的 70 多秒降到 4 秒上下差距主要在上下文切换次数上。代价是可读性下降而且要求CUST_NAME非空否则LENGTH(NULL)会让该行从结果里消失需要额外加WHERE CUST_NAME IS NOT NULL或者用NVL兜底。4.3 什么情况下仍然该用包函数集合化方案不是万能的下面几种场景我仍然会用包函数单行详情展示调用次数少SQL 里写PKG_PINYIN.F_GET_PY(NAME)最直观需要按分隔符输出、大小写可切换的灵活形态LISTAGG拼分隔符要额外处理首尾字符不如函数省事转换逻辑需要频繁调整比如临时切换某批字的多音字优先级函数改完即生效视图得重建数据量在万行以内两者的差异肉眼看不出来选维护成本低的。判断界线大概在五万行。低于这个数函数调用带来的额外开销还在可接受范围高于这个数或者要进 WHERE、ORDER BY就老实写集合化 SQL。另外DETERMINISTIC在批量调用里能帮上一点忙同一条 SQL 内相同入参只算一次。客户名重复率高的时候比如大量「张伟」「王芳」命中率相当可观。但这个优化只作用于单条 SQL 的执行期跨语句不共享别指望它替代结果缓存。5. 避坑排查五条踩坑记录和一套自检顺序这套包从写完到稳定中间踩的坑基本集中在字符编码、多音字优先级和权限三块。下面五条是按出现频率排的每条都按「现象 → 原因 → 解决」写遇到问题时可以对着翻。5.1 五条高频问题第一条整批数据转出来全是问号。现象是导出 CSV 打开一看所有中文变成???但直接在库里查T_PINYIN_DICT内容正常。原因是客户端或导入导出工具的NLS_LANG与库字符集不匹配。库是 AL32UTF8工具按 ZHS16GBK 解析字节序列对不上就全部替换成问号。解决分两步先确认库字符集SELECT VALUE FROM NLS_DATABASE_PARAMETERS WHERE PARAMETERNLS_CHARACTERSET再把客户端的NLS_LANG设成AMERICAN_AMERICA.AL32UTF8重连后再导出。用命令行工具时在会话级别执行ALTER SESSION SET NLS_LANGUAGE不够NLS_LANG必须在连接建立前设好。第二条转换结果尾部少一两个字。现象是F_GET_PY(张三丰)返回zhangsan最后一个字丢了。原因是输出变量或目标列按字节声明比如VARCHAR2(10)三个字的全拼需要 15 字节以上超出部分被静默截断或者有人在某处改用了SUBSTRB。解决是把所有相关声明改成字符语义VARCHAR2(200 CHAR)这类写法再用LENGTH(F_GET_PY(col))和LENGTH(col)的比值抽查正常应在 1 到 6 之间出现 0.33 倍左右的异常值就说明有截断。第三条同一个名字两次跑出两种拼音。现象是报表里「重」有时转zhong有时转chong而且不固定。原因是字典表里同一个字有多条IS_DEFAULT 1的记录ROWNUM 1取到哪条取决于执行计划本质是随机的。解决是先跑第 2 章那条 GROUP BY HAVING 查重语句把多余的默认标记清掉然后补上基于函数的唯一索引堵住入口。已经上线的库如果不敢加索引至少在包体里把ORDER BY PRIORITY补上让结果确定。第四条加了函数之后原本走索引的查询变成全表扫描。现象是WHERE F_GET_PY(NAME) LIKE zhang%执行计划从 INDEX RANGE SCAN 变成 TABLE ACCESS FULL耗时从 0.1 秒涨到几十秒。原因是函数包在列外面优化器无法用列上的普通索引只能逐行计算。解决不是调优 SQL而是换思路新增一个物理列存拼音用触发器或定时 MERGE 维护查询直接走这个列上的索引。函数索引也能用但字典表一变索引就失效维护成本高不如物化列稳。具体做法第 6 章展开。第五条跨 schema 调用编译报错。现象是 A 用户下建包成功B 用户调用时报ORA-06550: PLS-00201: identifier PKG_PINYIN must be declared。原因是授权只给了包没给底层字典表的查询权限或者同义词没建。解决是按顺序补齐-- 在包属主用户下执行 GRANT EXECUTE ON PKG_PINYIN TO APP_USER; GRANT SELECT ON T_PINYIN_DICT TO APP_USER; -- 或者建同义词省掉前缀 CREATE OR REPLACE PUBLIC SYNONYM PKG_PINYIN FOR OWNER_USER.PKG_PINYIN;注意包体里如果用了AUTHID CURRENT_USER权限检查会以调用者身份执行这时调用方必须自己有字典表 SELECT 权限用默认的AUTHID DEFINER权限检查以包属主身份执行只需要EXECUTE。两者选一个别混着用。5.2 上生产前的自检顺序每次部署这套包我会按固定顺序跑一遍五分钟能挡掉八成问题。-- 1. 字符集确认 SELECT PARAMETER, VALUE FROM NLS_DATABASE_PARAMETERS WHERE PARAMETER IN (NLS_CHARACTERSET, NLS_LENGTH_SEMANTICS); -- 2. 字典表默认读音唯一性 SELECT COUNT(*) FROM ( SELECT HANZI FROM T_PINYIN_DICT WHERE IS_DEFAULT 1 GROUP BY HANZI HAVING COUNT(*) 1 ); -- 3. 常用字覆盖率抽检正常应全部非空 SELECT HANZI, F_GET_PY(HANZI) AS PY FROM (SELECT 张 HANZI FROM DUAL UNION ALL SELECT 重 FROM DUAL UNION ALL SELECT 长 FROM DUAL UNION ALL SELECT 行 FROM DUAL); -- 4. 混合字符串回退验证 SELECT PKG_PINYIN.F_GET_PY(张A1-三, FULL, -) FROM DUAL; -- 5. 性能基线一万次调用的耗时 SET TIMING ON SELECT COUNT(PKG_PINYIN.F_GET_PY(CUST_NAME)) FROM T_CUSTOMER WHERE ROWNUM 10000;第 1 步确认字符集和长度语义第 2 步挡重复默认读音第 3 步抽四个多音字看默认值是否符合业务预期第 4 步验证未收录字符是否原样保留第 5 步留一个性能基线数字后面字典表变大或者逻辑改动后对比用。第 5 步如果超过 3 秒说明该走集合化了。6. 进阶落地物化拼音列与回归验证的实操包能跑通只是起点真正要让拼音参与业务检索还得解决「怎么用得快、改得稳」这两个问题。这一章给一套我带过的项目里实际用了两年的做法拼音存物理列触发器维护函数索引只作为临时手段最后配一份固定用例做回归。6.1 物化列加触发器的写入路径在客户表上直接加两列一列全拼一列首字母长度按业务字段上限的 6 倍预留。ALTER TABLE T_CUSTOMER ADD ( PY_FULL VARCHAR2(200 CHAR), PY_FIRST VARCHAR2(60 CHAR) ); CREATE INDEX IDX_CUST_PY_FULL ON T_CUSTOMER (PY_FULL); CREATE INDEX IDX_CUST_PY_FIRST ON T_CUSTOMER (PY_FIRST); CREATE OR REPLACE TRIGGER TRG_CUST_PY BEFORE INSERT OR UPDATE OF CUST_NAME ON T_CUSTOMER FOR EACH ROW BEGIN IF :NEW.CUST_NAME IS NOT NULL THEN :NEW.PY_FULL : PKG_PINYIN.F_GET_PY(:NEW.CUST_NAME, FULL, NULL, 0); :NEW.PY_FIRST : PKG_PINYIN.F_GET_PY(:NEW.CUST_NAME, FIRST, NULL, 0); ELSE :NEW.PY_FULL : NULL; :NEW.PY_FIRST : NULL; END IF; END; /触发器只监听CUST_NAME的变化名字不动就不重算避免无关更新带来额外开销。BEFORE而不是AFTER这样能在同一行写入前把值算好少一次 UPDATE。写入路径上单行转换耗时在毫秒级对 OLTP 影响可以接受如果写并发特别高可以改成异步队列但多数业务用不上。存量数据用一次 MERGE 刷进去MERGE INTO T_CUSTOMER T USING ( SELECT CUST_ID, CUST_NAME FROM T_CUSTOMER WHERE PY_FULL IS NULL ) S ON (T.CUST_ID S.CUST_ID) WHEN MATCHED THEN UPDATE SET T.PY_FULL PKG_PINYIN.F_GET_PY(S.CUST_NAME, FULL, NULL, 0), T.PY_FIRST PKG_PINYIN.F_GET_PY(S.CUST_NAME, FIRST, NULL, 0);十万行量级分批提交更稳加WHERE ROWNUM 10000循环跑每批之间COMMIT避免大事务把 UNDO 撑满。查询侧就简单了前缀匹配直接走索引SELECT CUST_ID, CUST_NAME, PY_FULL FROM T_CUSTOMER WHERE PY_FULL LIKE zhangsan%;有人会问能不能用函数索引省掉这两列。可以但有两个硬约束函数必须标DETERMINISTIC以及字典表变更后索引不会自动失效需要手工ALTER INDEX ... REBUILD或者先 DROP 再建。多音字优先级一调整索引里的值就是旧的查询结果和函数实时计算对不上这种不一致排查起来很费时间。物化列虽然多占空间但值是什么、什么时候更新的都能直接查到出问题好定位。6.2 回归用例表与固定验证脚本字典表不是静态的业务上隔三差五会补生僻字、调多音字优先级。每次改完如果不验证很容易在某个边角上翻车。我的习惯是维护一张固定用例表改完字典就跑一遍全绿再上线。用例入参期望输出验证点常用名全拼张三丰zhangsanfeng基本映射多音字默认重庆zhongqing默认读音是否被误改英文数字混合张A1zhanga1非汉字原样保留生僻字未收录扩展区汉字原字返回回退分支全角标点张三李四zhangsanlisi标点不被吃掉空值NULLNULL三值逻辑超长文本200 字姓名串长度与入参成比例截断检查用例表本身就是一个普通表跑起来是一条对比 SQLCREATE TABLE T_PY_CASE ( CASE_ID NUMBER PRIMARY KEY, IN_STR VARCHAR2(200 CHAR), EXPECT VARCHAR2(400 CHAR), MODE VARCHAR2(10) DEFAULT FULL ); INSERT INTO T_PY_CASE VALUES (1, 张三丰, zhangsanfeng, FULL); INSERT INTO T_PY_CASE VALUES (2, 重庆, zhongqing, FULL); INSERT INTO T_PY_CASE VALUES (3, 张A1, zhanga1, FULL); INSERT INTO T_PY_CASE VALUES (4, 张三李四, zhangsanlisi, FULL); COMMIT; -- 跑一遍返回 0 行表示全部通过 SELECT CASE_ID, IN_STR, EXPECT, PKG_PINYIN.F_GET_PY(IN_STR, MODE) AS ACTUAL FROM T_PY_CASE WHERE PKG_PINYIN.F_GET_PY(IN_STR, MODE) EXPECT OR (EXPECT IS NULL AND PKG_PINYIN.F_GET_PY(IN_STR, MODE) IS NOT NULL);第二条「重庆 → zhongqing」这个期望值看着别扭但它是当前方案的真实行为写进用例的意义在于哪天有人把默认读音改成chong用例会立刻红提醒所有人这是一次有意的变更得同步评估存量数据。如果业务确实需要chongqing正确做法不是改用例而是在转换前加一层词组替换把常见地名、姓氏先整体映射掉再走逐字转换。这套用例我一般放在版本库的test/目录下改包必须带着跑一遍。有次图省事跳过了结果字典表里一个批次导入把「长」的默认读音覆盖成chang第二天报表里所有「张」姓客户的首字母排序全乱了回头定位花了半天。从那以后我每次动字典表或者包体都先把字符集、长度、默认读音唯一性这三项和回归用例一起跑完再提交宁可多花五分钟。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站