数据库SQL的优化

前期介绍:
在项目上线初期,业务数据量相对较少,SQL的执行效率对程序运行效率的影响可能不太明显,因此开发和运维人员可能无法判断SQL对程序的运行效率有多大。但随着时间的积累,业务数据量的增多,SQL的执行效率对程序的运行效率的影响逐渐增大,因此对SQL的优化就很有必要。


优化SQL可以使其更有效地使用索引、减少硬盘I/O等,从而提高程序的运行效率。


优化SQL可以使其更好地利用缓存,从而降低程序的响应时间。


优化SQL可以使其更好地处理大数据量,从而减少程序的运行时间。


优化SQL可以使其更符合程序的业务需求,从而提高程序的性能和用户体验。


一、查询SQL尽量不要使用select *,而是具体字段。

1、反例

select * from emp

2、正

select id,sex,user_name,salary from emp

3、解释
使用 * 号是因为使用 * 号会查询出表中所有列的数据,
查询所有列可能会导致性能问题,特别是当表中包含大量列时,查询所有列可能会导致查询速度变慢。
查询所有列可能会导致数据冗余,特别是当表中包含多个关联表时,查询所有列可能会导致返回大量重复数据。
指定需要查询的列可以使查询结果更易于理解和维护,特别是当表中包含大量列时,指定需要查询的列可以使查询结果更加清晰和易于理解。
 

 二、where中使用默认值代替NULL
说明:

        为了方便 我就使用* 代替了。

1、反例
select * from emp where  salary is not null
2、正例
select * from emp  where  salary > 0
3、解释
提高查询效率:在 WHERE 子句中使用 NULL 值需要进行 IS NULL 或 IS NOT NULL 的判断,这需要额外的计算成本。而使用默认值 0 代替 NULL 值可以使查询条件更加简单明确,避免进行额外的判断,从而提高查询效率。
避免 NULL 值带来的问题:NULL 值具有特殊的语义,即无法进行任何比较。因此,在 WHERE 子句中使用 NULL 值可能会导致查询结果不准确或无法执行。使用默认值 0 代替 NULL 值可以避免这种情况,确保查询结果的准确性和可执行性。
提高代码可读性和可维护性:使用默认值 0 代替 NULL 值可以使代码更加清晰易懂,提高可读性。同时,在修改查询条件时,使用默认值 0 也可以避免忘记处理 NULL 值的情况,提高代码的可维护性。
 

三、避免在where子句中使用 or 来连接条件
1、反例
select * from emp  where id = 1 or salary = 2500.10
使用 or 可能会引起索引失效,从而进行全表扫描
or  操作符连接的两个条件只要有一个条件成立,就会返回相应的结果集。因此,当使用 where id=1 or salary=2500.10 进行查询时,如果 id 字段上有索引,但 salary 字段上没有索引,那么查询引擎会优先使用 id 索引进行查询,因为 id 字段上的条件一定成立。但如果要查询的 salary 值不在 id 索引的范围内,那么查询引擎可能需要进行全表扫描或放弃使用索引,导致 id 索引失效。
2、正例

select * from emp  where id = 1
union all
select * from emp  where salary = 2500.10
使用 UNION ALL 用于将两个SELECT语句的结果合并在一起,形成一个结果集。
与UNION不同的是,UNION ALL不会去除重复的数据行,而是将所有符合条件的数据行都列出来。
在 salary 字段上创建索引,这样查询引擎就可以使用两个字段上的索引来优化查询,提高查询性能。
使用 UNION ALL 操作符将两个查询结果合并,这样可以避免使用 OR 操作符,从而避免 id 索引失效的问题。
四、尽量使用数值替代字符串类型
1、正例
主键(id):primary key优先使用数值类型int,tinyint。

性别(sex) :0代表女,1代表男,数据库没有布尔类型,mysql推荐使用tinyint。

2、解释
引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了,因此数值类型可以大大提高查询和连接的性能。
字符串和数字之间的比较需要进行隐式的类型转换,这会导致查询性能的降低。
索引的优缺点之一是可以加速表与表之间的连接,特别是在实现数据的参考完整性方面特别有意义。通过使用数值类型,可以在创建索引时提高查询性能和连接性能。
在使用分组和排序字句进行数据检索时,数值类型可以显著减少查询中分组和排序的时间。
五、使用varchar代替char
1、反例
`address` char(100) DEFAULT NULL COMMENT '地址'
sql中使用char(100)作为数据类型有一些缺点。首先,char(100)会占用更多的磁盘空间,因为每个字符都需要固定长度的存储空间。其次,使用char(100)时,如果存储的字符串长度小于100个字符,会出现浪费存储空间的情况。
另外,如果某些字段涉及到文件排序或者基于磁盘的临时表时,使用char(100)可能会消耗更多的内存,因为固定长度的字符型数据在内存中是连续的空间。因此,在使用char(100)时需要考虑这些缺点,并根据实际需求选择更适合的数据类型。
2、正例
`address` varchar(100) DEFAULT NULL COMMENT '地址'
存储灵活:相比于固定长度的数据类型,varchar(100)可以存储长度为100的字符串,也可以存储长度小于100的字符串,或者不存储任何字符,因此可以更加灵活地处理数据。
内存消耗较小:对于较短的字符串,使用varchar(100)可以减少内存消耗。例如,如果需要存储一个长度为10的字符串,使用固定长度的数据类型需要11个字节的内存空间,而使用varchar(100)只需要2个字节的内存空间。
可以避免浪费磁盘空间:如果使用固定长度的数据类型存储长度不足的值,会浪费一些磁盘空间。而使用varchar(100)可以根据实际需要使用或浪费较少的磁盘空间。
可以提高查询性能:在某些情况下,使用varchar(100)可以提高查询性能。例如,如果一个表中的某些列经常需要被查询,而这些列使用固定长度的数据类型可能会浪费一些内存空间,从而导致查询性能下降。而使用varchar(100)可以根据实际情况使用更少的内存空间,从而提高查询性能。
六、char与varchar2的区别
1、CHAR的长度是固定的,VARCHAR2的长度是可以变化的。
长度固定 vs 长度可变:CHAR的长度是固定的,而VARCHAR2的长度是可以变化的。例如,存储字符串“101”,对于CHAR(10),表示你存储的字符将占10个字节(包括7个空字符),而同样的VARCHAR2(10)则只占用3个字节的长度。
存储空间:CHAR比VARCHAR2更节省存储空间,因为CHAR类型在数据库中以空格填充,而VARCHAR2则不会。但是,VARCHAR2在效率上比CHAR稍差一点。
性能:CHAR类型在某些方面比VARCHAR2类型稍快,因为它具有固定的长度,因此在处理和搜索数据时,它可以更快地定位和读取。


2、什么时候选择CHAR什么时候选择VARCHAR2
存储需求:如果需要存储的字符数据长度固定且长度较小,则使用CHAR更为节省空间。如果数据长度变化范围较大,或者长度较小,则使用VARCHAR2更为节省空间。
性能需求:如果应用程序需要快速高效的查询和搜索数据,并且对数据的长度有要求,则使用CHAR可能更合适。如果对性能要求不高,而对数据长度和变化范围有要求,则更适合使用VARCHAR2。
注意: 在使用VARCHAR2时,如果一个列经常被修改,并且每次修改的数据长度不同,这可能会导致Row Migration(行迁移)现象,这可能会影响数据库性能并增加I/O负担。在这种情况下,使用CHAR可能更好。

CHAR中还会自动补齐空格,因为你INSERT到一个CHAR字段自动补充了空格的,但是SELECT后空格没有删除,因此CHAR类型查询的时候一定要记得使用TRIM函数去除字符串两端的空格。

3、如何使用TRIM
在查询CHAR类型的列时,如果要使用TRIM函数去除字符串两端的空格,可以使用以下语法:

select * from emp  where TRIM(text) = 'csdn';
emp是要查询的表名,text是要查询的CHAR类型的列名,csdn是要匹配的字符串。TRIM函数将去除csdn中字符串两端的空格,并返回与csdn相等的唯一值。
需要注意:使用TRIM函数可能会影响查询性能。如果查询速度非常重要,则可以考虑将CHAR类型的列转换为VARCHAR2类型,因为VARCHAR2类型会自动去除字符串两端的空格。但是,这种方法可能会导致存储空间的浪费。
七、避免在where子句中使用!=或<> 等操作符
1、反例
select * from emp where salary !=2500.10
 
select * from emp where salary <>2500.10
2、解释
当查询语句中使用 IS NULL 或 IS NOT NULL 运算符进行空值判断时,数据库无法利用索引进行查询,因为索引只是一种用于快速查找数据的数据结构,无法判断数据是否为空。此时,数据库需要遍历整个表来查找符合条件的行,从而导致索引失效。
即使在查询语句中使用了 = 或 > 等运算符进行等值或范围查询,但如果查询条件中包含了一个未使用索引的列,也会导致索引失效。
  3、举例
select * from emp  where user_name ='choudidi'  and  salary is not null;
在这个查询语句中,user_name 列使用了索引,但 salary 列没有使用索引。因此,即使 user_name 列的条件满足,数据库仍然需要遍历整个表来查找符合条件的行,从而导致索引失效。
八、inner join 、left join、right join,优先使用inner join
优先使用INNER JOIN的原因是其性能相对更好。INNER JOIN是等值连接,只保留两张表中完全匹配的结果集,而LEFT JOIN和RIGHT JOIN会返回左表或右表的所有行,即使在另一张表中没有匹配的记录。
因此,如果使用LEFT JOIN或RIGHT JOIN,可能会导致返回的数据量较大,从而影响查询性能。
INNER JOIN将数据集的较小数据驱动,而LEFT JOIN和RIGHT JOIN则将较大的数据集驱动,这也是MySQL优化原则之一。
小表驱动大表,小的数据集驱动大的数据集,从而让性能更优。
九、提高group by语句的效率
1、反例
select department, avg(salary) from emp 
group by department
having department ='1' or department = '3';
 先分组,再过滤
2、正例
select department,avg(salary) from emp 
where department ='1' or department = '3' 
group by department;
先过滤,后分组

筛选出符合条件的记录:where子句中的条件筛选出了部门为'1'或'3'的员工记录,只对符合条件的记录进行后续计算。

按部门分组:group by子句将结果集按照部门进行分组,这样每个部门的数据都会单独计算平均值,避免了将所有记录混合在一起计算平均值的情况。
十、清空表时优先使用truncate 
速度更快:truncate比delete更快,因为它不会记录在日志中,也不需要在事务日志中为所删除的每行记录一项。
占用的资源更少:truncate通过释放存储表数据所用的数据页来删除数据,而不是在事务日志中记录页的释放,而delete语句每次删除一行,并在事务日志中为所删除的每行记录一项。
保持表结构及其列、约束、索引等不变:truncate删除表中的所有行,但表结构及其列、约束、索引等保持不变。新行标识所用的计数值重置为该列的种子。如果要保留标识计数值,请改用delete。如果要删除表定义及其数据,请使用drop table语句。
不适用于参与了索引视图的表:truncate不能用于参与了索引视图的表,而delete可以。
不激活触发器:truncate不会激活触发器,而delete会。truncate不会激活触发器的原因是它不会记录各行的日志删除操作。由于truncate操作不会记录在事务日志中,所以它不会激活delete触发器。
十一、操作delete或者update语句,加limit或使用循环分批次删除
降低数据库负载:一次性删除(或更新)大量数据会对数据库性能造成较大的压力,导致其他操作变慢或阻塞。分批次删除可以降低数据库负载,保证其他操作的正常运行。
提高数据安全性:删除或更新大量数据时,如果不小心出现错误操作,可能会导致数据丢失或不可恢复。分批次删除可以降低操作风险,即使不小心删除也可以通过binlog日志恢复进行回滚操作。
便于代码实现:一次性删除大量数据可能会导致内存溢出或其他异常情况,使用循环分批次删除可以避免这些问题。同时,使用循环分批次删除也可以降低代码复杂度,提高代码可读性和可维护性。
锁表:一次性大量删除太多数据,可能造成锁表,会有lock wait timeout exceed的错误,所以建议分批操作。
减少锁定时间:删除或更新数据时,数据库会对相关行进行锁定,确保数据一致性。如果一次性删除大量数据,锁定时间会较长,影响其他操作。而分批次删除可以缩短锁定时间,提高并发性能。
1、正例
1、使用limit限制删除数量

delete from emp limit 1000;
-- 或
delete from emp where 条件 limit 1000;  # 每次删除 1000 行数据
-- 或者
UPDATE emp SET column1 = value1, column2 = value2, ... WHERE condition LIMIT 1000;  # 每次更新 1000 行数据
 可以在delete或update语句中添加limit关键字,指定每次删除的数据量。列如以上SQL每次删除1000条数据:
2、使用循环分批次删除

CREATE DEFINER = `root` @`localhost` PROCEDURE `P_xiao_jian` () BEGIN
    DECLARE
        i INT DEFAULT 1;#Routine body goes here...
    START TRANSACTION;
    
    SET @counter = 1;
    WHILE
            @counter <= 1000 DO DELETE FROM order_for_goods WHERE order_id >= @counter;
        
        SET @counter = @counter + 1;
        
    END WHILE;
    COMMIT;
 
END
定义一个名为 P_xiao_jian 的存储过程,使用 CREATE PROCEDURE 语句创建。
定义一个名为 @counter 的整型变量,并初始化为 1。
使用 START TRANSACTION 语句开始一个事务。
使用 WHILE 循环,循环条件为 @counter 小于等于 1000。
在循环体内,使用 DELETE FROM 语句删除 order_for_goods 表中 order_id 大于等于当前循环变量 @counter 的所有记录。
循环结束后,使用 COMMIT 语句提交事务。
使用 END 语句结束存储过程的定义。
 
十二、UNION操作符
UNION操作符用于将两个或多个SELECT语句的结果集合并成一个结果集。
select user_name,salary from emp 
union
select departmentname from department
union在进行表连接后会筛选掉重复的记录,所以在表连接后会对所产生的结果集进行排序运算,删除重复的记录再返回结果。
在运行时先取出两个表的结果,再用排序空间进行排序删除重复的记录,最后返回结果集,如果表数据量大的话可能会导致用磁盘进行排序。
 
推荐:采用union all操作符替代union,因为union all操作只是简单的将两个结果合并后就返回。

十三、批量插入性能提升
1、反例
多条提交
insert into emp (id,user_name) values(1,'提交1条');
 
insert into emp (id,user_name) values(2,'提交2条');
2、正例
批量提交
insert into emp (id,user_name) values(1,'提交1条'),(2,'提交2条');
当执行INSERT、UPDATE或DELETE语句默认SQL有事务控制,导致每条都需要事务开启和事务提交,而批量处理是一次事务开启和提交,效率提升明显,达到一定量级,效果显著。
十四、表连接不宜太多,索引不宜太多,建议5个以内
1、表连接不宜太多原因
关联的表个数越多,编译的时间和开销也就越大。
每次关联内存中都生成一个临时表,应该把连接表拆开成较小的几个执行,可读性更高。
如果一定需要连接很多表才能得到数据,那么意味着这是个不理想的的设计。
可以查看阿里规范手册,其中包含一条建议多表联查三张表以下
通常情况下,建议将表连接的数量控制在5个以内,这样可以提高查询的效率和性能。如果需要连接更多的表,可以考虑对查询进行拆分或重构,以减少连接的数量。此外,还可以使用索引、临时表、视图等技术来优化查询性能。


2、索引不宜太多原因
增加额外的开销:索引需要维护,包括更新、插入和删除等操作,而这些操作都会增加额外的开销。特别是对于频繁更新的表,索引可能会成为性能瓶颈。
占用磁盘空间:索引需要存储在磁盘上,因此会增加磁盘空间的占用。过多的索引可能会使查询变得缓慢,因为数据量越大,查询所花费的时间可能比表里索引的时间还要短,索引可能不会产生优化效果。
降低查询效率:索引设计不合理或者缺少索引都会对数据库性能造成不良影响。过多的索引可能会使查询变得复杂,降低查询效率。
十五、避免在索引列上使用内置函数
说明:

     在索引列上使用内置函数可能会导致索引失效,从而降低查询性能。这是因为索引是为了快速访问表中数据而创建的,但是内置函数可能会改变索引列中的数据,导致索引无法识别数据。

为了避免在索引列上使用内置函数,可以考虑以下几种方法:

优化查询语句:如果必须使用内置函数,可以尝试优化查询语句,以减少函数的使用次数,或者使用其他方法来避免使用内置函数。
创建计算列:如果必须使用内置函数,可以创建一个计算列,并在该列上创建索引。这样,内置函数的结果将存储在新列中,而不是原始列中,从而避免影响索引。
创建视图:如果必须使用内置函数,可以创建一个视图,并在视图中使用内置函数。然后,在查询中使用视图而不是表,以避免在索引列上使用内置函数。
更改数据类型:如果可能,将索引列的数据类型更改为适合内置函数的类型,以避免使用内置函数。例如,如果要在索引列上使用日期函数,可以将数据类型更改为日期类型。
十六、组合索引
说明:

       如果查询中排序的顺序与组合索引的列顺序不匹配,那么查询效率可能会下降。这是因为数据库无法利用索引进行排序,而只能使用其他算法进行排序,从而导致查询效率下降。

create index idx_username_tel on employees (deptid,username,createtime);
 
select username,tel from employees  where deptid= 1 and username = 'CSDN' order by deptid,position,createtime desc; 
虽然创建了基于deptid、username和createtime列的索引,但是查询条件中只使用了deptid=1和username=csdn列,没有利用到createtime列。因此,这个查询不能利用到基于createtime列的索引,查询性能可能会受到影响。同时,如果表中的数据量非常大,创建索引可能会对系统的性能产生较大的影响。
 

十七、复合索引最左特性
说明:

      复合索引(也称为联合索引)具有"最左前缀"特性。这意味着,当查询条件中使用了复合索引中的第一个列(最左边的列),则该复合索引可以被使用。如果查询条件中使用了复合索引中的多个列,则该复合索引也可以被使用,但是前提是这些列的顺序必须与复合索引中的顺序相同。 

1、创建复合索引
CREATE INDEX idx_name_age ON employees (name, age);
2、假设我们有一个查询,需要按name和age进行排序,可以使用该复合索引(如下)
SELECT * FROM employees WHERE name = 'John' AND age = 25 ORDER BY name, age;
在这个查询中,我们使用了复合索引中的第一个列name和第二个列age。由于这些列的顺序与复合索引中的顺序相同,因此该复合索引可以被使用。

3、然而,如果我们改变了查询条件中列的顺序,或者添加了其他函数或条件,可能会导致复合索引无法被使用(如下)
-- 复合索引无法被使用  
SELECT * FROM employees WHERE age = 25 AND name = 'John' ORDER BY name, age;  
  
-- 复合索引无法被使用  
SELECT * FROM employees WHERE SUBSTRING(name, 1, 1) = 'J' AND age = 25 ORDER BY name, age;
在第一个查询中,我们改变了查询条件中列的顺序,因此复合索引无法被使用。在第二个查询中,我们添加了一个函数SUBSTRING,这使得复合索引无法被使用。因此,在使用复合索引时,需要注意查询条件中列的顺序,以及是否添加了任何函数或条件。 

十八、优化like语句
1、反例
select * from emp where user_name like '%王' (不使用索引)
 
select * from emp where user_name like '%王%' (不使用索引)
2、正例
select * from emp where user_name like '王%' (使用索引) 。
3、解释
使用前缀匹配,当你知道要搜索的字符串具有某个特定的前缀时,可以使用前缀匹配来提高查询效率。例如,如果你知道要搜索的名字都以 "张" 开头,可以使用 LIKE '张%'。
首先尽量避免模糊查询,如果必须使用,不采用全模糊查询,也应尽量采用右模糊查询, 即like '...%',是会使用索引的,左模糊like '%...'无法直接使用索引,但可以利用reverse + function index的形式,变化成 like '...%'。
全模糊查询是无法优化的,一定要使用的话建议使用搜索引擎。
reverse + function index的形式如下:

SELECT * FROM emp WHERE LEFT(user_name, 1) LIKE '肖%';
这将返回名字以 "肖" 开头的所有名字。在这个语句中,LEFT(user_name, 1) 提取名字左侧的第一个字符,并将其与 '肖%' 进行比较。注意,这里使用了 % 通配符来表示任意字符(包括 0 个字符),因此 '肖%' 表示以 "肖" 开头的任意字符串。
十九、使用explain分析你SQL执行计划
1、type类型
system:表仅有一行,基本用不到;
const:表最多一行数据配合,主键查询时触发较多;
eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了const类型;
ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取;
range:只检索给定范围的行,使用一个索引来选择行。当使用=、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN或者IN操作符,用常量比较关键字列时,可以使用range;
index:该联接类型与ALL相同,除了只有索引树被扫描。这通常比ALL快,因为索引文件通常比数据文件小;
all:全表扫描;
性能排名:system > const > eq_ref > ref > range > index > all。
实际sql优化中,最后达到ref或range级别。

2、Extra常用关键字
Using index:只从索引树中获取信息,而不需要回表查询;
Using where:WHERE子句用于限制哪一个行匹配下一个表或发送到客户。除非你专门从表中索取或检查所有行,如果Extra值不为Using where并且表联接类型为ALL或index,查询可能会有一些错误。需要回表查询。
Using temporary:mysql常建一个临时表来容纳结果,典型情况如查询包含可以按不同情况列出列的GROUP BY和ORDER BY子句时;
 
二十、其他SQL优化方式
1、设计表的时候,所有表和字段都添加相应的注释。

2、SQL书写格式,关键字大小保持一致,使用缩进。

3、修改或删除重要数据前,要先备份。

4、很多时候用 exists 代替 in 是一个好的选择

5、where后面的字段,留意其数据类型的隐式转换。

在SQL中,当在WHERE子句中指定条件时,如果条件中的字段类型与查询中指定的字段类型不匹配,SQL可能会进行隐式的类型转换,也称为“隐式转换”或“隐式类型转换”。
例如,如果一个字段是一个日期类型,而条件中指定的值是一个字符串类型,SQL可能会尝试将字符串值转换为日期类型。同样,如果一个字段是一个数值类型,而条件中指定的值是一个字符串类型,SQL可能会尝试将字符串值转换为数值类型。
这种隐式类型转换在某些情况下可能是有用的,但在其他情况下可能会导致错误的结果。因此,建议在编写查询时始终显式指定要比较的值和字段的类型,以避免出现意外的问题。
如:

SELECT * FROM emp WHERE NAME=12345
因为不加单引号时,是字符串跟数字的比较,它们类型不匹配
MySQL会做隐式的类型转换,把它们转换为数值类型再做比较
6、尽量把所有列定义为NOT NULL

NOT NULL列更节省空间,NULL列需要一个额外字节作为判断是否为 NULL的标志位。
NULL列需要注意空指针问题,NULL列在计算和比较的时候,需要注意空指针问题。
7、伪删除设计

8、数据库和表的字符集尽量统一使用UTF8

可以避免乱码问题。
可以避免,不同字符集比较转换,导致的索引失效问题。
9、select count(*) from table;

这样不带任何条件的count会引起全表扫描,并且没有任何业务意义,是一定要杜绝的。
10、避免在where中对字段进行表达式操作

SQL解析时,如果字段相关的是表达式就进行全表扫描
字段干净无表达式,索引生效
11、关于临时表

避免频繁创建和删除临时表,以减少系统表资源的消耗;
在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log;
如果数据量不大,为了缓和系统表的资源,应先create table,然后insert;
如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除。先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
12、索引不适合建在有大量重复数据的字段上,比如性别,排序字段应创建索引

13、去重distinct过滤字段要少

带distinct的语句占用cpu时间高于不带distinct的语句
当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据
然而这个比较、过滤的过程会占用系统资源,如cpu时间
14、尽量避免大事务操作,提高系统并发能力

15、所有表必须使用Innodb存储引擎

Innodb「支持事务,支持行级锁,更好的恢复性」,高并发下性能更好,所以呢,没有特殊要求(即Innodb无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用Innodb存储引擎。
16、尽量避免使用游标

因为游标的效率较差,如果游标操作的数据超过1万行,那么应该考虑改写。
 

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