SQL Load总结

公司需要将大量数据入库,原本使用mybatis insert数据,但是效率十分低,所以考虑kafka,备用方案sql load。
我是调查sql load方案的,虽然最后没有选择这个方案,但是在这过程中也学习到了很多,在这里总结一下。
我在网上搜到的信息都是类似的,看起来就出自2篇博客,都是入门,对实际操作的帮助不太大。
我这篇包括2个部分,前期调查的概念部分,后期实现的代码部分
参考书:《Oracle Database 11G 数据库管理艺术》
上思维导图
SQL Load总结_第1张图片

调查

功能

Sql Load是Oracle的数据加载工具,通常用于导入大批量的外部数据。

传统加载和直接加载

SQL Load分为传统加载和直接加载

传统加载:在原理上同insert相同,但是每次提交一组记录,从而提高效率
直接加载:利用数据构造列数组结构,用这些结构来格式化Oracle数据块,然后直接写入数据库表。

传统加载和直接加载的特点和区别都在思维导图里面了,精简的总结一下有这几条

  1. 直接加载要比传统加载要快,因为不涉及到insert;
  2. 同传统加载不同,直接加载只会产生少量的日志,这一点在dos执行sqlldr时就有体现,传统加载刷出很多信息
  3. 直接加载有一定局限性,比较明显的局限就是,
    (1). 在执行直接加载时需要保证该表没有锁;
    (2). 加载过程中不能修改表结构;
    (3). 插入数据时不应用表约束和触发器,将自动禁用所有外部键检查和检查约束,但仍保留非空、唯一和主键约束。(若指定了REENABLE子句,则随着sql load的完成,将自动启用被禁用的约束,否则需要手动开启约束)
    (4)若主键重复,会使主键失效,若导入时需要保留主键,建议使用传统装载,或装载前取消主键,删除索引,装载后重建
  4. 不能使用直接加载的场景(我自己都没遇到,也不太明白这些是啥):
    (1). 使用群表
    (2). 同时装载父表和子表,嵌套表
    (3). 装载VARRAY,BFILE列,对象列
    (4). 正在使用ORACLE NET装载
    (5). 需要使用sql函数,如果装载大量数据同时需要使用sql函数,则推荐使用外部表或表函数

结构:一般包含4种文件
5. 数据文件:dat、csv、txt --包含待导入的数据,每行映射到数据库中一条记录,包含若干字段值,可包含分隔符便于导入时切割数据
6. 控制文件:ctl --sql load命令
7. 日志文件:log --导入时的日志文件,显示多少正确被导入,多少出问题了没被导入,没导入的数据存到哪个文件里了
8. 问题数据文件:bad --导入时出问题的数据被保存到了这个文件

数据文件

我只试了txt类型的数据文件,我写的生成测试文件的代码能生成的文件的形式如下,以“|”分隔数据,因为数据的长度不固定,所以使用的是分隔符关键字拆分数据,后来我有想过,如果在写文件的时候能够固定每个字段数据的长度,则使用位置关键字POSITION(m:n)确定m到n位的数据,写入对应字段,这样因为省掉了查找分隔符的时间,能提高效率,具体能提高多少,我没试验过:
SQL Load总结_第2张图片

控制文件

控制文件的格式一般不会变,我用到的格式如下

LOAD DATA
CHARACTERSET AL32UTF8             --设置字符集
INFILE '数据文件全路径名'                  --指定数据文件
APPEND                                             --指定加载数据的方式,APPEND(追加),INSERT(表为空时插入,表不为空时失败),REPLACE(先清除掉表中数据,再插入)
INTO TABLE 表名                               --指定表名
FIELDS TERMINATED BY '|'              --指定分隔符
OPTIONALLY  ENCLOSED BY "'"      --有的数据文件两端会带引号,忽略引号
TRAILING NULLCOLS                       --若当前字段数据为空,则自动为null
(字段1,字段2,字段3,字段4, ... ... ,字段n)            --指定映射列

我们通过时间戳生成数据文件,然后通过控制文件load,所以这个控制文件里的数据文件名是实时都在变的,所以我是在java中创建控制文件然后再写到磁盘的,如果数据文件名,表名和字段名不变的话,也可以直接写好一个固定的控制文件,直接调命令执行即可。

Tip.
Q. 在导入数据时发现有DATE类型的数据,但是数据文件中是字符串,如何在导入数据时转换为DATE?
A. 在编写控制文件时,在字段后添加数据类型的说明date "yyyy-mm-dd",书里说的支持的主要数据类型有INTEGER,CHAR,FLOAT等,我试过DATE也可以。

Q.若表中要求有自增主键怎么办?
A.Oracle不像mysql,有自动的自增主键,Oracle的自增主键是通过序列Sequence和触发器Trigger实现的,Sql Load的传统加载能够正确触发自增主键触发器,但是直接加载不行,第一次加载跟第二次加载的主键是重复的,这样会使主键失效,我们当时的情况是,load进来的数据还需要进一步整理,因此我们后来的方案是,建一张temp表,该表没有主键,用直接加载的方式先把数据load进来,然后再用存储过程整理数据,向目标表中update或insert数据(没等做这一部分,方案就被pass了),这里给大家提供一个参考吧。

以上这些是最基本的用法,能够满足常规的大批量数据的导入。
参考控制文件如下

OPTIONS (skip=0) 
LOAD DATA 
CHARACTERSET AL32UTF8 
INFILE 'E:\Books\test2.txt' 
APPEND 
INTO TABLE TEST_TABLE 
FIELDS TERMINATED BY '|' 
OPTIONALLY  ENCLOSED BY "'" 
TRAILING NULLCOLS 
(CODE,VALUE,OTHER)

执行命令

有了数据文件,和对应的控制文件,就可以用dos调用sqlldr命令执行sql load操作了,前提是,你有安装Oracle客户端,如果没有客户端,就会提示sqlldr操作不是内部命令,这个时候,你需要装一个oracle客户端,或者放弃这个方案…

命令如下

sqlldr 用户名/密码@数据库地址  control=控制文件全路径名  log=日志文件全路径名   direct=true parallel=true

参数解释

  1. sqlldr是一个oracle命令,你点开控制台,输入这个命令就可以了,如果想在代码中执行,则需要使用Process类,具体如何操作见后续的代码。
  2. 数据库的用户名和密码就不用多说了,主要说这个数据库地址,数据库地址可以是你本地的localhost,也可以是远程的,远程的是一个IP地址和端口号以及数据库名,Sql Load支持使用本地的控制文件,将本地数据文件中的内容直接load到远程的数据库,只需要用一个远程数据库的IP即可,还是挺方便的。
  3. 控制文件和日志文件都是全路径名
  4. direct=true这个参数,意思是以直接加载的形式load数据,默认为false,即以传统加载的形式load数据
  5. parallel=true这个参数,书上说在数据量大时会允许并行运行,但是啊可但是,对他的解释就这一句,连怎么用都没说,我试了一下,并没有发现有什么明显的效率上的提高,看起来并没有并行运行。执行命令的时候每次也就只能一条一条的执行sqlldr,也不能一次执行多条,所以这是什么情况,咱也不知道,咱也没处问,十分迷茫。

代码

上代码了,代码参考http://www.zuidaima.com/share/4110896091040768.htm?f=archive_zip

生成测试数据文件

生成5000行,以“|”分隔的,10个字段的数据

public static void writeTestDataFile(String filePath) throws IOException {
        File fout = new File(filePath);
        FileOutputStream fos = new FileOutputStream(fout);
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos));
        for (Long i = 0L; i < 5000; i++) { //测试数据5000
            bw.write(i.toString());
            for(int j = 1; j < 10; j++){ //测试字段10
                bw.write( "|测试数据"+j);
            }
            bw.newLine();
        }
        bw.close();
    }

生成控制文件

 /**
     * * 写控制文件.ctl
     * @param fileFullName 数据文件全路径名
     * @param tableName 表名
     * @param fieldName 要写入表的字段,用逗号分隔,用括号括起
     * @param ctlfileFullName 控制文件全路径名
     */
    public static void ctlFileWriter(String fileFullName,String tableName,String fieldName,String ctlfileFullName)
    {
        FileWriter fw = null;
        String strctl = "OPTIONS (skip=0)" +
        " LOAD DATA CHARACTERSET AL32UTF8 INFILE '"+fileFullName+"'" +   //设置字符集编码和数据文件路径
        " APPEND INTO TABLE "+tableName+"" +
        " FIELDS TERMINATED BY '|'" +  //分隔符号
        " OPTIONALLY  ENCLOSED BY \"'\"" +
        " TRAILING NULLCOLS "+fieldName+"";
         try {
            fw = new FileWriter(ctlfileFullName);
            fw.write(strctl);
         } 
         catch (IOException e) 
        {
               e.printStackTrace();
        }
        finally {
            try 
            {
                fw.flush();
                fw.close();
            } 
            catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }

执行命令,加载数据

/**
     * 调用系统DOS命令
     * @param user 用户名
     * @param psw 密码
     * @param Database 数据库地址
     * @param ctlfileFullName 控制文件全路径名
     * @param logfileFullName 日志文件全路径名
     */
    public static void Executive(String user,String psw,String Database,String ctlfileFullName,String logfileFullName)
    {
        InputStream ins = null;
        //要执行的DOS命令
        String dos="sqlldr "+user+"/"+psw+"@"+Database+" control="+ctlfileFullName+" log="+logfileFullName+" direct=true parallel=true";//命令
        //Linux环境下注释掉不需要CMD 直接执行DOS就可以
        
        try
        {
            Process process = Runtime.getRuntime().exec(dos);
            ins = process.getInputStream(); // 获取执行cmd命令后的信息
            BufferedReader reader = new BufferedReader(new InputStreamReader(ins,Charset.forName("UTF-8")));
            String line = null;
            while ((line = reader.readLine()) != null)
            {
                System.out.println("调用dos的执行结果:" + line); // 输出
            }
            int exitValue = process.waitFor();
            if(exitValue==0)
            {
                System.out.println("返回值:" + exitValue+"\n数据导入成功");
                
            }else
            {
                System.out.println("返回值:" + exitValue+"\n数据导入失败");
                
            }
            process.getOutputStream().close(); // 关闭
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

调度代码

public static void doSqlLoad() {
    	
        //写控制文件.ctl
        String fileRoute = "E:\\Books\\";//文件地址路径
        String fileName = "test2.txt";//数据文件名
        String tableName = "TEST_SQLLOAD";//表名
        StringBuffer fieldNameBf = new StringBuffer("(");//要写入表的字段
        fieldNameBf.append("VALUE1,");
        fieldNameBf.append("VALUE10)");
        String fieldName = fieldNameBf.toString();
        String ctlfileName = "sqlloader.ctl";//控制文件名

        stlFileWriter(fileRoute+fileName,tableName,fieldName,fileRoute+ctlfileName);
        //执行的DOS命令
        String user = "USETNAME";
        String psw = "123456";
        String Database = "//localhost:1521/test";//IP要指向数据库服务器的地址
 
        String logfileName = "test_sqlloader.log";
        Executive(user,psw,Database,fileRoute+ctlfileName,fileRoute+logfileName);
    }

以上就是写测试文件,控制文件,执行sqlldr命令,实现sql load数据的全部代码。

你可能感兴趣的:(数据库)