在SQL Server中的XML列中查找最大日期

时间:2018-02-16 14:57:20

标签: sql sql-server xml tsql xquery

我在表格中有一个名为RecentlyViewedXml的XML列,结构如下:

<RecentlyViewedEntityData etc="2">
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business 1</Title>        
        <LastAccessed>1/1/2010</LastAccessed>
    </RecentlyViewedItem>
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business</Title>      
        <LastAccessed>1/5/2010</LastAccessed>
    </RecentlyViewedItem>
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business</Title>      
        <LastAccessed>1/3/2010</LastAccessed>
    </RecentlyViewedItem>
</RecentlyViewedEntityData>

我正在尝试从LastAccessed元素获取最大日期(理想情况下,与该节点中的数据对应的其余节点项。

我尝试了几个选项,但我的主要问题是我不知道[Last]节点是否始终具有最大日期。我正在使用它,但它没有通过QA

Cast(RecentlyViewedXml as xml).query('data(/RecentlyViewedEntityData/RecentlyViewedItem[last()]/LastAccessed[last()])')

我愿意接受任何想法。

谢谢!

4 个答案:

答案 0 :(得分:2)

首先:使用与文化相关的日期格式非常危险。试试这个:

SET LANGUAGE GERMAN;
SELECT CAST('1/3/2010' AS DATE);

SET LANGUAGE ENGLISH;
SELECT CAST('1/3/2010' AS DATE);

您可以将CONVERT()第三个​​参数(在您的情况下为103)一起使用以避免这种情况,但任何隐式演员都会使用您的系统&#39; s设置。

另一点是XML中的数据。您的'1/1/2010'日期未正确序列化。这应该是ISO8601

一些背景知识:您肯定知道,许多数据类型不会以存储方式显示。像3这样的数字实际上是二进制模式。每当你想要一个人阅读它时,它必须翻译到一个字符串表示。每当数据必须嵌入到基于字符串的容器中时,它们必须被序列化。只要在序列化和反序列化中应用相同的规则,这就可以正常工作。但阅读方必须依赖适当的价值观。

试试这个:

DECLARE @Xml XML='N<root>
                    <data>
                      <SomeInt>1</SomeInt>
                      <SomeDate>2017-01-01</SomeDate>
                      <BadDate>1/3/2010</BadDate>
                    </data>
                    <data>
                      <SomeInt>5</SomeInt>
                      <SomeDate>2017-01-05</SomeDate>
                      <BadDate>4/3/2010</BadDate>
                    </data>
                    <data>
                      <SomeInt>3</SomeInt>
                      <SomeDate>2017-01-03</SomeDate>
                      <BadDate>5/1/2010</BadDate>
                    </data>
                   </root>';

简单的XQuery函数max()将返回最高int

SELECT @xml.value(N'max(//SomeInt)','int') MaxInt;

但这对于(正确的ISO8601)日期不起作用(即使the Remarks-section of the function's docu听起来不同):

SELECT @xml.value(N'max(//SomeDate)','date') MaxDate; --returns NULL

您可以使用嵌入式FLWOR query逐个转换所有值:

SELECT @xml.value(N'max(for $d in //SomeDate return $d cast as xs:date?)','date') MaxDate;

但这对您的非ISO8601日期不起作用:

SELECT @xml.value(N'max(for $d in //BadDate return $d cast as xs:date?)','date') MaxDate;

回到你的问题

您可以获取一个派生表来读取您未显示的值(类似于导入数据的临时表)并使用T-SQL的能力来处理:

DECLARE @mockup TABLE  (Id INT IDENTITY , YourXml XML)

INSERT INTO @mockup VALUES 
(N'<RecentlyViewedEntityData etc="2">
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business 1</Title>        
        <LastAccessed>1/1/2010</LastAccessed>
    </RecentlyViewedItem>
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business</Title>      
        <LastAccessed>1/5/2010</LastAccessed>
    </RecentlyViewedItem>
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business</Title>      
        <LastAccessed>1/3/2010</LastAccessed>
    </RecentlyViewedItem>
</RecentlyViewedEntityData>');

- 将派生表检索为CTE:

WITH DerivedTable AS
(
    SELECT itm.value(N'(Type/text())[1]',N'nvarchar(max)') AS [Type]
          ,itm.value(N'(DisplayName/text())[1]',N'nvarchar(max)') AS [DisplayName]
          ,itm.value(N'(Title/text())[1]',N'nvarchar(max)') AS [Title]
          ,CONVERT(DATE,itm.value(N'(LastAccessed/text())[1]',N'nvarchar(max)'),103) AS [LastAccessed]
    FROM @mockup AS m
    OUTER APPLY m.YourXml.nodes(N'/RecentlyViewedEntityData/RecentlyViewedItem') AS A(itm)
)
SELECT TOP 1 * 
FROM DerivedTable
ORDER BY LastAccessed DESC;

在正确转换(转换)为原始DATE类型后,您可以使用与ORDER BY相关联的TOP 1来获取最大值。

更新:具有多行

的表的解决方案

您的评论是正确的,但这比自助加入更容易:

DECLARE @mockup TABLE  (Id INT IDENTITY , YourXml XML)

INSERT INTO @mockup VALUES 
(N'<RecentlyViewedEntityData etc="2">
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business 1</Title>        
        <LastAccessed>1/1/2010</LastAccessed>
    </RecentlyViewedItem>
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business</Title>      
        <LastAccessed>1/5/2010</LastAccessed>
    </RecentlyViewedItem>
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business</Title>      
        <LastAccessed>1/3/2010</LastAccessed>
    </RecentlyViewedItem>
</RecentlyViewedEntityData>')
,(N'<RecentlyViewedEntityData etc="2">
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business 1</Title>        
        <LastAccessed>1/1/2010</LastAccessed>
    </RecentlyViewedItem>
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business</Title>      
        <LastAccessed>1/5/2010</LastAccessed>
    </RecentlyViewedItem>
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business</Title>      
        <LastAccessed>1/3/2010</LastAccessed>
    </RecentlyViewedItem>
</RecentlyViewedEntityData>');

- 将派生表检索为CTE,包括行的id:

WITH DerivedTable AS
(
    SELECT Id
          ,itm.value(N'(Type/text())[1]',N'nvarchar(max)') AS [Type]
          ,itm.value(N'(DisplayName/text())[1]',N'nvarchar(max)') AS [DisplayName]
          ,itm.value(N'(Title/text())[1]',N'nvarchar(max)') AS [Title]
          ,CONVERT(DATE,itm.value(N'(LastAccessed/text())[1]',N'nvarchar(max)'),103) AS [LastAccessed]
    FROM @mockup AS m
    OUTER APPLY m.YourXml.nodes(N'/RecentlyViewedEntityData/RecentlyViewedItem') AS A(itm)
)
SELECT TOP 1 WITH TIES * 
FROM DerivedTable
ORDER BY ROW_NUMBER() OVER(PARTITION BY ID ORDER BY LastAccessed DESC);

ORDER BY会使用ROW_NUMBER()子句调用OVER()。这将在日期中添加排名,按行的ID进行分区。 TOP 1 WITH TIES将返回1ROW_NUMBER的所有行。这将是每个表格行的最顶层结果。

答案 1 :(得分:0)

这是我的第一个想法:

declare @xdoc xml = '<RecentlyViewedEntityData etc="2">
<RecentlyViewedItem>
    <Type></Type>
    <DisplayName>Contact</DisplayName>
    <Title>My Book of Business 1</Title>        
    <LastAccessed>1/1/2010</LastAccessed>
</RecentlyViewedItem>
<RecentlyViewedItem>
    <Type></Type>
    <DisplayName>Contact</DisplayName>
    <Title>My Book of Business</Title>      
    <LastAccessed>1/5/2010</LastAccessed>
</RecentlyViewedItem>
<RecentlyViewedItem>
    <Type></Type>
    <DisplayName>Contact</DisplayName>
    <Title>My Book of Business</Title>      
    <LastAccessed>1/3/2010</LastAccessed>
</RecentlyViewedItem>
</RecentlyViewedEntityData>';

DECLARE @xhandle INT

EXEC sp_xml_preparedocument @xhandle OUTPUT, @xdoc

;WITH tmp 
AS
(
SELECT  *
FROM    OPENXML(@xhandle, '//RecentlyViewedItem', 2)  
    WITH (
    DisplayName NVARCHAR(50)  'DisplayName',
    Title NVARCHAR(50) 'Title',
    LastAccessed DATE 'LastAccessed'
    )  
)
SELECT * FROM tmp WHERE tmp.LastAccessed = (SELECT MAX(LastAccessed) FROM tmp)

EXEC sp_xml_removedocument @xhandle

答案 2 :(得分:0)

也许这个?

DECLARE @XML xml = '
<RecentlyViewedEntityData etc="2">
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business 1</Title>        
        <LastAccessed>1/1/2010</LastAccessed>
    </RecentlyViewedItem>
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business</Title>      
        <LastAccessed>1/5/2010</LastAccessed>
    </RecentlyViewedItem>
    <RecentlyViewedItem>
        <Type></Type>
        <DisplayName>Contact</DisplayName>
        <Title>My Book of Business</Title>      
        <LastAccessed>1/3/2010</LastAccessed>
    </RecentlyViewedItem>
</RecentlyViewedEntityData>';

WITH XMLData AS (
    SELECT  X.RR.value('(Type/text())[1]','int') AS [Type], --Guessed the datatype here
            X.RR.value('(DisplayName/text())[1]','varchar(50)') AS DisplayName,
            X.RR.value('(Title/text())[1]','varchar(50)') AS Title,
            X.RR.value('(LastAccessed/text())[1]','date') AS LastAccessed
    FROM @xml.nodes('RecentlyViewedEntityData/RecentlyViewedItem') X(RR)),
RNs AS(
    SELECT *,
           ROW_NUMBER() OVER (ORDER BY LastAccessed DESC) AS RN
    FROM XMLData)
SELECT [Type],
       DisplayName,
       Title,
       LastAccessed
FROM RNs
WHERE RN = 1;

答案 3 :(得分:0)

使用Xquery可以轻松解析

    DECLARE @TB TABLE  (Id int , XCol XML)

    INSERT INTO @TB 
     VALUES (1, '<RecentlyViewedEntityData etc="2">
        <RecentlyViewedItem>
            <Type></Type>
            <DisplayName>Contact</DisplayName>
            <Title>My Book of Business 1</Title>        
            <LastAccessed>1/1/2010</LastAccessed>
        </RecentlyViewedItem>
        <RecentlyViewedItem>
            <Type></Type>
            <DisplayName>Contact</DisplayName>
            <Title>My Book of Business</Title>      
            <LastAccessed>1/5/2010</LastAccessed>
        </RecentlyViewedItem>
        <RecentlyViewedItem>
            <Type></Type>
            <DisplayName>Contact</DisplayName>
            <Title>My Book of Business</Title>      
            <LastAccessed>1/3/2010</LastAccessed>
        </RecentlyViewedItem>
    </RecentlyViewedEntityData>')


    SELECT MAX(LastAccessed) LastAccessed , DisplayName , Title
    FROM
    ( 
    SELECT TRY_CONVERT(DATE,x.c.value('./LastAccessed[1]', 'varchar(100)')) LastAccessed,
           x.c.value('./DisplayName[1]', 'varchar(100)') DisplayName,
           x.c.value('./Title[1]', 'varchar(100)') Title
    FROM @tb 
        CROSS APPLY Xcol.nodes ('/RecentlyViewedEntityData/RecentlyViewedItem') x(c)
    ) P
    GROUP BY DisplayName , Title

注意 如果您的结构如上所述,则此查询将生成您需要的结果。

NOTE2 我使用了自SQL 2012以来可用的TRY_CONVERT,如果您的版本较旧,例如2008年你需要使用常规的转换或转换,但是你需要使用case语句来处理将日期的字符串值转换为日期数据类型的失败。