1

我有一个查询 Web 服务的 SQL CLR UDF。由于这可能代价高昂,尤其是在函数内是多行查询的一部分,因此我希望尽可能避免调用 Web 服务。在每种情况下,相同的输入将产生相同的输出(例如,如果我的输入是'abc',我将始终得到'xyz'并且没有什么不同,同样'def'将始终产生'tuv'等)。

我做了一些测试,似乎 SQL 没有做任何类型的缓存,所以总是会调用 Web 服务。

示例案例:我有一个MyTable带有字段的表MyField1。虽然MyTable有 500 行,但MyField1始终只有 3 个可能值之一。示例查询:

SELECT MyFunction(MyField1) FROM MyTable

发生的情况是 Web 服务将被调用 500 次,表中的每一行调用一次。我更喜欢的是 Web 服务只被调用 3 次(每个不同的值一次),并从一些缓存中读取重复值。

示例代码:

[SqlFunction]
public static SqlString MyFunction(SqlString input)
{
    if (input.IsNull) return SqlString.Null;

    using (var webService = new MyWebService())
    {
        string result = webService.Call(input.Value);

        return new SqlString(result);
    }
}

我真正希望将其保存在特定于上下文的缓存中。也就是说,缓存只存在于在单个存储过程的调用中或在单个查询窗口等中缓存结果。是否有任何可用的机制来完成我所追求的?

4

2 回答 2

1

编辑说明接近尾声的更新版本

尽管此答案不使用缓存,但它应该最大限度地减少对您的函数的调用次数。使用多个CTEs来查找 myField1 的不同值,然后使用您的函数在 web 服务中查找不同的值,然后将这些值加入 MyTable。下面的例子可能更清楚:

SQL小提琴

MS SQL Server 2008 架构设置

CREATE TABLE MyTable
(
    ID int PRIMARY KEY IDENTITY,
    MyField1 VARCHAR(1)
);


CREATE FUNCTION MyFunction
(
    @input As VARCHAR(1)
)
RETURNS VARCHAR(10)
AS
BEGIN
    -- This could be a CLR Function
    -- Return the result of the function
    RETURN  CASE @input WHEN 'A' THEN 'aaaaaaaaaa' WHEN 'B' THEN 'bbbbbbbbbb' ELSE 'ccccccccc' END

END;

-- DATA SET UP
DECLARE @i INT = 0
DECLARE @Field VARCHAR(1)
WHILE @i < 1000
BEGIN
    SELECT @Field = CASE @i % 3 WHEN 1 THEN 'A' WHEN 2 THEN 'B' ELSE 'C' END
    INSERT INTO MyTable (MyField1) VALUES  (@Field)
    SET @i = @i + 1
END

查询 1

;WITH DistinctMyField1CTE
AS
(
    SELECT DISTINCT MyField1
    FROM MyTable
),
LookupValuesCTE
AS
(
    SELECT MyField1, dbo.MyFunction(MyField1) As MyOutputField
    FROM DistinctMyField1CTE
)
SELECT TOP 20 T1.Id, T1.MyField1, T2.MyOutputField
FROM MyTable T1
INNER JOIN LookupValuesCTE T2
    ON T1.MyField1 = T2.MyField1
ORDER BY T1.ID

结果

| ID | MYFIELD1 | MYOUTPUTFIELD |
---------------------------------
|  1 |        C |     ccccccccc |
|  2 |        A |    aaaaaaaaaa |
|  3 |        B |    bbbbbbbbbb |
|  4 |        C |     ccccccccc |
|  5 |        A |    aaaaaaaaaa |
|  6 |        B |    bbbbbbbbbb |
|  7 |        C |     ccccccccc |
|  8 |        A |    aaaaaaaaaa |
|  9 |        B |    bbbbbbbbbb |
| 10 |        C |     ccccccccc |
| 11 |        A |    aaaaaaaaaa |
| 12 |        B |    bbbbbbbbbb |
| 13 |        C |     ccccccccc |
| 14 |        A |    aaaaaaaaaa |
| 15 |        B |    bbbbbbbbbb |
| 16 |        C |     ccccccccc |
| 17 |        A |    aaaaaaaaaa |
| 18 |        B |    bbbbbbbbbb |
| 19 |        C |     ccccccccc |
| 20 |        A |    aaaaaaaaaa |

编辑: 请注意,当我检查 aboce 的 SQL Profiler 跟踪时,我看到对 UDF 的 1000 次调用,换句话说,查询分析器正在生成一个扩展 CTE 并为每一行调用一次 UDF 的计划。

下面使用一个表变量来确保 UDF 只被调用 3 次。我在 SQL Profiler 中对此进行了跟踪,并且效率更高。这使用与上面相同的表和函数。需要附加 SQLFiddle

SQL小提琴

MS SQL Server 2008 架构设置

CREATE TABLE MyTable
(
    ID int PRIMARY KEY IDENTITY,
    MyField1 VARCHAR(1)
);


CREATE FUNCTION MyFunction
(
    @input As VARCHAR(1)
)
RETURNS VARCHAR(10)
AS
BEGIN
    -- This could be a CLR Function
    -- Return the result of the function
    RETURN  CASE @input WHEN 'A' THEN 'aaaaaaaaaa' WHEN 'B' THEN 'bbbbbbbbbb' ELSE 'ccccccccc' END

END;

-- DATA SET UP
DECLARE @i INT = 0
DECLARE @Field VARCHAR(1)
WHILE @i < 1000
BEGIN
    SELECT @Field = CASE @i % 3 WHEN 1 THEN 'A' WHEN 2 THEN 'B' ELSE 'C' END
    INSERT INTO MyTable (MyField1) VALUES  (@Field)
    SET @i = @i + 1
END

查询 1

DECLARE @TempTable TABLE
(
    MyField1 VARCHAR(1) PRIMARY KEY,
    MyOutputField VARCHAR(10) NULL
)

INSERT INTO @TempTable (MyField1)
SELECT DISTINCT MyField1
FROM MyTable


-- UPDATE Separately otherwise the function gets called
-- for every row in MyTable
UPDATE @TempTable
    SET MyOutputField = dbo.MyFunction(MyField1)

SELECT TOP 20 T1.ID, T1.MyField1, T2.MyOutputField
FROM MyTable T1
INNER JOIN @TempTable T2
    ON T1.MyField1 = T2.MyField1

结果

| ID | MYFIELD1 | MYOUTPUTFIELD |
---------------------------------
|  2 |        A |    aaaaaaaaaa |
|  5 |        A |    aaaaaaaaaa |
|  8 |        A |    aaaaaaaaaa |
| 11 |        A |    aaaaaaaaaa |
| 14 |        A |    aaaaaaaaaa |
| 17 |        A |    aaaaaaaaaa |
| 20 |        A |    aaaaaaaaaa |
| 23 |        A |    aaaaaaaaaa |
| 26 |        A |    aaaaaaaaaa |
| 29 |        A |    aaaaaaaaaa |
| 32 |        A |    aaaaaaaaaa |
| 35 |        A |    aaaaaaaaaa |
| 38 |        A |    aaaaaaaaaa |
| 41 |        A |    aaaaaaaaaa |
| 44 |        A |    aaaaaaaaaa |
| 47 |        A |    aaaaaaaaaa |
| 50 |        A |    aaaaaaaaaa |
| 53 |        A |    aaaaaaaaaa |
| 56 |        A |    aaaaaaaaaa |
| 59 |        A |    aaaaaaaaaa |
于 2012-12-06T12:52:04.280 回答
1

我想出了这个解决方案,但我知道所有的锁定本身都可能代价高昂,而且我不确定我是否保证了这个线程的安全并且没有死锁。此外,它不符合我仅在特定上下文中保持缓存活动的愿望。

缓存助手:

private class CustomCache
{
    private class CacheObject
    {
        private DateTime _expires;
        private string _value;

        public string Value { get { _expires = DateTime.Now.AddSeconds(5.0); return _value; } }
        public DateTime Expires { get { return _expires; } }

        public CacheObject(string value)
        {
            _value = value;
            _expires = DateTime.Now.AddSeconds(5.0);
        }
    }

    private Dictionary<string, CacheObject> _cache = new Dictionary<string,CacheObject>();
    private object _cacheLock = new object();

    public string this[string key]
    {
        get
        {
            return _cache[key].Value;
        }
    }

    public void Add(string key, string value)
    {
        lock (_cacheLock)
        {
            if (!_cache.ContainsKey(key))
            {
                // Add the key and value to the dictionary.
                _cache.Add(key, new CacheObject(value));

                // Create a thread to check expiration on the object and remove from the dictionary.
                var t = new System.Threading.Thread(arg =>
                {
                    var k = (string)arg;
                    bool exists;
                    do
                    {
                        System.Threading.Thread.Sleep(2000);
                        lock (_cacheLock)
                        {
                            exists = ((_cache.ContainsKey(k)) && (_cache[k].Expires > DateTime.Now));
                        }
                    }
                    while (exists);
                    lock (_cacheLock)
                    {
                        _cache.Remove(k);
                    }
                });
                t.Start(key);
            }
        }
    }

    public bool Contains(string key)
    {
        bool contains;
        lock (_cacheLock)
        {
            contains = _cache.ContainsKey(key);
        }
        return contains;
    }
}

修改后的 UDF 代码:

private static CustomCache Cache = new CustomCache();

[SqlFunction]
public static SqlString MyFunction(SqlString input)
{
    if (input.IsNull) return SqlString.Null;

    if (!Cache.Contains(input.Value))
    {
        // Not in cache; retrieve from the service.
        using (var webService = new MyWebService())
        {
            string result = webService.Call(input.Value);

            Cache.Add(input.Value, result);
        }
    }

    return new SqlString(Cache[input.Value]);
}
于 2012-11-29T15:28:50.047 回答