【问题标题】:Using merge to combine matching records使用merge合并匹配的记录
【发布时间】:2019-06-28 20:18:54
【问题描述】:

我正在尝试将一个表中的匹配记录合并到另一个表的单个记录中。我知道这可以通过 group by 和 sum()、max() 等来完成,我的困难是不属于 group by 的列是我需要连接的 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'

但是,我得到的只是与#source 相同的记录。

在我看来,合并中的匹配是在语句执行开始时针对表的“状态”尝试的,因此 whenmatch 永远不会执行。有没有办法告诉 DBMS 匹配更新后的目标表?

我设法通过使用游标获得了我需要的结果,因此合并语句执行了n次,n是#source中的记录数,因此合并实际上执行了匹配时部分。

它的问题在于性能,以这种方式删除重复项大约需要 5 分钟才能将 63K 记录合并为 42K。

有没有更快的方法来实现这一点?

【问题讨论】:

  • 5 分钟对于 63K 行来说是相当多的;虽然 tempdb 中的日志写入应自动延迟,但我想知道您的进程是否因大量独立的单事务 update 语句而减慢;您是否尝试过将光标/合并循环包装在一个“开始/提交 tran”对中?目标是限制日志写入的数量,这反过来可能会加快速度;另一个考虑是merge 语句存在性能问题,所以想知道是否......
  • ... 您可以将 merge 替换为您自己的 if/then/else 块,该块将公共帐户的事件附加到变量中,然后当帐户更改时,您刷新附加的事件'last' account to #target(你的光标需要有一个'order by account...'子句);目标是 a) 查看merge 是否存在问题并减少对#target 的写入次数(在这种情况下,您将执行 42K 插入且没有更新);您可以使用和不使用“开始/提交 tran”包装器(围绕光标循环)进行测试,看看是否有任何区别
  • 原来我忽略了创建目标表的主键。一旦我这样做了,执行时间就会下降到大约 16 秒。在循环周围添加开始/提交似乎会减少一两秒。
  • 啊,是的,那(添加 PK/index 以加快查找速度)绝对可以产生差异;通过向 100KB 表添加索引,我已经将 2.1 小时的批处理过程缩短了 2 小时……一定会喜欢那些“简单”的修复;无论如何,很高兴听到您发现了性能问题

标签: sql merge sap-ase


【解决方案1】:

当使用 UPDATE 语句更新 @variable 时,它​​有一个鲜为人知的方面(记录不充分?),它允许您在基于集合的 UPDATE 操作中累积/连接 @variable 中的值。

这更容易用一个例子来“解释”:

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

专业版:

  • 单个 UPDATE 语句处理单个帐户值

缺点:

  • 仍然需要一个游标来处理一系列帐户值
  • 如果您想要的最终输出是单个结果集,那么您需要将中间结果(例如,@account 和 @update)存储在 (temp) 表中,然后针对该 (temp) 表运行最终 SELECT 以生成所需的结果集
  • 虽然您实际上并未更新物理表,但如果您无权“更新”表,则可能会遇到问题

注意:您可以将游标/更新逻辑放在存储过程中,通过代理表调用过程,这将允许将一系列“选择@account,@更新”语句的输出返回到将进程作为单个结果集调用......但这是一个完整的“关于(有点)复杂的编码方法的另一个主题。

对于您的流程,您将需要一个光标来循环遍历您唯一的一组帐户值,但您将能够消除遍历给定帐户的事件列表的光标开销。最终结果是您应该会看到运行流程所需的时间有所改善。

【讨论】:

  • 有趣的是,您回答了 OP 的需求(即相当于 MySQL 的 GROUP_CONCAT 或 Postgres 的 STRING_AGG 以及最近 SQL Server 2017 在 Sybase 中的 STRING_AGG()):stackoverflow.com/a/45092295/1422451
  • @Parfait 当然,但是 UDF 解决方案需要 ASE 16(OP 正在运行 ASE 15)和一个相对较新的版本,该版本修复了早期实现表变量的一些错误;有了更多的解决方法,可能可以为这个 OP/questoin 提供一个 UDF,但它必须为这个特定的表进行硬编码(ASE 16 的表变量的好处之一 =>构建通用 UDF 的能力)
  • ... 请记住,ASE 16 / 表变量解决方案需要相当复杂的实现格式,即将父查询的副本作为参数传递给 UDF ...
  • 确实,我已经使用特定于表的 UDF 解决了这种情况,但是在这种情况下,我需要对只有“选择”权限的生产数据库运行查询,所以我可以'不创建 UDF。此外,它是一次性报告。
  • 我喜欢循环遍历唯一帐户值的想法。但是,如果合并记录与源记录的比率更高,则改进会更明显。就我而言,它约为 66%。如果唯一帐户值少于源记录的一半,则性能提升会更多。
【解决方案2】:

在应用给定的建议并与我们的 DBA 交谈后,获胜的想法是放弃合并并在循环中使用逻辑条件。

添加开始/提交似乎将执行时间减少了 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 中,它们必须是特定于表的,并且需要每列编写一个函数。此外,这仅在开发环境中可行,由于只读权限而未授权生产。

总之,游标循环与合并语句相结合是一种使用某些值的连接来组合记录的简单解决方案。需要包含用于匹配的列的主键或索引来提高性能。

条件逻辑可以带来更好的性能,但代价是代码更复杂(代码越多,越容易出错)。

【讨论】:

    猜你喜欢
    • 2019-08-10
    • 2020-07-18
    • 2016-10-11
    • 1970-01-01
    • 2010-12-26
    • 2013-08-07
    • 2018-07-01
    • 2022-12-16
    • 1970-01-01
    相关资源
    最近更新 更多