Использование объединения для объединения соответствующих записей - PullRequest
0 голосов
/ 28 июня 2019

Я пытаюсь объединить совпадающие записи из таблицы в одну запись другой таблицы.Я знаю, что это можно сделать с помощью group by и sum (), max () и т. Д., Моя сложность заключается в том, что столбцы, которые не являются частью группы, - это varchars, которые мне нужно объединить.

Я использую Sybase ASE 15, поэтому у меня нет такой функции, как MySQL group_concat или аналогичной.

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

create table #source_t(account varchar(10), event varchar(10))

Insert into #source_t(account, event) values ('account1','event 1')
Insert into #source_t(account, event) values ('account1','event 2')
Insert into #source_t(account, event) values ('account1','event 3')

create table #target(account varchar(10), event_list varchar(2048))

merge into #target as t
    using #source_t as s
    on t.account = s.account
    when     matched then update set event_list = t.event_list + ' | ' + s.event
    when not matched then insert(account, event_list) values (s.account, s.event)

select * from #target

drop table #target

drop table #source_t

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

account, event_list
'account1', 'event 1 | event 2 | event 3'

Тем не менее, все I 've получил те же записи, что и # source.

Мне кажется, что сопоставление в слиянии предпринимается по отношению к «состоянию» таблицы в начале выполнения оператора, поэтому при сопоставлении никогда не выполняется.Есть ли способ сообщить СУБД о соответствии с обновленной целевой таблицей?

Мне удалось получить нужные мне результаты с помощью курсора, поэтому оператор слияния выполняется n раз, а n - это количество записей.в #source, таким образом, слияние фактически выполняет при совпадении part.

Проблема заключается в производительности, поэтому удаление дубликатов занимает около 5 минут для объединения 63K записей в 42K.

Есть ли более быстрый способ добиться этого?

Ответы [ 2 ]

1 голос
/ 29 июня 2019

Существует малоизвестный (плохо документированный?) Аспект оператора UPDATE при его использовании для обновления @variable, который позволяет накапливать / объединять значения в @variable как часть операции UPDATE на основе набора.

Это легче объяснить с помощью примера:

create table source
(account  varchar(10)
,event    varchar(10)
)
go

insert source values ('account1','event 1')
insert source values ('account1','event 2')
insert source values ('account1','event 3')

insert source values ('account2','event 1')

insert source values ('account3','event 1')
insert source values ('account3','event 2')
go

declare @account      varchar(10),
        @event_list   varchar(40)   -- increase the size to your expected max length 

select  @account = 'account1'

-- allow our UPDATE statement to cycle through the events for 'account1',
-- appending each successive event to @event_list

update  source
set     @event_list = @event_list + 
                      case when @event_list is not NULL then ' | ' end + 
                      event
from    source
where   account = @account

-- we'll display as a single-row result set; we could also use a 'print' statement ... 
-- just depends on what format the calling process is looking for

select  @account     as account,
        @event_list  as event_list
go

 account    event_list
 ---------- ----------------------------------------
 account1   event 1 | event 2 | event 3

PRO:

  • один оператор UPDATE для обработки значения одного счета

CON:

  • все еще нужен курсор для обработки серии значений счета
  • , если желаемым конечным результатом является один набор результатов, вам нужно сохранить промежуточные результаты (например, @account и @update) в таблице (temp), затем запустите окончательный SELECT для этой таблицы (temp), чтобы получить желаемый набор результатов
  • , пока вы фактически не обновляете физическую таблицу, вымогут возникнуть проблемы, если у вас нет доступа к «обновлению» таблицы

ПРИМЕЧАНИЕ. Вы можете поместить логику курсора / ОБНОВЛЕНИЕ в сохраненный процесс, вызвать его через прокси-таблицу иэто было бы всенизкий вывод из серии операторов «select @ account, @ update», которые будут возвращены вызывающему процессу в виде единого набора результатов ... но это целая «другая тема» о (несколько) сложном методе кодирования.

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

0 голосов
/ 02 июля 2019

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

Добавление start / commit, по-видимому, сокращает время выполнения на 1,5–3 секунды.

Добавление первичного ключа к целевой таблице дало наилучшее сокращение, сократив время выполнения примерно до 13 секунд.

Преобразование слияния в условную логику в этом случае было наилучшим вариантом, достигая результата примерно за 8 секунд.

При использовании условных выражений первичный ключ в целевой таблице наносит небольшой ущерб (около 1 секунды), но впоследствии его существенное сокращение значительно сокращает время, поскольку эта таблица является лишь предыдущим шагом для большого соединения. (То есть результат этого слияния записей последний раз используется в объединении с 11+ таблицами.) Поэтому я сохранил P.K.

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

Вот упрощенный пример.

create table #source_t(account varchar(10), event varchar(10));

Insert into #source_t(account, event) values ('account1','event 1');
Insert into #source_t(account, event) values ('account1','event 2');
Insert into #source_t(account, event) values ('account1','event 3');

Insert into #source_t(account, event) values ('account2','came');
Insert into #source_t(account, event) values ('account2','saw');
Insert into #source_t(account, event) values ('account2','conquered');

create table #target(
    account varchar(10), -- make primary key if the result is to be joined afterwards.
    event_list varchar(2048)
);

declare ciclo cursor for
select account, event
from #source_t c
order by account --,...
for read only;

declare @account varchar(10), @event varchar(40), @last_account varchar(10), @event_list varchar(1000)

open ciclo

fetch ciclo into @account, @event

set @last_account = @account, @event_list = null

begin tran

    while @@sqlstatus = 0 BEGIN 

        if @last_account <> @account begin  -- if current record's account is different from previous, insert into table the concatenated event string  
            insert into #target(account, event_list) values (@last_account, @event_list)        
            set @event_list = null -- Empty the string for the next account
        end

        set @last_account = @account -- Copy current account to the variable that holds the previous one
        set @event_list = case @event_list when null then @event else @event_list + ' | ' + @event end -- Concatenate events with separator

        fetch ciclo into @account, @event
    END

    -- after the last fetch, @@sqlstatus changes to <> 0, the values remain in the variables but the loop ends, leaving the last record unprocessed.
    insert into #target(account, event_list) values (@last_account, @event_list)

commit tran

close ciclo

deallocate cursor ciclo;

select * from #target;

drop table #target;

drop table #source_t;

Результат:

account |event_list                 |
--------|---------------------------|
account1|event 1 | event 2 | event 3|
account2|saw | came | conquered     |

Этот код работал достаточно быстро в моем реальном случае использования. Однако его можно было бы дополнительно оптимизировать, отфильтровав исходную таблицу, чтобы она содержала только те значения que, которые необходимы для последующего объединения. В связи с этим я сохранил окончательный объединенный набор результатов (за исключением объединения с #target) в другой временной таблице, оставив некоторые столбцы пустыми. Затем #source_t был заполнен с использованием только учетных записей, присутствующих в наборе результатов, обработал его до #target и, наконец, использовал #target для обновления конечного результата. При этом время выполнения для производственной среды сократилось примерно до 8 секунд (включая все этапы).

Решения UDF уже решали эту проблему для меня раньше, но в ASE 15 они должны быть привязаны к таблице и должны написать одну функцию на столбец. Кроме того, это возможно только в среде разработки, не авторизованной для производства из-за привилегий только для чтения.

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

Условная логика приводит к еще большей производительности, но идет на более сложный код (чем больше кода, тем больше вероятность ошибок).

...