当数据中不存在时,SQL Server 动态数据透视返回空值

发布于 2024-08-10 13:45:47 字数 2696 浏览 2 评论 0原文

我有 3 个表(tblPreferencetblCustomertblCustomerPreference),如下所示:

tblPreference:
ID       | Name            | DefaultValue
(int PK) | (nvarchar(100)) | (nvarchar(100))
-------------------------------
1        | Preference1     | 1
2        | Preference2     | Yes
3        | Preference3     | 1

tblCustomer:
CustomerID | ...
(int PK)
--------------------
1          | ...
2          | ...
3          | ...

tblCustomerPreference:
ID       | CustomerID | PreferenceID | Value
(int PK) | (int)      | (int)        | (nvarchar(100))
-------------------------------------------------------
1        | 1          | 1            | 0
2        | 1          | 2            | Yes
3        | 2          | 1            | 0
4        | 2          | 2            | No

我正在创建此数据的数据透视表,因此它是使用以下存储过程将所有内容都放在一行中,以便它始终会拉回所有首选项,如果它找到客户特定值,它将返回该值,否则它将返回默认值:

CREATE PROCEDURE [dbo].[usp_GetCustomerPreferences] @CustomerID int AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from interfering with SELECT statements.
    SET NOCOUNT ON;

    DECLARE @PivotColumns nvarchar(max)
    DECLARE @PivotColumnsSelectable nvarchar(max)
    SELECT @PivotColumns = COALESCE(@PivotColumns + ',','') + QUOTENAME(Preference.Name),
           @PivotColumnsSelectable = COALESCE(@PivotColumnsSelectable + ',' + Char(10),'') + Preference.Source + '.' + QUOTENAME(Preference.Name) + ' AS ' + QUOTENAME(Preference.Name)
    FROM (SELECT [Name],
                 'PreferencePivot' AS [Source]
          FROM [dbo].[tblPreference]) Preference

    DECLARE @sqlText nvarchar(max)
    SELECT @sqlText = 'SELECT ' + @PivotColumnsSelectable + '
    FROM (SELECT tblPreference.Name AS PreferenceName,
                CASE
                    WHEN tblCustomerPreference.Value IS NOT NULL THEN tblCustomerPreference.Value
                    ELSE tblPreference.DefaultValue
                END AS Value,
                @innerCustomerID AS CustomerID
            FROM tblCustomerPreference
                RIGHT JOIN tblPreference ON tblCustomerPreference.PreferenceID = tblPreference.ID
            WHERE (tblCustomerPreference.CustomerID = @innerCustomerID OR tblCustomerPreference.ID IS NULL)) data
            PIVOT (MAX(Value)
                   FOR PreferenceName IN (' + @PivotColumns + ')) PreferencePivot'

    EXECUTE sp_executesql @sqlText, N'@innerCustomerID int', @CustomerID
END

我遇到的问题是当我查询时对于 CustomerID 1 或 2,一切都会按预期返回,所有值均按预期填充。但如果我查询 CustomerID 3,它将为其他客户填充的任何 PreferenceID 返回 NULL。如果我在没有 PIVOT 表达式的情况下运行查询,它将返回按预期填充的所有首选项。只有当我对数据进行 PIVOT 时,NULL 才会出现。我希望我错过了一些简单的事情,但我没有看到错误。

I have 3 tables (tblPreference, tblCustomer, tblCustomerPreference) that look something like the following:

tblPreference:
ID       | Name            | DefaultValue
(int PK) | (nvarchar(100)) | (nvarchar(100))
-------------------------------
1        | Preference1     | 1
2        | Preference2     | Yes
3        | Preference3     | 1

tblCustomer:
CustomerID | ...
(int PK)
--------------------
1          | ...
2          | ...
3          | ...

tblCustomerPreference:
ID       | CustomerID | PreferenceID | Value
(int PK) | (int)      | (int)        | (nvarchar(100))
-------------------------------------------------------
1        | 1          | 1            | 0
2        | 1          | 2            | Yes
3        | 2          | 1            | 0
4        | 2          | 2            | No

I'm creating a pivot of this data so it's all in a single row using the following stored procedure so that it will always pull back all preferences and if it finds a Customer specific value it will return that otherwise it returns the default value:

CREATE PROCEDURE [dbo].[usp_GetCustomerPreferences] @CustomerID int AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from interfering with SELECT statements.
    SET NOCOUNT ON;

    DECLARE @PivotColumns nvarchar(max)
    DECLARE @PivotColumnsSelectable nvarchar(max)
    SELECT @PivotColumns = COALESCE(@PivotColumns + ',','') + QUOTENAME(Preference.Name),
           @PivotColumnsSelectable = COALESCE(@PivotColumnsSelectable + ',' + Char(10),'') + Preference.Source + '.' + QUOTENAME(Preference.Name) + ' AS ' + QUOTENAME(Preference.Name)
    FROM (SELECT [Name],
                 'PreferencePivot' AS [Source]
          FROM [dbo].[tblPreference]) Preference

    DECLARE @sqlText nvarchar(max)
    SELECT @sqlText = 'SELECT ' + @PivotColumnsSelectable + '
    FROM (SELECT tblPreference.Name AS PreferenceName,
                CASE
                    WHEN tblCustomerPreference.Value IS NOT NULL THEN tblCustomerPreference.Value
                    ELSE tblPreference.DefaultValue
                END AS Value,
                @innerCustomerID AS CustomerID
            FROM tblCustomerPreference
                RIGHT JOIN tblPreference ON tblCustomerPreference.PreferenceID = tblPreference.ID
            WHERE (tblCustomerPreference.CustomerID = @innerCustomerID OR tblCustomerPreference.ID IS NULL)) data
            PIVOT (MAX(Value)
                   FOR PreferenceName IN (' + @PivotColumns + ')) PreferencePivot'

    EXECUTE sp_executesql @sqlText, N'@innerCustomerID int', @CustomerID
END

The issue I'm running into is that when I query for CustomerID 1 or 2, everything comes back as expected with all values populated as expected. But if I query for CustomerID 3, it will return a NULL for any PreferenceID's that are populated for other customers. If I run the query without the PIVOT expression it returns all Preferences populated as expected. It's only when I PIVOT the data does the NULL creep in. I'm hoping I missed something simple, but I'm not seeing the error.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

独闯女儿国 2024-08-17 13:45:47

您在 CustomerID 的 1 和 2 中看到偏好 3 默认值的唯一原因是因为偏好 3 没有 tblCustomerPreference 记录,而不是因为 CustomerID=1 / 的组合不存在 tblCustomerPreference 记录偏好 3 和客户 ID=2 / 偏好 3。

在 RIGHT JOIN 条件中,您指定仅在首选项值上连接 tblCustomerPreference 和 tblPreference - 这只会实现 tblPreference 中没有 NO 匹配 ANY tblCustomerPreference 中的 customerID。如果您在 customerID = @innerCustomerID 上为该子句添加附加连接条件,您现在将执行您正在查找的操作:即给我所有首选项记录和任何匹配 tblCustomerPreference for CustomerID=@innerCustomerID。

不再获得 Customer 3 的结果。

通过简单地添加 CustomerID 1 和 Preference3 的 tblCustomerPreference 记录来尝试一下,您会注意到,您不仅会开始看到 Customer2 的 Prefernce3 值为 NULL,而且甚至 就像这就是您在 WHERE 子句中尝试执行的操作,但由于在查询处理期间 JOIN 在 WHERE 子句之前处理(遵循语句处理的正确逻辑顺序),因此您将获得严格基于首选项的中间结果集组合而不是客户和偏好组合。

所以,做一些小改变就可以了。基本上,只需向指定特定客户的 RIGHT JOIN 子句添加一个附加条件(即 @innerCustomerID)并删除整个 WHERE 子句即可。请注意,这还会产生副作用,即实际返回任何传递的 @CustomerID 的所有默认值,这些值甚至不作为客户存在 - 如果您想更改它以不为不存在的客户返回任何内容,只需添加一个检查在查询之前或包含一个 whereexists() 过滤器:

alter PROCEDURE [dbo].[usp_GetCustomerPreferences] @CustomerID int AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from interfering with SELECT statements.
    SET NOCOUNT ON;

    DECLARE @PivotColumns nvarchar(max)
    DECLARE @PivotColumnsSelectable nvarchar(max)
    SELECT @PivotColumns = COALESCE(@PivotColumns + ',','') + QUOTENAME(Preference.Name),
           @PivotColumnsSelectable = COALESCE(@PivotColumnsSelectable + ',' + Char(10),'') + Preference.Source + '.' + QUOTENAME(Preference.Name) + ' AS ' + QUOTENAME(Preference.Name)
    FROM (SELECT [Name],
                 'PreferencePivot' AS [Source]
          FROM [dbo].[tblPreference]) Preference

    DECLARE @sqlText nvarchar(max)
    SELECT @sqlText = 'SELECT ' + @PivotColumnsSelectable + '
    FROM (SELECT tblPreference.Name AS PreferenceName,
                CASE
                    WHEN tblCustomerPreference.Value IS NOT NULL THEN tblCustomerPreference.Value
                    ELSE tblPreference.DefaultValue
                END AS Value,
                @innerCustomerID AS CustomerID
            FROM tblCustomerPreference
                RIGHT JOIN tblPreference 
                ON tblCustomerPreference.PreferenceID = tblPreference.ID
                AND tblCustomerPreference.CustomerID = @innerCustomerID
            ) data
            PIVOT (MAX(Value)
                   FOR PreferenceName IN (' + @PivotColumns + ')) PreferencePivot'

 print @sqlText

    EXECUTE sp_executesql @sqlText, N'@innerCustomerID int', @CustomerID
END

The only reason you're even seeing preference3 default values in CustomerID's 1&2 is because there is NO tblCustomerPreference record for preference3, not because there isn't a tblCustomerPreference record for the combination of CustomerID=1 / Preference3 and CustomerID=2 / Preference3.

In your RIGHT JOIN condition, you are specifying to only join between tblCustomerPreference and tblPreference on the preference value only - this will only ever materialize a record from tblPreference that has NO matching record for ANY customerID in tblCustomerPreference. If you add an additional join condition on customerID = @innerCustomerID for that clause, you'll now be doing what you are looking for: i.e. give me ALL preference records and ANY matching tblCustomerPreference for CustomerID=@innerCustomerID.

Try it by simply adding in a tblCustomerPreference record for CustomerID 1 and Preference3, you'll notice that you'll start seeing not only NULL for the Prefernce3 value for Customer2, but you'll no longer even get a result for Customer 3.

Looks like this is what you were trying to do in your WHERE clause, but since JOINs are processed before the WHERE clause during query processing (following the proper logical ordering of statement processing), you're getting an intermediate resultset that is strictly based on preference combination as opposed to customer AND preference combination.

So, a couple of small changes and you should be good. Basically just add an additional condition to your RIGHT JOIN clause specifying a specific customer, i.e. @innerCustomerID and remove your entire WHERE clause and you're all set. Note that this will also have the side-effect of actually returning all default values for any @CustomerID passed that doesn't even exist as a customer - if you want to change that to return nothing for non-existent customers, simply add a check prior to the query or include a where exists() filter:

alter PROCEDURE [dbo].[usp_GetCustomerPreferences] @CustomerID int AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from interfering with SELECT statements.
    SET NOCOUNT ON;

    DECLARE @PivotColumns nvarchar(max)
    DECLARE @PivotColumnsSelectable nvarchar(max)
    SELECT @PivotColumns = COALESCE(@PivotColumns + ',','') + QUOTENAME(Preference.Name),
           @PivotColumnsSelectable = COALESCE(@PivotColumnsSelectable + ',' + Char(10),'') + Preference.Source + '.' + QUOTENAME(Preference.Name) + ' AS ' + QUOTENAME(Preference.Name)
    FROM (SELECT [Name],
                 'PreferencePivot' AS [Source]
          FROM [dbo].[tblPreference]) Preference

    DECLARE @sqlText nvarchar(max)
    SELECT @sqlText = 'SELECT ' + @PivotColumnsSelectable + '
    FROM (SELECT tblPreference.Name AS PreferenceName,
                CASE
                    WHEN tblCustomerPreference.Value IS NOT NULL THEN tblCustomerPreference.Value
                    ELSE tblPreference.DefaultValue
                END AS Value,
                @innerCustomerID AS CustomerID
            FROM tblCustomerPreference
                RIGHT JOIN tblPreference 
                ON tblCustomerPreference.PreferenceID = tblPreference.ID
                AND tblCustomerPreference.CustomerID = @innerCustomerID
            ) data
            PIVOT (MAX(Value)
                   FOR PreferenceName IN (' + @PivotColumns + ')) PreferencePivot'

 print @sqlText

    EXECUTE sp_executesql @sqlText, N'@innerCustomerID int', @CustomerID
END
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文