☰
Oracle INSERT INTO 详解:语法、批量插入与性能优化
2026/10/5 7:49:37 网站建设 项目流程

1. INSERT INTO 基础语法与核心用法

做Oracle开发这么多年,每次看到新人写INSERT INTO语句,我都会先让他们把基础语法背熟。这个语法本身看着简单,但真正用好了、用对了,其实有不少门道。我们先从最标准的写法说起。

1.1 单行插入的标准写法

INSERT INTO最基础的形态就是往表里插一行数据,语法长这样:

INSERT INTO 表名 (列1, 列2, 列3, ...) VALUES (值1, 值2, 值3, ...);

这里有几个关键点。列名列表和VALUES中的值必须一一对应,顺序不能乱。如果你省略列名列表,那么Oracle会默认按照建表时的字段顺序来匹配值,这种写法很不推荐,因为一旦表结构变动(比如加了字段),你的INSERT语句直接报错,问题排查起来非常痛苦。

我们来看一个实际例子。假设有一张员工表:

CREATE TABLE emp ( empno NUMBER(4) PRIMARY KEY, ename VARCHAR2(20), job VARCHAR2(30), sal NUMBER(8,2), hiredate DATE );

最规范的插入写法是:

INSERT INTO emp (empno, ename, job, sal, hiredate) VALUES (1001, '张三', '开发工程师', 15000, SYSDATE);

这里我要强调一个习惯:永远写完整列名,永远让列名和值一一对应。哪怕你只是给一张表插入数据,也要把列名列全。这不仅是规范问题,更是团队协作的底线。你永远不知道半年后接手你代码的人是谁,也不知道这张表会被改成什么样。

再看一个细节:字符串用单引号包裹,数字直接写,日期用TO_DATE或SYSDATE这样的函数处理。很多新手在插入日期字段时会直接写'2024-01-01',这在某些会话参数下可能隐式转换成功,但遇到NLS_DATE_FORMAT不对的环境就会直接报ORA-01861类似的错误。后面我会专门讲这个问题。

1.2 多行插入的三种实现方式

开发中经常需要一次插入多行数据。Oracle 11g提供了几种方案,我们逐一分析一下。

第一种是写多条INSERT语句,用斜杠或分号分隔,批量执行:

INSERT INTO emp (empno, ename, job, sal, hiredate) VALUES (1002, '李四', '测试工程师', 12000, SYSDATE); INSERT INTO emp (empno, ename, job, sal, hiredate) VALUES (1003, '王五', '运维工程师', 13000, SYSDATE); COMMIT;

这种方式直白、可读性好,适合插入几十条以内的场景。但缺点也明显,每一条都是一次独立的SQL执行,在网络往返和数据操作上有较大开销。

第二种是Oracle 11g支持的INSERT ALL语法,一条语句插多行:

INSERT ALL INTO emp (empno, ename, job, sal, hiredate) VALUES (1004, '赵六', 'DBA', 20000, SYSDATE) INTO emp (empno, ename, job, sal, hiredate) VALUES (1005, '孙七', '架构师', 25000, SYSDATE) INTO emp (empno, ename, job, sal, hiredate) VALUES (1006, '周八', '产品经理', 18000, SYSDATE) SELECT 1 FROM DUAL;

这里有个小知识点,INSERT ALL语句最后必须带一个SELECT子句,哪怕只是从DUAL中选一个常量。它的原理是:SELECT子句返回多少行,后面的每个INTO子句就执行多少次循环插入。如果只写SELECT 1 FROM DUAL,就是一行触发一次批量插入。如果用SELECT ... FROM一个大表,就能把大表里的数据按条件分发到多个目标表。

第三种是INSERT INTO ... SELECT,从一个结果集批量插入到目标表:

INSERT INTO emp (empno, ename, job, sal, hiredate) SELECT id, name, job_type, salary, create_time FROM temp_emp WHERE status = 'ACTIVE';

这是实际工作中最常用的方式,比如数据迁移、临时表落地、汇总结果入库,都靠它。INSERT INTO ... SELECT不需要VALUES子句,直接把查询结果灌进表里,插入行数等于SELECT返回的行数。

我自己的习惯是:少于10条手工数据,直接用第一种多写几条;有大量数据要插,优先考虑第三种;需要同时往多张表插,才用INSERT ALL。方案选型取决于场景,没有绝对的好坏。

1.3 用子查询作为VALUES来源的写法

还有一种容易被忽略的写法——VALUES后面直接跟子查询:

INSERT INTO emp (empno, ename, job, sal, hiredate) VALUES ( (SELECT MAX(empno) + 1 FROM emp), '吴九', '安全工程师', (SELECT AVG(sal) * 1.2 FROM emp), SYSDATE );

这种写法适合从一个动态结果中取单个值作为插入内容。但我要提醒一句:子查询里如果有聚合函数,务必确保它只返回一行一列,否则会报ORA-01427: single-row subquery returns more than one row。实际项目中这种写法不常用,但面试题喜欢出,掌握它对理解Oracle的执行机制有帮助。

2. 插入数据时的类型转换与空值处理

这一步开始进入真正容易出错的地方。INSERT语句写错并不可怕,可怕的是语句能执行成功但数据是错的。类型转换和空值处理就是两种“坑你没商量”的场景。

2.1 隐式类型转换的隐患

Oracle是弱类型语言,很多场景下会自动做类型转换。比如VARCHAR2和NUMBER之间、VARCHAR2和DATE之间,都能做隐式转换。但“能转”不等于“该转”。

举一个真实的案例。我之前处理过一个工单,某张表中有一个VARCHAR2类型的字段phone_no,存储手机号。某天开发同事写了一条插入语句:

INSERT INTO contact (contact_id, phone_no, remark) VALUES (1, 13812345678, '客户电话');

手机号没有加引号,Oracle会把13812345678当数字处理,然后隐式转换成字符'13812345678',这时候长度刚刚好,没出事。但如果手机号是02134567890,Oracle会尝试转成字符,结果可能变成'2134567890'——前面的0丢了!这种问题极难排查,因为SQL执行成功、数据也能查出来,但值就是不对。

所以第一条铁律:字符类型字段的值,永远加单引号;数字类型字段的值,不要加引号,不要依赖隐式转换。如果要插入的字符串本身含有单引号,比如一个人名叫O'Brien,就要写两个连续的单引号转义:

INSERT INTO contact (contact_id, phone_no, remark) VALUES (2, '13812345678', 'O''Brien负责人');

日期字段的隐式转换是另一个重灾区。Oracle在把字符转成日期时,用的是当前会话的NLS_DATE_FORMAT参数。默认情况下可能是'DD-MON-RR',也可能是'YYYY-MM-DD',取决于安装时的配置。如果你写:

INSERT INTO emp (empno, ename, job, sal, hiredate) VALUES (1007, '郑十', '人事', 9000, '2024-13-01');

这个字符串不是合法日期,Oracle一定会报ORA-01843: not a valid month。更隐蔽的是,字符串看起来合法但格式和NLS参数不匹配时,也可能报ORA-01861: literal does not match format string。

规避方案很简单,插入日期时坚持用TO_DATE显式转换:

INSERT INTO emp (empno, ename, job, sal, hiredate) VALUES (1007, '郑十', '人事', 9000, TO_DATE('2024-01-15', 'YYYY-MM-DD'));

这样无论会话参数怎么变,你的SQL都能稳定执行。

2.2 NULL与空字符串的陷阱

Oracle里有个非常反直觉的特性:空字符串''在Oracle中被视为NULL。这和SQL Server、PostgreSQL都不一样。很多从MySQL转过来的人在这里栽过跟头。

这意味着什么呢?如果你插入一个空值到VARCHAR2字段:

INSERT INTO emp (empno, ename, job, sal, hiredate) VALUES (1008, '', '临时工', 5000, SYSDATE);

ename字段存进去的实际上是NULL,而不是''。查询时用WHERE ename = ''是查不到的,必须用WHERE ename IS NULL。这个行为直接影响你的业务查询逻辑,我遇到不止一次因为这个问题导致的线上数据问题。

那么什么时候用NULL,什么时候用显式值?我的建议是:

  • 需要在语义上表示“无值”的字段,直接写NULL,或者省略该列。
  • 需要表示“空字符串但有含义”的字段(比如协议号、占位符),Oracle 11g下做不到,只能存一个特殊标记比如'EMPTY'或'#'。
  • 主键、唯一约束、NOT NULL约束的字段,插入NULL一定会报ORA-01400: cannot insert NULL into,这个要格外注意。

还有一类容易踩的坑是默认值。表设计时如果一个字段设置了DEFAULT,而你INSERT时写了NULL,那么Oracle会真的把NULL存进去,不会用默认值替换。DEFAULT值只在INSERT语句中完全不提这个列时才生效。这是很多DBA面试常问的点。举个例子:

CREATE TABLE t_test ( id NUMBER PRIMARY KEY, status VARCHAR2(10) DEFAULT 'NEW', remark VARCHAR2(200) ); -- 下面两条插入的结果不一样 INSERT INTO t_test (id, status, remark) VALUES (1, NULL, '第一条'); INSERT INTO t_test (id, remark) VALUES (2, '第二条'); -- 第一条status是NULL,第二条status是'NEW'

如果你希望即使显式插入NULL也触达默认值,就得用NVL或COALESCE在应用层先处理。这点很多开发人员没意识到。

2.3 主键冲突、唯一约束与触发器联动

插入数据时最容易遇到的硬性错误就是主键冲突。表里已经存在相同主键值,再插一条,Oracle会报ORA-00001: unique constraint violated。这个错误非常常见,尤其是并发环境下两条会话同时插入相同主键。

我推荐的处理方案是:

  • 简单场景,插入前先SELECT判断是否存在,存在则UPDATE。
  • 高并发场景,不要SELECT+INSERT,要用MERGE(后面会讲)。
  • 序列为主键的场景,确保用sequence.NEXTVAL获取值,不要直接手工指定数字。

另外,表上若有触发器,INSERT时会联动执行。触发器里做的操作可能改变插入的数据,也可能在执行时报错导致整个INSERT回滚。开发时要清楚表上挂了哪些触发器。Oracle里可以通过查询DBA_TRIGGERS或USER_TRIGGERS来确认:

SELECT trigger_name, status, trigger_type, triggering_event FROM user_triggers WHERE table_name = 'EMP';

触发器带来的一个经典坑是:如果触发器对插入的每一行执行一条额外的更新或其他操作,批量插入1万条数据时会非常慢,性能直接退化到让人抓狂。这个我在后面性能优化部分再展开。

3. 高级插入技巧与批量性能优化

前面的内容覆盖了INSERT的基本功,这一步我们聊点更进阶的。实际项目中,数据量上去了,你才会真正开始关注INSERT的性能和灵活性。

3.1 INSERT ALL / INSERT FIRST 的条件分发

很多人只知道INSERT ALL能一次插多行,不知道它最强的用法是按条件把同一份数据分发到不同的表。这就是所谓的“条件插入”。

语法是这样的:

INSERT ALL WHEN sal >= 20000 THEN INTO emp_high (empno, ename, job, sal, hiredate) WHEN sal >= 10000 THEN INTO emp_mid (empno, ename, job, sal, hiredate) ELSE INTO emp_low (empno, ename, job, sal, hiredate) SELECT empno, ename, job, sal, hiredate FROM temp_emp;

这个执行逻辑是:SELECT每返回一行,就从上到下依次判断WHEN条件,如果匹配就插入对应的表。INSERT ALL是没有短路效果的——一行数据如果同时满足多个WHEN条件,会被插入多张表。如果你想实现类似CASE WHEN的短路效果,一行只进一张表,就要用INSERT FIRST:

INSERT FIRST WHEN sal >= 20000 THEN INTO emp_high (empno, ename, job, sal, hiredate) WHEN sal >= 10000 THEN INTO emp_mid (empno, ename, job, sal, hiredate) ELSE INTO emp_low (empno, ename, job, sal, hiredate) SELECT empno, ename, job, sal, hiredate FROM temp_emp;

INSERT FIRST一旦某个条件命中,就不再检查后面的条件。这是一个很实用的数据归档工具。比如按月把流水数据分到不同分区表,或者按类型把日志数据分到不同业务表,一句SQL就能搞定,比在应用层写循环优雅得多。

我之前做过一个业务归档的活,源表有3000多万条流水,要按业务类型拆到十几个子表里。如果用Java程序一条条处理,估计要跑一个小时。换成INSERT FIRST加适当并行,几分钟就完成了。这个语法的价值在数据量大时体现得尤其明显。

3.2 大批量插入的提速方案

当你要插入的数据量达到几十万、上百万级别时,普通的INSERT语句已经不够看了。这里有几个经过验证的提速方案。

第一个是关闭表的日志选项。对目标表执行:

ALTER TABLE emp NOLOGGING;

然后执行INSERT INTO ... SELECT,再切回来:

ALTER TABLE emp LOGGING;

NOLOGGING会把插入操作产生的重做日志和归档日志降到最低,大幅减少写日志的I/O开销。但代价是:如果数据库在这期间崩了,这部分数据恢复不了,需要重新导入。所以只适合导入中间表、临时表或可以重建的数据。

第二个是用APPEND提示。常规INSERT语句是直接往表的高水位线以下的空闲块里插,会跟已有数据混在一起。而:

INSERT /*+ APPEND */ INTO emp (empno, ename, job, sal, hiredate) SELECT ... FROM temp_emp;

这个提示会让Oracle直接把数据追加到表尾部,绕过空闲块扫描,速度提升非常明显。它和NOLOGGING配合使用效果更佳。注意APPEND对大表和小表都有效,但APPEND提示在并发插入时要注意,因为加了这个提示后,它会锁住表不让其他会话并发插入(至少在某些锁机制下是这样),这点要特别小心。

第三个是分批提交。如果数据量极大,一次INSERT太多容易撑爆UNDO表空间,导致ORA-30036这样的错误。建议每10万行左右做一次COMMIT。在PL/SQL里可以这样写:

DECLARE v_cnt NUMBER := 0; BEGIN FOR rec IN (SELECT * FROM temp_emp) LOOP INSERT INTO emp (empno, ename, job, sal, hiredate) VALUES (rec.empno, rec.ename, rec.job, rec.sal, rec.hiredate); v_cnt := v_cnt + 1; IF MOD(v_cnt, 100000) = 0 THEN COMMIT; END IF; END LOOP; COMMIT; END;

不过说句实在话,能用SQL完成的批量插入就不要用PL/SQL循环。只有需要对每一行做复杂的逻辑判断、或数据本身来自非关系型源时,才建议用FOR循环加批提交。

第四个是并行DML。在数据量特别大的场景下,可以启用并行插入:

ALTER SESSION ENABLE PARALLEL DML; INSERT /*+ PARALLEL(emp, 4) */ INTO emp SELECT /*+ PARALLEL(temp_emp, 4) */ * FROM temp_emp;

并行度根据CPU核数和IO能力来定,一般4到8之间比较稳妥。并行DML会把插入任务拆成多个并行执行单元,利用多核CPU优势。但要注意,并行DML在执行期间会持有表锁,同时会占用较多的资源,不适合在高峰期对核心表操作。

3.3 MERGE语句与“有则更新,无则插入”经典场景

实际开发中,“这条记录如果存在就更新,不存在就插入”是永远躲不开的需求。传统做法是SELECT判断,然后UPDATE或INSERT,容易产生竞态条件,也不高效。Oracle 11g里应该用MERGE。

基本语法如下:

MERGE INTO emp t USING (SELECT 1008 AS empno, '钱十一' AS ename, '顾问' AS job, 28000 AS sal FROM DUAL) s ON (t.empno = s.empno) WHEN MATCHED THEN UPDATE SET t.ename = s.ename, t.job = s.job, t.sal = s.sal WHEN NOT MATCHED THEN INSERT (t.empno, t.ename, t.job, t.sal, t.hiredate) VALUES (s.empno, s.ename, s.job, s.sal, SYSDATE);

ON子句里的条件是匹配逻辑,通常是主键或唯一键。WHEN MATCHED时执行UPDATE,WHEN NOT MATCHED时执行INSERT。这个语句的优势在于:一条SQL完成两种操作,不产生中间的读与写竞态窗口,执行效率也更高。

使用MERGE有几个要注意的点:

  • ON条件中的字段必须是唯一性的,否则多行匹配会导致报错ORA-30926: unable to get a stable set of rows in the source tables。
  • USING子句可以是一张表、一个视图、一个子查询。数据量大时,USING子查询的语句会影响整体性能。
  • WHEN MATCHED分支中的UPDATE可以只更新部分列,也可以更新全部列。
  • 如果插入的源数据里存在重复数据,MERGE之前最好去重,不然很容易踩ORA-30926的坑。

我自己一般把MERGE作为“同步数据”的默认选项。比如每天从外部系统同步一批用户数据来更新本地库,直接MERGE一把梭过去,比用别的方式省事多了。

3.4 回滚与事务边界的重要性

插入操作默认是在一个事务里的,如果没有显式COMMIT,数据对其他会话不可见,自己可以看到(受隔离级别影响)。这时候如果会话断开,未提交的数据会自动回滚。这个机制既是保护,也可能造成困扰。

我在生产环境见过一个事故现场:某同事在PL/SQL Developer里执行了一批INSERT,结果会话卡住被管理员杀掉,数万条插入全部回滚,等于白干。他以为是数据库“丢了数据”,其实是事务没有被提交。

正确的做法是:

  • 测试环境执行完INSERT,及时COMMIT。
  • 生产环境做大表插入前,先确认数据量和影响,再决定提交节奏。
  • 大批量数据分片提交,每一片COMMIT一次,避免一次事务过大。

另外,用PL/SQL Developer、Navicat这类工具连Oracle时,要留意工具的自动提交设置。有的默认开了自动提交,有的默认没开,导致不同环境下同样一条INSERT行为差异很大。我建议显式地把COMMIT写进脚本,别依赖工具设置。

4. 插入时的高频报错与排查实战

无论你写多少年代码,INSERT时总会碰到各种报错。这里我把最常见的几类整理出来,并且给出排查思路,直接照着查就行。

4.1 高频错误速查表

错误代码典型信息含义与排查方向
ORA-00001unique constraint violated主键/唯一键冲突,检查是否重复插入同键值
ORA-01400cannot insert NULL into向NOT NULL字段插入了NULL,检查列值和表结构
ORA-01401inserted value too large for column插入的字符串超过字段长度,用LENGTH()检查数据
ORA-01843not a valid month日期字符串不是合法月份,检查月份值
ORA-01861literal does not match format string日期字符与NLS格式不匹配,改用TO_DATE
ORA-00904invalid identifierSQL中引用了不存在的列,检查列名拼写
ORA-00947not enough valuesVALUES数量比列名数量少,逐一对齐
ORA-00913too many valuesVALUES数量比列名数量多,检查多写或少写列名
ORA-30036unable to extend segment by 8 in undo tablespaceUNDO空间不足,缩小单次事务规模
ORA-30926unable to get a stable set of rows in the source tablesMERGE时源表产生多行匹配,先对源表去重

我把ORA-00947和ORA-00913列在一起说,因为这两个正好相反。一个值是少了,一个是多了。遇到这两个错误,不要慌,从头到尾数一遍列名和VALUES里的值,几乎都能找到答案。

4.2 用回滚段与SQL追踪排查问题

排查插入问题时,除了看报错,还可以用Oracle提供的工具做诊断。这里介绍一个我常用的思路。

当一条INSERT语句超时或异常慢时,我会先去查执行计划,看看是不是走了全表扫描或者有性能瓶颈:

EXPLAIN PLAN FOR INSERT INTO emp (empno, ename, job, sal, hiredate) SELECT * FROM temp_emp; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

执行计划里最值得关注的是有没有出现“TABLE ACCESS FULL”、“SORT”、“HASH JOIN”这样的操作,以及Estimated Rows与实际返回行数是否差距很大。

如果插入期间数据库会话卡住,可以使用V$SESSION和V$LOCK视图来查看会话状态。比如:

SELECT sid, serial#, status, event, wait_class FROM v$session WHERE username = '你的用户';

这里的EVENT会显示会话当前等在什么事件上,比如eng: TX - row lock contention说明是等锁,log file sync说明是等日志写入。

有一条经验我特别想说:INSERT卡住时先考虑锁,别急着看SQL性能。并发环境下,一条看似简单的INSERT语句等了几分钟不返回,很可能就是有别的会话锁住了目标表或某一行。查询阻塞源:

SELECT blocking_session, sid, serial#, wait_class FROM v$session WHERE blocking_session IS NOT NULL;

找到阻塞SESSION之后,跟业务方确认那个会话是不是可以终止。如果是僵尸会话,可以用ALTER SYSTEM KILL SESSION 'sid,serial#'来清理。这个操作要慎用,确认是无害会话再杀。

4.3 字符集与中文乱码的排查

Oracle数据库插入中文数据出现乱码,几乎是绕不开的坑。这种情况下,INSERT语法没有任何问题,也不报错,但查询出来的中文字符变成了问号或者其他乱码。根本原因基本都出在字符集不一致。

排查步骤分三步:

  • 查看数据库字符集:SELECT value FROM nls_database_parameters WHERE parameter = 'NLS_CHARACTERSET';
  • 查看客户端字符集:在Linux下执行echo $NLS_LANG,或在Windows的注册表里看NLS_LANG。
  • 确认文件或程序编码方式。

如果数据库字符集是AL32UTF8,客户端NLS_LANG也设成AL32UTF8,基本不会乱码。如果数据库是ZHS16GBK,但客户端NLS_LANG设成AL32UTF8,插入的数据经客户端编码后,在数据库端解释时会产生错位,进而出现乱码。

在SQL*Plus里执行插入前,先执行:

ALTER SESSION SET NLS_LANGUAGE='SIMPLIFIED CHINESE';

同时确保系统环境变量NLS_LANG和数据库字符集匹配。这样处理之后,绝大多数字符集问题都能消除。如果做导出导入,还要注意dmp文件内部字符集的一致性,这属于另一个话题,这里不展开。

4.4 插入性能突然变慢的排查思路

INSERT语句原本执行很快,某天突然变慢,这背后的原因大概率不在INSERT本身,而在于它的执行路径上。我通常按以下顺序排查。

先查看是否有索引或触发器的影响。索引过多会拖慢插入,因为每次插入都要维护索引。尤其针对VARCHAR2字段建了多个索引,再赶上大批量插入,索引维护的开销会非常可观。触发器则意味着每条插入后可能还有额外的SQL要执行。看看表结构:

SELECT index_name, column_name, uniqueness FROM user_ind_columns WHERE table_name = 'EMP';

然后检查表和索引的统计信息是否过期。统计信息不准会导致优化器选择错误的执行计划,比如INSERT INTO ... SELECT本来应该走嵌套循环却走了哈希连接。执行:

EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'EMP', CASCADE => TRUE);

最后检查UNDO表空间和归档日志是否充足。UNDO空间不足时,长事务会被迫频繁回滚或直接报ORA-30036。归档日志空间满则会导致LGWR进程卡住,数据库整体写入变慢。这两类问题和INSERT本身关系不大,但表现症状都是插入变慢或停滞。

顺着这个思路排查,绝大多数“INSERT突然变慢”的问题都能定位到真正的根因。

5. 实战场景与最后提醒

前面几章已经把INSERT的语法、注意事项、高级技巧和排查方法都过了一遍,最后我结合自己实际做过的项目场景,把一些散落的经验收拢一下。

5.1 数据初始化脚本中的INSERT规范

我会习惯性地加注释和分批提交。比如给客户交付一套初始化脚本,里面如果有上千条INSERT,我会这样做:

-- 插入基础数据,分块执行,每200条一个事务 SET FEEDBACK ON INSERT INTO dict_type (type_id, type_code, type_name) VALUES (1, 'STATUS', '状态'); INSERT INTO dict_type (type_id, type_code, type_name) VALUES (2, 'LEVEL', '级别'); COMMIT;

每200条左右COMMIT一次,既能控制UNDO占用,也便于出错时定位是哪个批次的问题。所有日期值都用TO_DATE包起来,所有字符串值都检查是否有单引号需要转义。

5.2 数据迁移时如何保证不丢不重

用INSERT INTO ... SELECT做数据迁移时,我最看重两件事:数据量对账和去重。迁移前先统计源表行数,迁移后再统计目标表行数。对不上就说明中间有问题。去重在Oracle里有个很便捷的写法,用ROW_NUMBER()排重:

INSERT INTO emp (empno, ename, job, sal, hiredate) SELECT empno, ename, job, sal, hiredate FROM ( SELECT e.*, ROW_NUMBER() OVER (PARTITION BY empno ORDER BY sal DESC) AS rn FROM temp_emp e ) WHERE rn = 1;

PARTITION BY后面跟的是唯一性判断的字段,一般就是主键或业务键。这个写法比用DISTINCT要灵活,因为你可以在分组的排序上做文章。比如想保留工资最高的那条记录,就按工资倒序排。

5.3 从MySQL或SQL Server迁移到Oracle时INSERT的几个坑

最近几年,很多传统行业项目在从SQL Server或MySQL迁移到Oracle,迁移过程中INSERT行为差异经常引发兼容性问题。这里拿我的经验重点说几点。

MySQL支持INSERT语句中带IGNORE选项,比如INSERT IGNORE INTO,Oracle不认这个语法。MySQL用ON DUPLICATE KEY UPDATE实现“有则更新、无则插入”,标准Oracle应该用MERGE。SQL Server支持多行VALUES用逗号分隔的一次性插入:

INSERT INTO emp (empno, ename) VALUES (1, 'a'), (2, 'b'), (3, 'c');

Oracle 11g不支持这种写法。以上这些,如果你的迁移代码里有,就必须在Oracle里改写,否则直接报语法错误。这也是我之前在兼容改造项目中花时间最多的地方。

空字符串被当成NULL这个坑,前面已经详细说了,这里再强调一次:迁移到Oracle后,之前所有用等于空字符串判断的业务逻辑,都要改成IS NULL。这个问题在数据比对阶段尤其容易漏。

5.4 关于INSERT INTO的几条长期经验

做Oracle开发这么多年,和INSERT纠缠的问题是所有数据库操作里最多的。我总结了几条长期有效的经验,分享给大家。

第一,能用SQL解决的就不要用循环。哪怕是复杂的业务逻辑,尽力先在SQL层面用MERGE、INSERT ALL、子查询组合出结果。数据库擅长集合运算,程序语言擅长逐行处理。把集合运算交给数据库,效率永远更高。

第二,对一张频繁插入的大表,不要建太多索引,二三个足够。每多一个索引,INSERT速度就会明显下降。索引尽量建在查询频率高且区分度高的字段上。

第三,大批量导入前,先和业务方确认好“数据是否可以回滚”,根据这个来决定是否用NOLOGGING和APPEND。如果数据非常重要、要求不能丢,就别贪图APPEND的提速,老老实实用普通插入。

第四,生产环境的任何插入脚本,先在测试库完整跑一遍,连数据量都要对齐。这一步省下来,大概率会在生产环境付出更大的代价。

第五,永远是先查一下目标表的约束和触发器再动大批量插入。花费五分钟查看系统表,比插入一半报错再从头排查快得多。

从最基本的单行插入到MERGE同步、到批量的条件分发、到性能优化和错误排查,INSERT INTO这一条SQL语句背后能承载的内容远比想象中多。希望这篇分享能帮你少踩几个坑,在这个细节决定成败的领域走得踏实一些。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询