【问题标题】:How to pivot sql data, and squash results into non-null rows by Date per ID如何按每个 ID 的日期将 sql 数据和压缩结果转换为非空行
【发布时间】:2016-12-15 21:13:23
【问题描述】:

帖子的标题不太好,但希望它会引起一些注意。

我在 T-SQL 中有一个非常复杂的情况,我无法完成。我希望有专业知识的人知道一个优雅而快速的解决方案,这样我的表现就不会受到影响。我正在处理数十亿行


前言

我有一个名为 Customers 的表,它有一个唯一的 ID。这些客户有文件,文件有属性,每个属性名称对应一个

表格:

  • 客户
  • 文件 -
  • 属性 - 包含名称和值

客户 ID 存在于所有这些表中,审计字段(例如 UpdatedDtmCreationDtm)也存在。


用例

我需要将所有客户加入他们的文件(过滤一些),然后将每个文件绑定到他们的属性(再次过滤这些)。这很简单,但会产生很多行,每个 customer x file x property 一个。

我知道属性名称永远不会改变,我只想返回一些选择,所以我使用了一个数据透视表并生成了一个漂亮的表,但在我开始执行更复杂的查询后它就崩溃了。


问题 首先,属性有一个 DateTime 用于更改它们的时间(UpdatedDtm),我需要在文件中返回从创建日期的 1 小时(CreationDtm)开始更改的所有内容表。

这导致我减少了我的潜在属性列表,但现在我有一个表,每个 ID 都有一个 RowNumber() 并且没有好的方法来旋转并选择第一个不是null 并且仍然保留表定义的列数。这很重要,因为我使用 Dynamic SQL 并将其放置在索引临时表中,并在 CustomerIDFileName 上使用 Composite Key


转点之前

| UpdatedDtm | CustomerID | FileName   | Property | Value          |
| ---------- | ---------- | ---------- | -------- | -------------- |
| 1/1/2015   | 1          | FileOne    | Size     | NULL           |
| 1/1/2015   | 1          | FileOne    | Format   | JPG            |
| 1/7/2015   | 1          | FileOne    | Size     | 88KB           |
| 1/7/2015   | 1          | FileOne    | Format   | JPG            |
| 1/7/2015   | 1          | FileOne    | Comment  | NULL           |
| 1/11/2015  | 1          | FileOne    | Comment  | NULL           |
| 1/1/2015   | 1          | FileTwo    | Size     | 91KB           |
| 1/1/2015   | 1          | FileTwo    | Format   | PNG            |
| 1/11/2015  | 1          | FileTwo    | Comment  | NULL           |
| 1/2/2015   | 2          | FileThree  | Size     | 74KB           |
| 1/2/2015   | 2          | FileThree  | Format   | XLS            |
| 1/2/2015   | 2          | FileThree  | State    | Open           |
| 1/7/2015   | 2          | FileThree  | State    | Closed         |
| 1/10/2015  | 2          | FileThree  | Comment  | NULL           |
| 1/1/2015   | 3          | FileFour   | Size     | 2KB            |
| 1/2/2015   | 3          | FileFour   | Size     | 10KB           |
| 1/3/2015   | 3          | FileFour   | Size     | 13KB           |
| 1/4/2015   | 3          | FileFour   | Size     | 21KB           |
| 1/5/2015   | 3          | FileFour   | Size     | 27KB           |
| 1/6/2015   | 3          | FileFour   | Size     | 32KB           |
| 1/7/2015   | 3          | FileFour   | Size     | 39KB           |
| 1/8/2015   | 3          | FileFour   | Size     | 44KB           |
| 1/1/2015   | 3          | FileFour   | Format   | TXT            |
| 1/1/2015   | 3          | FileFour   | Comment  | NULL           |

请不要问我为什么要以这种方式设置数据库或更改架构。这是一成不变的,我无法控制。我需要能够解决所描述的用例。


枢轴后(预期)

| CustomerID | FileName   | Size | Format | State  | Comment |
| ---------- | ---------- | ---- | ------ | ------ | ------- |
| 1          | FileOne    | 88KB | JPG    | NULL   | NULL    |
| 1          | FileTwo    | 91KB | PNG    | NULL   | NULL    |
| 2          | FileThree  | 74KB | XLS    | Closed | NULL    |
| 3          | FileFour   | 44KB | TXT    | NULL   | NULL    |

我包含了一些 NULL 值和缺失值,以展示我需要保留相同的列属性,而不管它们是否有数据,但我还需要将数据压缩为我的日期范围内的第一个非空值。


代码(我的尝试)

IF Object_id('tempdb..#FilesQuery') IS NOT NULL DROP TABLE #FilesQuery;
CREATE TABLE #FilesQuery (
    SeqNum          int,
    CustomerID      numeric(16,0),
    FileName        varchar(64),
    PropertyName    varchar(64),
    PropertyValue   varchar(64)
)
INSERT INTO #FilesQuery
SELECT
     CASE WHEN P.[Value] IS NOT NULL
          THEN ROW_NUMBER() OVER (partition by C.CustomerID order by UpdatedDtm)
          ELSE 0
     END as SeqNum,
     C.CustomerID
    ,F.Name  as FileName
    ,P.Name  as PropertyName
    ,P.Value as PropertyValue

FROM Customers C
INNER JOIN Files F ON F.CustomerID = C.CustomerID
LEFT JOIN Properties P
    ON P.CustomerID = C.CustomerID
    AND P.FileID = F.FileID

WHERE F.FileName IN ('FileOne','FileTwo','FileThree','FileFour')
    AND P.Name IN ('Size','Format','State','Comment')

--PIVOT
DECLARE @cols AS nvarchar(MAX)
SELECT @cols = STUFF(
    (SELECT DISTINCT ',' + QUOTENAME(PropertyName)
       FROM #FilesQuery fq
        FOR XML PATH(''),TYPE).value('.','NVARCHAR(MAX)'),1,1,'')

DECLARE @dynSql AS nvarchar(MAX)
SET @dynSql = '
    SELECT DISTINCT *
    FROM (
        SELECT
            fq.CustomerID,
            fq.FileName,
            fq.PropertyName,
            fq.PropertyValue
        FROM #FilesQuery fq
    ) SRC
    PIVOT (
        Max([PropertyValue])
        FOR PropertyName IN (' + @cols + ')
    ) PVT
'

IF Object_id('tempdb..#Results') IS NOT NULL DROP TABLE #Results;
CREATE TABLE #Results (
    CustomerID      varchar(16) NOT NULL,
    FileName        varchar(64) NOT NULL,
    FileSize        varchar(64) NULL,
    FileFormat      varchar(64) NULL,
    FileState       varchar(64) NULL,
    FileComment     varchar(64) NULL,
    CONSTRAINT pk_CustDoc PRIMARY KEY (CustomerID,FileName)
)
INSERT INTO #Results EXEC @dynSql;

很抱歉这段代码不完整,这是我的工作部分。我进行的其他尝试导致数据拉取错误。

我尝试使用 SeqNum 和 case 语句的组合来尝试为每一行选择第一个非 null 值,以便数据都在一行上,但它最终更像。

FileOne NULL NULL Open NULL
FileOne NULL JPG  NULL NULL

等等……

我一直在努力解决这个特殊情况,并且即将报废,它通过循环执行一些程序性操作,但这会浪费我的查询时间和性能。

谁有好的解决方案?我是不是想多了?

【问题讨论】:

  • 嗯,在执行PIVOT 之前,您是否尝试通过仅选择每个文件/属性组合的最新(最高 UpdatedDtm)来将数据集减少到仅相关行?这样PIVOT 应该已经能够直接为您提供您正在寻找的输出,不是吗?抱歉,我暂时无法测试 TSQL...
  • 是的,我会将数据分块到 bin 中,例如 1 小时前、2 小时等,但用例的中心是选择每列的第一个非空值并仍然保留所有列,与允许空值的交叉连接一样。

标签: sql sql-server tsql pivot notnull


【解决方案1】:

您应该在PIVOT 之前过滤您的数据,您将获得您想要的结果。这是一个cte版本,向您展示如何获得所需的步骤。

;WITH cteDefineRowPrecedence AS (
    SELECT *
       ,ROW_NUMBER() OVER (PARTITION BY CustomerId, FileName, Property ORDER BY
          CASE WHEN Value IS NOT NULL THEN 0 ELSE 1 END
          ,UpdatedDtm DESC) as RowNum
    FROM
       @Table
)

, cteDesiredRwows AS (
    SELECT
       CustomerId
       ,FileName
       ,Property
       ,Value
    FROM
       cteDefineRowPrecedence t
    WHERE
       t.RowNum = 1
       AND t.Value IS NOT NULL
)

SELECT *
FROM
    cteDesiredRwows t
    PIVOT (
       MAX(Value)
       FOR Property IN (Size,[Format],[State],Comment)
    ) p
ORDER BY
    CustomerId
    ,FileName

这是一个嵌套查询版本,可以更轻松地嵌入/放入您的动态 sql....

SELECT *
FROM
    (
       SELECT CustomerId, FileName, Property, Value
       FROM
          (SELECT *
             ,ROW_NUMBER() OVER (PARTITION BY CustomerId, FileName, Property ORDER BY
                CASE WHEN Value IS NOT NULL THEN 0 ELSE 1 END
                ,UpdatedDtm DESC) as RowNum
          FROM
             @Table) r
       WHERE
          r.RowNum = 1
          AND r.Value IS NOT NULL
    ) t
    PIVOT (
       MAX(Value)
       FOR Property IN (Size,[Format],[State],Comment)
    ) p
ORDER BY
    CustomerId
    ,FileName

【讨论】:

  • 我是 CTE 的新手,但我尝试了您的查询,但它一直告诉我“消息 207,第 16 级,状态 1,第 2 行无效的列名称‘属性’。”有什么想法吗?
  • 您的文档将属性显示为列名,还是我弄错了?该错误意味着它无法在您的表中找到该名称的列。如果您尝试将我写的内容与您的查询合并,也许您在某个地方拼错了?
  • 我仔细检查并再次检查,也许我需要关闭我的会话并开始一个新的查询。我的缓存可能保留了错误的表定义。编辑:刚试过,同样的交易。我从中提取的临时表在 Select 中有正确命名的列,所以我不知道为什么 dynSql 不能使用别名。
  • ctrl + shift + r 将更新您的自动完成/智能感知
  • 嵌套内部选择'r'有效。子查询't'有效。所以我对自己说,剩下的唯一一件事就是 Order By,由于某种原因,我不小心将 Property 而不是 FileName 作为我的 order by。问题解决了。谢谢!
【解决方案2】:

您可能需要在 CTE 定义中添加 WHERE 条件,以将日期/时间范围限制在您想要的范围内。

   WITH CTE AS (
    SELECT DISTINCT
        CustomerID
        , FileName
        , Property
        , Value
    FROM
        <table_name>
    )
    SELECT *
    FROM
        CTE
        PIVOT (MAX(value) FOR Property IN( 'Size', 'Format', 'State', 'Comment')) p

【讨论】:

  • 我无法在 CTE 中获取 where 语句来获取一系列日期并获取其中最年轻或最旧的第一个非空值。这是一个快速而肮脏的解决方案,它依靠 Max() 来获得结果,我想摆脱它。
  • Max() 运算符用于枢轴,而不是过滤结果。 Pivot 总是需要一个聚合函数,在这种情况下,Max() 是显而易见的选择。关键是您需要确保从 CTE 返回的 的组合是唯一的。
猜你喜欢
  • 1970-01-01
  • 2020-08-13
  • 2013-04-27
  • 2019-12-11
  • 1970-01-01
  • 2018-02-25
  • 1970-01-01
  • 1970-01-01
  • 2019-12-26
相关资源
最近更新 更多