面试必问的数据库-4.1:sql-优化

1:关于批量插入数据(100万)数据优化

思路:

1:变多次提交为一次

2:使用批量操作

整理了两种方法:

方法一:

	1. public static void insert() {  
	2.         // 开时时间  
	3.         Long begin = new Date().getTime();  
	4.         // sql前缀  
	5.         String prefix = "INSERT INTO tb_big_data (count, create_time, random) VALUES ";  
	6.         try {  
	7.             // 保存sql后缀  
	8.             StringBuffer suffix = new StringBuffer();  
	9.             // 设置事务为非自动提交  
	10.             conn.setAutoCommit(false);  
	11.             // Statement st = conn.createStatement();  
	12.             // 比起st,pst会更好些  
	13.             PreparedStatement pst = conn.prepareStatement("");  
	14.             // 外层循环,总提交事务次数  
	15.             for (int i = 1; i <= 100; i++) {  
	16.                 // 第次提交步长  
	17.                 for (int j = 1; j <= 10000; j++) {  
	18.                     // 构建sql后缀  
	19.                     suffix.append("(" + j * i + ", SYSDATE(), " + i * j * Math.random() + "),");  
	20.                 }  
	21.                 // 构建完整sql  
	22.                 String sql = prefix + suffix.substring(0, suffix.length() - 1);  
	23.                 // 添加执行sql  
	24.                 pst.addBatch(sql);  
	25.                 // 执行操作  
	26.                 pst.executeBatch();  
	27.                 // 提交事务  
	28.                 conn.commit();  
	29.                 // 清空上一次添加的数据  
	30.                 suffix = new StringBuffer();  
	31.             }  
	32.             // 头等连接  
	33.             pst.close();  
	34.             conn.close();  
	35.         } catch (SQLException e) {  
	36.             e.printStackTrace();  
	37.         }  
	38.         // 结束时间  
	39.         Long end = new Date().getTime();  
	40.         // 耗时  
	41.         System.out.println("cast : " + (end - begin) / 1000 + " ms");  
	42.     }  

输出时间:cast : 23 ms

该方法目前测试是效率最高的方法!

把sql分为两部分,一部分是前面固定的东西,

另一部分是后面的value值。

就是固定前面的参数什么的,然后用stringBuffer拼接后面的value值。 最终一条sql的格式如下:

INSERT INTO tb_big_data (count, create_time, random)

VALUES

(91, SYSDATE(), 4.662727113323584),

(182, SYSDATE(), 95.53429081199045),

(273, SYSDATE(), 86.90738680822206),

(364, SYSDATE(), 265.96191796796916)

【一个sql组成一个事务,但是后面有多个value。一条sql,多个value】

这里还要设置事务提交为false,让我们自己控制每次提交的事务的量。

因为:

如果没有setAutoCommit(false);那么对于每一条insert语句,都会产生一条log写入磁盘,所以虽然设置了批量插入,但其效果就像单条插入一样,导致插入速度十分缓慢。

每条sql都会产生自动提交事务到数据库,不是批量的。

参数: autoCommit --》为 true表示启用自动提交模式;为 false表示禁用该模式

默认的话为自动提交,每当执行一个update ,delete或者insert的时候都会自动提交到数据库,无法回滚事务。

问题:做一个将excel数据导入数据库的程序时?

由于数据量大,准备采用jdbc的批量插入。于是采用preparedStatement.addBatch();

当加入1w条数据时,再执行插入操作,preparedStatement.executeBatch()。

原本以为是批量操作,应该效率很快,

结果插入65536条数据一共花30多分钟,完全出乎我的意料。

于是百度或者请教别人,他们在处理这种大批量数据导入的时候是如何处理的,发现他们也是用的jdbc批量插入处理,但与我不同是:他们使用了con.setAutoCommit(false);然后再preparedStatement.executeBatch()之后,再执行con.commit();

于是再试,什么叫奇迹?就是刚刚导入这些数据花了半小时,而加了这两句话之后,现在只用了15秒钟就完成了。于是去查查了原因,在网上发现了如下一段说明:

      * When importing data into InnoDB, make sure that MySQL does not have autocommit mode enabled because that

      requires a log flush to disk for every insert. To disable autocommit during your import operation, surround it with

      SET autocommit and COMMIT statements:

      SET autocommit=0;

     ... SQL import statements ...

     COMMIT;

第一次,正是因为没有setAutoCommit(false);那么对于每一条insert语句,都会产生一条log写入磁盘,所以虽然设置了批量插入,但其效果就像单条插入一样,导致插入速度十分缓慢。

部分代码如下:
String sql = "insert into table *****"; 
con.setAutoCommit(false); 
ps = con.prepareStatement(sql); 
for(int i=1; i<65536; i++){     
    ps.addBatch();     // 1w条记录插入一次     
    if (i % 10000 == 0){          
         ps.executeBatch();          
         con.commit();      
    } 
} // 最后插入不足1w条的数据 ps.executeBatch(); con.commit();
 

1:做项目是否都有必要设置conn.setAutoCommit(false)?

为什么?主要是为什么!

我认为有必要,conn.setAutoCommit()的功能是每执行一条SQL语句,就作为一次事务提交。但一般在项目中很有可能需要执行多条SQL语句作为一个事务。若有一个执行不成功,就会rollback();

2、当n个sql 视为一个整体(原子性),要么都执行,要么一个不执行时候

通过 setAutoCommit 进行控制。

setAutoCommit(false);

sql语句;

sql语句;

......

commit();

方法二:

	1. public static void insertRelease() {  
	2.         Long begin = new Date().getTime();  
	3.         String sql = "INSERT INTO tb_big_data (count, create_time, random) VALUES (?, SYSDATE(), ?)";  
	4.         try {  
	5.             conn.setAutoCommit(false);  
	6.             PreparedStatement pst = conn.prepareStatement(sql);  
	7.             for (int i = 1; i <= 100; i++) {  
	8.                 for (int k = 1; k <= 10000; k++) {  
	9.                     pst.setLong(1, k * i);  
	10.                     pst.setLong(2, k * i);  
	11.                     pst.addBatch();  
	12.                 }  
	13.                 pst.executeBatch();  
	14.                 conn.commit();  
	15.             }  
	16.             pst.close();  
	17.             conn.close();  
	18.         } catch (SQLException e) {  
	19.             e.printStackTrace();  
	20.         }  
	21.         Long end = new Date().getTime();  
	22.         System.out.println("cast : " + (end - begin) / 1000 + " ms");  
	23.     }  

注:注释就没有了,和上面类同,下面会有分析!

控制台输出:cast : 111 ms

执行时间是上面方法的5倍!

这种方式是:先把整个sql写出来,在set进去value对应的值。

set一个sql后,然后pst.addBatch();  加入进去。

最后当加入了1000个sql后,

  1. pst.executeBatch();  
  2. conn.commit();  

最终行成的sql是:

INSERT INTO tb_big_data (count, create_time, random) VALUES (91, SYSDATE(), 4.662727113323584),

INSERT INTO tb_big_data (count, create_time, random) VALUES (182, SYSDATE(), 95.53429081199045),

INSERT INTO tb_big_data (count, create_time, random) VALUES (273, SYSDATE(), 86.90738680822206),

INSERT INTO tb_big_data (count, create_time, random) VALUES (364, SYSDATE(), 265.96191796796916)

【多个sql组成一个事务】

总结:

方法一和方法二很类同,

唯一不同的是方法一采用的是“insert into tb (...) values(...),(...)...;的方式执行插入操作,

方法二则是“insert into tb (...) values (...);insert into tb (...) values (...);...”的方式,要不是测试,我也不知道两者差别是如此之大!

当然,这个只是目前的测试,具体执行时间和步长也有很大关系!如过把步长改为100,可能方法就要几分钟了吧,这个可以自己测试哈。。。

2:mysql亿级数据优化

第一阶段:
1,一定要正确设计索引
2,一定要避免SQL语句全表扫描,所以SQL一定要走索引(如:一切的 > < != 等等之类的写法都会导致全表扫描)
3,一定要避免 limit 10000000,20 这样的查询(查询1000000000行开始之后的20条数据,尼玛,真坑爹啊)
4,一定要避免 LEFT JOIN 之类的查询,不把这样的逻辑处理交给数据库
5,每个表索引不要建太多,大数据时会增加数据库的写入压力
第二阶段:
1,采用分表技术(大表分小表)
a)垂直分表:将部分字段分离出来,设计成分表,根据主表的主键关联
b)水平分表:将相同字段表中的记录按照某种Hash算法进行拆分多个分表
2,采用mysql分区技术(必须5.1版以上,此技术完全能够对抗Oracle),与水平分表有点类似,但是它是在逻辑层进行的水平分表。
第三阶段(服务器方面):

1,采用memcache之类的内存对象缓存系统,减少数据库读取操作
2,采用主从数据库设计,分离数据库的读写压力
3,采用Squid之类的代理服务器和Web缓存服务器技术
1.mysql千万级别数据肯定是没问题的,毕竟现在的流向web2.0网站大部分是mysql的
2.合理分表也是必须的,主要涉及横向分表与纵向分表,如把大小字段分开,或者每100万条记录在一张表中等等,像上面的这个表可以考虑通过uid的范围分表,或者通过只建立索引表,去掉相对大的字段来处理.
3.count()时间比较长,但是本身是可以缓存在数据库中或者缓存在程序中的,因为我们当时使用在后台所以第一页比较慢但是后面比较理想
4.SELECT id 相对SELECT * 差距还是比较大的,可以通过上面的方法来使用SELECT id + SELECT * ... IN 查询来提高性能
5.必要的索引是必须的,还是要尽量返回5%-20%的结果级别其中小于5%最理想;
6.mysql分页的前面几页速度很快,越向后性能越差,可以考虑只带上一页,下一页不带页面跳转的方法,呵呵这个比较垃圾但是也算是个方案,只要在前后多查一条就能解决了.比如100,10 你就差99,12,这样看看前后是否有结果.

某个大神的总结。

3:简单说下防止sql注入

1:第一种采用预编译语句集,它内置了处理SQL注入的能力,只要使用它的setString方法传值即可:

就是使用PreparedStatement 来执行sql。

String sql= "select * from users where username=? and password=?;

PreparedStatement preState = conn.prepareStatement(sql);

preState.setString(1, userName);

preState.setString(2, password);

ResultSet rs = preState.executeQuery();

 

原理就是:

尽量使用占位符。不要使用 + 拼接sql字符串 

 

如果使用hibernate,不需要关心sql注入问题."select l from Login l where l.name= :name"相当于"select l from Login l where l.name=?";在下面给name赋值就可以了

 

防sql注入一般用preparestatment

也就是占位符。

另一种解决方案就是使用hibernate

 

但是:

SQL的拼接有时候是必须的,虽然使用预处理有诸多好处。

防SQL注入的根本方法是过虑参数中的特殊符号,如:单引号、空格等

 

2:第二种是采用正则表达式将包含有 单引号('),分号(;) 和 注释符号(--)的语句给替换掉来防止SQL注入

public static String TransactSQLInjection(String str){

return str.replaceAll(".*([';]+|(--)+).*", " ");

}

userName=TransactSQLInjection(userName);

password=TransactSQLInjection(password);

String sql="select * from users where username='"+userName+"' and password='"+password+"' "

Statement sta = conn.createStatement();

ResultSet rs = sta.executeQuery(sql);

 

3:那么ibatis怎么防止sql注入呢?

对于ibaits参数引用可以使用#和$两种写法,其中#写法会采用预编译方式,将转义交给了数据库,不会出现注入问题;如果采用$写法,则相当于拼接字符串,会出现注入问题。

例如,如果属性值为“' or '1'='1 ”,采用#写法没有问题,采用$写法就会有问题。

对于like语句,难免要使用$写法,

 1. 对于Oracle可以通过'%'||'#param#'||'%'避免;

 2. 对于MySQL可以通过CONCAT('%',#param#,'%')避免;

 3. MSSQL中通过'%'+#param#+'% 。 

如下3种SQL语句:

mysql: 
    select * from t_user where name like concat('%',#name #,'%')  
     
oracle: 
    select * from t_user where name like '%'||#name #||'%' 
     
SQL Server:
    select * from t_user where name like '%'+#name #+'% 

当然最根本的办法前后端都做参数校验,确保不放过任何可漏网之鱼。

猜你喜欢

转载自blog.csdn.net/u010953880/article/details/88351405