Слияние полей из разных строк запроса.
Автор Юрий Шерман   
16.01.2002 г.
Как соединить данные полей из разных строк запроса в одно поле? Вашему вниманию предлагается оптимальное решение.

Автор: Юрий Шерман
www.tour-soft.com

Слияние полей из разных строк запроса. Новая версия.
Для полей из разных строк запроса в SQL имеется целый ряд групповых функций: Sum, Min, Max и т.д. Однако если Вам надо объединить поля из разных строк, разделяя их, например, запятой, то такой групповой операции Вы не найдете. Придется писать собственное решение.

Постановка задачи.
Пусть у Вас имеется Tab1 (таблица или запрос) с полями: ID и Fam.
ID - групповой номер, он может повторяться, но не пуст (для упрощения).
Fam - поле с фамилией.

Требуется создать запрос TabUnion с двумя полями:
- ID, значения которого взяты из Tab1, но не повторяются;
- FamUnion, которое состоит из списка фамилий через запятую.
Список должен состоять из фамилий с одним ID.

Рассмотрим решение задачи от самого простого случая до самого общего.

Случай 1. Имеются упрощающие предположения.
Предположение 1: Tab1 должна быть таблицей, отсортированной по ID.
Предположение 2: в Tab1 должно быть не менее двух различных значений ID.
Предположение 3: пустых фамилий нет.

Текст запроса TabUnion1:
Select ID, Last(UnionStr1(ID,Fam)) AS FamUnion FROM Tab1
GROUP BY ID;

Описание функции UnionStr1:

Public Function UnionStr1(ID, Fam)
Static IDOld, FamUnion
If IDOld <> ID Then
IDOld = ID
FamUnion = Null
End If

FamUnion = (FamUnion + ", ") & Fam
UnionStr1 = FamUnion
End Function

Оценка скорости: время обработки пропорционально C*n, где C – константа, n – количество записей в Tab1. Очевидно, что оценку улучшить нельзя. То есть метод оптимален.

Случай 2. Отличается от случая 1 снятием предположения 2.

То есть, допускается случай, когда все записи Tab1 имеют один ID.
Тогда приходится заниматься установкой начального значения IDOld.

Текст запроса TabUnion2:
Select ID, Last(UnionStr2(ID,Fam)) AS FamUnion
FROM Tab1
WHERE IsEmpty(UnionStr2())
GROUP BY ID;

Описание функции UnionStr2:

Public Function UnionStr2(Optional ID, Optional Fam)
Static IDOld, FamUnion
If IsMissing(ID) Then
IDOld = Empty
Exit Function
End If
If IDOld <> ID Then
IDOld = ID
FamUnion = Null
End If
FamUnion = (FamUnion + ", ") & Fam
UnionStr2 = FamUnion
End Function

Оценка скорости: такая же, как и в случае 1.
Замечание: чтобы снять предположение о не пустых фамилиях можно (чтобы избежать лишних запятых) использовать в более тяжеловесную конструкцию.
В функциях место строки:
FamUnion = (FamUnion + ", ") & Fam
следует писать:
FamUnion = IIf (IsNull(FamUnion), Fam, FamUnion & (", " + Fam))

Случай 3. Tab1 таблица или запрос. Предположений о сортированности по ID и о пустых фамилиях нет.

Предположение: нет повторяющихся пар: ID, Fam.

В этом случае необходима промежуточная сортировка Tab1 по ID.

Текст промежуточного запроса Tab1Sort для сортировки Tab1:
Select DISTINCT ID, Fam FROM Tab1 ORDER BY ID, Fam;

Текст запроса TabUnion3:
Select ID, Last(UnionStr2(ID,Fam)) AS FamUnion
FROM Tab1Sort
WHERE IsEmpty(UnionStr2())
GROUP BY ID;

Здесь применяется та же функция, что и в случае 2.

Оценка скорости: оценка ухудшается из-за применения сортировки. Известно, что сортировку быстрее, чем C*n*log(n) сделать нельзя. Обычно используют сортировку методом Шелла, которая имеет оценку C*n*log(n)^2. Эта же оценка является оценкой быстродействия в данном случае. Быстрее, очевидно, сделать нельзя.
Замечание: в промежуточной сортировке используется предикат DISTINCT. Он превращает запрос Tab1Sort в статический. Без этого метод неработоспособен. Но этот же предикат удаляет повторяющиеся пары ID, Fam. Поэтому необходимо указанное выше предположение.

Случай 4. Tab1 таблица или запрос. Никаких дополнительных предположений нет.

Для применения метода слияния из случая 3 необходимо все строки Tab1 сделать уникальными.
Для этого используем нумерацию строк запроса, изложенную в статье «Нумерация строк запроса. Новый подход».

Текст промежуточного запроса Tab1Num для нумерации Tab1:
Select DISTINCT Numeration(ID) As Num, ID, Fam
FROM Tab1
WHERE Numeration()=0;

Описание функции Numeration дано в статье, указанной выше.

Текст промежуточного запроса Tab1SortNum для сортировки Tab1Num:
Select DISTINCT ID, Fam, Num FROM Tab1Num ORDER BY ID, Fam;

И, наконец, текст запроса TabUnion4:
Select ID, Last(UnionStr2(ID,Fam)) AS FamUnion
FROM Tab1SortNum
WHERE IsEmpty(UnionStr2())
GROUP BY ID;

Здесь применяется та же функция, что и в случае 2.

Оценка скорости:
оценка скорости та же, что и в случае 3, так как нумерация имеет оценку C*n.

Решение поставленной задачи завершено.

Другой вариант постановки задачи.

Предположения о Tab1 остаются прежними.
Теперь требуется заполнить пустую таблицу Tab2, содержащую поля ID и FamUnion значениями, описанными в основной задаче.

Метод 1. Очевидный.
Формируем запрос TabUnion одним из способов, указанных выше.
Затем записываем его результат в Tab2.

Метод 2. Заполнение таблицы Tab2 с помощью Recordset.
Это можно сделать с помощью следующей функции:

Public Function UnionStr3()
Dim R As Recordset, W As Recordset
Dim IDOld, FamUnion

Set R = CurrentDb.OpenRecordset("SELECT * FROM Tab1 ORDER BY ID,Fam")
If R.EOF Then Exit Function

Set W = CurrentDb.OpenRecordset("Tab2")

Do
NewID:
IDOld = R!ID
FamUnion = Null

Union:
FamUnion = (FamUnion + ", ") & R!Fam
R.MoveNext

If R.EOF Then GoTo WriteUnion
If IDOld <> R!ID Then

WriteUnion:
W.AddNew
W!ID = IDOld
W!FamUnion = FamUnion
W.Update
Else
GoTo Union
End If
If Not R.EOF Then GoTo NewID
Exit Do
Loop
End Function

Область применения: в том случае, когда кроме составления списка нужно сделать еще какие-нибудь нестандартные операции.
Недостаток: по факту работает значительно медленнее метода 1.


Метод 3. Заполнение таблицы при помощи запросов (по Митину).
Ссылка: http://c85.cemi.rssi.ru/Access/AnsPointDetail.idc?QID=14147

Оригинальный и неочевидный метод.
Выполняется с помощью двух запросов.
1. Запись в Tab2 уникальных ID без фамилий.
Текст запроса WriteID:
INSERT INTO Tab2 (ID) Select DISTINCT ID FROM Tab1;

2. Запись списков фамилий.
Текст запроса TabUnion5:
UPDATE Tab2 INNER JOIN Tab1 On Tab2.ID = Tab1.ID
Set Tab2.FamUnion = ([Tab2].[FamUnion]+", ") & [Tab1].[Fam];

Оценка скорости: по-видимому, работает быстрее метода 1 за счет отсутствия вызова специальных функций.
Недостаток: "Однако в T-SQL это непрокатывает" (Митин).


Общий недостаток всех методов: при большом объеме данных все методы заполнения Tab2 работают достаточно долго.
Если результат (список фамилий) используется часто, то лучше Tab2 сделать постоянной таблицей в базе общего доступа. Далее корректировать ее при изменении Tab1. Так как одиночная корректировка Tab1 меняет только один список в Tab2, то при этом объем работы будет мал и выполнится быстро.

Реализация для базы Access: в форме, в которой можно изменить Tab1 следует на события создания/измения/удаления записи молча проводить синхронные изменения в Tab2.

Собственно такую же рекомендацию для MS SQL дает и Митин: "Я в общем то для обеспечения максимальной производительности остановился на решении, когда на триггерах поддерживается перечень упаковок в основной табл. при изменении записей в табл. с упаковками."


Просмотров: 70965

  Коментарии (10)
 1 Написал(а) Этот e-mail защищен от спам-ботов. Для его просмотра в вашем браузере должна быть включена поддержка Java-script , в 06:58 15.07.2008
чудненько :)  
просто была примерно такая задача, 
'Переводит цифровое поле из таблицы 
'Поле таблицы 
' 1 
' 2 
' 3 
' 4 
' 5 
' 9 
' 11 
' 12 
' 13 
'в строку с перечислением 
'цифровых значений ввиде строки "1-5,9,11-13" 
'Позволяет изрядно сократить некоторые отчеты, 
решил конечно, но тут вкусно сделано :)  
спасибо
 2 Написал(а) Этот e-mail защищен от спам-ботов. Для его просмотра в вашем браузере должна быть включена поддержка Java-script , в 13:31 08.09.2008
Благодарю! Посидел, разобрался (раньше vb+access ничего не писал) 
Решил проблему, когда таблица не сортирована, с помощью двумерного массива: Проверка "имени1" с последующим занесением "переменых2" (через запятую, как указано в примере). 
В SQL запросе функции добавил Last() [опять же как в примере], что позволило выдать конечное значения для каждого "имени1". 
Не удалось создать выгрузку как у serg, потому как "переменные2" содержит как строковые, так и целые значения. :x
 3 Написал(а) Этот e-mail защищен от спам-ботов. Для его просмотра в вашем браузере должна быть включена поддержка Java-script , в 13:33 08.09.2008
Использование в коде 
Set 
значительно увеличивает процессы выгрузки.
 4 Написал(а) Макс, в 14:25 25.11.2009
Подскажите, а где писать Описание функции UnionStr1?
 5 Написал(а) Макс, в 17:07 25.11.2009
В методе3 по Митину который, что-то не хочет работать...
 6 Написал(а) Никита, в 19:41 03.07.2010
При отображении результатов запроса происходит обрезка последнего значения функции UnionStr (остаются первые 255 символов). Можно ли обойти это ограничение?
 7 Написал(а) Евгений, в 11:19 18.06.2015
Сюда нужно добавить ещё 2 предположения: 
Предположение 4: Складываемое поле не должно быть MEMO, поле усекается до 255 символов. Этого можно избежать используя другие методы(Recordset или Митина) 
Предположение 5: Нет сортировки складываемого поля.
 8 Написал(а) Александр, в 10:24 06.01.2018
Спасибо! Очень пригодилось
 9 Написал(а) Сергей, в 20:42 24.12.2018
Запускаю первый раз Ваш запрос Select ID, Last(UnionStr1(ID,Fam)) AS FamUnion FROM Tab1. 
Получается все хорошо. 
Запускаю второй раз этот же запрос. Данные от первого раза не пропали и к ним прибавляются новые.  
И так каждый раз. 
Как обнулить запрос. 
Этот запрос я хочу использовать в различных отчетах. И в первом отчете все нормально. А в последующих данные накладываются на предыдущие. 
Как обнулять запрос.
 10 Написал(а) Этот e-mail защищен от спам-ботов. Для его просмотра в вашем браузере должна быть включена поддержка Java-script , в 12:52 21.03.2019
Чтобы данные с одинаковыми ID не дублировались (случай 2), нужно исправить в текстах запросах слово IsEmpty на Not IsEmpty

Добавить коментарий
Имя:
E-mail
Коментарий:



Код:* Code