7

作業中のデータベースにStackOverflowのようなタグ付けシステムがあります。そして、WHERE句のタグの数が特定されていないことに基づいて結果を検索するストアドプロシージャを作成しています。結果をフィルタリングするために、0〜10個のタグが存在する可能性があります。したがって、たとえば、ユーザーは「apple」、「orange」、および「banana」でタグ付けされたアイテムを検索することができ、結果には3つのタグすべてが含まれている必要があります。タグ付け用の相互参照テーブルも扱っているため、クエリはさらに複雑になりますが、この質問の目的のために、これについては説明しません。

文字列操作を実行し、exec()関数にクエリを送信してこれを処理できることはわかっていますが、動的SQLに関連するパフォーマンスの問題は避けたいと思います。SQLがストアドプロシージャのクエリプランをキャッシュするのが最善だと思います。

このタイプのシナリオで動的SQLを回避するために使用したテクニックは何ですか?

人気のある要望により、私が取り組んでいるクエリは次のとおりです。

SELECT ft.[RANK], s.shader_id, s.page_name, s.name, s.description, s.download_count, s.rating, s.price FROM shader s 
INNER JOIN FREETEXTTABLE(shader, *, @search_term) AS ft ON s.shader_id = ft.[KEY]
WHERE EXISTS(SELECT tsx.shader_id FROM tag_shader_xref tsx INNER JOIN tag t ON tsx.tag_id = t.tag_id WHERE tsx.shader_id = s.shader_id AND t.tag_name = 'color')
AND EXISTS(SELECT tsx.shader_id FROM tag_shader_xref tsx INNER JOIN tag t ON tsx.tag_id = t.tag_id WHERE tsx.shader_id = s.shader_id AND t.tag_name = 'saturation')
ORDER BY ft.[RANK] DESC

これは機能しますが、ハードコーディングされています。'color'タグと'saturation'タグを検索するように設定されていることがわかります。

4

8 に答える 8

13

この問題および同様の問題に関する広範な概要については、http://www.sommarskog.se/dyn-search-2005.htmlを参照してください。

あなたの質問に固有のものはここの部分です:http://www.sommarskog.se/dyn-search-2005.html#AND_ISNOTNULL

また、クエリプランはキャッシュされる可能性があるため、(単純な)動的ソリューションは(おそらく複雑な)静的ソリューションよりも必ずしも遅いとは限らないことも考慮に入れてください。http://www.sommarskog.se/dyn-search-2005.htmlを参照してください。 #dynsql

したがって、現実的なクエリを考慮に入れて、現実的な量のデータに対してオプションを慎重にテスト/測定する必要があります(たとえば、1つまたは2つのパラメータを使用した検索は、10を使用した検索よりもはるかに一般的です)。


編集:質問者はコメントでこれを最適化する正当な理由を与えたので、「時期尚早」の警告を少し邪魔にならないように移動しました:

ただし、(標準の;)警告の言葉が適用されます。これは時期尚早の最適化のようなにおいがします。-このsprocは、動的SQLの使用が大幅に遅くなる(つまり、アプリで実行されている他のものと比較して)ことが頻繁に呼び出されると確信していますか?

于 2009-08-23T01:09:28.497 に答える
3

だから、これは私が予想したよりも簡単でした。これを処理するためにかなり単純なクエリを実装した後、すぐに思ったよりもはるかに優れたパフォーマンスが得られました. したがって、他のソリューションを実装してテストする必要があるかどうかはわかりません。

現在、データベースは約 200 のシェーダーと 500 のタグでいっぱいです。検索用語の有無にかかわらず、さまざまな数のタグを使用してストアド プロシージャに対して 35 の異なる検索クエリを実行するという、やや現実的なテストだと思われるテストを実行しました。これらすべてを 1 つの SQL ステートメントにまとめ、ASP.NET で結果をベンチマークしました。この 35 回の検索を 200 ミリ秒未満で一貫して実行しました。5 回の検索に減らすと、時間は 10 ミリ秒に短縮されます。そういう演出はすごいです。データベースのサイズが小さいことが役に立ちます。しかし、クエリがインデックスをうまく利用することも役立つと思います。

クエリで変更したことの 1 つは、タグの検索方法です。名前ではなく ID でタグを検索しています。これを行うことで、結合を 1 つ減らすことができ、検索にインデックスを使用できるという利点があります。そして、「dbo」も追加しました。SQLがユーザーごとにクエリをキャッシュすることを学習した後、テーブル名の前に。

誰かが興味を持っている場合に備えて、完成したストアド プロシージャを次に示します。

ALTER PROCEDURE [dbo].[search] 
    @search_term    varchar(100) = NULL,
    @tag1           int = NULL,
    @tag2           int = NULL,
    @tag3           int = NULL,
    @tag4           int = NULL,
    @tag5           int = NULL,
    @tag6           int = NULL,
    @tag7           int = NULL,
    @tag8           int = NULL,
    @tag9           int = NULL,
    @tag10          int = NULL
AS
BEGIN
    SET NOCOUNT ON;

    IF LEN(@search_term) > 0
        BEGIN
            SELECT s.shader_id, s.page_name, s.name, s.description, s.download_count, s.rating, s.price FROM dbo.shader s 
            INNER JOIN FREETEXTTABLE(dbo.shader, *, @search_term) AS ft ON s.shader_id = ft.[KEY]
            WHERE (@tag1 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag1))
            AND   (@tag2 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag2))
            AND   (@tag3 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag3))
            AND   (@tag4 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag4))
            AND   (@tag5 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag5))
            AND   (@tag6 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag6))
            AND   (@tag7 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag7))
            AND   (@tag8 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag8))
            AND   (@tag9 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag9))
            AND   (@tag10 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag10))
            ORDER BY ft.[RANK] DESC
        END
    ELSE
        BEGIN
            SELECT s.shader_id, s.page_name, s.name, s.description, s.download_count, s.rating, s.price FROM dbo.shader s 
            WHERE (@tag1 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag1))
            AND   (@tag2 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag2))
            AND   (@tag3 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag3))
            AND   (@tag4 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag4))
            AND   (@tag5 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag5))
            AND   (@tag6 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag6))
            AND   (@tag7 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag7))
            AND   (@tag8 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag8))
            AND   (@tag9 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag9))
            AND   (@tag10 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag10))
        END
END

すべてのオプションを使い果たしたわけではありませんが、データベース設計がこのタスクに対して非常にうまく機能していることを証明できたので、これは良い練習になりました。また、この質問を投稿することで多くのことを学びました。exec() はクエリ プランをキャッシュしないため、悪いことはわかっていました。しかし、sp_executesql がクエリ プランをキャッシュすることを知りませんでした。これは非常にすばらしいことです。また、共通テーブル式についても知りませんでした。そして、Henrik Opel が投稿したリンクには、この種のタスクに関する優れたヒントが満載です。

もちろん、データベースが大幅に拡大した場合は、1 年後にこれを再訪する可能性があります。それまでは、皆様のご協力に感謝いたします。

アップデート:

この検索エンジンの実例をhttp://www.silverlightxap.com/controlsにオンラインで公開しています。

于 2009-08-23T17:54:36.297 に答える
1

不確定な数のパラメーターを使用する場合、動的SQLを回避するにはどうすればよいですか?

代わりに、適切なパラメーター化された(準備された)SQLテンプレートを動的に生成できます。

パラメータが初めて表示されたときにステートメントテンプレートを作成して準備し、同じ数のパラメータが再び表示されたときに再利用できるように、準備されたステートメントをキャッシュします。

これは、アプリケーションまたは十分に洗練されたストアドプロシージャで実行できます。

私はこのアプローチを、たとえば、最大10個のタグを取り、それらのいずれかがNULLであることを処理するためのグロディロジックを備えたプロシージャよりもはるかに好みます。

この質問でのBillKarwinのGROUP BY答えは、おそらく作成するのが最も簡単なテンプレートです。つまり、IN述語のプレースホルダーを連結し、COUNT句を更新するだけです。xref1タグごとの結合を含む他のソリューションでは、テーブルのエイリアス(たとえば、、など)を段階的に増やす必要がありますxref2

于 2009-08-23T02:20:00.670 に答える
1

私はこの問題に対する2つのタイプの解決策を見てきました:

1つ目は、探しているタグごとに1回(必要に応じて外部参照を介して)shaderテーブルを結合することです。tags内部結合の結果には、すべてのタグに一致するシェーダーのみが含まれます。

SELECT s.*
FROM shader s
JOIN tag_shader_xref x1 ON (s.shader_id = x1.shader_id)
JOIN tag t1 ON (t1.tag_id = x1.tag_id AND t1.tag_name = 'color')
JOIN tag_shader_xref x2 ON (s.shader_id = x2.shader_id)
JOIN tag t2 ON (t2.tag_id = x2.tag_id AND t2.tag_name = 'saturation')
JOIN tag_shader_xref x3 ON (s.shader_id = x3.shader_id)
JOIN tag t3 ON (t3.tag_id = x3.tag_id AND t3.tag_name = 'transparency');

2番目の解決策は、そのタグに1回結合し、タグを必要な3つに制限してから、一致をカウントできるようにすることですGROUP BYshader_idすべてのタグが見つかった場合にのみ、カウントは3になります(外部参照テーブルでの一意性を前提としています)。

SELECT s.shader_id
FROM shader s
JOIN tag_shader_xref x ON (s.shader_id = x.shader_id)
JOIN tag t ON (t.tag_id = x.tag_id 
  AND t.tag_name IN ('color', 'saturation', 'transparency'))
GROUP BY s.shader_id
HAVING COUNT(DISTINCT t.tag_name) = 3;

どちらを使うべきですか?データベースのブランドがいずれかの方法をどの程度最適化しているかによって異なります。私は通常MySQLを使用しますが、これはではうまく機能しないGROUP BYため、前者の方法を使用することをお勧めします。Microsoft SQL Serverでは、後者のソリューションの方が適している場合があります。

于 2009-08-23T01:20:29.873 に答える
1

EXISTS 句で相関サブクエリが重複しているため、クエリは共通テーブル式 (CTE) を使用するのに最適です。

WITH attribute AS(
  SELECT tsx.shader_id,
         t.tag_name
    FROM TAG_SHADER_XREF tsx ON tsx.shader_id = s.shader_id
    JOIN TAG t ON t.tad_id = tsx.tag_id)
SELECT ft.[RANK], 
       s.shader_id, 
       s.page_name, 
       s.name, 
       s.description, 
       s.download_count, 
       s.rating, 
       s.price 
  FROM SHADER s 
  JOIN FREETEXTTABLE(SHADER, *, @search_term) AS ft ON s.shader_id = ft.[KEY]
  JOIN attribute a1 ON a1.shader_id = s.shader_id AND a1.tag_name = 'color'
  JOIN attribute a2 ON a2.shader_id = s.shader_id AND a2.tag_name = 'saturation'
 ORDER BY ft.[RANK] DESC

CTE を使用して、EXISTS も JOIN に変換しました。

動的 SQL の使用に関する元の質問に言えば、唯一の代替手段は、適用する前にエスケープ基準について受信パラメーターをチェックすることです。いいえ:

WHERE (@param1 IS NULL OR a1.tag_name = @param1)

@param1 に NULL 値が含まれている場合、括弧内の SQL の後半部分は実行されません。そうしないと、使用されない可能性のあるJOINなどを作成するため、動的SQLアプローチを好みます-これはリソースの無駄です。

動的 SQL にはどのようなパフォーマンスの問題があると思いますか? を使用sp_executesqlすると、クエリ プランがキャッシュされます。exec率直に言って、( orを使用して) クエリが syntax/etc で検証されている場合、クエリ プランがキャッシュされないのは奇妙だと思いますsp_executesql。検証はクエリ プランの前に行われますが、その後のステップがスキップされるのはなぜですか?

于 2009-08-23T01:34:28.763 に答える
0

これは最速の方法ではないかもしれませんが、タグごとにクエリ文字列を生成してから、それらを「INTERSECT」で結合できますか?

編集:sprocタグが表示されなかったため、これが可能かどうかはわかりません。

于 2009-08-23T01:14:21.410 に答える
0

Henrikの答えに賛成しましたが、考えられる別の方法は、検索タグを一時テーブルまたはテーブル変数に取得してから、JOINを実行するか、サブSELECTでIN句を使用することです。検索されたすべてのタグを含む結果が必要なため、最初にクエリ タグの数を数えてから、一致したタグの数がその数に等しい結果を見つける必要があります。

値をテーブルに入れる方法は? タグがストアド プロシージャに渡され、SQL Server 2008 を使用している場合は、新しいテーブル値パラメーター機能を使用して、テーブル変数をストアド プロシージャに直接渡すことができます。

それ以外の場合、単一の文字列でタグを受け取る場合は、ここに示す SplitString 関数など、テーブルを返すストアド関数を使用できます。次のようなことができます:

... WHERE @SearchTagCount = (SELECT COUNT(tsx.shader_id) FROM tag_shader_xref tsx
INNER JOIN tag t ON tsx.tag_id = t.tag_id
WHERE tsx.shader_id = s.shader_id AND t.tag_name IN (SELECT * FROM dbo.SplitString(@SearchTags,',')))
于 2009-08-23T01:53:55.933 に答える
-1

タグを「apple」、「orange」で区切るコンマと一緒に文字列化し、ストアドプロシージャでIN句を使用する1つのパラメータに渡します。

もちろん、これらのタグのルックアップテーブルからの値(キー)がある場合は、それらを使用します。

編集:

結果にはすべてのタグが必要なので...

残念ながら、あなたが何をしても、SPは再生成される計画の危険にさらされると思います。

オプションのパラメーターを使用し、CASEとISNULLを使用して引数を作成できます。

私はまだこれはあなたのSPがキャッシュされた良さのほとんどを失ったことを意味すると思いますが、それはまっすぐなexec'string'よりも優れていると私は信じています。

于 2009-08-23T00:03:41.407 に答える