字符串解析 VARCHAR 列

String Parsing VARCHAR Column

我正在使用一个数据库,在这个数据库中,phone 调用用户与客户端的位置被保存到单个 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>

目前,我有一个存储过程,它使用游标遍历 "live" 数据库 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

所以,你可以看到光标每次都在解析完整的笔记正文,提取两个字符串分隔符之间的值,将值插入到table,然后返回table .

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

FN_PARSE_TEXT 中的代码使用 CHARINDEX()、LEN()、SUBSTRING() 等来读取 "Start string" 和 "end string" 之间的文本。我不会 post 完整的功能,因为有很多不相关的内务处理代码,但可以提炼为:

--********************************************************************************
-- 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)))

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

我考虑过创建一个夜间批处理过程,在下班时间进行所有解析,然后将数据转储到扁平化 table 中。然后我会报告扁平化的数据,而不是试图实时解析细节。当然,这打开了它自己的蠕虫罐头,理想情况下我想继续点击实时数据。根据用户在 运行 查询时提供的日期范围,可能会返回超过 10,000 行。

我想到的另一种选择是使用 CLR 方法来处理循环和解析 - 但我在这方面没有太多经验,而且我不确定这是否比我现在在做什么。

我读过一些文章(例如,https://sqlperformance.com/2012/07/t-sql-queries/split-strings),讨论诸如 CTE 之类的事情,但我在这方面的经验并不多,所以我不确定如何实现这一飞跃。我读过的大多数 "string parsing" 文章都针对定界符相同的场景——比如解析逗号分隔的字符串。但是因为我有 "rolling" 个分隔符,所以我不确定如何处理它。

无论如何,如果您仍然和我在一起,我们将不胜感激。

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

我要么将所有这些经常选择的列添加到我的 table,要么为它们创建一个单独的 table,可以链接到这个 [=26] 的主键=].

然后,理想情况下,我会在插入时填充这些列。最好让 INSERT 通过存储过程,但如有必要,可以在 table.

上放置一个插入触发器

如果我不能在 INSERT 时执行此操作,那么我会 运行 执行解析并尽可能频繁地填充列的作业 requires/allows。

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

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

只是为了提供一个具体示例,说明如何使用 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)

XML 在 SQL 中分解的文档可能会有点乱七八糟,主要是因为 SQL 正在实现独立于 SQL 和 Microsoft 的 xquery。也就是说,对于做基本的事情,通常 MSDN 文档可以让你很好地开始。

这里有几篇好文章

您可以使用 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;

第一个结果 select

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

第二个结果 select

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)