公司需要将大量数据入库,原本使用mybatis insert数据,但是效率十分低,所以考虑kafka,备用方案sql load。
我是调查sql load方案的,虽然最后没有选择这个方案,但是在这过程中也学习到了很多,在这里总结一下。
我在网上搜到的信息都是类似的,看起来就出自2篇博客,都是入门,对实际操作的帮助不太大。
我这篇包括2个部分,前期调查的概念部分,后期实现的代码部分
参考书:《Oracle Database 11G 数据库管理艺术》
上思维导图
Sql Load是Oracle的数据加载工具,通常用于导入大批量的外部数据。
SQL Load分为传统加载和直接加载
传统加载:在原理上同insert相同,但是每次提交一组记录,从而提高效率
直接加载:利用数据构造列数组结构,用这些结构来格式化Oracle数据块,然后直接写入数据库表。
传统加载和直接加载的特点和区别都在思维导图里面了,精简的总结一下有这几条
结构:一般包含4种文件
5. 数据文件:dat、csv、txt --包含待导入的数据,每行映射到数据库中一条记录,包含若干字段值,可包含分隔符便于导入时切割数据
6. 控制文件:ctl --sql load命令
7. 日志文件:log --导入时的日志文件,显示多少正确被导入,多少出问题了没被导入,没导入的数据存到哪个文件里了
8. 问题数据文件:bad --导入时出问题的数据被保存到了这个文件
我只试了txt类型的数据文件,我写的生成测试文件的代码能生成的文件的形式如下,以“|”分隔数据,因为数据的长度不固定,所以使用的是分隔符关键字拆分数据,后来我有想过,如果在写文件的时候能够固定每个字段数据的长度,则使用位置关键字POSITION(m:n)确定m到n位的数据,写入对应字段,这样因为省掉了查找分隔符的时间,能提高效率,具体能提高多少,我没试验过:
控制文件的格式一般不会变,我用到的格式如下
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
参数解释
上代码了,代码参考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数据的全部代码。