ITPub博客

首页 > 数据库 > 数据库开发技术 > 通用SQL优化经典等价改写【七】——避免使用触发器

通用SQL优化经典等价改写【七】——避免使用触发器

原创 数据库开发技术 作者:梁敬彬 时间:2025-05-03 12:46:41 0 删除 编辑


往期回顾:



1. 引言

在数据库设计与开发中,触发器是一种常用的数据库对象,可以在表发生特定事件时自动执行预定义的操作。然而,过度依赖触发器会带来一系列问题,尤其是性能方面的挑战。本文将通过实际案例,详细分析触发器的性能影响,并提供等价改写的解决方案。

2. 触发器的两大主要问题

2.1 代码管理问题

触发器代码隐藏在数据库中,不在应用程序的版本控制系统中管理,导致:

应用开发人员难以感知触发器的存在
代码变更不易追踪
系统行为复杂度增加,调试难度提高

2.2 性能问题

触发器尤其是行级触发器(FOR EACH ROW)会导致严重的性能问题:

每修改一行数据就会触发一次操作
大批量操作时会产生大量递归调用
触发器中的SQL语句会增加数据库引擎负担
可能导致锁争用和死锁

3. 案例演示:触发器性能影响分析


下面通过一个实际案例,直观展示触发器对性能的影响。

3.1 测试环境准备

首先创建测试表和触发器:


-- 设置显示参数
set pagesize 3000;
set linesize 300;
set trim on
-- 创建测试表
drop table t purge;
create table t(msg_id number(15), user_id number(9), msg varchar2(4000));
-- 创建行级触发器,在插入前检查msg_id是否存在
create trigger tri_t
  before insert on t
  for each row
declare
  v_cnt number(1);
begin
  select count(*)
    into v_cnt
    from t
   where msg_id = :new.msg_id
     and rownum = 1;
end tri_t;
/

这个触发器会在每次插入新行时查询表中是否已存在相同的msg_id。

3.2 性能测试:启用触发器

-- 启用触发器
SQL> set timing on
SQL> alter trigger tri_t enable;
触发器已更改
已用时间: 00: 00: 00.03
-- 执行批量插入(3万条记录)
SQL> declare
  2    v_msgid number(15);
  3    v_usrid number(9);
  4  begin
  5    dbms_random.seed(1000000000);
  6    for i in 1 .. 30000 loop
  7      v_usrid := dbms_random.value;
  8      v_msgid := i;
  9      execute immediate 'insert into t (msg_id, user_id, msg) values (:p1, :p2,:p3)'
 10        using v_msgid, v_usrid, 'msg_' || i;
 11    end loop;
 12  end;
 13  /
PL/SQL 过程已成功完成。
已用时间: 00: 00: 46.62

结果: 启用触发器时,插入3万条记录耗时46.62秒。

3.3 性能测试:禁用触发器


-- 清空表
SQL> truncate table t;
表被截断。
已用时间: 00: 00: 00.35
-- 禁用触发器
SQL> alter trigger tri_t disable;
触发器已更改
已用时间: 00: 00: 00.00
-- 执行相同的批量插入
SQL> declare
  2    v_msgid number(15);
  3    v_usrid number(9);
  4  begin
  5    dbms_random.seed(1000000000);
  6    for i in 1 .. 30000 loop
  7      v_usrid := dbms_random.value;
  8      v_msgid := i;
  9      execute immediate 'insert into t (msg_id, user_id, msg) values (:p1, :p2, :p3)'
 10        using v_msgid, v_usrid, 'msg_' || i;
 11    end loop;
 12  end;
 13  /
PL/SQL 过程已成功完成。
已用时间: 00: 00: 01.81



结果: 禁用触发器时,插入3万条记录仅需1.81秒。


3.4 性能差异分析

通过对比可以看出:

启用触发器时:46.62秒
禁用触发器时:1.81秒
性能差异:约25.7倍
这一巨大差异主要源于:

行级触发器为每行执行一次,导致3万次触发器调用
每次触发器内都执行一次SELECT COUNT(*)查询
随着表数据量增加,查询性能进一步下降
产生大量的递归SQL和内部读取

4. 等价改写方案

针对触发器引起的性能问题,可以采用以下等价改写方案:

4.1 方案一:前置条件检查

如果触发器用于验证数据合法性,可以在应用层或存储过程中预先检查:

-- 批量插入前先检查重复值
declare
  v_msgid number(15);
  v_usrid number(9);
  v_duplicate_exists boolean := false;
begin
  -- 一次性检查重复值
  select case when count(*) > 0 then true else false end
    into v_duplicate_exists
    from (select distinct i 
          from (select level i from dual connect by level <= 30000))
    where exists (select 1 from t where msg_id = i);
    
  if v_duplicate_exists then
    raise_application_error(-20001, '存在重复的MSG_ID值');
  end if;
  
  -- 确认无重复后批量插入
  dbms_random.seed(1000000000);
  for i in 1 .. 30000 loop
    v_usrid := dbms_random.value;
    v_msgid := i;
    execute immediate 'insert into t (msg_id, user_id, msg) values (:p1, :p2,:p3)'
      using v_msgid, v_usrid, 'msg_' || i;
  end loop;
end;
/



4.2 方案二:批量处理替代行级触发

将行级触发器改为语句级触发器,并使用批量处理:


-- 改为语句级触发器并使用批量处理
create or replace trigger tri_t_statement
  after insert on t
  declare
    -- 用于临时存储需要处理的数据
    cursor c_inserted is
      select msg_id from t where msg_id not in 
      (select msg_id from t group by msg_id having count(*) = 1);
  begin
    -- 一次性处理所有重复数据
    for r in c_inserted loop
      -- 处理逻辑
      dbms_output.put_line('检测到重复的MSG_ID: ' || r.msg_id);
    end loop;
  end;
/



4.3 方案三:使用约束代替触发器

对于唯一性检查,可以使用数据库约束代替触发器:

-- 使用唯一约束替代唯一性检查触发器alter table t add constraint uk_t_msg_id unique (msg_id);

4.4 方案四:独立存储过程批量处理

将触发器逻辑移至独立的存储过程,在批量操作后一次性执行:


-- 创建独立存储过程
create or replace procedure validate_msg_ids as
  v_duplicates number;
begin
  -- 检查是否存在重复MSG_ID
  select count(*)
  into v_duplicates
  from t
  group by msg_id
  having count(*) > 1
  fetch first row only;
  
  if v_duplicates > 0 then
    dbms_output.put_line('检测到重复的MSG_ID');
    -- 处理重复记录的逻辑
  end if;
end;
/
-- 在数据加载后调用一次
declare
  v_msgid number(15);
  v_usrid number(9);
begin
  -- 批量插入
  dbms_random.seed(1000000000);
  for i in 1 .. 30000 loop
    v_usrid := dbms_random.value;
    v_msgid := i;
    execute immediate 'insert into t (msg_id, user_id, msg) values (:p1, :p2,:p3)'
      using v_msgid, v_usrid, 'msg_' || i;
  end loop;
  
  -- 数据加载完成后调用一次验证
  validate_msg_ids;
end;
/



5. 最佳实践建议

5.1 何时避免使用触发器

以下情况应当避免使用触发器:

大批量数据加载或更新操作
关键性能路径
多表级联操作
复杂业务逻辑
需要频繁维护的功能

5.2 何时可以考虑使用触发器

触发器在某些场景下仍然有其价值:

审计日志记录(小批量情况)
强制执行简单的业务规则
维护派生列或冗余数据(考虑延迟触发)
系统级约束实施

5.3 触发器替代方案

使用数据库约束(主键、唯一键、检查约束等)
使用应用层验证逻辑
使用存储过程和批处理
使用物化视图定期刷新

6. 结论

触发器虽然为数据库提供了强大的自动化能力,但在性能敏感的应用中应当谨慎使用。如本文案例所示,简单的行级触发器就可能导致性能下降25倍以上。通过等价改写,将行级触发器逻辑转换为批量处理或前置检查,可以在保持功能等价的前提下显著提升系统性能。

在实际开发中,应当根据业务需求和性能要求,选择合适的实现方式,避免过度依赖触发器,尤其是避免在高频率操作的表上使用行级触发器。通过合理的设计和实现,可以在保证业务功能的同时,最大化系统性能。


在这里插入图片描述


公众号:收获不止数据库

微信:ljbyxl1213

系列回顾



来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/9091385/viewspace-3083129/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
历任中电福富研究院副理事长、中盾安信研究院副院长、宁德时代技术顾问等职务,受聘电信集团专家和国家十四五课题负责人,著有《收获,不止Oracle》《收获,不止SQL优化》等多本畅销数据库技术书籍。

注册时间:2024-02-06

  • 博文量
    162
  • 访问量
    198261