用于parsing多行数据的SQL?

我有一个不得不从excel导入数据到数据库的不幸的任务。 表格看起来像这样:

IssueID References 1234 DocID1<cr>DocID2<cr>DocID3 1235 DocID1 1236 DocID2 1237 DocID2<cr>DocID3 

参考文献是一个多行文本字段。 我想要做的是创build一个与问题表的一对多关系的文档表,而不是有这些多行参考。

我有下面的表格定义:

问题:IssueKey,IssueID,IssueFields

Doc:DocKey,DocID,DocRev,DocOwner等

DocLink:LinkKey,DocKey,IssueKey

由于这将反复运行,Doc表已经存在,并且定义了DocID。 所以,我想要做的是有一个查询或VBA代码search引用列中的每个DocID,并添加一个基于IssueID的链接,如果还不存在。

简单,对吗?

杰夫

澄清:

1)我有第三列“Val1”,表明还有其他栏目,但似乎混淆了这个问题。 实际上在源表中有很多(很多,被忽略的)列,但是我只关心上面的两个。

2)我不必parsing分隔符或任何太偏执:引用包含一个或多个唯一定义的文档参考号码(存储为文本)。 所以,一个LIKEfilter会逐个打开IssueID的列表。

3)这是一个可接受的输出的例子:

 IssueID References 1234 DocID1 1234 DocID2 1234 DocID3 1235 DocID1 1236 DocID2 1237 DocID2 1237 DocID3 

理想的解决scheme将采取原来的Excel表(顶部)和这两个表:

 IssueKey IssueID 1 1234 2 1235 3 1236 4 1237 DocKey DocID 1 DocID1 2 DocID2 3 DocID3 

并填充/更新链接表:

 LinkKey IssueKey DocKey 1 1 1 2 1 2 3 1 3 4 2 1 5 3 2 6 3 3 

4)下面是我对解决scheme的一个例子(上面创build了#3)。 不幸的是,它使Access崩溃,所以我不能分辨语法是否正确(编辑以反映上面的字段名称)。

 SELECT Q1.IssueID, D1.DocID FROM Docs AS D1, Issues AS Q1 WHERE Q1.IssueID IN ((SELECT Q2.IssueID from Issues AS Q2 where (Q2.References) Like D1.DocID)); 

5)暂时放弃访问,我在MySQL中有以下工作:

 SELECT Q1.IssueID, D1.DocID FROM Docs AS D1, Issues AS Q1 WHERE Q1.IssueID IN ((SELECT Q2.IssueID from Issues AS Q2 where (Q2.References) Like '%DocID1%')); 

这是我所期望的 – 我得到每个IssueID都带有对DocID1的引用,对表中的每个Doc重复。 有了上面的数据,它会看起来像:

 IssueID References 1234 DocID1 1234 DocID2 1234 DocID3 1235 DocID1 1235 DocID2 1235 DocID3 

现在我只想用'%'+ D1.DocID +'%'replace'%DocID1%' – 将结果限制为实际匹配的文档ID。 出于某种原因,我得到零logging时,我这样做 – 我想我有相关领域的通配符错误的语法。

6)以下工作在MySQL中提供以上#3,但是同样的查询翻译为访问崩溃:

 SELECT Q1.IssueID, D1.DocID FROM Docs AS D1, Issues AS Q1 WHERE Q1.IssueID IN ((SELECT Q2.IssueID from Issues AS Q2 where (Q2.References) Like CONCAT('%',D1.DocID,'%'))); 

[在访问它成为(' '&D1.DocID&' ')]

结论:访问很糟糕

这已被选为答案:

 Q2.References LIKE ("*" & D1.DocID & "*")); 

不过,我不认为这是安全的。

考虑一下名为“参考”的列的值是否包含这些数据:

 DocID1<cr>DocID999<cr>DocID3 

另一个表中存在一个DocID = 9的值。

这里的问题是

 "DocID1<cr>DocID999<cr>DocID3" LIKE "*" & "DocID9" & "*" 

将评估为TRUE ,这可能是不可取的。

为了解决这个问题,我认为search/连接条件中的值应该通过使用分隔字符围绕值来保证安全

 (CHR(13) & Q2.References & CHR(13)) LIKE ("*" & CHR(13) & D1.DocID & CHR(13) & "*")); 

我的第一select是将一个快速的应用程序放在C#或VB.Net中来处理这个问题。

如果这是不可行的,我会有一个“导入”表,把所有东西都照原样。 然后我将使用游标来迭代表中的logging。 在游标内部,我将跟踪IssueId和Val1并parsingReferences列以创build我的子logging。 这部分我会打包到一个存储过程。

我build议你研究SQL Server集成服务(SSIS)。 这个工具是用尽可能less的代码尽快完成这种数据导入/导出的。

阅读有关它。 做一些实验,看看是否有任何例子与你想要做的事情接近。

http://en.wikipedia.org/wiki/SQL_Server_Integration_Services http://www.microsoft.com/downloads/details.aspx?familyid=b1145e7a-a4e3-4d14-b1e7-d1d823b6a447&displaylang=en

由于这是反复运行,我会问(强烈build议)他们提供了一个适当的文件,在每一行出现的问题ID和有效。 这很容易处理。 您需要确定这些字段的值是否正确导入到您的系统中。

基于以下注释:在SQL Server中,您可以构build一个函数来根据charindex为逗号分割数据。 如果你search谷歌的fn_split,你会发现这个样本。 不知道你会如何做到这一点在Access中,但它可能是一个interative过程中,你寻找最后一个逗号,并把所有东西过去它保留表,然后摆脱命令,然后再做,直到没有更多的逗号。 最简单的做法是按照这种方式导入表,在表中按照需要的方式操纵数据,然后将最终结果放到真正的表中。

你的意思是(键入,没有testing):

 Dim rs As DAO.Recordset Dim rsIn As DAO.Recordset ''Or ADO if you link directly to Excel Set rs=CurrentDB.OpenRecordset( _ "SELECT * FROM DocLinks dl INNER JOIN Docs d ON dl.DocKey=d.DocKey") Do While Not rsIn.EOF astrDocs=Split(rsIn!References, vbCrLf) For Each strDoc In astrDocs rs.FindFirst "DocID='" & strDoc & "'" If rs.NoMatch Then strSQL="INSERT INTO DocLinks (DocID, IssueID) " _ & "VALUES ('" strDoc & "'," & rsIn!IssueID & ")" CurrentDB.Execute strSQL, dbFailOnError End If Next rsIn.MoveNext Loop 

编辑重新评论

如果DocID具有固定的长度,那么可以考虑这些内容:

 SELECT Sequence.Seq , ImportTable.IssueID , Mid(Replace([References],"<cr>",""),[seq],6) AS Docs FROM Sequence, ImportTable WHERE ([seq]+5) Mod 6=0) AND Mid(Replace([References],"<cr>",""),[seq],6))<>"" AND Mid(Replace([References],"<cr>",""),[seq],6)) Not In (SELECT DocID FROM Docs) 

您将需要一个序列表,其中从1到至less为参考最大长度的整数。

这可以很容易地在SQL中完成。 我写了一个TVF(表值函数)专门用于分行文本,演示了如何:

  ALTER function [dbo].[fnSplit3]( @parameter varchar(Max) -- the string to split , @Seperator Varchar(64) -- the string to use as a seperator ) RETURNS @Items TABLE( ID INT -- the element number , item VARCHAR(8000) -- the split-out string element , OffSet int -- the original offest --( not entirley accurate if LEN(@Seperator) > 1 because of the Replace() ) ) AS BEGIN /* "Monster" Split in SQL Server 2005 From Jeff Moden, 2008/05/22 BYoung, 2008/06/18: Modified to be a Table-Valued Function And to handle CL/LF or LF-only line breaks Test: (scripts all procs & views in master) Select Lines.Item From Master.sys.syscomments C CROSS APPLY dbo.fnSplit3(C.text, char(13)+char(10)) Lines Order by C.ID, Lines.ID Test2: (scripts all triggers in your database) Select Lines.Item From sys.sql_modules M Join sys.objects O on O.object_id = M.object_id CROSS APPLY dbo.fnSplit3(M.definition, char(13)+char(10)) Lines Where O.Type = 'TR' Order by O.create_date, Lines.ID */ Declare @Sep char(1) Set @Sep = char(10) --our seperator character (convenient, doesnt affect performance) --NOTE: we make the @Sep character LF so that we will automatically -- parse out rogue LF-only line breaks. --===== Add start and end seprators to the Parameter so we can handle -- all the elements the same way -- Also change the seperator expressions to our seperator -- character to keep all offsets = 1 SET @Parameter = @Sep+ Replace(@Parameter,@Seperator,@Sep) +@Sep -- This reduces run-time about 10% ;WITH cteTally AS (--==== Create a Tally CTE from 1 to whatever the length -- of the parameter is SELECT TOP (LEN(@Parameter)) ROW_NUMBER() OVER (ORDER BY t1.object_id) AS N FROM Master.sys.system_Columns t1 CROSS JOIN Master.sys.system_Columns t2 ) INSERT into @Items SELECT ROW_NUMBER() OVER (ORDER BY N) AS Number, SUBSTRING(@Parameter, N+1, CHARINDEX(@Sep, @Parameter, N+1)-N-1) AS Value , N+1 FROM cteTally WHERE N < LEN(@Parameter) AND SUBSTRING(@Parameter, N, 1) = @Sep --Notice how we find the seperator Return END 

为了使用您当前的表格和数据做到这一点:

 SELECT Issues.IssueID, Lines.Item as Reference From Issues Cross Apply dbo.fnSplit3(Issues.Reference, char(13)) Lines Order By IssueID, Reference 

我在这里遇到了一个基于集合的SQL解决scheme的问题。 我之前做过这样的事情,我不得不有点恢复记忆,但是我遇到了一个问题。 我认为这是一个引擎问题(function/错误?),但我可以做一些愚蠢的事情。 也许有人与Jet / ACE亲密,谁可以阅读VBA可以看看这个答案的末尾的代码,并希望把这个前进…?

基本的方法是使用整数序列表与MID()expression式来parsing数据列(由于REFERENCES是一个SQL关键字,所以我已经重命名为MyReferences)。

这里有一些MS Access VBA使用SQL DDL / DML重新创buildtesting表/数据。 注意第一个SELECT查询返回子string和星号和末尾分隔符; 显然,我们正在寻找两个分隔符都是分隔符的行( CHR(13) 。 第二个SELECT查询只是为所需的分隔符添加了search条件,但却带有“无效过程调用”的错误。 当使用无效参数值调用MID()expression式时会发生这种情况

 SELECT MID('A', 0, 0) 

我猜是什么事是优化器没有使用子查询作为一个“捷径”,而是在序列表的search条件之前,评估MID()expression式。 如果是这样,这有点愚蠢,我想不出一种强制评估顺序的方法。

那么,是我的还是引擎出现问题?

 Sub main() Dim sql As String sql = _ "DROP TABLE ImportTable;" On Error Resume Next ' Table may not exist CurrentProject.Connection.Execute sql On Error GoTo 0 sql = _ "DROP TABLE Sequence;" On Error Resume Next ' Table may not exist CurrentProject.Connection.Execute sql On Error GoTo 0 sql = _ "CREATE TABLE ImportTable ( " & _ "IssueID INTEGER NOT NULL UNIQUE, MyReferences VARCHAR(90) NOT NULL);" CurrentProject.Connection.Execute sql sql = _ "INSERT INTO ImportTable VALUES (1234, 'DocID1' & Chr(13) & 'DocID22' & Chr(13) & 'DocID3');" CurrentProject.Connection.Execute sql sql = _ "CREATE TABLE Sequence (seq INTEGER NOT NULL UNIQUE);" CurrentProject.Connection.Execute sql sql = _ "INSERT INTO Sequence VALUES (-1);" CurrentProject.Connection.Execute sql sql = _ "INSERT INTO [Sequence] (seq) SELECT Units.nbr + Tens.nbr" & _ " FROM ( SELECT" & _ " nbr FROM ( SELECT 0 AS nbr FROM [Sequence] UNION" & _ " ALL SELECT 1 FROM [Sequence] UNION ALL SELECT 2 FROM" & _ " [Sequence] UNION ALL SELECT 3 FROM [Sequence] UNION" & _ " ALL SELECT 4 FROM [Sequence] UNION ALL SELECT 5 FROM" & _ " [Sequence] UNION ALL SELECT 6 FROM [Sequence] UNION" & _ " ALL SELECT 7 FROM [Sequence] UNION ALL SELECT 8 FROM" & _ " [Sequence] UNION ALL SELECT 9 FROM [Sequence] ) AS" & _ " Digits ) AS Units, ( SELECT nbr * 10 AS nbr FROM" & _ " ( SELECT 0 AS nbr FROM [Sequence] UNION ALL SELECT" & _ " 1 FROM [Sequence] UNION ALL SELECT 2 FROM [Sequence]" & _ " UNION ALL SELECT 3 FROM [Sequence] UNION ALL SELECT" & _ " 4 FROM [Sequence] UNION ALL SELECT 5 FROM [Sequence]" & _ " UNION ALL SELECT 6 FROM [Sequence] UNION ALL SELECT" & _ " 7 FROM [Sequence] UNION ALL SELECT 8 FROM [Sequence]" & _ " UNION ALL SELECT 9 FROM [Sequence] ) AS Digits )" & _ " AS Tens;" CurrentProject.Connection.Execute sql sql = _ "SELECT DT1.IssueID, DT1.parsed_text, DT1.delimiter_1, DT1.delimiter_2 " & _ "FROM ( " & _ "SELECT I1.IssueID, MID(I1.MyReferences, S1.seq, S2.seq - S1.seq - LEN(CHR(13))) AS parsed_text, " & _ " MID(CHR(13) & I1.MyReferences & CHR(13), S1.seq, LEN(CHR(13))) AS delimiter_1, " & _ " MID(CHR(13) & I1.MyReferences & CHR(13), S2.seq, LEN(CHR(13))) AS delimiter_2 " & _ "FROM ImportTable AS I1, Sequence AS S1, Sequence AS S2 " & _ "WHERE S1.seq < S2.seq " & _ "AND S2.seq - S1.seq - LEN(CHR(13)) > 0 " & _ "AND S1.seq BETWEEN 1 AND LEN(CHR(13)) + LEN(I1.MyReferences) + LEN(CHR(13)) " & _ "AND S2.seq BETWEEN 1 AND LEN(CHR(13)) + LEN(I1.MyReferences) + LEN(CHR(13)) " & _ ") AS DT1;" Dim rs As ADODB.Recordset Set rs = CurrentProject.Connection.Execute(sql) MsgBox rs.GetString sql = _ "SELECT DT1.IssueID, DT1.parsed_text, DT1.delimiter_1, DT1.delimiter_2 " & _ "FROM ( " & _ "SELECT I1.IssueID, MID(I1.MyReferences, S1.seq, S2.seq - S1.seq - LEN(CHR(13))) AS parsed_text, " & _ " MID(CHR(13) & I1.MyReferences & CHR(13), S1.seq, LEN(CHR(13))) AS delimiter_1, " & _ " MID(CHR(13) & I1.MyReferences & CHR(13), S2.seq, LEN(CHR(13))) AS delimiter_2 " & _ "FROM ImportTable AS I1, Sequence AS S1, Sequence AS S2 " & _ "WHERE S1.seq < S2.seq " & _ "AND S2.seq - S1.seq - LEN(CHR(13)) > 0 " & _ "AND S1.seq BETWEEN 1 AND LEN(CHR(13)) + LEN(I1.MyReferences) + LEN(CHR(13)) " & _ "AND S2.seq BETWEEN 1 AND LEN(CHR(13)) + LEN(I1.MyReferences) + LEN(CHR(13)) " & _ ") AS DT1 " & _ "WHERE DT1.delimiter_1 = CHR(13) " & _ "AND DT1.delimiter_2 = CHR(13);" Set rs = CurrentProject.Connection.Execute(sql) MsgBox rs.GetString End Sub 

FWIW这里是我多年前写入的一个PROCEDURE用于将分隔列表parsing为表格。 对于最多255个字符的值,它似乎工作正常; 任何更多的,你会得到一个非常讨厌的ACE / Jet引擎错误。 再一次,我不明白什么是引擎不能应付的问题。 无论如何,我的观点是,这是有效的(小值),我不明白为什么我不能适应手头的问题:

 CREATE PROCEDURE ListToTable ( delimted_text MEMO, delimiter VARCHAR(4) = ',' ) AS SELECT MID(I1.input_string, S1.seq, MIN(S2.seq) - S1.seq - LEN(delimiter)) AS param FROM ( SELECT DISTINCT delimted_text AS input_string FROM Sequence AS S3 WHERE S3.seq BETWEEN 1 AND LEN(delimted_text) ) AS I1, Sequence AS S1, Sequence AS S2 WHERE MID(delimiter & I1.input_string & delimiter, S1.seq, LEN(delimiter)) = delimiter AND MID(delimiter & I1.input_string & delimiter, S2.seq, LEN(delimiter)) = delimiter AND S1.seq < S2.seq AND S1.seq BETWEEN 1 AND LEN(delimiter) + LEN(delimted_text) + LEN(delimiter) AND S2.seq BETWEEN 1 AND LEN(delimiter) + LEN(delimted_text) + LEN(delimiter) GROUP BY I1.input_string, S1.seq HAVING LEN(MID(I1.input_string, S1.seq, MAX(S2.seq) - S1.seq - LEN(delimiter))) > 0; 

我认为在标题中使用“parse”这个词使得所有人都感到困惑。 Access中的错误是在查询(而不是表)上执行的相关查询导致挂起。 相反,我创build了一个临时表,将References列(带有多行文本)广告给Issues表,以便我可以访问其他字段。 最后的查询创build上面描述的链接表,以及DocID和IssueID以供参考:

 SELECT Q1.IssueID, Q1.IssueKey, D1.DocKey, D1.DocID FROM Issues AS Q1, Documents AS D1 WHERE Q1.IssueID in (SELECT Q2.IssueID FROM Issues AS Q2 WHERE Q2.References LIKE ("*" & D1.DocID & "*")); 

内部select拉引用列中具有给定文档的问题列表。 外层select对每个文档执行此操作,从而生成聚合列表。