Home 数据库

0 17

MySQL 数据类型选择

MySQL 中定义数据字段的类型对你数据库的优化是非常重要的。

MySQL 支持多种类型,大致可以分为三类:数值、日期/时间和字符串(字符)类型。


数值类型

MySQL 支持所有标准 SQL 数值数据类型。

这些类型包括严格数值数据类型(INTEGER、SMALLINT、DECIMAL 和 NUMERIC),以及近似数值数据类型(FLOAT、REAL 和 DOUBLE PRECISION)。

关键字INT是INTEGER的同义词,关键字DEC是DECIMAL的同义词。

BIT数据类型保存位字段值,并且支持 MyISAM、MEMORY、InnoDB 和 BDB表。

作为 SQL 标准的扩展,MySQL 也支持整数类型 TINYINT、MEDIUMINT 和 BIGINT。下面的表显示了需要的每个整数类型的存储和范围。

img

无论在什么数据库中,数值和字符串这两种数据字段类型都用的最多,并且往往量级非常大,在这种背景下,选择的字段类型的优劣对性能影响非常大,因此我们有必要熟知上表中各种类型的使用及差别。

一、TINYINT

  1. 表示范围 :(-128,127) or (0,255)

  2. 占用字节:1字节

  3. 【强制】表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型是unsigned tinyint

  4. 最小显示长度(TINYINT(1) 和 TINYINT(2)的区别)

    TINYINT(m)

 m表示的是显示数据宽度,不同中数据类型它的数据宽度也是有差别的。数据宽度和数据类型的取值范围彼此之间是相互独立的,没有任何联系。 bigint(m)默认宽度为20,如果创建时表中设置了zerofill关键字(默认是用0填充的)。zerofill含义是:往表中插入的数值比定义的长度小的时候,会在数 值前进行补值。 img

  1. TINYINT和ENUM的区别

  TINYINT(1)或ENUM(’真’,’假’)?
        用ENUM枚举当存储只有2个值时只占用一个位的宽度,0或1,但会花更多的时间去寻找枚举查询的开始。
        用TINYINT(1)默认就会占用4个位的宽度(0000)
        得出结论:比如要存储一个介于0-9之间的值,为了查询获取这个值,建议用TINYINT(1)会更快,但如果你是为了大量记录枚举(“真”,“假”),那么用ENUM( ‘true’ , ‘false’) 搜索会更快。一般的,咱们如果存的是纯数字的话,建议用tinyint,如果是字符串,且是固定长度的,建议用char,而enum的枚举字段,使用的使用需要慎重考虑,避免带来不必要的麻烦

       注:TINYINT(1)在使用Mybatis代码生成器时,会自动转为Boolean类型!!!

二、INT或INTEGER

  1. 表示范围 :(-2 147 483 648,2 147 483 647) or (0,4 294 967 295)

  2. 占用字节:4字节

  3. The keyword INT is a synonym for INTEGER

  4. 如果不需要存取负值,最好加上unsigned

三、BIGINT

1.Mysql里有个数据类型bigint在java转换成实体对象时,处理不当容易出现以下异常:

java.lang.ClassCastException: java.lang.Long cannot be cast to java.math.BigInteger

只需要注意以下情况,就可避免此类异常:当数据库中该属性添加unsigned,则在对象中对应的属性类型应该为BigInteger; ​ 当数据库中该属性未添加unsigned,则在对象中对应的属性类型应该为Long。可以成功映射为Long的表用的是BIGINT(20),但是出问题的表使用的是BIGINT(20) UNSIGNED。如果不是无符号类型,BIGINT(20)的取值范围为-9223372036854775808~9223372036854775807。与Java.lang.Long的取值范围完全一致,mybatis会将其映射为Long;而BIGINT(20) UNSIGNED的取值范围是0 ~ 18446744073709551615,其中一半的数据超出了Long的取值范围,Mybatis将其映射为BigInteger。mysql数据库字段bigint使用 – 一心二念 – 博客园 (cnblogs.com)

2.存储手机号码用bigint(11)还是varchar(11)

空间:BIGINT——8字节CHAR——11字节
效率:BIGINT效率更高
使用:32位某些应用中bigint太大会溢出,要进行各种转换,麻烦
结论:使用char(11)更好

3.用 BIGINT 做主键,而不是 INT;

在真实业务场景中,整型类型最常见的就是在业务中用来表示某件物品的数量。例如上述表的销售数量,或电商中的库存数量、购买次数等。在业务中,整型类型的另一个常见且重要的使用用法是作为表的主键,即用来唯一标识一行数据。

整型结合属性 auto_increment,可以实现自增功能,但在表结构设计时用自增做主键,希望你特别要注意以下两点,若不注意,可能会对业务造成灾难性的打击:

用 BIGINT 做主键,而不是 INT;自增值并不持久化,可能会有回溯现象(MySQL 8.0 版本前)。 从表 1 可以发现,INT 的范围最大在 42 亿的级别,在真实的互联网业务场景的应用中,很容易达到最大值。例如一些流水表、日志表,每天 1000W 数据量,420 天后,INT 类型的上限即可达到。可以看到,当达到 INT 上限后,再次进行自增插入时,会报重复错误,MySQL 数据库并不会自动将其重置为 1。

第二个特别要注意的问题是,MySQL 8.0 版本前,自增不持久化,自增值可能会存在回溯问题!(23条消息) MySQL-数字类型自增是真的坑_Five在努力的博客-CSDN博客

其实,在海量互联网架构设计过程中,为了之后更好的分布式架构扩展性,不建议使用整型类型做主键,更为推荐的是字符串类型。合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。

正例:人的年龄用unsigned tinyint(表示范围0-255,人的寿命不会超过255岁);海龟就必须是smallint,但如果是太阳的年龄,就必须是int;如果是所有恒星的年龄都加起来,那么就必须使用bigint。

四、FLOAT,DOUBLE,DECIMAL

  1. 表示范围 :较大

  2. 占用字节:4字节,8字节

  3. MySQL 中使用浮点数和定点数来表示小数。

浮点类型有两种,分别是单精度浮点数FLOAT)和双精度浮点数DOUBLE);定点类型只有一种,就是 DECIMAL。浮点类型和定点类型都可以用(M, D)来表示,其中M称为精度,表示总共的位数;D称为标度,表示小数的位数。浮点数类型的取值范围为 M(1~255)和 D(1~30,且不能大于 M-2),分别表示显示宽度和小数位数。M 和 D 在 FLOAT 和DOUBLE 中是可选的,FLOAT 和 DOUBLE 类型将被保存为硬件所支持的最大精度。DECIMAL 的默认 D 值为 0、M 值为 10。

  1. DOUBLE 实际上是以字符串的形式存放的(精准)

  2. 长度一定下,浮点数表示范围更大,缺点则是精度问题

在 MySQL 中,定点数以字符串形式存储,在对精度要求比较高的时候(如货币、科学数据),使用 DECIMAL 的类型比较好,另外两个浮点数进行减法和比较运算时也容易出问题,所以在使用浮点数时需要注意,并尽量避免做浮点数比较。

  1. 单精度float比双精度效率更高

  2. 浮点数判断相等不能用== 或者equals,应当使用

  3. 【强制】小数类型为decimal,禁止使用float和double。

说明:float和double在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过decimal的范围,建议将数据拆成整数和小数分开存储。


日期类型

表示时间值的日期和时间类型为DATETIME、DATE、TIMESTAMP、TIME和YEAR。

每个时间类型有一个有效值范围和一个”零”值,当指定不合法的MySQL不能表示的值时使用”零”值。

TIMESTAMP类型有专有的自动更新特性,将在后面描述。

img


字符串类型

字符串类型指CHAR、VARCHAR、BINARY、VARBINARY、BLOB、TEXT、ENUM和SET。该节描述了这些类型如何工作以及如何在查询中使用这些类型。img

一、CHAR和VARCHAR

CHAR数据类型是MySQL中的固定长度的字符类型。我们经常声明CHAR类型的长度,指定我们要存储的最大字符数。例如,CHAR(20)最多可以容纳20个字符。

如果要存储的数据是固定大小,则应使用CHAR数据类型。与VARCHAR这种情况相比,您将获得更好的性能。

CHAR数据类型的长度可以是0到255之间的任何值。当您存储CHAR值时,MySQL会将其值用空格填充到您声明的长度。

当您查询CHAR值时,MySQL将删除尾随空格。

请注意,如果启用PAD_CHAR_TO_FULL_LENGTH SQL模式,MySQL将不会删除尾随空格。

区别:

  1. CHAR的长度是不可变的,而VARCHAR的长度是可变的

  2. CHAR的存取速度要比VARCHAR快得多

  3. 存储方式不同

varchar比char节省空间,但在效率上比char稍微差一些。varchar比char节省空间,是因为varchar是可变字符串,比如:用varchar(5)存储字符串“abc”,只占用3个字节的存储空间,而用char(5)存储,则占用5个字节(“abc ”)。varchar比char效率稍差,是因为,当修改varchar数据时,可能因为数据长度不同,导致数据迁移(即:多余I/O)。其中,oracle对此多余I/O描述的表达是:“行迁移”(Row Migration)。

  1. “行迁移”(Row Migration)

    *“当一行的记录初始插入时是可以存储在一个block中的,由于更新操作导致行增加了,而block的自由空间已经完全满了,这个时候就产生了行迁移。在这种情况下,oracle将会把整行数据迁移到一个新的block中,oracle会保留被迁移的行的原始指针指向新的存放行数据的block,这就意味着被迁移行的ROW ID是不会改变的。"*
    
      其中要解释一下:block是oracle中最小的数据组织与管理单位,是数据文件磁盘储存空间单位,也是**数据库I/O最小单位(****也就是说,读和写都是一个block的大小,所以如果block没满时,更新内容长度变更的varchar字段,和更新内容长度没变的varchar字段,I/O次数是一样,不存在额外消耗,只有在block满时,才会出现额外I/O,所以char和varchar性能之间的性能差异,是相当细微的,绝大多数情况下可以忽略不计,所以上文描述的“稍”差的含义)**。
    
      所以,我的开发经验是:“**用varchar代替char的效率有所下降,但不大**”。
  2. 占用空间不同

CHAR的存储方式是,一个英文字符(ASCII)占用1个字节,一个汉字占用两个字节;而VARCHAR的存储方式是,一个英文字符占用2个字节,一个汉字也占用2个字节。

varchar是可变长字符串,不预先分配存储空间(这里说的是存储时不预先分配存储空间,但是在查询需要创建临时表时,会按照设置的长度预先分配内存空间,因此这个设置长度时不可以随意,详情见下varchar的长度设计),长度不要超过5000,如果存储长度大于此值,定义字段类型为TEXT,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

     6.VARCHAR类型的实际长度是它的值的实际长度+1。为什么”+1″呢?

这一个字节用于保存实际使用了多大的长度。


二、MySQL中的varchar长度设计

 之前看到一篇博文,讲的是在定义varchar(n)时,需要用多大就设置多大,而我当时在分析时,对数据量估算错误了,设置了varchar(5000),被leader和导师批评了一番,因此特意学习varchar的长度设计:
尽管varchar(n)是存储可变长字符串的,但是其n也不是越大越好,理论如下
当MySql在查询需要创建临时表的时候(union,order by、group by,子查询),在MySql读取数据之前,是只知道varchar的长度n,不知道实际数据的长度的,但是读取数据之前需要预分配内存空间,MySql是根据varchar(n)中的n来进行分配内存的,这样也是最合理的方式,不可能分配小于n个字符的空间,因此针对varchar(1000)设置就会预先分配1000个字符空间,很显然这个是十分不靠谱的设计。

三、varchar(50)和varchar(255)有性能上的差别么?

对于INNODB,varchar(50)varchar(255)这两者在存放方式上完全一样:1-2 byte保存长度,实际的字符串存放在另外的位置,每个字符1 byte到4 byte不定(视编码和实际存储的字符而定)。所以将一个字段从varchar(50)长度改成varchar(100)长度不会导致表的重建。但如果把长度从varchar(50)改成varchar(256)就不一样了,表示长度会需要用到2 byte或更多。既然255长度以下对INNODB都一样,而且我们平时基本上也不太会使用到MYISAM,那么是不是为了省心,我们就可以把255长度以下的字段的类型都设置成varchar(255)了呢?
非也。
因为内存表(临时表)介意。
虽然我们不会明文创建内存表,但所有的中间结果都会被数据库引擎存放在内存表(MySQL在有些查询情况下需要创建内存表)。我们可以通过EXPLAIN或者SHOW STATUS可以查看MYSQL是否使用了内存表用来帮助完成某个操作。
而内存表会按照定义的varchar(n)的n来分配内存。以utf-8编码为例,对于varchar(255),每一行所占用的内存就是长度的2 byte + 3 * 255 byte。对于100条数据,光一个varchar字段就占约1GB内存。如果我们该用varchar(50),就可以剩下来约80%的内存空间。
除此之外,255长度也可能会对索引造成坑。MySQL在5.6版本及之前的最大长度是767 byte。但MySQL 5.5版本后开始支持4个byte的字符集utf8mb4(沙雕表情用到的字符太多,长度不够用)。255 * 4 > 767,所以索引就放不下varchar(255)长度的字段了。虽然MySQL在5.7版本后将限制改成了3072 byte,但如果是多字段的联合索引还是有可能会超过这个限制

所以我们的结论就是:在长度够用的情况下,越短越好。

四、varchar(255)和varchar(256)的区别

在varchar长度接近256时,varchar长度设置成255的好处:

1、方便InnoDB建索引,对于 MyISAM,可以对前 1000 个字节做索引,对于 InnoDB,则只有 767 字节。(来源依据)。255X3=765

2、少申请一个字节,记录字符创长度,一个8位的tinyint,可以表示的无符号数值的范围是,0-255,如果长度超过了255,需要在申请个字节

五、varchar(n)长度定义知识总结

1.在长度够用的情况下,越短越好(因为长了会导致查询生成临时表时降低性能且占用内存)。

2.在varchar长度接近256时,varchar长度设置成255更好

六、参考文章

0 10

今天在分析需求的时候,rock哥突然叫我过去,跟我讨论一个需求的建表方案。

 

我和rock负责的这个需求是对一个运营人员(BD)所拥有的门店数据的统计展示。首先我们设计了权限管理,每个BD拥有查看对应门店统计数据的权限,对应到库表是:t_BDRelatedShop_info

即 运营人员的ID和其所拥有权限的门店ID相对应;而另一张表,则是 门店统计数据表:t_shopStatistics_info

按道理来说,要展示一个BD所拥有权限的门店的统计数据,仅仅涉及到三个部分,即 Uid,ShopId,ShopStatisticsInfo,我们完全可以把这三个部分整合到一张表里,这样进行单表查询,效率最高,结构最简单,那为什么还要分两张表呢?

Rock哥告诉我,分表主要有两方面的考虑

架构层面:前文提到 t_BDRelatedShop_info 这个表是用来做权限管理的,也就是说,这个表应该是独立的;如果把 Uid,ShopId,ShopStatisticsInfo 这三部分信息冗余到一张表里,那么是                         不利于系统维护和调整的;

业务层面:1.我们现有的权限审批系统采用的是 T+1 模式,简单来说就是 如果当前BD想要修改权限,比如新增一个可以查看的门店,这个BD提交的权限申请要等到第二天才能生效;

                     2.我们现有的业务表和数据表是分开存放的,出于数据量和安全考虑

        分表之后我们需要先从t_BDRelatedShop_info表中查出BD所拥有的门店统计数据权限的ShopIdList,然后通过ShopIdList再去t_shopStatistics_info表中查出每一个门店所对应的统计信              息,组成列表ShopStatisticsList返回;很显然,分两张表虽然可以解决上述问题,但是在性能上是有损耗的,因为要进行两次查表。

 

联表查询

这个时候有朋友会说,你傻啊,你不会做联表查询吗?

是的,我考虑过,可是Rock哥告诉我,如果建两张表,那么这两张表并不在一个库之中,MySQL的跨库Join查询做的并不怎么好,尤其是一旦BD和门店的数量一上去,Join查询的性能就更加糟糕;

 

双写实现

既然MySQL的跨库Join查询效率不高,那么我们将其改成MySQL的同库Join不就好了吗?

t_BDRelatedShop_info 权限管理表的数量级并不算大,我们可以采用双写的方式实现

即 在t_BDRelatedShop_info表所在的库中也建一个t_shopStatistics_infoB表,作为t_shopStatistics_infoA表的从表,每一次对t_shopStatistics_infoA表的操作都同步到t_shopStatistics_infoB表中,这样可以保证实时同步BD的权限;

BD申请修改权限——>t_shopStatistics_infoA表修改——>同步到t_shopStatistics_infoB表

BD申请查看门店数据统计——>t_shopStatistics_infoB表Join t_shopStatistics_info表

 

至此,该问题暂时被解决了,而这个问题让我了解了很多新东西

  1. 跨库查询和分库分表 分库分表的几种常见玩法及如何解决跨库查询等问题-阿里云开发者社区 (aliyun.com)
  2. 单个数据库实例所能承载的数据量
  3. 联表查询

0 25

disinct 关键字

 

至少SQL Server,HQL,Oracle都有效

通常SQL中对表中数据去重,会首先想到 distinct 关键字,

能实现的需求

1. distinct可以对单个字段去重
select distinct name from A


2. 对多个字段去重时,此时所列的字段需要同时满足才会起到去重效果,否则不会去重

select distinct name, id from A       --name和id同时重复才能去重,有一个不一样都不会去重

        --上面的写法是对name和id列都去重,而不是仅对name去重,id不去重

select distinct * from A      --*所代表的所有字段都重复时,才能去重

不能实现的需求

3. 不能实现指定字段去重,其他字段不去重的效果
select name, distinct id from A   --仅希望对id列进行去重,name列不去重,xxx这样是不行的,无法实现 ,而且会提示错误,因为distinct必须放在开头

问题:
如果想实现针对某一字段去重,其他字段是否重复不关心的效果怎么处理?
答:
可以采用row_number()的窗口函数
转换问题,可以转换为对重复的行取top1,这样使用over()函数,对指定列分组,排序,然后结合row_number()给每一组的数据一列序列,再对集合取序列为1的行

结合一些用例更容易懂一些。

0 16

间隙锁和next-key lock(转自超级大佬)

0.521字数 1,767阅读 3,938
本篇作为学习笔记,文章内容来自“极客时间”专栏《MySQL实战45讲》,如有侵权,请告知,必即时删除。为了便于说明问题,建表和初始化语句如下:

 

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。

顾名思义,间隙锁,锁的就是两个值之间的空隙。比如文章开头的表 t,初始化插入了 6 个记录,这就产生了 7 个间隙。

间隙锁1.png

这样,当你执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6 个记录加上了行锁,还同时加了 7 个间隙锁。这样就确保了无法再插入新的记录。

也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。

跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系

这句话不太好理解,我给你举个例子:

间隙锁2.png

这里 session B 并不会被堵住。因为表 t 里并没有 c=7 这个记录,因此 session A 加的是间隙锁 (5,10)。而 session B 也是在这个间隙加的间隙锁。它们有共同的目标,即:保护这个间隙,不允许插入值。但,它们之间是不冲突的。

间隙锁行锁合称 next-key lock每个 next-key lock 是前开后闭区间。也就是说,我们的表 t 初始化以后,如果用 select * from t for update 要把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。

今天分析的问题都是在可重复读隔离级别下的,间隙锁是在可重复读隔离级别下才会生效的。所以,你如果把隔离级别设置为读提交的话,就没有间隙锁了。

加锁规则

加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”。

  1. 原则 1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 是前开后闭区间。
  2. 原则 2:查找过程中访问到的对象才会加锁。
  3. 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
  4. 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候(非等号),next-key lock 退化为间隙锁。
  5. 一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
案例一:等值查询间隙锁

第一个例子是关于等值条件操作间隙:

间隙锁3.png

由于表 t 中没有 id=7 的记录,所以用我们上面提到的加锁规则判断一下的话:

  1. 根据原则 1,加锁单位是 next-key lock,session A 加锁范围就是 (5,10];
  2. 同时根据优化 2,这是一个等值查询 (id=7),而 id=10 不满足查询条件,next-key lock 退化成间隙锁,因此最终加锁的范围是 (5,10)。

所以,session B 要往这个间隙里面插入 id=8 的记录会被锁住,但是 session C 修改 id=10 这行是可以的。

案例二:非唯一索引等值锁

第二个例子是关于覆盖索引上的锁:

间隙锁4.png

这里 session A 要给索引 c 上 c=5 的这一行加上读锁。

  1. 根据原则 1,加锁单位是 next-key lock,因此会给 (0,5]加上 next-key lock。
  2. 要注意 c 是普通索引,因此仅访问 c=5 这一条记录是不能马上停下来的,需要向右遍历,查到 c=10 才放弃。根据原则 2,访问到的都要加锁,因此要给 (5,10]加 next-key lock。
  3. 但是同时这个符合优化 2:等值判断,向右遍历,最后一个值不满足 c=5 这个等值条件,因此退化成间隙锁 (5,10)。
  4. 根据原则 2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么 session B 的 update 语句可以执行完成。

但 session C 要插入一个 (7,7,7) 的记录,就会被 session A 的间隙锁 (5,10) 锁住。

lock in share mode 只锁覆盖索引但是如果是 for update不一样 执行 for update 时,系统会认为你接下来要更新数据,因此顺便给主键索引上满足条件的行加上行锁

这个例子说明,锁是加在索引上的同时,它给我们的指导是,如果你要用 lock in share mode 来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。比如,将 session A 的查询语句改成 select d from t where c=5 lock in share mode。你可以自己验证一下效果。

案例三:主键索引范围锁

第三个例子是关于范围查询的。

间隙锁5.png

现在我们就用前面提到的加锁规则,来分析一下 session A 会加什么锁呢?

  1. 开始执行的时候,要找到第一个 id=10 的行,因此本该是 next-key lock(5,10]。 根据优化 1, 主键 id 上的等值条件,退化成行锁,只加了 id=10 这一行的行锁。
  2. 范围查找就往后继续找,找到 id=15 这一行停下来,因此需要加 next-key lock(10,15]。

所以,session A 这时候锁的范围就是主键索引上,行锁 id=10 和 next-key lock(10,15]。这样,session B 和 session C 的结果你就能理解了。

这里你需要注意一点,首次 session A 定位查找 id=10 的行的时候,是当做等值查询来判断的,而向右扫描到 id=15 的时候,用的是范围查询判断。

0 26

对于explain命令的使用并不熟悉,只了解它是用来进行SQL优化的,今天特此记录.

 

以下来源于知乎:

刷面试题的时候,不知道你们有没有见过MySQL这两个命令:explainprofile(反正我就见过了)..

之前虽然知道这两个命令大概什么意思,但一直没有去做笔记。今天发现自己的TODO LIST有这么两个命令,于是打算来学习一番,记录一下~

使用的MySQL的版本为5.6.38

一、explain命令

1.1体验explain命令

首先我们来体验一下explain命令是怎么使用的,以及输出的结果是什么:

explain select * from jd_user ;

输出结果:

发现很使用起来很简单,只要explain后边跟着SQL语句就完事了(MySQL5.6之前的版本,只允许解释SELECT语句,从 MySQL5.6开始,非SELECT语句也可以被解释了)。

 

1.2为什么需要explain命令

我们很多时候编写完一条SQL语句,往往想知道这条SQL语句执行是否高效。或者说,我们建立好的索引在这条SQL语句中是否使用到了,就可以使用explain命令来分析一下!

  • 简单来说:通过explain命令我们可以学习到该条SQL是如何执行的随后解析explain的结果可以帮助我们使用更好的索引,最终来优化它!

通过explain命令我们可以知道以下信息:表的读取顺序数据读取操作的类型哪些索引可以使用哪些索引实际使用了,表之间的引用每张表有多少行被优化器查询等信息。

// 好了,我们下面看一下explain出来的结果是怎么看的。

1.3读懂explain命令结果

explain命令输出的结果有10列:id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra

1.3.1id

包含一组数字,表示查询中执行SELECT子句或操作表的顺序

在id列上也会有几种情况:

  • 如果id相同执行顺序由上至下。
  • 如果id不相同,id的序号会递增,id值越大优先级越高,越先被执行
    • (一般有子查询的SQL语句id就会不同)

1.3.2select_type

表示select查询的类型

select_type属性下有好几种类型:

  • SIMPLLE:简单查询,该查询不包含 UNION 或子查询
  • PRIMARY:如果查询包含UNION 或子查询,则最外层的查询被标识为PRIMARY
  • UNION:表示此查询是 UNION 中的第二个或者随后的查询
  • DEPENDENT:UNION 满足 UNION 中的第二个或者随后的查询,其次取决于外面的查询
  • UNION RESULT:UNION 的结果
  • SUBQUERY:子查询中的第一个select语句(该子查询不在from子句中)
  • DEPENDENT SUBQUERY:子查询中的 第一个 select,同时取决于外面的查询
  • DERIVED:包含在from子句中子查询(也称为派生表)
  • UNCACHEABLE SUBQUERY:满足是子查询中的第一个 select 语句,同时意味着 select 中的某些特性阻止结果被缓存于一个 Item_cache 中
  • UNCACHEABLE UNION:满足此查询是 UNION 中的第二个或者随后的查询,同时意味着 select 中的某些特性阻止结果被缓存于一个 Item_cache 中

类型有点多啊,我加粗的是最常见的,起码要看得懂加粗的部分。

1.3.3table

该列显示了对应行正在访问哪个表(有别名就显示别名)。

当from子句中有子查询时,table列是 <derivenN>格式,表示当前查询依赖 id=N的查询,于是先执行 id=N 的查询

1.3.4type

该列称为关联类型或者访问类型,它指明了MySQL决定如何查找表中符合条件的行,同时是我们判断查询是否高效的重要依据

以下为常见的取值

  • ALL:全表扫描,这个类型是性能最差的查询之一。通常来说,我们的查询不应该出现 ALL 类型,因为这样的查询,在数据量最大的情况下,对数据库的性能是巨大的灾难
  • index:全索引扫描,和 ALL 类型类似,只不过 ALL 类型是全表扫描,而 index 类型是扫描全部的索引,主要优点是避免了排序,但是开销仍然非常如果在 Extra 列看到 Using index,说明正在使用覆盖索引,只扫描索引的数据,它比按索引次序全表扫描的开销要少很多。
  • range:范围扫描,就是一个限制的索引扫描,它开始于索引里的某一点,返回匹配这个值域的行。这个类型通常出现在 =、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN、IN() 的操作中,key 列显示使用了哪个索引,当 type 为该值时,则输出的 ref 列为 NULL,并且 key_len 列是此次查询中使用到的索引最长的那个。
  • ref:一种索引访问,也称索引查找,它返回所有匹配某个单个值的行。此类型通常出现在多表的 join 查询, 针对于非唯一或非主键索引, 或者是使用了最左前缀规则索引的查询。
  • eq_ref:使用这种索引查找,最多只返回一条符合条件的记录。在使用唯一性索引或主键查找时会出现该值,非常高效。
  • const、system:该表至多有一个匹配行,在查询开始时读取,或者该表是系统表,只有一行匹配。其中 const 用于在和 primary key 或 unique 索引中有固定值比较的情形。
  • NULL:在执行阶段不需要访问表。

1.3.5possible_keys

这一列显示查询可能使用哪些索引来查找

 

1.3.6key

这一列显示MySQL实际决定使用的索引。如果没有选择索引,键是NULL。

1.3.7key_len

这一列显示了在索引里使用的字节数,当key列的值为 NULL 时,则该列也是 NULL

1.3.8ref

这一列显示了哪些字段或者常量被用来和key配合从表中查询记录出来。

1.3.9row

这一列显示了估计要找到所需的行而要读取的行数,这个值是个估计值,原则上值越小越好。

1.3.10extra

其他的信息

常见的取值如下:

  • Using index:使用覆盖索引,表示查询索引就可查到所需数据不用扫描表数据文件,往往说明性能不错
  • Using Where:在存储引擎检索行后再进行过滤,使用了where从句来限制哪些行将与下一张表匹配或者是返回给用户。
  • Using temporary:在查询结果排序时会使用一个临时表,一般出现于排序、分组和多表 join 的情况,查询效率不高,建议优化。
  • Using filesort:对结果使用一个外部索引排序,而不是按索引次序从表里读取行,一般有出现该值,都建议优化去掉,因为这样的查询 CPU 资源消耗大。

0 28

正文

数据库调优其实一般情况都是我们的SQL调优,SQL的调优就可以解决大部分问题了,当然也不排除SQL执行环节的调优。

数据库的组成可能很多小伙伴都忘记了,那我们再看一遍结构图吧。

我们所谓的调优也就是在执行器执行之前的分析器优化器阶段完成的,那我们开发工作中怎么去调优的呢?

帅丙一般在开发涉及SQL的业务都会去本地环境跑一遍SQL,用explain去看一下执行计划,看看分析的结果是否符合自己的预期,用没用到相关的索引,然后再去线上环境跑一下看看执行时间(这里只有查询语句,修改语句也无法在线上执行)。

遇SQL不决explain,但是这里就要说到第一个坑了。

排除缓存干扰

因为在MySQL8.0之前我们的数据库是存在缓存这样的情况的,我之前就被坑过,因为存在缓存,我发现我sql怎么执行都是很快,当然第一次其实不快但是我没注意到,以至于上线后因为缓存经常失效,导致rt(Response time)时高时低。

后面就发现了是缓存的问题,我们在执行SQL的时候,记得加上SQL NoCache去跑SQL,这样跑出来的时间就是真实的查询时间了。

我说一下为什么缓存会失效,而且是经常失效。

如果我们当前的MySQL版本支持缓存而且我们又开启了缓存,那每次请求的查询语句和结果都会以key-value的形式缓存在内存中的,大家也看到我们的结构图了,一个请求会先去看缓存是否存在,不存在才会走解析器。

缓存失效比较频繁原因就是,只要我们一对表进行更新,那这个表所有的缓存都会被清空,其实我们很少存在不更新的表,特别是我之前的电商场景,可能静态表可以用到缓存,但是我们都走大数据离线分析,缓存也就没用了。

大家如果是8.0以上的版本就不用担心这个问题,如果是8.0之下的版本,记得排除缓存的干扰。

Explain

最开始提到了用执行计划去分析,我想explain是大家SQL调优都会回答到的吧。

因为这基本上是写SQL的必备操作,那我现在问大家一个我去阿里面试被问过的一个问题:explain你记得哪些字段,分别有什么含义?

当时我就回答上来三个,我默认大家都是有数据库基础的,所以每个我这里不具体讨论每个字段,怕大家忘记我贴一遍图大家自己回忆一下。

那我再问大家一下,你们认为统计这个统计的行数就是完全对的么?索引一定会走到最优索引么?

当然我都这么问了,你们肯定也知道结果了,行数只是一个接近的数字不是完全正确的,索引也不一定就是走最优的,是可能走错的。

我的总行数大概有10W行,但是我去用explain去分析sql的时候,就会发现只得到了9.4W,为啥行数只是个近视值呢?

看过基础章节的小伙伴都知道,MySQL中数据的单位都是页,MySQL又采用了采样统计的方法,采样统计的时候,InnoDB默认会选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。

我们数据是一直在变的,所以索引的统计信息也是会变的,会根据一个阈值,重新做统计。

至于MySQL索引可能走错也很好理解,如果走A索引要扫描100行,B所有只要20行,但是他可能选择走A索引,你可能会想MySQL是不是有病啊,其实不是的。

一般走错都是因为优化器在选择的时候发现,走A索引没有额外的代价,比如走B索引并不能直接拿到我们的值,还需要回到主键索引才可以拿到,多了一次回表的过程,这个也是会被优化器考虑进去的。

他发现走A索引不需要回表,没有额外的开销,所有他选错了。

如果是上面的统计信息错了,那简单,我们用analyze table tablename 就可以重新统计索引信息了,所以在实践中,如果你发现explain的结果预估的rows值跟实际情况差距比较大,可以采用这个方法来处理。

还有一个方法就是force index强制走正确的索引,或者优化SQL,最后实在不行,可以新建索引,或者删掉错误的索引。

0 21

Structured Query Language 结构化查询语言

为啥不叫非结构化查询语言呢?NoSQL

为啥不叫结构化操作语言呢?Operate

为啥不叫结构化查询操作呢?

因为这个是针对结构化数据的,查询

 

那么什么是结构化数据呢?一摞书,一摞笔记本,一捆钢笔。这是半理想状态。

书随意摆放,笔记本随意摆放,钢笔夹在书里,笔记本里,放在笔筒里,这是最不理想的状态

一摞按照书名首字母排序摆放在书架的书,一摞按照科目(语文、英语、化学…)顺序摆放的笔记本,一捆按照品牌顺序摆放的钢笔。这是最理想的状态。显然这样的状态是结构化,有规律,按照一定规则安排、处理的。(最直观的是图书馆及图书管理学)如果不是实际物品,而是抽象的概念,那么这个结构化就是现在的关系型数据库(DataBase)

查询结构化数据的语言就是结构化语言。查询化学这个笔记本,只需要定位到笔记本存放的位置(笔记本统一放置在一起,一个地方,甚至是一摞这种状态),第三个就是了。

而select * from notebook where notebook_name =“化学”就是比较合理的查询语句,翻译过来就是从笔记本中找出名字是化学的那个东西(*不管是什么,都直接拿过来给我就可以)

继而形成了SQL。

作者:LazyYoun
链接:https://www.zhihu.com/question/349924681/answer/851943256

一、什么是JDBC?

通俗点讲,J 就是java,DB就是database,C 就是connectivity,中文意思就是java数据库连接,简单点说吧,就是通过java语言去操作数据库。原来我们操作数据库是在控制台使用sql语句来操作数据库,JDBC使用java语言向数据库发送sql语句。

二、JDBC的原理

以前有一个叫SUN的公司,他们公司的数据库工程师想开发一个操作全世界数据库的接口和规范的实现,一开工结果有点难度啊,原因是个大数据库服务器差异性太大了,于是SUN公司的负责人把这些数据库工程师叫到一起开了一个会,讨论的结果是我们不去做具体的实现了,我们定义一个接口规范,让个大数据库厂商去实现,于是各大数据库厂商按照SUN公司的规范提供了一套操作自己数据库的API,也就是JDBC接口的驱动实现类。

注:JDBC是定义的接口规范,JDBC接口的驱动实现类是由各大厂商自己提供的

三、JDBC详解

JDBC的全称是Java数据库连接(Java Database connect),它是一套用于执行SQL语句的Java API。应用程序可通过这套API连接到关系数据库,并使用SQL语句来完成对数据库中数据的查询、更新和删除等操作。应用程序使用JDBC访问数据库的方式如下图所示。

从上图可以看出,应用程序使用JDBC访问特定的数据库时,需要与不同的数据库驱动进行连接。由于不同数据库厂商提供的数据库驱动不同,因此,为了使应用程序与数据库真正建立连接,JDBC不仅需要提供访问数据库的API,还需要封装与各种数据库服务器通信的细节。为了帮助大家更好地理解应用程序如何通过JDBC访问数据库,下面通过一张图来描述JDBC的具体实现细节,如下图

preview

从上图中可以看出,JDBC的实现包括三部分。

(1)JDBC驱动管理器:负责注册特定的JDBC驱动器,主要通过java.sql. Driver Manager类实现。

(2)JDBC驱动器API由Sun公司负责制定,其中最主要的接口是java.sql. Driver接口。

(3)JDBC驱动器:它是一种数据库驱动,由数据库厂商创建,也称为JDBC驱动程序JDBC驱动器实现了JDBC驱动器API,负责与特定的数据库连接,以及处理通信细节。

2、JDBC常用API

在开发JDBC程序前,首先了解一下JDBC常用的API。JDBC API主要位于java.sql包中,该包定义了一系列访问数据库的接口和类,具体如下。

1. Driver接口

Driver接口是所有JDBC驱动程序必须实现的接口,该接口专门提供给数据库厂商使用。在编写JDBC程序时,必须要把指定数据库驱动程序或类库加载到项目的classpath中。
2. DriverManager类

Driver Manager类用于加载JDBC驱动并且创建与数据库的连接。在Driver Manager类中,定义了两个比较重要的静态方法。如表所示:

registerDriver(Driver driver) 

该方法用于向 DriverManager中注册给定的JDBC驱动程程序

getConnection(String url,String user,String pwd)

该方法用于建立和数据库的连接,并返回表示连接的 Connection对象

3、Connection接口

Connection接口代表Java程序和数据库的连接,在Connection接口中,定义了一系列方法,具体如表所示。

getMetaData()
该方法用于返回表示数据库的元数据的 DatabaseMetaData对象

createStatement()
用于创建一个Statement对象来将SQL语句发送到数据库

prepareStatement(String sql)
用于创建一个PreparedStatement对象来将参数化的SQL语句发送到数据库

prepareCall(String sql)
用于创建一个CallableStatement对象来调用数据库存储过程

4、Statement接口

Statement接口用于向数据库发送SQL语句,在Statement接口中,提供了三个执行SQL语句的方法,具体如表所示。

execute(String sql)

用于执行各种SQL语句,该方法返回一个boolean类型的值,如果为true,表示所执行的SQL语句具备查询结果,可通过Statement的getResultSet方法获得查询结果。

executeUpdate(String sql)

用于执行SQL中的Insert、update和delete语句。该方法返回一个int类型的值,表示数据库中受该SQL语句影响的记录的数目。

executeQuery(String sql)

用于执行SQL中的select语句,该方法返回一个表示查询结果的ResultSet对象

5. PreparedStatement接口

PreparedStatement是Statement的子接口,用于执行预编译的SQL语句。在PreparedStatement接口中,提供了一些基本操作的方法,具体如表下所示。

executeUpdate()

在此PreparedStatement对象中执行SQL语句,该语句必须是个DML语句或者是无返回内容的SQL语句,比如DDL语句。

executeQuery()

在此PreparedStatement对象中执行SQL查询,该方法返回的ResultSet对象

setInt(int parameterIndex, int x)

将指定参数设置为给定的int值

setFloat(int parameterIndex, float x)

指定参数设置为给定的float值

setString(int parameterIndex, String x)

将指定参数设置为给定的String值

setDate(int parameterIndex, Date x)

将指定参数设置为给定的Date值

addBatch()

将一组参数添加到此PreparedStatement对象的批处理命令中

setCharacterStream(parameterIndex, reader, length)

将指定的输入流写入数据库的文本字段

setBinaryStream(parameterIndex, x, length)

将二进制的输入流数据写入到二进制字段中

需要注意的是,上表中的setDate()方法可以设置日期内容,但参数Date的类型是java.sq.Date,而不是java.util.Date。

6、CallableStatement接口

CallableStatement是PreparedStatement的子接口,用于执行SQL存储过程。在Callablestatement按接口中,提供了一些基本操作的方法,具体下表所示:

registerOutParameter(int parameterIndex,int sqlType)

按顺序位置将OUT参数注册为SQL类型。其中,parameterIndex表示顺序位置,sqlType表示SQL类型

setNull(String parameter Name, int sqlType)

将指定参数设置为SQL类型的NULL

setString(String parameterName, String x)

查询最后一个读取的OUT参数是否为SQL类型的NULL

wasNull()

查询最后一个读取的OUT参数是否为SQL类型的NULL

getlnt(int parameterIndex)

以Java语言中int值的形式获取指定的数据库中INTEGER类型参数的值

需要注意的是,由于 CallableStatement接口继承PreparedStatement,PreparedStatement接口又继承了 Statement,因此CallableStatement接口中除了拥有自己特有的方法,也同时拥有了这两个父接口中的方法。

7、ResultSet接口

ResultSet接口表示 select查询语句得到的结果集,该结果集封装在一个逻辑表格中。在 ResultSet接口内部有一个指向表格数据行的游标,ResultSet对象初始化时,游标在表格的第一行之前。下表中列举了ResultSet接口中的常用方法。

getString(int columnIndex)

用于获取指定字段的String类型的值,参数columnIndex代表字段的索引
getString(String columnName)

用于获取指定字段的String类型的值,参数column Name代表字段的名称
getInt(int columnIndex)

用于获取指定字段的int类型的值,参数columnIndex代表字段的索引

getInt(String columnName)

用于获取指定字段的int类型的值,参数columnName代表字段的名称

getDate(int columnIndex)

用于获取指定字段的Date类型的值,参数columnIndex代表字段的索引
getDate(String columnName)

用于获取指定字段的Date类型的值,参数column Name代表字段的名称
next()

将游标从当前位置向下移一行
absolute(int row)

将游标移动到此Resultset对象的指定行
afterLast()

将游标移动到此ResultSet对象的末尾,即最后一行之后
beforeFirst()

将游标移动到此Resultset对象的开头,即第一行之前
previous()
将游标移动到此ResultSet对象的上一行
last()

将游标移动到此ResultSet对象的最

从上表中可以看出,ResultSet接口中定义了大量的getXxx()方法,采用哪种getXxx()方法取决于字段的数据类型。程序既可以通过字段的名称来获取指定数据,也可以通过字段的索引来获取指定的数据,字段的索引是从1开始编号的。

四、JDBC实现步骤

DBC编程大致按照以下几个步骤进行。

(1) 加载并注册数据库驱动,具体方式如下。

DriverManager.registerDriver(Driver driver);

(2) 通过Driver Manager获取数据库连接,具体方式如下。

Connection conn= DriverManager.getConnection(String url, String user, String pass);
从上述方式可以看出,getConnection()方法中有三个参数,它们分别表示数据库url、登录数据库的用户名和密码。数据库山通常遵循如下形式的写法。

jdbc:subprotocol:subname

上面的URL写法中jdbc部分是固定的,subprotocol指定链接达到特定数据库的驱动程序,而subname部分则很不固定,也没有什么规律,不同数据库的形式可能存在较大差异,一Mysql数据库为例,其形式如下:

jdbc:mysql://hostname:port/databasename

(3)通过Connection对象获取Statement对象。Connection创建Statement的方式有如下三种。

① createStatement(): 创建基本的Statement对象

② prepareStatement(): 创建PreparedStatement对象。

③ preparCall(): 创建CallableStatement对象。

以创建基本的Statement对象为例,具体方式如下。

Statement stmt=conn.createStatement();

(4)使用Statement执行SQL语句。所有的Statement都有如下三种方法来执行语句。

①execute():可以执行任何SQL语句。

②executeQuery():通常执行查询语句,执行后返回代表结果集的Resultset对象。

③executeUpdate():主要用于执行DML和DDL语句。执行DML语句,如INSERT、UPDATE或 DELETE时,返回受SQL语句影响的行数,执行DDL语句返回0。

以executeQuer()方法为例,具体方式如下。

//执行SQL语句,获取结果集ResulSet
ResultSet rs=stmt.executQuery(sql);

(5)操作ResultSet结果集。如果执行的SQL语句是查询语句,执行结果将返回Resultset对象,该对象里保存了SQL语句查询的结果。程序可以通过操作该ResultSet对象来取出查询结果。 ResultSet对象提供的方法主要可以分为以下两类。

①next()、previous()、first()、last()、beforeFirst()、afterLast()、absolute()等移动记录指针的方法

②getXxx()获取指针指向行,特定列的值。

(6)回收数据库资源。关闭数据库连接,释放资源,包括关闭ResultSet、Statement和Connection等资源。

五、JDBC实现案例

1、搭建实验环境

CREATE DATABASE chapter01;
USE chapter01;
CREATE TABLE users(
      id INT PRIMARY KEY AUTO_INCREMENT,
      name VARCHAR(40),
      password VARCHAR(40),
      email VARCHAR(60),
      birthday DATE
)CHARACTER SET utf8 COOLLATE utf8_genneral_ci;

数据库和表创建成功后,再向users表中插入三条数据,SQL语句如下所示。

INSERT INTO users(NAME,PASSWORD,email,birthday)
VALUES('zs','123456','zs@sina.com','1980-12-04');
INSERT INTO users(NAME,PASSWORD,email,birthday)
VALUES('lisi',123456,1isi@sina.com,'1981-12-04');
INSERT INTO users(NAME,PASSWORD,email,birthday)
VALUES('wangwu',123456,'wangwu@sina.com','1979-12-04');

2、导入数据库驱动

新建Java工程chapter01,将要访问的数据库驱动文件添加到classpath中。由于应用程序访问的是MySQL数据库,因此,将MySQL的数据库驱动文件mysql-connector-java-5.0.8-bin.jar添加到classpath中即可。

3、编写JDBC程序

在工程chapter01中,新建Java类Example01,该类用于读取数据库中的users表,并将结果输出,如例下面案例所示。

package cn.itcast.jdbc.example;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Date;
public class Example01 {
    public static void main(String[] args) throws SQLException {
        //1.注册数据库的驱动
        DriverManager.registerDriver(new com.mysql.jdbc.Driver());
        //2.通过 DriverManager获取数据库连接
        String url="jdbc:mysql://localhost:3306/chapter01";
        String usernames="root";
        String password="itcast";
        Connection conn=DriverManager.getConnection(url, username, password);
        //3.通过 Connection对象获取 Statement对象
        Statement stmt= conn.createStatement();
        //4.使用 Statement执行SQL语句
        String sql="select * from users";
        ResultSet rs=stmt.executeQuery(sql);
        //5、操作 ResultSet结果集
        System.out.println("id|name|password|email|birthday");
        while (rs.next()) {
            int id=rs.getInt("id");     //通过列名获取指定字段的值
            String name=rs.getString("name");
            String psw=rs.getString("password");
            String email=rs.getString("email");
            Date birthday=rs.getDate("birthday");
            System.out.println(id+"|"+name+"|"+psw+"|"+email+"|"+birthday); 
        }
        //6.回收数据库
        rs.close();
        stmt.close();
        conn.close();
    }
}

程序执行后,会讲从users表中读取到的数据打印到控制台。

在上面案例中演示了JDBC访问数据库的步骤。首先注册MySQL的数据库驱动器类,通过 DriverManager获取一个Connection对象,然后使用Connection对象创建了一个Statement对象,Statement对象能够通过executeQuery()方法执行SQL语句,并返回结果集ResultSet对象。最后,通过遍历Resultset对象便可得到最终的查询结果。需要注意的是,在实现第一个JDBC程序时,还有两个方面需要改进,具体如下。

六、加餐

1、注册驱动

在注册数据库驱动时,虽然DriverManager.registerDriver(new com. mysql.jdbc.Driver())方法可以完成,但会使数据库驱动被注册两次。这是因为Driver类的源码中,已经在静态代码块中完成了数据库驱动的注册。所以,为了避免数据库驱动被重复注册,只需要在程序中加载驱动类即可,具体加载方式如下所示。

Class.forName("com.mysqk.jdbc.Driver");

Class.forName() 方法

此方法含义是:加载参数指定的类,并且初始化它。

Java class.forname 详解 | 菜鸟教程 (runoob.com)

2、释放资源

由于数据库资源非常宝贵,数据库允许的并发访问连接数量有限,因此,当数据库资源使用完毕后,一定要记得释放资源。为了保证资源的释放,在Java程序中,应该将最终必须要执行的操作放在finally代码块中,具体方式如下。

if(rs!=null) {
    try {
        rs.close();
    }catch (SQLException e) {
        e.printStackTrace();
    }
    rs=null;
}
if(stmt!=null) {
    try {
        stmt.close();
    }catch (SQLException e) {
        e.printStackTrace();
    }
    stmt=null;
}
if(conn!=null) {
    try {
        conn.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
    conn=null;
}

文章转载链接:什么是JDBC,JDBC的原理是什么 - 简书 (jianshu.com)
什么是JDBC?这篇文章告诉你 - 知乎 (zhihu.com)