如何在 CLR 函数中获取 SQL 字符串的排序规则?

Posted

技术标签:

【中文标题】如何在 CLR 函数中获取 SQL 字符串的排序规则?【英文标题】:How can I get a SQL String's collation within a CLR function? 【发布时间】:2015-01-03 12:52:48 【问题描述】:

我正在用 C# 编写一个 Levenshtein Distance 函数来计算两个字符串之间的编辑距离。问题是我想用不同的排序规则多次调用该方法,但只有一个排序规则可以通过 SQL 到 CLR 接口 - 这是数据库的默认排序规则。

这里是CLR函数的代码:

[SqlFunction(IsDeterministic = true, Name = "LevenshteinDistance")]
public static SqlInt64 Distance(SqlString textA, SqlString textB)

    // get a collation-aware comparer so string/character comparisons 
    // will match the inputs' specified collation
    var aCompareInfo = textA.CompareInfo;
    var compareOptions = ConvertCompareOptions(textA.SqlCompareOptions);
    var aLength = textA.Value.Length;
    var bLength = textB.Value.Length;

    // degenerate cases
    if (aCompareInfo.Compare(textA.Value, 0, aLength, textB.Value, 0, bLength, compareOptions) == 0)  return 0; 
    if (aLength == 0)  return bLength; 
    if (bLength == 0)  return aLength; 

    // create two work vectors of integer distances
    var previousDistances = new SqlInt64[Maximum(aLength, bLength) + 1];
    var currentDistances = new SqlInt64[Maximum(aLength, bLength) + 1];

    // initialize previousDistances (the previous row of distances)
    // this row is A[0][i]: edit distance for an empty textA
    // the distance is just the number of characters to delete from textB
    for (var i = 0; i < previousDistances.Length; i++)
    
        previousDistances[i] = i;
    

    for (var i = 0; i < aLength; i++)
    
        // calculate currentDistances from the previous row previousDistances

        // first element of currentDistances is A[i+1][0]
        //   edit distance is delete (i+1) chars from textA to match empty textB
        currentDistances[0] = i + 1;

        // use formula to fill in the rest of the row
        for (var j = 0; j < bLength; j++)
        
            var cost = (aCompareInfo.Compare(textA.Value, i, 1, textB.Value, j, 1, compareOptions) == 0) ? 0 : 1;
            currentDistances[j + 1] = Minimum(currentDistances[j] + 1, previousDistances[j + 1] + 1, previousDistances[j] + cost);
        

        // copy currentDistances to previousDistances for next iteration
        for (var j = 0; j < previousDistances.Length; j++)
        
            previousDistances[j] = currentDistances[j];
        
    

    return currentDistances[bLength];

将 CLR 程序集部署到 SQL Server (2008 R2) 并像这样调用它之后:

print dbo.LevenshteinDistance('abc' collate Latin1_General_CI_AI, 'ABC' collate Latin1_General_CI_AI)
print dbo.LevenshteinDistance('abc' collate Latin1_General_CS_AS_KS_WS, N'ABC' collate Latin1_General_CS_AS_KS_WS)

两个调用都返回零 (0)。因为我为第二次调用指定了区分大小写的排序规则,所以我希望第二次调用返回三 (3)。

在 SQL Server 中使用 CLR 函数,是否可以指定数据库默认值以外的排序规则并在 CLR 函数中使用它们?如果有,怎么做?

【问题讨论】:

【参考方案1】:

在 Internet 上没有看到任何替代方案或对此问题的回复,我决定将所需的排序规则属性指定为函数参数,并根据输入或从数据库。

[SqlFunction(IsDeterministic = true, Name = "LevenshteinDistance")]
public static SqlInt64 Distance(SqlString textA, SqlString textB, int? lcid, bool? caseInsensitive, bool? accentInsensitive, bool? kanaInsensitive, bool? widthInsensitive)

    // get a collation-aware comparer so string/character comparisons 
    // will match the inputs' specified collation
    //var aCompareInfo = textA.CompareInfo;
    var aCompareInfo = CultureInfo.GetCultureInfo(lcid ?? textA.LCID).CompareInfo;
    //var compareOptions = ConvertCompareOptions(textA.SqlCompareOptions);
    var compareOptions = GetCompareOptions(caseInsensitive, accentInsensitive, kanaInsensitive, widthInsensitive);

    // ...  more code ...

    // first comparison
    if (aCompareInfo.Compare(textA.Value, 0, aLength, textB.Value, 0, bLength, compareOptions) == 0)  return 0; 

    // ...  more code ...

    var cost = (aCompareInfo.Compare(textA.Value, i, 1, textB.Value, j, 1, compareOptions) == 0) ? 0 : 1;

    // ...  more code ...


private static CompareOptions GetCompareOptions(bool? caseInsensitive, bool? accentInsensitive, bool? kanaInsensitive, bool? widthInsensitive)

    var compareOptions = CompareOptions.None;

    compareOptions |= (caseInsensitive ?? false) ? CompareOptions.IgnoreCase : CompareOptions.None;
    compareOptions |= (accentInsensitive ?? false) ? CompareOptions.IgnoreNonSpace : CompareOptions.None;
    compareOptions |= (kanaInsensitive ?? false) ? CompareOptions.IgnoreKanaType : CompareOptions.None;
    compareOptions |= (widthInsensitive ?? false) ? CompareOptions.IgnoreWidth : CompareOptions.None;

    return compareOptions;

更新我的程序集和 UDF 声明后,我可以像这样调用函数:

print dbo.LevenshteinDistance('abc', 'ABC', null, 1, 1, 1, 1)
print dbo.LevenshteinDistance('abc', 'ABC', null, 0, 0, 0, 0)

现在第一次调用返回 0(数据库默认文化,一切不敏感),而第二次调用返回 3(数据库默认文化,一切敏感)。

【讨论】:

你好。已经有几年了,但是当我再次查看这个问题时,我注意到了一个潜在的问题:无法指示二进制排序规则(*_BIN*_BIN2)。当然,需要模拟二进制排序规则可能是一个不常见的用例,因此可能不切实际。但是,如果需要,您可以通过将 LCID 作为负值传递并使用符号作为标志来指示“二进制”。但是,这会使采用数据库默认排序规则的 LCID 的 NULL 场景无效。或者,由于您使用的是bool?,您可以将所有 4 个设置为NULL 解释为二进制。 另外,仅供参考:从技术上讲,您不需要/不需要使用 int?bool? 作为参数类型,因为 SqlInt32SqlBoolean 类型已经分别封装该行为(实际上,所有Sql* 类型都可以)。事实上,SqlBoolean 甚至具有IsTrue 的属性(true 如果设置为 true,否则为 false,即使 NULL)和IsFalsetrue,如果设置为 false,否则为 false,即使NULL). 已经太多年了!我记得写过这段代码,但由于某种原因,我们从未将它投入生产。相反,我不得不将它重写为几个 T-SQL 用户定义函数。 ? 尽管性能受到影响,但至少它们可以正常工作,并且在 SQL Server 上安装自定义 DLL 时没有人会被冒犯。【参考方案2】:

如何在 CLR 函数中获取 SQL 字符串的排序规则?

很遗憾,你不能。根据Collation and CLR Integration Data Types 的 TechNet 页面,在“参数排序”部分:

当您创建公共语言运行时 (CLR) 例程并且绑定到该例程的 CLR 方法的参数是 SqlString 类型时,SQL Server 会使用默认排序规则创建该参数的实例包含调用例程的数据库。如果参数不是 SqlType(例如,String 而不是 SqlString),则来自数据库的排序规则信息不与参数关联.

因此,您目睹的关于 textA 输入参数的 CompareInfoSqlCompareOptions 属性的行为是,虽然不幸/令人沮丧/难以理解,但至少与文档所说的系统应该正常工作是一致的.

因此,您通过单独的输入参数传递属性的解决方案是可行的方法(尽管您真的应该使用SqlInt32SqlBoolean 的SqlTypes ;-)。

【讨论】:

【参考方案3】:

如果您的解决方案不涉及大于 4K 的字符串,有些人可能会认为另一种更好的方法。使您的数据类型为“对象”而不是 SqlString。这等效于 SQL_VARIANT。尽管变体比标准类型产生更多开销,但它们可以保存具有任意排序规则的字符串。

SELECT dbo.ClrCollationTest(N'Anything' collate latin1_general_cs_as),
       dbo.ClrCollationTest(N'Anything' collate SQL_Latin1_General_CP1_CI_AS);

当CLR这样编码时,上面分别返回0和1:

public static SqlBoolean ClrCollationTest(object anything)

    if (anything is SqlString)
        return new SqlBoolean(((SqlString)anything).SqlCompareOptions.HasFlag(SqlCompareOptions.IgnoreCase));
    else throw new ArgumentException(anything.GetType().Name + " is not a valid parameter data type.  SqlString is required.");

【讨论】:

这是一个误导性的答案。排序规则不仅仅包含“比较选项”:它还包括哪些字典/代码页/排序和等价规则。如果您无法捕获 SQL_Latin1_General_CP1 vs latin1_general vs ??那么你真的没有排序规则。另外需要注意的是,使用SQL_VARIANT/object有以下两个缺点:1)速度慢,2)不能容纳任何blob类型,例如NVARCHAR(MAX),因此您只能使用NVARCHAR(4000) 也许问题的标题被证明具有误导性。我建议了一种方法来获得 OP 所需的一切,而无需任何参数,只有在 OP 发布了一个明显足够的“解决方案”,只涉及 compareOptions 值。我怀疑 Levenshtein Distance 是否会用于长度超过 400 的字符串,少得多 4000 字节,这使得这个建议看起来相当有用,而不是误导。 好点。我做了一些进一步的测试,发现当使用object 时,LCID 确实可以正确通过,这允许在与SqlCompareOptions 结合使用时获得完整的 COLLATE 设置。所以这似乎回答了这个问题。如果您添加这 2 个注释(最大尺寸和性能命中),那么我会赞成。我知道这种特定情况不需要 MAX 类型,但是阅读本文的人应该知道这两个缺点,然后再尝试将其应用到任何地方;-)。 好吧,某人(即“社区”)做了这两个更新,所以我投了赞成票。 是我,我只是没有登录。

以上是关于如何在 CLR 函数中获取 SQL 字符串的排序规则?的主要内容,如果未能解决你的问题,请参考以下文章

CLR SQL Server UDF 问题

PCB MS SQL CLR聚合函数(函数作用,调用顺序,调用次数) CLR说明

SQL CLR 用户定义函数 (C#) 在返回的字符串中的每个现有字符之间添加空字符 (\0)

PCB MS SQL 标量函数(CLR) 实现转Json方法

PCB MS SQL表值函数与CLR 表值函数 (例:字符串分割转表)

使用CLR函数在Visual Studio 2019中将Hierarchyid转换为字符串