T-SQL:当长度大于VarChar(MAX)时,我如何比较两个类型为XML的变量?

8

使用仅限于 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,级别 16,状态 1,第 7 行XML 数据类型无法进行比较或排序,除非使用 IS NULL 运算符。

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


1
你是要说前2GB吗?你的XML文件真的大于2MB吗?你真的需要知道它们是否完全一致吗?如果是这样,最好在SQL Server之外进行比较。 - Aaron Bertrand
7
你希望进行的比较是否将 <e /> 视为等同于 <e></e> - pilcrow
4
VARCHAR(MAX)可以存储高达2 GB的数据(是托尔斯泰《战争与和平》大小的150倍),而你的XML文件比这还要大?!?! - marc_s
1
你不能将XML存储超过2GB,这与varchar(max)相匹配。但严格来说,应该是varbinary(max)。 - gbn
5个回答

2

请查看以下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

这里是源代码


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

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

为了解决这个问题,当将XML传递给函数或编辑函数时,必须将其包装在root元素中。例如:
SELECT dbo.CompareXml('<r><data/></r>', '<r><data/><data234/></r>')  

注意 这个方法不适用于 XML 片段,例如当没有单一的根元素时,比如 SELECT dbo.CompareXml('<data/>', '<data/><data234/>') 它似乎只会比较第一个节点。 - brewmanz
叹气 请不要发布您不理解的代码。它不返回布尔值/位,而是返回整数。简单浏览代码可以发现,返回值为0意味着“匹配”,值为“1”意味着“不匹配”。请运行SELECT dbo.CompareXml('<data/>', '<data></data>'),dbo.CompareXml('<data/>', '<data></data>'),dbo.CompareXml('<data/>', '<data1234></data1234>'),dbo.CompareXml('<data/>', '<data></data><dataa/>')并检查结果。只有第三个返回“1”表示“不同”。 - brewmanz
我可以很好地采用和更改代码;我已经这样做了相当长的一段时间。让我们澄清一些事实。你说“检查这个代码”,我做到了,并警告并评论了它何时会失败。然后你否认了我的评论,说我错了,并给出了错误的代码工作描述。然后我提供了一个具体的测试来显示其缺陷。然后你删除了你不正确的评论,并建议我更改测试以隐藏潜在的问题!我建议你考虑不要超出你的能力范围。 - brewmanz
@brewmanz,我同意。注意,这段代码是几年前发布的,当时我正在尝试解决一个问题。我不记得这段代码的确切工作方式了。我将把它添加到答案中。我正在删除过时或垃圾评论。 - gotqn

1

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

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


0

你可以将字段转换为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中微小的差异可能会导致相同的哈希值(冲突),错误地表明它们相等,而实际上它们并不相等。我知道你在回答开头就提到了这一点,但我认为可以更明确地表达。 - Aaron Bertrand
@AaronBertrand 首先,您无法在 SQL Server 的 XML 字段中存储大于 2GB 的数据。其次,使用适当的哈希算法(例如 MD5、SHA1),可以保证结果不会发生冲突。 - Oleg Dok
抱歉,我的意思是接近2GB,而不是超过2GB。而且,我不确定你是否能保证没有冲突;几天前,黄先生似乎展示了一个:http://www.sqlnotes.info/2012/01/20/md5-collision/ 他的冲突并不是基于XML的,但它仍然是对你“保证”的一个违反的例子... - Aaron Bertrand
@AaronBertrand 对于 MD5 来说可能是可以的,但可以忽略不计,而对于 SHA1 来说,仍然是不可能的。 - Oleg Dok
我认为这里的问题是其中一个XML可能包含额外的空格或属性可能被交换,这种情况下哈希比较将失败。 - new2ios

0

如果您可以使用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

-1
我偶然发现了这篇相当全面的文章,它更详细地比较了两个XML条目的内容以确定它们是否相同。这很有道理,因为节点中属性的顺序可能不同,即使它们的值完全相同。我建议你仔细阅读并实施该函数,看看它是否适用于你...我快速尝试了一下,似乎对我有效?

链接已不再可用。 - Chris Surfleet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接