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

时间:2015-08-27 12:56:02

标签: sql sql-server tsql stored-procedures

我有一个名为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

有关proc的信息

  1. proc要求所有参数,并且它们都不是NULL
  2. proc的目的是根据预定义的优先级返回表中的单个匹配行。优先级为ClientId>国家>区域>语言
  3. ClientUrls表中的三个列可以为空。如果一列包含NULL,则它引用" All"。例如如果LanguageId为NULL,则它引用" AllLanguages"。因此,如果我们向proc发送一个5的LanguageId,我们首先查找,否则我们会尝试找到一个为NULL的。
  4. 优先级矩阵(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 |
    +----------+-----------+----------+------------+-------------------------------+
    

    返回此行是因为NULL RegionId与正确的LanguageId匹配的优先级高于使用正确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
    

4 个答案:

答案 0 :(得分:2)

这很容易(如果我理解正确的话)。你可以用很少的代码完成它。此外,如果需要,它很容易扩展。

这是一个工作示例

--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行超过1,1,1,Null选项

答案 1 :(得分:1)

未经测试,但你可以这样做:

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

通过为每个匹配项赋值并选择最高值,您可以减少所需的代码。但是你会增加所需的案例陈述数量,而不是工会数量。

这也可以是函数而不是存储过程。因此,在其他查询中可以更容易地使用它

答案 2 :(得分:0)

您可以将where子句更改为:

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

编码方式,它的代码更少。 但调整问题更多。

答案 3 :(得分:0)

另一种选择可能是尝试操纵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