SQL для анализа многострочных данных? - PullRequest
2 голосов
/ 27 мая 2009

К сожалению, мне приходится регулярно импортировать данные из Excel в базу данных. Таблица выглядит примерно так:

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

Список литературы - многострочное текстовое поле. Я пытаюсь создать таблицу Docs с отношением один-ко-многим к таблице Issue, а не с этими многострочными ссылками.

У меня определены следующие таблицы:

Проблема: IssueKey, IssueID, IssueFields

Документ: DocKey, DocID, DocRev, DocOwner и т. Д.

DocLink: LinkKey, DocKey, IssueKey

Поскольку это будет выполняться неоднократно, таблица Doc уже будет существовать с определенными DocID. Итак, я хочу сделать запрос или поиск кода VBA для каждого DocID в столбце «Ссылки» и добавить ссылку на основе IssueID, если она еще не существует.

Простой, верно?

Jeff

Разъяснения:

1) У меня был третий столбец под названием «Val1», чтобы показать, что были другие столбцы, но это, казалось, запутало проблему. На самом деле в исходной таблице есть много (от многих до самых игнорируемых) столбцов, но я забочусь только о двух выше.

2) Мне не нужно анализировать разделитель или что-то слишком параноидальное: References содержит один или несколько уникально определенных ссылочных номеров документов (хранящихся в виде текста). Таким образом, фильтр 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%'));

Это работает, как я и ожидал - я получаю каждый IssueID со ссылкой на DocID1, повторяемый для каждого документа в таблице. С приведенными выше данными это будет выглядеть так:

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

Теперь я просто хочу заменить «% DocID1%» на «%» + D1.DocID + «%» - ограничив результаты теми идентификаторами документов, которые действительно совпадают. По какой-то причине я получаю ноль записей, когда я делаю это - я думаю, что у меня есть синтаксис для неправильной подстановки подстановочных знаков в коррелированном поле.

6) Следующее работает для предоставления # 3 выше в 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        
        CONCAT('%',D1.DocID,'%')));

[при доступе становится ('' & D1.DocID & '')]

Вывод: доступ отстой

Ответы [ 8 ]

2 голосов
/ 03 июня 2009

Этот ответ был выбран:

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) & "*"));
1 голос
/ 27 мая 2009

Поскольку это должно выполняться многократно, я бы попросил (настоятельно рекомендую) предоставить мне правильный файл, в котором идентификатор и действительный номер появляются в каждой строке. Это гораздо проще обрабатывать. Вам необходимо точно знать, какие значения для этих полей необходимо правильно импортировать в вашу систему.

Основываясь на комментариях: В SQL Server вы могли бы создать функцию для разделения данных на основе charindex для запятых. Если вы ищете в Google fn_split, вы найдете образец этого. Не уверен, как бы вы сделали это в Access, но, вероятно, это будет интерактивный процесс, в котором вы будете искать последнюю запятую и перемещать все, что за ней, в таблицу хранения, а затем избавляться от команды, а затем повторять до тех пор, пока больше не будет запятых , Проще всего выполнить такой импорт в промежуточные таблицы, где вы можете манипулировать данными так, как вам нужно, и затем поместить окончательный результат в ваши реальные таблицы.

0 голосов
/ 02 июня 2009

Я думаю, что использование слова "parse" в названии сбило с толку всех. Ошибка в Access заключалась в том, что коррелированный запрос, выполняемый над запросом (вместо таблицы), вызывает зависание. Поэтому вместо этого я создал временную таблицу, в которой столбец «Ссылки» (с многострочным текстом) добавляется в таблицу «Вопросы», чтобы у меня был доступ к другим полям. Последний запрос создает таблицу ссылок, описанную выше, вместе с 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 & "*"));

Внутренний выбор извлекает список проблем, для которых задан данный документ в столбце ссылок. Внешний выбор выполняет это для каждого документа, в результате чего получается сводный список.

0 голосов
/ 02 июня 2009

У меня возникли проблемы с поиском решения SQL на основе множеств. Я делал подобные вещи раньше, мне пришлось немного освежить свою память, но я столкнулся с проблемой. Я думаю это проблема (функция / ошибка?) С двигателем, но я мог бы сделать что-то глупое. Возможно, кто-то знакомый с Jet / ACE и кто умеет читать VBA, может взглянуть на код в конце этого ответа и надеяться на это ...?

Основной подход состоит в том, чтобы использовать таблицу целых чисел Sequence с выражением MID() для анализа столбца данных (который я переименовал в MyReferences, потому что REFERENCES является ключевым словом SQL).

Вот несколько MS Access VBA для воссоздания тестовых таблиц / данных с использованием SQL DDL / DML. Обратите внимание, что первый запрос SELECT возвращает подстроки и звездочки и конечные разделители; очевидно, мы ищем строки, в которых оба разделителя являются символом-разделителем, в данном случае CHR(13). Второй запрос SELECT просто добавляет условия поиска для желаемых разделителей, но содержит ошибки с «Недопустимым вызовом процедуры»; это происходит, когда выражение MID() вызывается с использованием недопустимых значений параметров, например,

SELECT MID('A', 0, 0)

Полагаю, что происходит, оптимизатор не использует подзапрос в качестве «ярлыка», а вместо этого оценивает выражение MID() перед условиями поиска из таблицы Sequence. Если это так, то это немного глупо, и я не могу придумать способ навязать порядок оценки.

Так, здесь мой или двигатель неисправен?

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;
0 голосов
/ 28 мая 2009

Это легко сделать в 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
0 голосов
/ 27 мая 2009

Вы имеете в виду (набрано, не проверено):

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 до максимальной длины ссылки.

0 голосов
/ 27 мая 2009

Я предлагаю вам изучить службы интеграции 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

0 голосов
/ 27 мая 2009

Мой первый выбор - собрать быстрое приложение на C # или VB.Net, чтобы справиться с этим.

Если бы это было нежизнеспособным, я бы имел таблицу «Импорт», которая бы принимала все как есть. Тогда я бы использовал курсор для итерации записей в таблице. Внутри курсора я буду отслеживать IssueId и Val1 и анализировать столбец References, чтобы создать мои дочерние записи. Эту часть я бы упаковал в хранимую процедуру.

...