【问题标题】:SQL for parsing multi-line data?用于解析多行数据的 SQL?
【发布时间】:2009-05-27 13:48:09
【问题描述】:

不幸的是,我不得不定期将数据从 excel 导入数据库。该表如下所示:

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

参考是一个多行文本字段。我要做的是创建一个与 Issue 表具有一对多关系的 Docs 表,而不是拥有这些多行引用。

我定义了以下表格:

问题:IssueKey、IssueID、IssueFields

文档:DocKey、DocID、DocRev、DocOwner 等

DocLink:LinkKey、DocKey、IssueKey

由于这将重复运行,Doc 表将已经存在并定义了 DocID。所以,我想要做的是对 References 列中的每个 DocID 进行查询或 VBA 代码搜索,如果不存在,则添加基于 IssueID 的链接。

简单吧?

杰夫

说明:

1) 我有一个名为“Val1”的第三列,以显示还有其他列,但这似乎混淆了问题。源表中实际上有很多(很多,最被忽略的)列,但我只关心上面的两个。

2) 我不必解析分隔符或任何过于偏执的东西:参考包含一个或多个唯一定义的文档参考号(存储为文本)。因此,LIKE 过滤器将根据具体情况显示 IssueID 列表。

3) 以下是可接受的输出示例:

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

理想的解决方案是使用原始的 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) 这是我期望的解决方案示例(创建上面的#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) 暂时放弃 Access,我在 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%'));

这正如我所期望的那样工作 - 我得到每个带有对 DocID1 的引用的 IssueID,并为表中的每个 Doc 重复。有了上面的数据,它看起来像:

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

现在我只想将 '%DocID1%' 替换为 '%'+D1.DocID+'%' - 将结果限制为实际匹配的文档 ID。出于某种原因,当我这样做时,我得到零记录 - 我认为我在相关字段上放置通配符的语法错误。

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 & '')]

结论:访问很烂

【问题讨论】:

  • 快速评论:这是多对多的关系;)
  • 授予 - 这就是我使用链接表的原因。我只是想通过要求一个方向的更新来简化问题:我想要每个 DocID 的 IssueID 列表。
  • SQL(Jet/ACE 风格)中的方法是解析文本列中的字符以查找分隔符——在您的情况下为 CHR(10)?您应该能够找到许多示例,例如这是我之前做的一个:groups.google.com/group/microsoft.public.access.modulesdaovba/…
  • ...问题是,您真的想要一个 SQL 解决方案,即您是否会找到一个更容易遵循的 VBA 解决方案?
  • 我更喜欢 SQL 解决方案,因为我不确定这将始终存在于 Access DB 中,并且我正在尝试了解一些更高级的 SQL 概念。

标签: sql mysql excel ms-access vba


【解决方案1】:

这已被选为答案:

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

但是,我认为这并不安全。

考虑名为“参考”的列的值之一是否包含此数据:

DocID1<cr>DocID999<cr>DocID3

另一个表中存在值DocID = 9

这里的问题是

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

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

为了解决这个问题,我认为搜索/连接条件中的值应该通过使用分隔符将值括起来来确保安全,例如

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

【讨论】:

  • 假设我没有说清楚:所有的 DocID 实际上都是唯一的 5 位数字,格式为文本,所以你描述的情况是不可能的。这是在一般解决方案中考虑的一个好点,如果我有任何代表,我会将其投票为“好消息”:)
【解决方案2】:

由于这是重复运行,我会要求(强烈建议)他们为我提供一个适当的文件,其中 issueID 和 valid 出现在每一行。这更容易处理。您需要确定这些字段的值是什么才能正确导入您的系统。

基于 cmets:在 SQL Server 中,您可以构建一个函数来根据逗号的 charindex 拆分数据。如果您在 Google 上搜索 fn_split,您会找到一个示例。不确定您将如何在 Access 中执行此操作,但这可能是一个交互过程,您可以在其中查找最后一个逗号并将其后的所有内容移到一个保留表中,然后删除该命令,然后再做一次,直到没有逗号为止.像这样对暂存表进行导入是最简单的,您可以在其中以您需要的方式操作数据,然后将最终结果放入您的真实表中。

【讨论】:

  • 我不确定我是否清楚:我想要获取的是每个 DocID 的 IssueID 列表,然后使用该数据更新链接表。我可以反复执行此操作: SELECT IssueID FROM Sheet WHERE References LIKE "DocID1" SELECT IssueID FROM Sheet WHERE References LIKE "DocID2" 等等。我只是想得到我的SQL 技能更上一层楼,我的直觉告诉我用 VBA 遍历表是不好的形式。
  • 您的问题是文件不是您可以轻松使用的格式。如果 issueid 为空,你怎么知道 issue id 到底是什么?我个人不会接受这种格式的文件。要弄清楚这一点,您将不得不反复找出每个问题 ID,并假设它上面的下一个问题是正确的。您需要使用某种 rowid 列导入它,以便您可以查找它。另一个问题可能是记录不一定按文件中的顺序导入。这就是为什么文件应该有一些东西来将问题 ID 与订单以外的引用联系起来。
  • 啊,现在我明白你的问题了。每行都有一个IssueID,问题是references列的单元格可能包含多个DocID
【解决方案3】:

我的第一选择是在 C# 或 VB.Net 中构建一个快速应用程序来处理这个问题。

如果这不可行,我将有一个“导入”表,它可以按原样处理所有内容。然后我会使用游标来迭代表中的记录。在光标内,我将跟踪 IssueId 和 Val1 并解析 References 列以创建我的子记录。这部分我会打包成一个存储过程。

【讨论】:

    【解决方案4】:

    我建议您研究 SQL Server 集成服务 (SSIS)。创建此工具是为了用最少的代码尽快完成此类数据导入/导出。

    阅读它。做一些动手实验,看看是否有任何示例与您尝试做的相近。

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

    【讨论】:

    • 我的选择是 Access 或 Oracle,因此 MSSQL Server 解决方案不会有帮助。
    • Visual Studio 解决方案有用吗?如果是这样...您无需读取或写入 SQL Server 即可使用 SSIS。您确实需要安装一个快速版(免费),以便在 Visual Studio 中为您提供商业智能项目。请参阅此页面上的“创建包”部分:accelebrate.com/sql_training/ssis_tutorial.htm<END_LINK>您不能使用 SSIS 包来访问 Oracle 和 MySQL。
    【解决方案5】:

    您的意思是(输入,未测试):

    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 到至少最大参考长度的整数。

    【讨论】:

    • 这是基本思想,虽然:a) 我希望用 SQL 语法来解决这个问题。我觉得应该有一些嵌套查询可以做我想做的事,但我无法完全理解它。 b)最终我想更新,而不是创建链接表。我可能会使用您的代码来创建将在更新查询中使用的临时表。感谢您的输入。最好的选择。
    • 以“INSERT INTO DocLinks ...”开头的 SQL 行旨在更新表,它是一个追加查询。据我所知,Access 中没有任何形式的嵌套查询适合您。
    • 我已经考虑过这一点,并提出了进一步的建议。
    【解决方案6】:

    这可以在 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 解决方案,请参见上面的 #6。现在我只停留在如何在 Access 中实现这一点。
    【解决方案7】:

    我在提出基于集合的 SQL 解决方案时遇到了问题。我以前做过这种事情,我不得不稍微刷新一下我的记忆,但是我遇到了一个问题。我认为这是引擎的问题(功能/错误?),但我可能会做一些愚蠢的事情。也许熟悉 Jet/ACE 并且可以阅读 VBA 的人可以查看此答案末尾的代码并希望将其向前推进...?

    基本方法是使用带有MID() 表达式的整数序列表来解析数据列(我已将其重命名为MyReferences,因为REFERENCES 是SQL 关键字)。

    这里有一些 MS Access VBA 可以使用 SQL DDL/DML 重新创建测试表/数据。请注意第一个 SELECT 查询返回子字符串以及星号和结尾分隔符;显然,我们正在寻找两个定界符都是定界字符的行,在这种情况下为CHR(13)。第二个SELECT 查询仅添加了所需分隔符的搜索条件,但出现“无效过程调用”错误;当使用无效参数值调用 MID() 表达式时会发生这种情况,例如

    SELECT MID('A', 0, 0)
    

    我猜发生的事情是优化器没有使用子查询作为“快捷方式”,而是在序列表中的搜索条件之前评估MID() 表达式。如果是这样,那就有点愚蠢了,我想不出一种强制评估顺序的方法。

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

    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,用于将分隔列表解析为表格。对于最多 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;
    

    【讨论】:

      【解决方案8】:

      我认为在标题中使用“解析”这个词已经把每个人都搞糊涂了。 Access 中的错误是对查询(而不是表)执行的相关查询会导致挂起。因此,我创建了一个临时表,将 References 列(带有多行文本)添加到 Issues 表中,以便我可以访问其他字段。最终查询创建了上述链接表,以及 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 & "*"));
      

      内部选择会在参考列中提取具有给定文档的问题列表。外部选择对每个文档执行此操作,从而生成聚合列表。

      【讨论】:

      • 而不是“混淆废话”,也许每个人都在给你怀疑的好处:) 你看,你的名为“参考”的列包含非标量数据违反了第一范式 (1NF) .因为规范化是一件好事,我们自然而然地假设您想清理您的数据。
      • ...因为在 SQL 中处理多值数据 (NFNF) 很痛苦。当然,ACE 和 Access2007 引入了多值数据类型,但没有用于处理多值数据的新表达式。我们认为您想通过规范化数据来修复数据,从而使您受益匪浅:)
      • 我确实在尝试规范化数据,但我认为我的假设不够清楚。请参阅下面的评论。
      • 进一步:其他答案让我感到惊讶的是,它们似乎都在遍历数据库并解析字段,而不是搜索它。因此我的结论是我在标题中使用“解析”这个词是错误的......
      • “我确实在尝试规范化数据”——那么你不需要引用..LIKE..DocID 谓词!相反,您的第一个任务 IMO 是将 NFNF 列“参考”更改为标量列。在 RM 理论中,这种违反 1NF 的行为被称为“重复组”。请参阅:en.wikipedia.org/wiki/…
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多