ARTICLE DETAIL

资讯详情

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

XML查询转SQL:Edge区间编码与递归CTE翻译器设计

XML查询转SQL:Edge区间编码与递归CTE翻译器设计 简介《XML查询语句转换成SQL语句的实现.pdf》是一份面向数据库开发、Web数据挖掘及XML数据处理人员的专业参考文档主要解决XML查询语言XPath/XQuery与关系数据库SQL语句之间的转换问题。资料基于W3C标准系统介绍XPath与XQuery的功能特性、表达式语法格式如Path : /step1/step2/.../stepN/并详细讲解Select与Reconstruct两种求值方式。文中重点给出了将XPath、XQuery语句转换成SQL的算法实现思路包括利用Edge表存储XML文档编码并逐步生成SQL子查询的完整过程对理解XML数据入库与查询优化具有直接帮助。资源为单一PDF文件文件大小107KB便于下载后快速阅读。目前已有1179人浏览学习无论是初学者掌握基本概念还是进阶者研究转换算法都能从中获得有价值的参考。1. 把 XML 查询语句翻译成 SQL本质是把树状匹配转成集合连接很多人处理 XML 数据时的第一反应是上解析器把文档读进内存再手动遍历节点。可一旦文档量上来或者查询要走 Web 数据挖掘、结构化分析流程这条路就废了。这篇论文给的路线完全不同先把 XML 文档无损地压平进一张关系表再把 XPath / XQuery 的每一个定位步骤编译成一段 SQL 的 WITH 子句让数据库自己完成节点筛选。查询不再是程序去遍历树而是翻译器生成一条完整的递归 CTE 链交给优化器去跑。适合做数据集成中间件、XML 存储层、或者需要把历史 XML 数据导入数仓的工程师也适合想理解 SQL 生成器怎么写的人。拆开看核心就三件事Edge 区间编码怎么建表、XPath 的 step 怎么切分、每个 axis 和 predicate 怎么映射到 JOIN 和子查询条件。2. Edge 区间编码XML 树进关系表的前提2.1 为什么选择区间编码而不是路径字符串XML 文档是一棵有序树。把树存进关系表最简单的做法是给每个节点记一条完整路径比如/site/people/person/name查询时用LIKE /site/people/%去模糊匹配。这条路在小数据量下能用但有两个硬伤一是无法表达第二个 person这类位置语义name[position()2]在路径字符串里根本算不出来二是LIKE前缀匹配无法利用索引做范围扫描慢 SQL 优化无从谈起。论文的 Edge 表方案把每个节点存成一行每行只记录三个关键值节点自己的id、父节点的parent_id、以及该节点整棵子树里最大的end_descendant_id。这三列构成一个区间编码判断两个节点的祖先后代关系从逐级向上找父节点变成了一次数值范围判断B是A的后代当且仅当B.id落在(A.id, A.end_descendant_id]之间。这个设计在 2005 年的关系数据库上就能用普通索引跑得动是整套翻译器的地基。2.2 Edge 表结构与生成 SQL建表语句按照论文的字段设计再做一点工程上的补强CREATE TABLE edge ( id INT PRIMARY KEY, -- 深度优先遍历时分配的节点编号 parent_id INT NULL, -- 父节点 id根节点为 NULL tag_name NVARCHAR(128) NOT NULL, -- 元素名或属性名 tag_value NTEXT NULL, -- 文本内容 end_descendant_id INT NOT NULL, -- 当前节点子树中最大的 id node_type TINYINT NOT NULL -- 1元素 2属性 3文本 ); CREATE INDEX idx_edge_parent ON edge(parent_id); CREATE INDEX idx_edge_end ON edge(end_descendant_id);id不是随意编的必须按文档顺序做深度优先前序遍历来分配。例如对下面这段 XMLsite people person id1nameTom/name/person person id2nameJerry/name/person /people /site对应的 Edge 表行大致是idparent_idtag_nameend_descendant_id1NULLsite821people732person543name454NULL(文本Tom)562person776name787NULL(文本Jerry)8site的end_descendant_id是 8表示整棵树的 id 范围是 1 到 8。first person的区间是 3 到 5它的兄弟second person区间是 6 到 7。后面翻译following-sibling轴时直接比较parent_id相等且id更大即可。表建好后把 XML 解析器输出的 SAX 事件按深度优先顺序灌进去同时回填每个节点的end_descendant_id一次扫描就能完成入库。解析阶段的数据结构就是解析栈进元素时压栈分配 id出元素时用当前最大 id 回填栈顶节点的end_descendant_id这样插入和回填可以在同一趟完成。3. 翻译器主流程一条 XPath 如何变成一串 WITH 子句3.1 算法骨架与运行方式有了 Edge 表翻译器的任务就很清晰了把XPath表达式的每个step编译成一段 SQL 片段每段对应一个 CTE后一个 CTE 引用前一个 CTE 的输出形成一条链。论文用了一个全局变量q做 CTE 编号计数器每翻译一个 step 就自增一次。根节点先用parent_id IS NULL定位最后一个 step 产出的 CTE 里的节点就是查询结果。def translate_xpath(xpath: str) - str: steps parse_xpath_steps(xpath) q 1 sql_parts [] # 第 1 个 CTE取根节点集合 sql_parts.append( fWITH Q{q} (id, parent_id, end_descendant_id) AS ( fSELECT id, parent_id, end_descendant_id fFROM edge WHERE parent_id IS NULL) ) # 逐个翻译 step追加 CTE for step in steps: q 1 sql_parts.append(translate_step(step, q)) # 收尾从最后一个 CTE 中取出完整子树并保持文档顺序 sql_parts.append( fSELECT E.* FROM edge E fJOIN Q{q} Q ON E.id Q.id AND E.id Q.end_descendant_id fORDER BY Q.id, E.id ) return ,\n.join(sql_parts)把WITH Q1(...), Q2(...), Q3(...)用逗号串联起来是标准 CTE 写法每一层都可以单独取出执行观察中间结果。Qq里存的是当前上下文节点集每个节点带id, parent_id, end_descendant_id三个字段分别表示当前节点的自身编号、父节点编号、子树右边界。翻译器拿到一个step时只需要基于当前Qq做一次连接生成Q(q1)。3.2 为什么用 WITH 链而不是嵌套子查询嵌套子查询在逻辑上等价但可读性和可调试性差很多。WITH 链的每一层都有一个名字执行计划里能单独看到每一段的扫描和连接代价。更重要的是论文里对predicate的翻译需要递归引用自身上下文比如position()要数当前节点之前同类节点的个数用嵌套子查询表达这种自引用会很绕而 WITH 子句天然支持这种分层引用。提示WITH在 SQL Server 2005 和 PostgreSQL 8.4 之后都可用MySQL 8.0 才支持这是论文当年的时代背景放到现在反而更顺手。翻译出的 SQL 是一个完整语句可以直接交给数据库执行。ORDER BY Q.id, E.id保证输出节点仍然按 XML 文档顺序排列这一点对 Reconstruct 求值方式非常关键因为重建 XML 片段时顺序错了文档就废了。3.3 收尾重建的两种策略Select 方式下算法只需要输出结果节点的 id 集合那么第 10 行只需要SELECT Q.id FROM Qq Q不需要再 JOIN Edge 表。Reconstruct 方式需要把结果节点连同它们的孩子一起抽出来重建 XML这时才需要上面的JOIN edge E ON E.id BETWEEN Q.id AND Q.end_descendant_id取出整个子树。论文的收尾语句默认了结果元素需要重构所以用了范围连接来取整棵子树。实际翻译器可以把两种收尾做成参数查询只需要 ID 时省掉一次范围 JOIN性能差别在千万级 Edge 表上很明显。4. step 三元组翻译axis、Nodetest、Predicate 的 SQL 生成4.1 一个 step 的结构拆解XPath 的每个step由三部分组成Axis::NodeTest[Predicate]。翻译器对三者分别生成 SQL 片段再拼接成一段完整的 CTE。论文的translatestep函数体现的就是这个组合逻辑。把每个轴单独拆出来看它们的 SQL 生成规则可以归纳成下面这张表axisSQL 生成条件语义说明childE.parent_id Q.id当前节点的直系子节点descendantE.id Q.id AND E.id Q.end_descendant_id当前节点所有后代followingE.id Q.end_descendant_id文档序中当前节点之后的所有节点precedingE.id Q.id文档序中当前节点之前的所有节点following-siblingE.id Q.id AND E.parent_id Q.parent_id当前节点之后的所有兄弟preceding-siblingE.id Q.id AND E.parent_id Q.parent_id当前节点之前的所有兄弟attributeE.parent_id Q.id AND E.node_type 2当前节点的属性节点以following为例说明这里的边界条件end_descendant_id是当前节点子树的最大 id所以E.id Q.end_descendant_id意味着取到的节点在当前节点整棵子树之后天然跳过了当前节点自己的所有后代符合 XPath 对following轴的定义。如果用E.id Q.id就会把当前节点的儿子、孙子都错误地算进来这是最容易写错的地方。对应的translatestep伪代码def translate_step(step: Step, q: int) - str: axis_sql translate_axis(step.axis, q) nodetest_sql translate_node_test(step.node_test) predicate_sql translate_predicate(step.predicates) return ( fQ{q} (id, parent_id, end_descendant_id) AS ( fSELECT E.id, E.parent_id, E.end_descendant_id fFROM edge E JOIN Q{q - 1} Q ON {axis_sql} fWHERE {nodetest_sql} {predicate_sql}) )translate_axis产出 JOIN 的 ON 条件translate_node_test产出对tag_name的过滤条件translate_predicate产出对位置或值的过滤。三者拼在一起就是一个完整 CTE。注意每个 axis 翻译出来的 SQL 里JOIN Q(q-1)引用的是上一层 CTE当前层只负责筛选出新节点集合不关心上一层的筛选逻辑这就是分层翻译的核心思想。4.2 child 轴的完整 CTE 展开论文里 child 轴的翻译代码是WITH Q(q1) (id, parent_id, end_descendant_id) AS ( SELECT E.id, E.parent_id, E.end_descendant_id FROM edge AS E, Q(q) AS Q WHERE E.parent_id Q.id )这条 SQL 的逻辑很直接拿上一层 Q 里的每个节点作为父亲去 Edge 表里找parent_id等于该节点id的行。执行计划会是Q(q)对idx_edge_parent索引做一次 Nested Loop Join。如果 Q 很小这个计划非常快。当时 SQL Server 的优化器对这种模式的处理已经成熟不需要人工干预。4.3 Nodetest 与谓词的翻译Nodetest 翻译成E.tag_name xxx但有个工程细节XML 元素名是大小写敏感的而 SQL Server 默认排序规则对NVARCHAR比较不区分大小写所以翻译时最好显式加上COLLATE Latin1_General_CS_AS否则name会匹配到Name和NAMEWHERE E.tag_name Nperson COLLATE Latin1_General_CS_AS论文对定位谓词[position()n]给出的方案是用子查询加COUNT计数数出当前节点之前有多少个满足 Nodetest 的兄弟节点如果这个数等于n-1说明当前节点就是第 n 个。翻译出的 SQL 骨架如下-- 假设要在所有 person 节点中取第 2 个 SELECT E.id, E.parent_id, E.end_descendant_id FROM edge E JOIN Q(q) Q ON E.parent_id Q.id WHERE E.tag_name Nperson AND SELECT COUNT(*) FROM edge E2 WHERE E2.parent_id E.parent_id AND E2.tag_name E.tag_name AND E2.id E.id ) 1 -- 前面有 1 个同类型节点所以是第 2 个子查询里的E2.id E.id隐含了文档顺序parent_id和tag_name双重相等保证只统计同父同类节点。这是一个纯集合运算写法没有窗口函数也能跑在 SQL Server 2000 时代是标准做法。翻译器还可以把多个谓词做 AND 拼接比如[position()2][id1]会生成两个条件同时加在 WHERE 里。关于谓词的表达范围需要对边界做一点说明position()的计数基准是当前 step 的上下文节点集也就是上一层 CTE 的输出而不是整棵文档树。所以child::person[position()2]统计的是同一父节点下第二个 person 子节点。如果上下文是一组由 where 条件筛出的离散节点那么position()的语义就会退化成结果集中的第几个翻译时必须在 WHERE 条件之外再包一层 ROW_NUMBER 窗口函数。论文没有展开这一层它隐含的前提是每个 step 的输入都是有序节点集。实际工程里如果要支持where从句加过滤条件后再定位需要调整谓词翻译策略把计数子查询改成基于窗口函数的等值过滤。4.4 定位谓词翻译的一个优化用count(*)子查询每次扫描同类节点在 XML 树很宽一个父节点有几千个子节点的时候性能不佳。SQL Server 2005 往后的版本可以直接用ROW_NUMBER()窗口函数SELECT id, parent_id, end_descendant_id FROM ( SELECT E.id, E.parent_id, E.end_descendant_id, ROW_NUMBER() OVER (PARTITION BY E.parent_id ORDER BY E.id) AS rn FROM edge E JOIN Q(q) Q ON E.parent_id Q.id WHERE E.tag_name Nperson ) t WHERE t.rn 2PARTITION BY E.parent_id把同一个父节点下的元素分成一组ORDER BY E.id保证按文档顺序编号。翻译器可以加一个配置开关检测数据库版本后自动选择 count 子查询或窗口函数。SELECT 外层加一层t.rn 2的过滤等价于[position()2]。5. XQuery 扩展转换、验证方法与现代数据库的对应关系5.1 XQuery 的 Before、After、Range 转换XQuery 在 XPath 基础上扩展了三个操作符翻译时可以直接复用上面已经建好的轴转换。Before操作符与preceding轴同构After与following同构谓词条件一个是E.id Q.id另一个是E.id Q.end_descendant_id。Range操作符[2 to 5]是位置谓词的泛化-- XQuery: /site/people/person[2 to 5] SELECT t.id, t.parent_id, t.end_descendant_id FROM ( SELECT E.id, E.parent_id, E.end_descendant_id, ROW_NUMBER() OVER (PARTITION BY E.parent_id ORDER BY E.id) AS rn FROM edge E JOIN Q(q) Q ON E.parent_id Q.id WHERE E.tag_name Nperson ) t WHERE t.rn BETWEEN 2 AND 5BETWEEN 的边界是闭区间直接语义对应 XQuery range 的to操作符。翻译器的实现里把 range 谓词处理成两个整数参数拼 SQL 时用参数占位符传入避免把常量直接拼进 SQL 串。5.2 翻译结果的验证方法翻译器写完第一件事不是对着整条 CTE 链调而是分层验证。每一层 CTE 单独执行对比 XPath 的每一步预期结果。比如/site/people/person/name先跑根节点 CTE 看 site 是否只有一行再跑 people 层看是否返回一个节点再跑 person 层数个数最后跑 name 层。哪一层节点集合和预期不符问题就锁定在哪一个 axis 或 Nodetest 的翻译上。对比结果我用文件快照做 diff# 打印每一层 CTE 的 id 列表 sqlcmd -S . -d test -Q WITH Q1 (id) AS (SELECT id FROM edge WHERE parent_id IS NULL), Q2 (id) AS (SELECT E.id FROM edge E JOIN Q1 ON E.parent_id Q1.id WHERE E.tag_namesite) SELECT * FROM Q2 -h -1 | tr -d /tmp/step2.txt-h -1去掉表头tr -d 去掉空白让输出变成纯 id 列表用diff和期望结果比对。对于位置谓词额外打印rn列确认编号是按文档序连续的。5.3 与 SQL Server 原生 XML 方法的取舍SQL Server 从 2005 版起自带的xml.nodes()函数能把 XML 节点展开成关系行配合 CROSS APPLY 可以写路径查询。它的定位逻辑是解析器内部实现的 XPath 子集不走 Edge 表。两种方案各有边界nodes()适合小文档的即时查询写起来快但每次查询都要重新解析 XML 的字符串表示大量文档做跨文档聚合时性能不稳而且它的位置谓词[1]只支持编译期常量的简单形式。Edge 表方案的优势是查询时数据已经是结构化行慢 SQL 优化可以直接走索引和统计信息解析代价一次付清适合仓库级别的批量分析。劣势是入库时要把 XML 拆成行写入路径复杂更新单点节点要维护end_descendant_id实现增量同步时容易出错。选型时不用纠结查询多、写入少选 Edge 表即席查询选nodes()。5.4 落库过程中的边界处理最后提醒一个绝大多数实现都会踩的坑XML 有实体引用和 CDATA。解析器展开实体后文本节点的长度和内容会变节点 ID 分配不受影响但tag_value列必须存展开后的文本。CDATA 部分要保留原始内容但同样要读取文本值否则重建 XML 时会把lt;再次转义成amp;lt;。对这个翻译器架构来说还有一点会影响谓词正确性。XPath 的text()Nodetest 在 Edge 表里对应node_type 3的文本节点如果入库时省略文本节点直接存值/name/text()这类查询在翻译成 SQL 后会因节点类型缺失而扫不到任何结果所以入库链路要保留文本节点行。压测时把文档深度、兄弟节点数和谓词位置三个维度单独设置用例验证翻译器在宽树和深树下的输出边界这部分数据也能直接沉淀成翻译器的回归测试集。本文还有配套的精品资源点击获取
返回列表