字符串解析VARCHAR列

时间:2017-09-28 19:06:02

标签: sql-server string parsing sqlclr

我正在使用一个数据库,其中用户将电话呼叫的详细信息保存在单个VARCHAR(MAX)列中。但是,为了详细报告(SSRS),VARCHAR列中嵌入的所有各种属性都需要转换为单独的列。为了完成这个解析,我在存储过程中使用了一个游标,它调用一个UDF来处理所需子字符串的提取。

当查询相对较少的行(不超过几百行)时,查询的性能是不错的。但是,在查询大量行(数千行)时,性能很差,查询通常会超时。所以,我正在寻找一种比我现有的方法更好的方法。

VARCHAR列包含完整的HTML标记。我删除了不相关的东西来说明我感兴趣的数据是如何存储的:

<table>
<tr>
<td class="key">Dial Result:</td><td id="DialResult" class="value">No Answer</td></tr>
<tr><td class="key">Client Name:</td><td id="ClientName" class="value">SMITH, BOB</td></tr>
<tr><td class="key">Number Dialed:</td><td id="NumberDialed" class="value">5555555555 [Day]</td></tr>
<tr><td class="key">Dial Count:</td><td id="DialCount" class="value">1</td></tr><tr><td class="key">Contact Made:</td><td id="ContactMade" class="value">No</td></tr>
<tr><td class="key">Campaign Called On:</td><td id="CampaignCalledOn" class="value">TEST CAMPAIGN</td></tr>
<tr><td class="key">Call Outcome:</td><td id="CallOutcome" class="value">No answer</td></tr>
<tr><td class="key">Email Sent:</td><td id="EmailSent" class="value">No</td></tr>
<tr><td class="key">Do Not Call Requested:</td><td id="DoNotCallRequested" class="value">No</td></tr>
<tr><td class="key">Product Purchased:</td><td id="ProductPurchased" class="value">No</td></tr>
<tr><td class="key">Order Number:</td><td id="OrderNumber" class="value">N/A</td></tr><tr><td class="key">Order Dollar Value:</td><td id="PurchaseAmount" class="value">0.00</td></tr>
<tr><td class="key">Purchased SKUs:</td><td id="PurchasedSKUs" class="value">N/A</td></tr>
</table>

目前,我有一个存储过程,它使用游标迭代“实时”数据库表中的行。过程游标如下所示:

OPEN MainNoteCursor;
FETCH NEXT FROM MainNoteCursor INTO @SequenceNumber,@ClientId,@ContactNumber,@UserDisplayName,@CreatorId,@DateCol,@NoteText

WHILE (@@FETCH_STATUS <> -1)
    BEGIN
        IF (@@FETCH_STATUS <> -2)
            BEGIN TRY
                DECLARE @NoteDate date = null
                DECLARE @DialResult varchar(255) = null
                DECLARE @ClientName varchar(255) = null
                DECLARE @NumberDialed varchar(255) = null
                DECLARE @DialCount int = null
                DECLARE @ContactMade varchar(3)  = null
                DECLARE @CampaignCalledOn varchar(255)  = null
                DECLARE @CallOutcome varchar(255) = null
                DECLARE @EmailSent varchar(3) = null
                DECLARE @DoNotCallRequested varchar(3) = null
                DECLARE @ProductPurchased varchar(3) = null
                DECLARE @OrderNumber varchar(255) = null
                DECLARE @PurchaseAmount money = null
                DECLARE @PurchasedSKUs varchar(255) = null

                SET @NoteDate = CONVERT(date, @DateCol)

                SET @DialResult = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="DialResult" class="value">'',''</td>'',1)
                SET @ClientName = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="ClientName" class="value">'',''</td>'',1)
                SET @NumberDialed = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="NumberDialed" class="value">'',''</td>'',1)
                SET @DialCount = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="DialCount" class="value">'',''</td>'',1)
                SET @ContactMade = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="ContactMade" class="value">'',''</td>'',1)
                SET @CampaignCalledOn = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="CampaignCalledOn" class="value">'',''</td>'',1)
                SET @CallOutcome = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="CallOutcome" class="value">'',''</td>'',1)
                SET @EmailSent = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="EmailSent" class="value">'',''</td>'',1)
                SET @DoNotCallRequested = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="DoNotCallRequested" class="value">'',''</td>'',1)
                SET @ProductPurchased = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="ProductPurchased" class="value">'',''</td>'',1)
                SET @OrderNumber = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="OrderNumber" class="value">'',''</td>'',1)
                SET @PurchaseAmount = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="PurchaseAmount" class="value">'',''</td>'',1)
                SET @PurchasedSKUs = [dbo].[FN_PARSE_TEXT] (@NoteText,''<TD id="PurchasedSKUs" class="value">'',''</td>'',1) 

                INSERT INTO @Return
                    ([SequenceNumber],[ClientId],[ContactNumber],[UserDisplayName],[CreatorId],
                    [DateCol],[NoteDate],[NoteText],[DialResult],[ClientName],[NumberDialed],[DialCount],
                    [ContactMade],[CampaignCalledOn],[CallOutcome],[EmailSent],[DoNotCallRequested],
                    [ProductPurchased],[OrderNumber],[PurchaseAmount],[PurchasedSKUs])
                VALUES
                    (@SequenceNumber,@ClientId,@ContactNumber,@UserDisplayName,@CreatorId,
                    @DateCol,@NoteDate,@NoteText,@DialResult,@ClientName,@NumberDialed,@DialCount,
                    @ContactMade,@CampaignCalledOn,@CallOutcome,@EmailSent,@DoNotCallRequested,
                    @ProductPurchased,@OrderNumber,@PurchaseAmount,@PurchasedSKUs)  
            END TRY             

            BEGIN CATCH
                --Nothing to do.
            END CATCH       
        FETCH NEXT FROM MainNoteCursor INTO @SequenceNumber,@ClientId,@ContactNumber,@UserDisplayName,@CreatorId,@DateCol,@NoteText
    END

CLOSE MainNoteCursor;
DEALLOCATE MainNoteCursor;

SELECT * FROM @Return

因此,您可以看到光标每次都在解析整个音符主体,在两个字符串分隔符之间提取值,将值插入表中,然后返回表。

在SSMS中执行时,我得到的结果如下:

enter image description here

FN_PARSE_TEXT中的代码使用CHARINDEX(),LEN(),SUBSTRING()等来读取“开始字符串”和“结束字符串”之间的文本。我不会发布完整的功能,因为有很多不相关的内务代码,但可以将其提炼为:

--********************************************************************************
-- CHARINDEX returns the position of the fist character in @StartKey, but we need to
-- start reading after the *last* character in @StartKey.  Re-adjust the position
-- at which we''ll start reading the string.
--********************************************************************************

SET @StartKeyIndex = @StartKeyIndex + @StartKeyLength
SET @ReadLength = @EndKeyIndex - @StartKeyIndex

--********************************************************************************
-- Start / End positions have been determined, so now read out the calculated number
-- of characters and return to the calling code.
--********************************************************************************

SET @ReturnValue = LTRIM(RTRIM(SUBSTRING(@noteText, @StartKeyIndex, @ReadLength)))

因此,在所有背景信息(我可以提供更多细节,如果需要)之后,我正在寻找更好的方法。

我想过创建一个隔夜批处理过程,它在非工作时间执行所有解析,然后将数据转储到展平的表中。然后我会报告扁平数据,而不是试图实时解析细节。当然,这会打开它自己的蠕虫病毒,我最好还是继续点击实时数据。根据用户在运行查询时提供的日期范围,可能会返回超过10,000行。

我想到的另一种选择是使用CLR方法来处理循环和解析 - 但我没有很多经验,我不确定它会比我更有效率我现在在做。

我读过文章(例如https://sqlperformance.com/2012/07/t-sql-queries/split-strings),谈论像CTE这样的事情,但我并没有太多的经验,所以我不确定如何实现这一目标。我读过的大多数“字符串解析”文章都适用于分隔符相同的场景 - 比如解析以逗号分隔的字符串。但由于我有“滚动”分隔符,我不知道如何处理它。

无论如何,如果你还在我身边,我们将非常感激任何意见。

3 个答案:

答案 0 :(得分:2)

只是提供一个有关如何使用XML实现此目标的实际示例,以下是我的工作方式。我只有一个示例文档可以离开这里,但是如果你有多个,那么同样适用。您只需为每个主键返回多行。

;with src (Id, NoteText) as
(
    select 1, cast('<table>
<tr>
<td class="key">Dial Result:</td><td id="DialResult" class="value">No Answer</td></tr>
<tr><td class="key">Client Name:</td><td id="ClientName" class="value">SMITH, BOB</td></tr>
<tr><td class="key">Number Dialed:</td><td id="NumberDialed" class="value">5555555555 [Day]</td></tr>
<tr><td class="key">Dial Count:</td><td id="DialCount" class="value">1</td></tr><tr><td class="key">Contact Made:</td><td id="ContactMade" class="value">No</td></tr>
<tr><td class="key">Campaign Called On:</td><td id="CampaignCalledOn" class="value">TEST CAMPAIGN</td></tr>
<tr><td class="key">Call Outcome:</td><td id="CallOutcome" class="value">No answer</td></tr>
<tr><td class="key">Email Sent:</td><td id="EmailSent" class="value">No</td></tr>
<tr><td class="key">Do Not Call Requested:</td><td id="DoNotCallRequested" class="value">No</td></tr>
<tr><td class="key">Product Purchased:</td><td id="ProductPurchased" class="value">No</td></tr>
<tr><td class="key">Order Number:</td><td id="OrderNumber" class="value">N/A</td></tr><tr><td class="key">Order Dollar Value:</td><td id="PurchaseAmount" class="value">0.00</td></tr>
<tr><td class="key">Purchased SKUs:</td><td id="PurchasedSKUs" class="value">N/A</td></tr>
</table>' as xml)
)

select 
    a.*,
    t.c.value('td[1]', 'varchar(max)')
from src a
cross apply a.NoteText.nodes('table/tr') as t(c)

SQL中的XML碎化文档可能有点受欢迎,主要是因为SQL正在实现独立于SQL和Microsoft的xquery。也就是说,为了做基本的东西,通常MSDN文档可以让你很好地开始。

以下是一些好文章

答案 1 :(得分:1)

如果您可以对数据库进行修改,我会尽我所能避免在查询时进行此解析。特别是如果这是一个查询,如果用户经常为CRM应用程序执行此操作。

我要将所有这些经常选择的列添加到我的表中,或者我会为它们创建一个可以链接到该表的主键的单独表。

然后,理想情况下,我会在INSERT时填充这些列。最好让INSERT通过存储过程,但如果需要,可以在表格上插入一个Insert Trigger。

如果我无法在INSERT时间执行此操作,那么我将运行一个执行解析的作业,并根据实际需要/允许来填充列。

对于大多数CRM应用程序,SELECT性能比INSERT性能更重要,因为它会影响多行,而INSERTS通常一次发生一次。因此,如果可能的话,我会尝试将性能负担转移到那里。

免责声明:我从未使用过Xedni在评论中引用的XML函数。所以我不能说我的建议和他的建议之间的性能差异。

答案 2 :(得分:0)

您可以使用XML转换您的值,并在转换结果后

DECLARE @xml xml = '<table>
<tr>
<td class="key">Dial Result:</td><td id="DialResult" class="value">No Answer</td></tr>
<tr><td class="key">Client Name:</td><td id="ClientName" class="value">SMITH, BOB</td></tr>
<tr><td class="key">Number Dialed:</td><td id="NumberDialed" class="value">5555555555 [Day]</td></tr>
<tr><td class="key">Dial Count:</td><td id="DialCount" class="value">1</td></tr><tr><td class="key">Contact Made:</td><td id="ContactMade" class="value">No</td></tr>
<tr><td class="key">Campaign Called On:</td><td id="CampaignCalledOn" class="value">TEST CAMPAIGN</td></tr>
<tr><td class="key">Call Outcome:</td><td id="CallOutcome" class="value">No answer</td></tr>
<tr><td class="key">Email Sent:</td><td id="EmailSent" class="value">No</td></tr>
<tr><td class="key">Do Not Call Requested:</td><td id="DoNotCallRequested" class="value">No</td></tr>
<tr><td class="key">Product Purchased:</td><td id="ProductPurchased" class="value">No</td></tr>
<tr><td class="key">Order Number:</td><td id="OrderNumber" class="value">N/A</td></tr><tr><td class="key">Order Dollar Value:</td><td id="PurchaseAmount" class="value">0.00</td></tr>
<tr><td class="key">Purchased SKUs:</td><td id="PurchasedSKUs" class="value">N/A</td></tr>
</table>'


Select 
 Tbl.Col.value('@id','varchar(max)') Id
,Tbl.Col.value('.','varchar(max)') Value
FRom
 @xml.nodes('/table/tr/td[@class="value"]') Tbl(Col)  


SELECT 
 DialResult
,ClientName
,NumberDialed
,DialCount
,ContactMade
,CampaignCalledOn
,CallOutcome
,EmailSent
,DoNotCallRequested
,ProductPurchased
,OrderNumber
,PurchaseAmount
,PurchasedSKUs
FROM
(
Select 
 Tbl.Col.value('@id','varchar(max)') Id
,Tbl.Col.value('.','varchar(max)') Value
FRom
 @xml.nodes('/table/tr/td[@class="value"]') Tbl(Col) ) T Pivot 
 (Max(Value) FOR  Id In(DialResult
,ClientName
,NumberDialed
,DialCount
,ContactMade
,CampaignCalledOn
,CallOutcome
,EmailSent
,DoNotCallRequested
,ProductPurchased
,OrderNumber
,PurchaseAmount
,PurchasedSKUs)) p;

首次选择的结果

Id                                                 Value
-------------------------------------------------- --------------------------------------------------
DialResult                                         No Answer
ClientName                                         SMITH, BOB
NumberDialed                                       5555555555 [Day]
DialCount                                          1
ContactMade                                        No
CampaignCalledOn                                   TEST CAMPAIGN
CallOutcome                                        No answer
EmailSent                                          No
DoNotCallRequested                                 No
ProductPurchased                                   No
OrderNumber                                        N/A
PurchaseAmount                                     0.00
PurchasedSKUs                                      N/A

第二次选择的结果

DialResult                                         ClientName                                         NumberDialed                                       DialCount                                          ContactMade                                        CampaignCalledOn                                   CallOutcome                                        EmailSent                                          DoNotCallRequested                                 ProductPurchased                                   OrderNumber                                        PurchaseAmount                                     PurchasedSKUs
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------------------------------------------
No Answer                                          SMITH, BOB                                         5555555555 [Day]                                   1                                                  No                                                 TEST CAMPAIGN                                      No answer                                          No                                                 No                                                 No                                                 N/A                                                0.00                                               N/A

(1 row(s) affected)