ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Oracle JSON字符串截取实战:正则与JSON_VALUE的边界与避坑

Oracle JSON字符串截取实战:正则与JSON_VALUE的边界与避坑 简介围绕Oracle数据库中JSON字符串截取这一常见需求这份小型PDF文档提供了完整的函数实现方案。内容以自定义函数parsejsonstr为核心依次说明p_jsonstr、startkey、endkey三个参数的含义与搭配方式并通过SELECT parsejsonstr(INFO,AGE,HEIGHT) FROM TTTT这类真实SQL示例演示从JSON对象中提取指定字段值的过程适合需要在Oracle中快速解析字段的DBA、后端开发及数据分析人员。文档还补充介绍了JSON_VALUE、JSON_QUERY等内置函数帮助读者对比自定义写法与官方JSON工具的使用边界。资源包仅含1个PDF文件压缩包大小32KB短小精悍便于在本地或移动端随时查阅。目前已有5259人学习下载。通过这份资料读者可以快速理解Oracle中基于INSTR和SUBSTR搭配截取JSON键值的原理也可将自定义函数直接改造用于存量系统的ETL或接口数据清洗场景。1. Oracle里截JSON字符串先想清楚你要的是“值”还是“片段”如果你接手过一张存了JSON字符串的Oracle业务表想从里把其中一个字段的值抠出来当过滤条件你大概率搜过“Oracle截取JSON字符串内容的方法”。网上答案多半是两条甩一条REGEXP_SUBSTR或者劝你改成JSON_VALUE。两条路都走得通但各有前提摸不清前提的人上午刚改完下午就被一个带转义引号的value打回原形。这篇文章就是把边界讲清楚什么时候手工截什么时候用官方JSON函数以及Oracle正则那三个最出名的坑。适合Oracle开发、数仓取数尤其是还在跑11g、12.1老库的同行。2. 为什么JSON在Oracle里不能按普通字符串直接切类型、转义与三个选型2.1 字段类型决定截取姿势VARCHAR2、CLOB、NVARCHAR2动工之前先确认JSON字段的真实类型。Oracle从12c开始才提供完整的JSON访问支持但很多业务表里存的其实是后端用Java、C#拼好写入的json字符串类型只有VARCHAR2或CLOB。VARCHAR2单行能放32767字节大多数记录没问题如果字段是CLOB很多字符串函数不能直接吃REGEXP_SUBSTR会遇到隐式转换问题数据一长就报ORA-06502。我一般会在写任何截取SQL之前跑一条探路语句-- 探路先看字段类型和最大长度决定能不能用字符串函数 select column_name, data_type from user_tab_columns where table_name JSON_TABLE_DATA; select max(length(json_txt)) as max_len from json_table_data;逻辑说明第一条看列类型第二条估算最大长度。如果列是CLOBlength能正确返回字符数但后续REGEXP_SUBSTR是否可靠取决于Oracle会不会隐式把CLOB转VARCHAR2。参数说明user_tab_columns是Oracle字典视图data_type会显示VARCHAR2或CLOBmax(length(...))只是一个上界CLOB场景最好配合dbms_lob.getlength再确认一遍。2.2 三个选型手工定位、正则截取、官方JSON函数把Oracle函数大全及举例翻一遍能用来处理JSON的其实就三组INSTR加SUBSTR做手工定位REGEXP_SUBSTR做模式匹配JSON_VALUE/JSON_TABLE做真正的解析。选哪个不取决于谁更“高级”而取决于三个约束数据库版本、JSON文本是否合法、目标value结构有多复杂。路径适用范围典型代价依赖版本INSTRSUBSTR无嵌套、key固定、value不含转义引号位置计算容易错维护难所有版本REGEXP_SUBSTR单层或浅嵌套、value是字符串/数字/布尔模式复杂嵌套和转义是硬伤所有版本JSON_VALUE/JSON_TABLEJSON合法、嵌套/数组、需要严格解析对非法文本会报错或返回NULL12c及以上很多老帖子一上来就写SUBSTR因为当年Oracle没有JSON函数现在如果你确认版本是19c且文本校验合法直接走官方JSON函数更安全。反过来如果文本是“看起来像JSON但不是合法JSON”的脏数据官方函数反而不如正则能捞出来——这一点是下面所有手工写法的核心价值。2.3 最小样例用INSTR定位键名再用SUBSTR切值先看最朴素的情况JSON是单层、无嵌套、无转义引号-- 样例数据{user_id:1001,user_name:Tom,level:L1} -- 目标取出 user_name 的 value即 Tom with src as ( select {user_id:1001,user_name:Tom,level:L1} as json_txt from dual ) select substr( json_txt, instr(json_txt, user_name:) length(user_name:), instr(json_txt, , instr(json_txt, user_name:) length(user_name:)) - instr(json_txt, user_name:) - length(user_name:) ) as user_name from src;逻辑说明instr找到子串user_name:的起始位置加上length(user_name:)后就是value中Tom的起始下标。随后从该位置开始instr再找下一个双引号这个双引号的位置减去value起始下标正好是Tom的长度交给substr返回。参数说明两层instr都必须写外层instr的第二个参数是搜索子串第三个参数是开始搜索的位置如果value本身是空字符串这里计算出的长度是0substr会返回空串行为是对的。这条SQL能跑但已经能看到问题一旦value里出现或\第二个instr就会找错引号位置。它只适合快速捞一把简单数据不适合做成通用函数。2.4 手工定位会在哪一步开始崩第一个崩点是值里带转义引号。例如desc:say \hi\hi后面的\会被当成value的内容而instr找“下一个双引号”时先撞上的是\里的引号截出来的结果变成say \。第二个崩点是value本身很短或为空位置计算没错但结果为空很容易误判成“字段不存在”。第三个崩点是key顺序变化同一个JSON换一个生成器key顺序一变死板的位置计算就越发不靠谱。顺带说一句如果你只是想截取JSON字符串前两位那substr(json_txt,1,2)就是答案和本文不在一个难度。JSON截取的难点从来不是“截断”而是找到value的起点和终点。如果你是在Oracle存储过程里做同样的事上面SQL可以直接放进select into但变量长度要按最大value预留否则运行时又会踩ORA-06502。2.5 还有一条“字符串长度”的暗坑NVARCHAR2与AL32UTF8当JSON列是NVARCHAR2时length按字符返回substr的行为在不同字符集下可能让你算出来的长度错位库里如果是AL32UTF8中文字符在length里算1但字节数是3。这里不是Oracle算错而是混淆了字符与字节。对JSON截取来说我们要的是字符位置所以统一用length而不是lengthb。多数情况下用substr不会出问题但一旦你为了调试把结果打印到文件发现中文乱码先查字符集别怀疑函数写错。3. 用REGEXP_SUBSTR截取JSON值四种场景的一套固定写法3.1 为什么正则比SUBSTR稳定得多手工用SUBSTR最大的问题是把“参数的逻辑”硬编码成了位置常量而JSON里的value起点和终点都是可变的。正则的思路是描述“value长什么样”让Oracle自己去找边界不依赖key在字符串里的绝对坐标。Oracle从10g起就有REGEXP_SUBSTR常规生产环境都能用不要求12c也不要求JSON合法。代价是语法难记尤其很多人不知道它还有第六个参数subexpr只返回捕获组而不是整段匹配——这个参数用熟了才算真正会用正则截JSON。3.2 单层字符串字段写对第六个参数还是取user_name正则写法比SUBSTR短得多-- 取 user_name:Tom 中的 Tom with src as ( select {user_id:1001,user_name:Tom,level:L1} as json_txt from dual ) select regexp_substr( json_txt, user_name:([^]*), -- 捕获组1非引号字符序列 1, -- 从第1个字符开始 1, -- 第一次匹配 n, -- 点号允许匹配换行实际用于跨行JSON 1 -- 返回第1个捕获组 ) as user_name from src;逻辑说明模式user_name:([^]*)先精确匹配key和引号再用([^]*)捕获value[^]*的意思是“任何不是双引号的字符出现任意次”所以它会停在value结尾的引号前。参数说明第1个参数是源串第2个是模式第3个1表示从源串第1个字符开始搜索第4个1表示取第1次匹配第5个n建议固定写上第6个1是返回第一个捕获组。如果不写第6个参数返回值是整个user_name:Tom而不是Tom这是新手最常见的一次翻车。3.3 数字、布尔和null模式里不能带引号字符串值有引号包裹数字、布尔、null没有。比如age:25和vip:true模式要改成非引号形式-- 取 age 的值支持负数和小数 with src as ( select {age:-1.5,vip:true,remark:null} as json_txt from dual ) select regexp_substr(json_txt, age:(-?[0-9](\.[0-9])?), 1, 1, n, 1) as age, regexp_substr(json_txt, vip:(true|false), 1, 1, n, 1) as vip, regexp_substr(json_txt, remark:(null), 1, 1, n, 1) as remark from src;逻辑说明-?表示负号可有可无[0-9]表示整数部分(\.[0-9])?表示小数部分可省略。布尔值和null直接按单词匹配因为它们后面的下一个字符是逗号或右花括号不会被吞进结果。注意一个细节JSON里的null在SQL里是三个字符的文本不是数据库NULL如果要转成真正的NULL外面再包一层NULLIF(..., null)。3.4 数组用occurrence参数按下标取元素数组场景是把两层REGEXP_SUBSTR和occurrence参数组合起来-- 样例{tags:[a,b,c]}想取第2个元素 b with src as ( select {tags:[a,b,c]} as json_txt from dual ) select regexp_substr( regexp_substr(json_txt, tags:\[[^]]*\]), -- 先截出整个数组片段 ([^]*), -- 再匹配片段里的带引号字符串 1, 2, n, 1 -- occurrence2 表示第二个元素 ) as second_tag from src;逻辑说明外层regexp_substr用\[[^]]*\]匹配从第一个左方括号到第一个右方括号之间的内容拿到tags:[a,b,c]。内层再在这个片段上做一次REGEXP_SUBSTR([^]*)匹配所有带引号的字符串其中第一个匹配是tags第二个才是a所以occurrence参数填2取到a填3取到b。想取数组里下标为0的元素这里要写2最容易迷糊。参数说明内层REGEXP_SUBSTR的第二个参数不用再带key名因为外层已经截出了数组区域第4个参数就是跳过前几个带引号字符串。3.5 嵌套对象别迷信非贪婪匹配网上很多正则答案喜欢用.*?表达“尽量少匹配”但Oracle的REGEXP_SUBSTR基于POSIX风格.*?里的?不是非贪婪修饰符实测中它不会按你预想停到最近的右花括号。所以address:\{.*?\}这种写法是常见隐形炸弹匹配结果往往会吞到整段文本最后一个}然后第六个参数根本拿不到目标字段。嵌套对象的处理思路有两个第一如果内层key在全文里只出现一次就直接搜内层key不走“先截外层”的路线-- 尽管city在嵌套对象里只要没有第二个city key直接搜是安全的 select regexp_substr( {user:{name:Tom,address:{city:Beijing}}}, city:([^]*), 1, 1, n, 1 ) as city from dual;第二如果同一个key会在多个嵌套层出现纯正则几乎不可靠直接放弃手工方案换JSON_VALUE。这不是能力问题是工具边界问题。硬写正则去配平花括号上线后的维护成本会远超你的预期。4. 直接上JSON_VALUE和JSON_TABLE什么场景不该手工写正则4.1 JSON_VALUE用路径表达式取单个字段如果库是12c以上且数据是通过标准JSON生成器写入的合法JSON最省事的截取方法是用官方JSON查询函数。JSON_VALUE用法非常直接-- 用路径表达式取一个标量值等价于手工截取 user_name select json_value( {user_id:1001,user_name:Tom,level:L1}, $.user_name returning varchar2(100) ) as user_name from dual;逻辑说明$.user_name是JSON Path$代表整条JSON.user_name代表取名为user_name的属性。返回类型用returning指定为varchar2。如果字段不存在或类型不匹配默认返回SQL NULL想让它报错可以追加ERROR ON ERROR。参数说明returning varchar2(100)不是必须但建议写因为Oracle会根据JSON内实际值推断类型不指定时有时会返回带引号的文本或类型不匹配的错误。JSON_VALUE本质上解决了一类问题只要路径写对你不需要关心字符串里的引号、转义和位置。它甚至能取嵌套对象里的值比如$.user.address.city这是手工正则几乎做不到的。4.2 JSON_TABLE一次展开多行多列的JSON数组如果JSON里有一个数组而你想把数组展开成多行JSON_VALUE就不够用了得用JSON_TABLE-- 样例{orders:[{id:1,amt:100},{id:2,amt:200}]} -- 目标把 orders 数组展开成两行 select jt.id, jt.amt from dual, json_table( {orders:[{id:1,amt:100},{id:2,amt:200}]}, $.orders[*] columns ( id number path $.id, amt number path $.amt ) ) jt;逻辑说明$.orders[*]遍历orders数组的每个元素columns定义每一行的列每个列用path指定该列在元素中的取值路径。返回结果就是两行1 100和2 200。参数说明json_table需要与一个表名一起出现在FROM中常用dual做无表测试如果源字段在真实表里就把字面量换成t.json_txt并在FROM子句以逗号连接该表。遇到数组套数组可以在一个columns里再写nested path把深层数组一并炸平。4.3 三种不能走官方函数的情况第一种是版本不够。虽然12c引入了JSON系列函数但很多生产环境还跑在Oracle 11g或者12.1的JSON_TABLE支持并不完整。第二种是文本不合法。业务里常有“看起来像JSON但混合了单引号、末尾逗号、不带引号的key”等脏串JSON_VALUE对这些数据直接返回NULL或报错。第三种是JSON其实包裹在一大段日志或上报文本里你需要先从整段文本中切出一小段JSON再解析——这时候外面的手工截取和里面的JSON_VALUE反而要配合。判断标准可以总结成一句话如果整条记录就是一个合法JSON用官方函数如果目标JSON只是某个大文本里的一个片段或者格式经不起RFC校验才轮到第3章的正则出场。两者不是替代关系而是先后关系先正则捞出片段再JSON_VALUE解析这种组合在实际取数里很常见。4.4 三兄弟的分工别搞混JSON_VALUE、JSON_QUERY、JSON_TABLE三兄弟分工不同。JSON_VALUE返回标量值适合取出后当数字或日期用JSON_QUERY返回一段JSON片段对象或数组适合把嵌套对象原样捞出来给前端继续用JSON_TABLE把JSON转成虚拟关系表适合入库、报表、JOIN。很多人只记住JSON_VALUE取对象时返回NULL就以为数据有问题其实是用错了函数。举例JSON_VALUE({a:{b:1}}, $.a)返回NULL而JSON_QUERY({a:{b:1}}, $.a)返回{b:1}。区分标量和对象很重要尤其是嵌套场景。5. JSON截取避坑五个实战翻车现场与改法5.1 REGEXP_SUBSTR返回整段“key:value”而不是value现象执行regexp_substr(json_txt, user_name:(.*))返回的是user_name:Tom不是Tom。原因REGEXP_SUBSTR默认返回整个匹配文本不会自动把括号里的捕获组单独拿出来。解决在第五个参数n后面补第六个参数1select regexp_substr(json_txt, user_name:([^]*), 1, 1, n, 1) from ...如果去掉第六参数看返回值是整段说明模式匹配到了如果返回NULL说明模式本身没命中和第六参数无关。排查时这招很管用。5.2 value里带转义引号截取结果断在半路现象desc:say \hi\ to me用([^]*)截出来的值是say \后面的内容全丢了。原因[^]*遇到转义引号\里的就认为value结束于是停在半路。解决把模式改成允许转义序列-- 用 (\\.|[^\\]) 同时处理转义字符和非引号、非反斜杠的普通字符 select regexp_substr( {desc:say \\hi\\ to me}, desc:((\\.|[^\\])*), 1, 1, n, 1 ) as desc_val from dual;逻辑说明\\.匹配反斜杠加任意一个字符比如\会被当成一个整体[^\\]匹配既不是双引号也不是反斜杠的普通字符两者用|连接整体可以重复任意次。参数说明正则模式中的反斜杠在SQL字面量里要按两个反斜杠写如果你在PL/SQL里再拼接一次字符串转义层数还要再加一层这也是这类SQL在脚本和存储过程之间迁移时最烦人的点。5.3 负数和带小数的数字取不全现象price:-19.90用price:([0-9]*)取到的只有19或空。原因模式没有包含负号和小数点。解决用-?[0-9](\.[0-9])?。这条适用于amount、score这类可能为负的价格字段。还有一种是科学计数法比如1.2E5得另写[0-9](\.[0-9])?([eE][-]?[0-9])?但在业务JSON里很少见我通常不主动扩展。5.4 CLOB字段直接截取报ORA-06502或缓冲区太小现象字段是CLOBREGEXP_SUBSTR执行直接报错或者取到的结果被截断到4000字符以内。原因REGEXP系列函数本质是字符函数涉及CLOB时Oracle会在内部做隐式转换超过VARCHAR2上限就爆。解决先用dbms_lob.substr把CLOB切成可处理片段再做正则-- 只处理前4000字符注意目标value必须出现在这段范围 select regexp_substr( dbms_lob.substr(json_txt, 4000, 1), user_name:([^]*), 1, 1, n, 1 ) from ...参数说明dbms_lob.substr(clob, amount, offset)的amount单位是字符不是字节如果目标字段确实出现在前一段可以用这个办法绕开CLOB限制。如果value落在第4000个字符以后这种截断会拿到NULL别把它当作“字段不存在”。5.5 同名key在不同层级出现取值张冠李戴现象{name:Tom,address:{name:Home}}想取address.name直接用name:(.*)取到了Tom。原因模式从前往后第一次匹配永远是外层name。解决如果内层嵌套里还有指路节点把路径写进正则比如address:\{[^}]*name:([^]*)如果嵌套太深别硬刚换JSON_VALUE。这里没有银弹正则的适用边界就是“可以用字段路径锁定”的情况路径越明确正则越可靠。6. 交付前做一次对照验证用官方函数反查手工截取结果手工截取正则最容易栽在一个问题上单条SQL测试通过批量数据里有几条特殊value直接返回NULL或错值你还不知道是哪条。我的习惯是写一个对照查询把手工截取结果和JSON_VALUE结果放在同一行让Oracle自己判断差异-- 对照验证手工结果 vs 官方结果 with base as ( select id, json_txt from json_table_data where json_txt is not null ) select b.id, regexp_substr(b.json_txt, user_name:([^]*), 1, 1, n, 1) as manual_val, json_value(b.json_txt, $.user_name returning varchar2(100)) as official_val from base b where nvl(regexp_substr(b.json_txt, user_name:([^]*), 1, 1, n, 1), #) ! nvl(json_value(b.json_txt, $.user_name returning varchar2(100)), #);逻辑说明manual_val和official_val在同一行不相等就会被这个查询捞出来方便你直接定位是哪几条数据出了问题。这个验证要求12c以上因为json_value本身需要12c。版本不够时可以退一步用instr检查手工结果是否再次出现在原JSON的合理位置附近虽然不如官方解析可靠但能抓住一批明显错位。另外一个实用技巧是把手工截取结果作为“线索”在PL/SQL里抽出20条样本打印原文和截取结果。肉眼扫一遍比对着函数文档猜半天更管用。我在一个订单JSON字段里遇到过value含HTML标签和\n的情况就是靠这种对照抓出来的。用JSON_VALUE做反查不是否定手工方法而是给手工正则加一道安全网。经过这类对照之后我才敢把一条截取SQL放进报表脚本或存储过程。希望帮到你。本文还有配套的精品资源点击获取
返回列表