当数据中不存在时,SQL Server 动态数据透视返回空值
我有 3 个表(tblPreference
、tblCustomer
、tblCustomerPreference
),如下所示:
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
您在 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() 过滤器:
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: