【问题标题】:Stored proc to return data based on nullable columns by a priority存储过程以优先级基于可空列返回数据
【发布时间】:2015-08-27 12:56:01
【问题描述】:

我有一个名为ClientUrls 的表,其结构如下:

+------------+----------------+----------+
| ColumnName |    DataType    | Nullable |
+------------+----------------+----------+
| ClientId   | INT            | No       |
| CountryId  | INT            | Yes      |
| RegionId   | INT            | Yes      |
| LanguageId | INT            | Yes      |
| URL        | NVARCHAR(2048) | NO       |
+------------+----------------+----------+

我有一个存储过程up_GetClientUrls,它采用以下参数:

@ClientId INT
@CountryId INT
@RegionId INT
@LanguageId INT

有关过程的信息

  1. 所有参数都是 proc 所必需的,它们都不会是 NULL
  2. proc 的目的是根据预定义的优先级返回表中的单个匹配行。优先级是 ClientId>Country>Region>Language
  3. ClientUrls 表中的三个列可以为空。如果一列包含 NULL,则表示“全部”。例如如果 LanguageId 为 NULL,则它指的是“AllLanguages”。因此,如果我们向 proc 发送一个 LanguageId 为 5 的值,我们会首先查找它,否则我们会尝试查找为 NULL 的那个。

优先级矩阵(1 优先)

+---------+----------+-----------+----------+------------+
| Ranking | ClientId | CountryId | RegionId | LanguageId |
+---------+----------+-----------+----------+------------+
|       1 | NOT NULL | NOT NULL  | NOT NULL | NOT NULL   |
|       2 | NOT NULL | NULL      | NOT NULL | NOT NULL   |
|       3 | NOT NULL | NOT NULL  | NULL     | NOT NULL   |
|       4 | NOT NULL | NULL      | NULL     | NOT NULL   |
|       5 | NOT NULL | NOT NULL  | NOT NULL | NULL       |
|       6 | NOT NULL | NULL      | NOT NULL | NULL       |
|       7 | NOT NULL | NULL      | NULL     | NULL       |
+---------+----------+-----------+----------+------------+

这是一些示例数据

+----------+-----------+----------+------------+-------------------------------+
| ClientId | CountryId | RegionId | LanguageId |             URL               |
+----------+-----------+----------+------------+-------------------------------+
|        1 |         1 | 1        | 1          | http://www.Website.com        |
|        1 |         1 | 1        | NULL       | http://www.Otherwebsite.com   |
|        1 |         1 | NULL     | 2          | http://www.Anotherwebsite.com |
+----------+-----------+----------+------------+-------------------------------+

存储过程调用示例

EXEC up_GetClientUrls   @ClientId = 1
                        ,@CountryId = 1
                        ,@RegionId = 1
                        ,@LanguageId = 2

预期响应(基于示例数据)

+----------+-----------+----------+------------+-------------------------------+
| ClientId | CountryId | RegionId | LanguageId |             URL               |
+----------+-----------+----------+------------+-------------------------------+
|        1 |         1 |     NULL | 2          | http://www.Anotherwebsite.com |
+----------+-----------+----------+------------+-------------------------------+

返回此行是因为匹配具有正确 LanguageId 的 NULL RegionId 比匹配具有正确 RegionId 的 NULL LanguageId 具有更高的优先级。

这是 proc 的代码(有效)。要真正解决我的问题,有没有更好的方法来写这个?如果我将来扩展此表,我将继续增加 UNION 语句的数量,因此它不是真正可扩展的。

实际存储过程

CREATE PROC up_GetClientUrls
    (
        @ClientId       INT
        ,@CountryId     INT
        ,@RegionId      INT
        ,@LanguageId    INT
    )
AS
    BEGIN

        SELECT TOP 1
            prioritised.ClientId
            ,prioritised.CountryId
            ,prioritised.RegionId
            ,prioritised.LanguageId
            ,prioritised.URL
        FROM
        (
            SELECT
                c.ClientId
                ,c.CountryId
                ,c.RegionId
                ,c.LanguageId
                ,c.URL
                ,1  [priority]
            FROM ClientUrls c
            WHERE c.ClientId = @ClientId
            AND c.CountryId = @CountryId
            AND c.RegionId = @RegionId
            AND c.LanguageId = @LanguageId
            UNION
                SELECT
                    c.ClientId
                    ,c.CountryId
                    ,c.RegionId
                    ,c.LanguageId
                    ,c.URL
                    ,2  [priority]
                FROM ClientUrls c
                WHERE c.ClientId = @ClientId
                AND c.CountryId IS NULL
                AND c.RegionId = @RegionId
                AND c.LanguageId = @LanguageId
            UNION
                SELECT
                    c.ClientId
                    ,c.CountryId
                    ,c.RegionId
                    ,c.LanguageId
                    ,c.URL
                    ,3  [priority]
                FROM ClientUrls c
                WHERE c.ClientId = @ClientId
                AND c.CountryId = @CountryId
                AND c.RegionId IS NULL
                AND c.LanguageId = @LanguageId
            UNION
                SELECT
                    c.ClientId
                    ,c.CountryId
                    ,c.RegionId
                    ,c.LanguageId
                    ,c.URL
                    ,4  [priority]
                FROM ClientUrls c
                WHERE c.ClientId = @ClientId
                AND c.CountryId IS NULL
                AND c.RegionId IS NULL
                AND c.LanguageId = @LanguageId
            UNION
                SELECT
                    c.ClientId
                    ,c.CountryId
                    ,c.RegionId
                    ,c.LanguageId
                    ,c.URL
                    ,5  [priority]
                FROM ClientUrls c
                WHERE c.ClientId = @ClientId
                AND c.CountryId = @CountryId
                AND c.RegionId = @RegionId
                AND c.LanguageId IS NULL
            UNION
                SELECT
                    c.ClientId
                    ,c.CountryId
                    ,c.RegionId
                    ,c.LanguageId
                    ,c.URL
                    ,6  [priority]
                FROM ClientUrls c
                WHERE c.ClientId = @ClientId
                AND c.CountryId IS NULL
                AND c.RegionId = @RegionId
                AND c.LanguageId IS NULL
            UNION
                SELECT
                    c.ClientId
                    ,c.CountryId
                    ,c.RegionId
                    ,c.LanguageId
                    ,c.URL
                    ,7  [priority]
                FROM ClientUrls c
                WHERE c.ClientId = @ClientId
                AND c.CountryId IS NULL
                AND c.RegionId IS NULL
                AND c.LanguageId IS NULL
        ) prioritised
        ORDER BY prioritised.[Priority]

    END

【问题讨论】:

  • 您的客户 ID 在表中是唯一的吗?我的意思是如果我能得到[Client_id 1 Country 1], [client_id 1, country 2]
  • ClientId、CountryId、RegionId和LanguageId都应该是唯一的(包括NULLS唯一)。
  • 你使用TOP 1 我应该在你当前的代码中检查TOP 1 WITH TIES,因为不知道你的索引它不是那么明显。
  • 您可以使用具有相同排列的 CASE 语句(即,第一个将是 CASE WHEN ClientID = @@ClientID AND CountryID = @@CountryID 等,等等。最后一个将有 3 个 IS NULL)。我认为你会有更好的表现,但我不确定。

标签: sql sql-server tsql stored-procedures


【解决方案1】:

这很容易(如果我理解正确的话)。你可以用很少的代码做到这一点。另外,如果需要,它很容易扩展。

这是一个工作示例

--Make a table
CREATE TABLE #ClientUrls (ClientId INT NOT NULL,CountryId INT NULL,RegionId INT NULL,LanguageId INT NULL,URL NVARCHAR(2048) NOT NULL)

--Put some data into it
INSERT INTO #ClientUrls (ClientId, CountryId, RegionId, LanguageId, URL)
VALUES
(1,1,1,1,'http://www.Website.com'),
(1,1,1,NULL,'http://www.Otherwebsite.com'),
(1,1,NULL,2,'http://www.Anotherwebsite.com')

--This would all be in your proc
----------------------------------------------
DECLARE @ClientId       INT = 1
DECLARE @CountryId      INT = 1
DECLARE @RegionId       INT = 1
DECLARE @LanguageId     INT = 2

--This is the interesting bit
----------------------------------------------
SELECT TOP 1 C.*

FROM    #ClientUrls AS C
ORDER BY 
    --Order the ones with the best hit count near the top
   IIF(ISNULL(C.ClientId,  @ClientId)   = @ClientId  ,1,0) +            
   IIF(ISNULL(C.CountryId, @CountryId)  = @CountryId ,2,0) +
   IIF(ISNULL(C.RegionId,  @RegionId)   = @RegionId  ,4,0) +
   IIF(ISNULL(C.LanguageId,@LanguageId) = @LanguageId,8,0) DESC,

    --Order the ones with the least nulls of each hit count near the top
   IIF(C.ClientId   IS NULL,0,1) +                                              
   IIF(C.CountryId  IS NULL,0,2) +
   IIF(C.RegionId   IS NULL,0,4) +
   IIF(C.LanguageId IS NULL,0,8) DESC

DROP TABLE #ClientUrls

就是这样。在旧版本的 SQL 中,您不能使用 IIF,但如果需要,您可以将其替换为 case 语句。

它是这样工作的。

每个匹配项都有一个值(有点像二进制数) 然后根据每个匹配项,我们使用该值,如果不匹配,则使用 0 通过将总数相加,我们将始终选择最佳匹配组合。

value          1           2           4         8            Total value 
+---------+----------+-----------+----------+------------+
| Ranking | ClientId | CountryId | RegionId | LanguageId |
+---------+----------+-----------+----------+------------+
|       1 | NOT NULL | NOT NULL  | NOT NULL | NOT NULL   |       15
|       2 | NOT NULL | NULL      | NOT NULL | NOT NULL   |       13
|       3 | NOT NULL | NOT NULL  | NULL     | NOT NULL   |       11  
|       4 | NOT NULL | NULL      | NULL     | NOT NULL   |       9
|       5 | NOT NULL | NOT NULL  | NOT NULL | NULL       |       7
|       6 | NOT NULL | NULL      | NOT NULL | NULL       |       5
|       7 | NOT NULL | NULL      | NULL     | NULL       |       1
+---------+----------+-----------+----------+------------+

我刚刚更新了此内容,以确保您通过 null 选项获得非 null 版本。

如果您编辑结果以返回比前 1 更多的结果,您可以以正确的顺序查看这些项目。即,如果您将语言从 2 更改为 1,您将获得 1,1,1,1 行 Over the 1,1,1,Null 选项

【讨论】:

  • 绝妙的答案! +1 也用于 IIF。我还没有遇到过这个功能。这个查询也比我的 UNION 语句执行得更好。
  • 我知道 IIF 函数,但我从不使用它,因为我已经 2 习惯了 case 语句。我真的应该开始使用它了。也像从精确匹配中拆分 NULL 匹配一样,使其更易于维护。 +1
  • 感谢 cmets。 1,2,4,8... 技巧是在谈论优先级时要牢记的一个方便的东西。如果您有两个具有相同优先级的项目,它甚至可以工作,并且很容易扩展或收回,因为这些数字仅用于计算。
【解决方案2】:

未测试,但您可以执行以下操作:

SELECT TOP 1 c.ClientId,
        c.CountryId,
        c.RegionId,
        c.LanguageId,
        c.URL
FROM ClientUrls c
ORDER BY CASE 
            WHEN c.ClientId = @ClientId
                THEN 1000
            ELSE 0
            END + 
        CASE 
            WHEN c.CountryId = @CountryId
                THEN 200
            WHEN c.CountryId IS NULL
                THEN 100
            ELSE 0
            END + 
        CASE 
            WHEN c.RegionId = @RegionId
                THEN 20
            WHEN c.CountryId IS NULL
                THEN 10
            ELSE 0
            END +  
        CASE 
            WHEN c.LanguageId = @LanguageId
                THEN 2
            WHEN c.CountryId IS NULL
                THEN 1
            ELSE 0
            END DESC

通过为每个匹配项指定一个值并选择最高值,您可以减少所需的代码。但是您将增加所需的 case 语句的数量,而不是联合的数量。

这也可以是一个函数而不是一个存储过程。所以它可以在其他查​​询中更容易使用

【讨论】:

    【解决方案3】:

    您可以将 where 子句更改为:

    AND (c.CountryID = @CountryID OR c.CountryID IS NULL)
    

    编码方面,它的代码更少。 但调优的问题要大得多。

    【讨论】:

      【解决方案4】:

      另一种方法可能是尝试操纵 NULL 值来创建层次结构,如下所示:

      WITH priorities as     (SELECT
                      c.ClientId
                      ,c.CountryId
                      ,c.RegionId
                      ,c.LanguageId
                      ,c.URL
                      ,COALESCE(
                                NULLIF(c.CountryId,@CountryId),
                                NULLIF(c.RegionId,@RegionId),
                                NULLIF(c.LanguageId,@LanguageId),
                                1000000)
                      + ISNULL(c.CountryId,200000)  
                      + ISNULL(c.RegionId,100000) 
                      + COALESCE(c.CountryId,RegionId,40000) 
                      + ISNULL(c.LanguageId,10000) 
                      + COALESCE(c.CountryId,c.LanguageId,4000) 
                      + COALESCE(c.CountryId,c.RegionId,c.LanguageId,1000)
                      [priority]
                  FROM ClientUrls c
                  WHERE c.ClientId = @ClientId
                  AND (c.CountryId = @CountryId
                  OR c.RegionId = @RegionId
                  OR c.LanguageId = @LanguageId)
                  )
      SELECT TOP 1 ClientId,CountryId,RegionId,LanguageId,URL FROM priorities ORDER BY priority DESC
      

      【讨论】:

        猜你喜欢
        • 2021-06-30
        • 2014-07-10
        • 2021-04-10
        • 1970-01-01
        • 2011-04-06
        • 1970-01-01
        • 1970-01-01
        • 2021-07-28
        • 2021-01-09
        相关资源
        最近更新 更多