Sql 按优先级返回基于可空列的数据的存储过程

Sql 按优先级返回基于可空列的数据的存储过程,sql,sql-server,tsql,stored-procedures,Sql,Sql Server,Tsql,Stored Procedures,我有一个名为ClientUrls的表,其结构如下: +------------+----------------+----------+ | ColumnName | DataType | Nullable | +------------+----------------+----------+ | ClientId | INT | No | | CountryId | INT | Yes | | RegionI

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

+------------+----------------+----------+
| ColumnName |    DataType    | Nullable |
+------------+----------------+----------+
| ClientId   | INT            | No       |
| CountryId  | INT            | Yes      |
| RegionId   | INT            | Yes      |
| LanguageId | INT            | Yes      |
| URL        | NVARCHAR(2048) | NO       |
+------------+----------------+----------+
我有一个存储过程
up\u GetClientUrls
,它接受以下参数:

@ClientId INT
@CountryId INT
@RegionId INT
@LanguageId INT
有关程序的信息

  • 所有参数都是proc所必需的,并且没有一个参数是空的
  • 该过程的目的是基于预定义的优先级返回表中的单个匹配行。优先顺序为ClientId>Country>Region>Language
  • ClientUrls表中的三个列可为空。如果一列包含空值,则表示“全部”。e、 g.如果LanguageId为空,则它表示“所有语言”。因此,如果我们向proc发送一个LanguageId为5的,我们会首先查找它,否则我们会尝试找到一个为NULL的
  • 优先级矩阵(1为第一位)

    以下是一些示例数据:

    +----------+-----------+----------+------------+-------------------------------+
    | 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
    
    预期响应(基于示例数据)

    返回此行是因为在空RegionId上匹配正确的LanguageId比在空LanguageId上匹配正确的RegionId优先级更高

    下面是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
    

    您可以将where子句更改为:

    AND (c.CountryID = @CountryID OR c.CountryID IS NULL)
    
    编码方面,它的代码更少。
    但调整问题更大。

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

    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
    
    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
    
    通过为每个匹配项指定一个值并选择最高值,可以减少所需的代码。但您将增加所需案例陈述的数量,而不是工会的数量


    这也可以是一个函数,而不是存储过程。因此,它可以更容易地用于其他查询的

    另一种方法可能是尝试操纵
    NULL
    值来创建继承权,如下所示:

    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
    
    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
    
    这很容易(如果我理解正确的话)。您只需要很少的代码就可以做到这一点。此外,如果需要,它将很容易扩展

    下面是一个工作示例

    --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语句替换IIF

    它是这样工作的

    每个匹配项都有一个值(有点像二进制数) 然后根据每个匹配项,我们使用值或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,1,1行

    您的客户端id在表中是唯一的吗?我的意思是如果我能得到
    [Client\u id 1 Country 1],[Client\u id 1,Country 2]
    ClientId,Country id,RegionId和LanguageId都应该是唯一的(包括空值是唯一的)。你使用
    TOP 1
    我还应该在你当前的代码中检查
    TOP 1和TIES
    ,因为在不知道索引的情况下,它就不那么明显了。您可以使用具有相同排列的CASE语句(即,当ClientID=@@ClientID和CountryID=@@CountryID等时,第一个将是CASE语句。最后一个将有3个为null)。我认为你会有更好的表现,但我不确定。回答很好+1用于IIF的使用。我还没见过那个函数。这个查询的性能也比我的UNION语句好。我知道IIF函数,但我从来没有使用过它,因为我对case语句已经习以为常了。我真的应该开始使用它了。另外,与将空匹配与精确匹配分开一样,这使得它更易于维护+1感谢您的评论。1,2,4,8。。。在谈论优先事项时,技巧是一件容易记住的事情。如果您有两个优先级相同的项目,它甚至可以工作,并且很容易扩展或收回,因为这些数字只在计算中使用过。