【问题标题】:T-SQL: How can I compare two variables of type XML when length > VarChar(MAX)?T-SQL:当长度 > VarChar(MAX) 时,如何比较两个 XML 类型的变量?
【发布时间】:2012-01-26 03:43:12
【问题描述】:

仅使用 SQL Server 2008 R2(这将在存储过程中),我如何确定两个 XML 类型的变量是否等价?

这是我想做的:

DECLARE @XmlA   XML
DECLARE @XmlB   XML

SET @XmlA = '[Really long Xml value]'
SET @XmlB = '[Really long Xml value]'

IF @XmlA = @XmlB
    SELECT 'Matching Xml!'

但你可能知道,它返回:

Msg 305, Level 16, State 1, Line 7 XML 数据类型不能 比较或排序,除非使用 IS NULL 运算符。

我可以转换为VarChar(MAX) 并进行比较,但这仅比较前 2MB。还有其他方法吗?

【问题讨论】:

  • 你的意思是前2GB吗?您的 XML 真的大于 2MB 吗?你真的需要知道它是否相同吗?如果是这样,最好在 SQL Server 之外进行比较。
  • 您想要的比较是否将<e /> 视为等于<e></e>
  • VARCHAR(MAX) 最多可以存储 2 GB 的数据(是托尔斯泰的 War and Peace 大小的 150 倍) - 您的 XML 大于这!?!?!?
  • 您不能存储与 varchar(max) 匹配的 XML > 2GB。但是,严格来说应该是 varbinary(max)

标签: sql-server xml tsql comparison


【解决方案1】:

检查这个 SQL 函数:

CREATE FUNCTION [dbo].[CompareXml]
(
    @xml1 XML,
    @xml2 XML
)
RETURNS INT
AS 
BEGIN
    DECLARE @ret INT
    SELECT @ret = 0


    -- -------------------------------------------------------------
    -- If one of the arguments is NULL then we assume that they are
    -- not equal. 
    -- -------------------------------------------------------------
    IF @xml1 IS NULL OR @xml2 IS NULL 
    BEGIN
        RETURN 1
    END

    -- -------------------------------------------------------------
    -- Match the name of the elements 
    -- -------------------------------------------------------------
    IF  (SELECT @xml1.value('(local-name((/*)[1]))','VARCHAR(MAX)')) 
        <> 
        (SELECT @xml2.value('(local-name((/*)[1]))','VARCHAR(MAX)'))
    BEGIN
        RETURN 1
    END

     ---------------------------------------------------------------
     --Match the value of the elements
     ---------------------------------------------------------------
    IF((@xml1.query('count(/*)').value('.','INT') = 1) AND (@xml2.query('count(/*)').value('.','INT') = 1))
    BEGIN
    DECLARE @elValue1 VARCHAR(MAX), @elValue2 VARCHAR(MAX)

    SELECT
        @elValue1 = @xml1.value('((/*)[1])','VARCHAR(MAX)'),
        @elValue2 = @xml2.value('((/*)[1])','VARCHAR(MAX)')

    IF  @elValue1 <> @elValue2
    BEGIN
        RETURN 1
    END
    END

    -- -------------------------------------------------------------
    -- Match the number of attributes 
    -- -------------------------------------------------------------
    DECLARE @attCnt1 INT, @attCnt2 INT
    SELECT
        @attCnt1 = @xml1.query('count(/*/@*)').value('.','INT'),
        @attCnt2 = @xml2.query('count(/*/@*)').value('.','INT')

    IF  @attCnt1 <> @attCnt2 BEGIN
        RETURN 1
    END


    -- -------------------------------------------------------------
    -- Match the attributes of attributes 
    -- Here we need to run a loop over each attribute in the 
    -- first XML element and see if the same attribut exists
    -- in the second element. If the attribute exists, we
    -- need to check if the value is the same.
    -- -------------------------------------------------------------
    DECLARE @cnt INT, @cnt2 INT
    DECLARE @attName VARCHAR(MAX)
    DECLARE @attValue VARCHAR(MAX)

    SELECT @cnt = 1

    WHILE @cnt <= @attCnt1 
    BEGIN
        SELECT @attName = NULL, @attValue = NULL
        SELECT
            @attName = @xml1.value(
                'local-name((/*/@*[sql:variable("@cnt")])[1])', 
                'varchar(MAX)'),
            @attValue = @xml1.value(
                '(/*/@*[sql:variable("@cnt")])[1]', 
                'varchar(MAX)')

        -- check if the attribute exists in the other XML document
        IF @xml2.exist(
                '(/*/@*[local-name()=sql:variable("@attName")])[1]'
            ) = 0
        BEGIN
            RETURN 1
        END

        IF  @xml2.value(
                '(/*/@*[local-name()=sql:variable("@attName")])[1]', 
                'varchar(MAX)')
            <>
            @attValue
        BEGIN
            RETURN 1
        END

        SELECT @cnt = @cnt + 1
    END

    -- -------------------------------------------------------------
    -- Match the number of child elements 
    -- -------------------------------------------------------------
    DECLARE @elCnt1 INT, @elCnt2 INT
    SELECT
        @elCnt1 = @xml1.query('count(/*/*)').value('.','INT'),
        @elCnt2 = @xml2.query('count(/*/*)').value('.','INT')


    IF  @elCnt1 <> @elCnt2
    BEGIN
        RETURN 1
    END


    -- -------------------------------------------------------------
    -- Start recursion for each child element
    -- -------------------------------------------------------------
    SELECT @cnt = 1
    SELECT @cnt2 = 1
    DECLARE @x1 XML, @x2 XML
    DECLARE @noMatch INT

    WHILE @cnt <= @elCnt1 
    BEGIN

        SELECT @x1 = @xml1.query('/*/*[sql:variable("@cnt")]')
    --RETURN CONVERT(VARCHAR(MAX),@x1)
    WHILE @cnt2 <= @elCnt2
    BEGIN
        SELECT @x2 = @xml2.query('/*/*[sql:variable("@cnt2")]')
        SELECT @noMatch = dbo.CompareXml( @x1, @x2 )
        IF @noMatch = 0 BREAK
        SELECT @cnt2 = @cnt2 + 1
    END

    SELECT @cnt2 = 1

        IF @noMatch = 1
        BEGIN
            RETURN 1
        END

        SELECT @cnt = @cnt + 1
    END

    RETURN @ret
END

这里是Source


该函数无法比较 XML 片段,例如当没有单个根元素时,例如:

SELECT dbo.CompareXml('<data/>', '<data/><data234/>') 

为了解决这个问题,您必须将 XML 包装在 root 元素中,然后将它们传递给函数或编辑函数以执行此操作。例如:

SELECT dbo.CompareXml('<r><data/></r>', '<r><data/><data234/></r>')  

【讨论】:

  • 注意 这对于 XML 片段失败,例如当没有单个根元素时,例如 SELECT dbo.CompareXml('', '') 似乎只是比较第一个节点
  • sigh 请不要发布您不理解的代码。它不返回布尔值/位,它返回一个整数。粗略看一下代码,返回值 0 表示“匹配”,值“1”表示“不匹配”。请运行 SELECT dbo.CompareXml('', '') , dbo.CompareXml('', '') , dbo.CompareXml ('', '') , dbo.CompareXml('', '') 并检查结果。只有第三个将“1”返回为“不同”
  • 我可以采纳&修改代码就好了;这样做已经有一段时间了。让我们在这里直接了解一些事实。你说“检查这个代码”,我做了,警告并评论它什么时候会失败。然后你否认了我的评论,说我错了,并且对代码的工作方式给出了错误的描述。然后我给出了一个具体的测试来显示它的失败。然后您删除您的错误评论并继续建议我更改测试以隐藏潜在问题!我建议你考虑不要偏离你的能力范围。
  • @brewmanz,我同意。请注意,代码是几年前发布的,当时我试图找到解决问题的方法。我不记得这段代码是如何工作的。我将把它添加到答案中。我正在删除已过时或只是垃圾邮件的 cmets。
【解决方案2】:

比较两个 XML 文档有很多不同的方法,很大程度上取决于你想容忍什么样的差异:你肯定需要容忍编码、属性顺序、无关紧要的空格、数字字符引用和使用属性分隔符,您可能还应该容忍在使用 cmets、命名空间前缀和 CDATA 方面的差异。因此,将两个 XML 文档作为字符串进行比较绝对不是一个好主意——除非您首先调用 XML 规范化。

对于许多目的,XQuery deep-equals() 函数做了正确的事情(并且或多或少等同于比较两个 XML 文档的规范形式)。我对 Microsoft 的 XQuery 的 SQL Server 实现了解不多,无法告诉您如何从 SQL 级别调用它。

【讨论】:

    【解决方案3】:

    您可以将字段转换为 varbinary(max),对它们进行散列并比较散列。但是如果 XML 是等价的但不完全相同,你肯定会错过

    要计算哈希,您可以使用 CLR 函数:

    using System;
    using System.Data.SqlTypes;
    using System.IO;
    
    namespace ClrHelpers
    {
        public partial class UserDefinedFunctions {
            [Microsoft.SqlServer.Server.SqlFunction]
            public static Guid HashMD5(SqlBytes data) {
                System.Security.Cryptography.MD5CryptoServiceProvider md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
                md5.Initialize();
                int len = 0;
                byte[] b = new byte[8192];
                Stream s = data.Stream;
                do {
                    len = s.Read(b, 0, 8192);
                    md5.TransformBlock(b, 0, len, b, 0);
                } while(len > 0);
                md5.TransformFinalBlock(b, 0, 0);
                Guid g = new Guid(md5.Hash);
                return g;
            }
        };
    }
    

    或者sql函数:

    CREATE FUNCTION dbo.GetMyLongHash(@data VARBINARY(MAX))
    RETURNS VARBINARY(MAX)
    WITH RETURNS NULL ON NULL INPUT
    AS
    BEGIN
        DECLARE @res VARBINARY(MAX) = 0x
        DECLARE @position INT = 1, @len INT = DATALENGTH(@data)
    
        WHILE 1 = 1
        BEGIN
            SET @res = @res + HASHBYTES('MD5', SUBSTRING(@data, @position, 8000))
            SET @position = @position+8000
            IF @Position > @len 
              BREAK
        END
        WHILE DATALENGTH(@res) > 16 SET @res= dbo.GetMyLongHash(@res)
        RETURN @res
    END
    

    【讨论】:

    • 如果 XML > 2MB(或更糟,> 2GB),我认为源 XML 中的细微差异有很多方法可以导致相同的散列值(冲突),错误地表明当他们真的不是时,他们是平等的。我知道您在回答的开头提到了这一点,但我认为可以更明确地表达。
    • @AaronBertrand 1st - 您不能在 sql server xml 字段中存储 > 2Gb。第二 - 使用适当的哈希算法。 (ie. md5, sha1) - 你保证没有碰撞结果
    • 对不起,我的意思是接近 2GB,不大于 2GB。而且我不太确定您是否可以保证没有碰撞;几天前,John Huang 似乎展示了一个:sqlnotes.info/2012/01/20/md5-collision 他的碰撞不是基于 XML,但它仍然是违反你的“保证”的一个例子......
    • @AaronBertrand 对于 MD5,它可能但可以忽略不计,对于 SHA1 - 仍然不可能
    • 我认为这里的问题是其中一个 XML 可以包含额外的空间或者可以切换属性,在这种情况下我认为哈希比较会失败。
    【解决方案4】:

    我偶然发现了this fairly comprehensive article,它更详细地实际比较了 2 个 XML 条目的内容以确定它们是否相同。这是有道理的,因为节点中属性的顺序可以不同,即使它们的值完全相同。我建议您通读一遍,甚至实现该功能,看看它是否适合您……我很快尝试了一下,它似乎适合我?

    【讨论】:

    • 链接已失效
    【解决方案5】:

    如果你可以使用SQL CLR,我建议使用XNode.DeepEquals Method写一个函数:

    var xmlTree1 = new XElement("Root",
        new XAttribute("Att1", 1),
        new XAttribute("Att2", 2),
        new XElement("Child1", 1),
        new XElement("Child2", "some content")
    );
    var xmlTree2 = new XElement("Root",
        new XAttribute("Att1", 1),
        new XAttribute("Att2", 2),
        new XElement("Child1", 1),
        new XElement("Child2", "some content")
    );
    Console.WriteLine(XNode.DeepEquals(xmlTree1, xmlTree2));
    

    如果你不能,你可以编写自己的函数(见SQL FIDDLE EXAMPLE):

    CREATE function [dbo].[udf_XML_Is_Equal]
    (
        @Data1 xml,
        @Data2 xml
    )
    returns bit
    as
    begin
        declare
            @i bigint, @cnt1 bigint, @cnt2 bigint,
            @Sub_Data1 xml, @Sub_Data2 xml,
            @Name varchar(max), @Value1 nvarchar(max), @Value2 nvarchar(max)
    
        if @Data1 is null or @Data2 is null
            return 1
    
        --=========================================================================================================
        -- If more than one root - recurse for each element
        --=========================================================================================================
        select
            @cnt1 = @Data1.query('count(/*)').value('.','int'),
            @cnt2 = @Data1.query('count(/*)').value('.','int')
    
        if @cnt1 <> @cnt2
            return 0        
    
        if @cnt1 > 1
        begin
            select @i = 1
            while @i <= @cnt1
            begin
                select
                    @Sub_Data1 = @Data1.query('/*[sql:variable("@i")]'),
                    @Sub_Data2 = @Data2.query('/*[sql:variable("@i")]')
    
                if dbo.udf_XML_Is_Equal_New(@Sub_Data1, @Sub_Data2) = 0
                    return 0
    
                select @i = @i + 1
            end
    
            return 1
        end
    
        --=========================================================================================================
        -- Comparing root data
        --=========================================================================================================
        if @Data1.value('local-name(/*[1])','nvarchar(max)') <> @Data2.value('local-name(/*[1])','nvarchar(max)') 
            return 0
    
        if @Data1.value('/*[1]', 'nvarchar(max)') <> @Data2.value('/*[1]', 'nvarchar(max)')
            return 0
    
        --=========================================================================================================
        -- Comparing attributes
        --=========================================================================================================
        select
            @cnt1 = @Data1.query('count(/*[1]/@*)').value('.','int'),
            @cnt2 = @Data1.query('count(/*[1]/@*)').value('.','int')
    
        if @cnt1 <> @cnt2
            return 0
    
        if exists (
            select *
            from
            (
                select
                    T.C.value('local-name(.)', 'nvarchar(max)') as Name,
                    T.C.value('.', 'nvarchar(max)') as Value
                from @Data1.nodes('/*[1]/@*') as T(C)
            ) as D1
            full outer join
            (
                select
                    T.C.value('local-name(.)', 'nvarchar(max)') as Name,
                    T.C.value('.', 'nvarchar(max)') as Value
                from @Data2.nodes('/*[1]/@*') as T(C)
            ) as D2
            on D1.Name = D2.Name
            where
                not
                (
                    D1.Value is null and D2.Value is null or
                    D1.Value is not null and D2.Value is not null and D1.Value = D2.Value
                )
        )
            return 0
    
    
        --=========================================================================================================
        -- Recursively running for each child
        --=========================================================================================================
        select
            @cnt1 = @Data1.query('count(/*[1]/*)').value('.','int'),
            @cnt2 = @Data2.query('count(/*[1]/*)').value('.','int')
    
        if @cnt1 <> @cnt2
            return 0    
    
        select @i = 1
        while @i <= @cnt1        
        begin
            select
                @Sub_Data1 = @Data1.query('/*/*[sql:variable("@i")]'),
                @Sub_Data2 = @Data2.query('/*/*[sql:variable("@i")]')
    
            if dbo.udf_XML_Is_Equal(@Sub_Data1, @Sub_Data2) = 0
                return 0
    
            select @i = @i + 1
        end
    
        return 1
    END
    

    【讨论】:

      猜你喜欢
      • 2010-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多