1

Webサービスを照会するSQLCLRUDFがあります。これはコストがかかる可能性があるため、特に関数内では複数の行に対するクエリの一部であるため、可能な限りWebサービスを呼び出さないようにしたいと思います。いずれの場合も、同じ入力で同じ出力が生成されます(たとえば、入力が「abc」の場合、常に「xyz」が取得され、何も変わりません。同様に、「def」でも常に「tuv」が生成されます)。

私はいくつかのテストを行いましたが、SQLはその最後でいかなる種類のキャッシュも行わないようです。そのため、Webサービスは常に呼び出されます。

例:MyTableフィールドを持つテーブルがありますMyField1MyTable500行ありますが、常にMyField13つの可能な値のうちの1つだけがあります。クエリの例:

SELECT MyFunction(MyField1) FROM MyTable

何が起こるかというと、Webサービスはテーブルの各行に1回ずつ、500回呼び出されます。私が好むのは、Webサービスが3回だけ呼び出され(個別の値ごとに1回)、重複する値についてキャッシュから読み取られることです。

サンプルコード:

[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

私はこの解決策を考え出しましたが、すべてのロック自体にコストがかかる可能性があることを認識しており、このスレッドを安全に保ち、デッドロックが発生しないようにしたかどうかはわかりません。さらに、特定のコンテキストでのみキャッシュを存続させたいという私の要望には応えられません。

キャッシングヘルパー:

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 に答える
1

編集注記最後に向かって更新されたバージョン

この回答はキャッシュを使用していませんが、関数への呼び出しの数を最小限に抑える必要があります。を使用してCTEsmyField1の個別の値を検索し、関数を使用して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 |

編集: 上記のSQLプロファイラートレースを確認すると、UDFへの1000回の呼び出しが表示されます。つまり、クエリアナライザーは、CTEを拡張し、行ごとに1回UDFを呼び出すプランを生成しています。

以下では、テーブル変数を使用して、UDFが3回だけ呼び出されるようにします。これをSQLプロファイラーで追跡しましたが、はるかに効率的です。これは、上記と同じテーブルと関数を使用します。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 に答える