MySQL
char和varchar的区别
1.长度可变性:
varchar类型用于存储可变长度的字符串,比固定长度类型更加节省空间。有一种情况除外:如果MySQL表使用ROW_FORMAT=FIXED创建的话,每一行都会使用定长存储。
char类型用于存储定长字符串。
2.存储方式
varchar需要1或者2个额外字节记录字符串的长度:如果列的最大长度小于或者等于255字节,则只用使用1个字节表示,否则需要两个字节表示。VARCHAR节省了存储空间,所以对性能也有帮助。但是,由于行是变长的,在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作。如果一个行占用的空间增长,并且在页内没有更多的空间可以存储,在这种情况下,不同的存储引擎的处理方式是不一样的。例如,MylSAM会将行拆成不同的片段存储,InnoDB则需要分裂页来使行可以放进页内。
char适合存储很短或者长度近似的字符串。例如密码的MD5值,因为这是一个定长值。对于经常改变的值,char要比varchar好,因为定长的char类型不容易产生碎片。对于非常短的列,char在空间存储上比varcahr更有效率,例如用char(1)来存储只有Y和N的值,如果采用单字节字符集只需要一个字节,但是varchar(1)却需要两个字节,因为还有一个记录长度的额外字节。
视图
视图是虚拟的表。与包含数据的表不一样,视图只包含使用时动态索引数据的查询。
创建视图:视图用create view语句来创建。
/*CREATE VIEW 视图名称 AS SQL查询语句*/
CREATE VIEW view1 AS
SELECT
account,
admin.name aname,
role.name rname
FROM
admin,
admin_role,
ROLE
WHERE admin.`id` = admin_role.adminid
AND admin_role.`roleid` = role.`id`
执行语句:
SELECT * FROM view1
修改:
#方式一:
CREATE OR REPLACE VIEW 视图名
AS
查询语句;
#方式二:
ALTER VIEW 视图名
AS
查询语句;
删除语句
DROP VIEW 视图1,视图2,...;
视图的特点
- 简化sql语句
- 提高SQL的重用性
- 保护基本表数据,提高了安全性
小结:
视图为虚拟的表。它包含的不是数据而是根据需要检索数据的查询。视图提供了一种封装SELECT语句的层次。
存储过程
存储过程:简单来说,存储过程就是为以后使用而保存的一条或者多条SQL语句。可以将其视为批文件,虽然他们的作用不仅限于批处理。
在存储过程的定义中可以定义参数,参数分为IN、OUT、ONOUT三种类型。
- IN类型:表示接受调用者传入的数据;
- OUT类型:表示向调用者返回数;
- INOUT类型:既可以接受调用者传入的参数,也可以向调用者返回数据。
存储过程的特点
- 通过处理封装在容易使用的单元中,简化了复杂的操作。
- 简化对变动的管理。如表名、列名、或者业务逻辑有了变化。只需要更改存储过程的代码。使用它的人不用修改自己的代码。
- 有助于提高应用程序的性能
- 减少应用程序与数据库服务器之间的流量。应为应用程序不必发送多个冗长的SQL语句,只用发送存储过程中的名称和参数即可。
缺点:
- 大量使用存储过程,这些存储过程的每个链接的内存使用量将大大增加。
- 如果在存储过程中大量使用逻辑操作,CPU的使用率也会增加。MySQL数据库最初的设计就侧重于高效的查询,而不是逻辑运算。
CREATE
PROCEDURE 数据库名.存储过程名([in变量名 类型,out 参数 2,...])
BEGIN
[DECLARE 变量名 类型 [DEFAULT 值];]
存储过程的语句块;
END;
定义一个存储过程:
CREATE
PROCEDURE `demo2`(IN s_sex CHAR(1),OUT s_count INT)
-- 存储过程体
BEGIN
-- 把SQL中查询的结果通过INTO赋给变量
SELECT COUNT(*) INTO s_count FROM student WHERE sex = s_sex;
SELECT s_count;
END$$
调用这个存储过程:
-- @s_count表示测试出输出的参数
CALL demo2 ('男',@s_count);
/* 结果
s_count
6
*/
触发器
触发器trigger是一种特殊的存储过程,其特殊性在于它并不需要用户直接调用,而是对表添加、修改、删除之前或者之后自动执行的存储过程。
触发器的特点
-
与表相关联
触发器定义在特定的表上,这个表称为触发器表。
-
自动激活触发器
当表中的数据执行INSERT、UPDATE或者DELETE操作时,如果表上的这个操作定义了触发器,该触发器自动执行,这是不可以撤销的。
-
不能直接调用
与存储过程不同,触发器不能被直接调用,也不能传递或者接受参数。
-
作为事务的一部分
触发器和激活触发器的语句一起作为一个单一的事务来对待,可以从触发器中的任何位置回滚。
定义触发器:
CREATE TRIGGER 触发器名称 触发时机 触发事件
ON 表名称
FOR EACH ROW -- 行级触发
BEGIN
语句
END;
语法解析:
- 发器名称:用来标识触发器的,由用户自定义。
- 触发时机:其值为before或者after。
- 触发事件:insert、update和delete
- 表名称:表示在哪张表建立触发器
- 语句:触发器程序体,触发程序可以使用begin和end作为开始和结束,中间包含多条语句;
删除用户时,自动删除用户菜单关系
DELIMITER $$
CREATE TRIGGER delete_user_menu BEFORE DELETE
ON t_user
FOR EACH ROW
BEGIN
DELETE FROM t_user_menu WHERE user_id = old.id;
END$$;
新增用户时 自动向其他表中插入数据
DELIMITER $$
CREATE TRIGGER save_user_log AFTER INSERT
ON user
FOR EACH ROW
BEGIN
INSERT INTO test(id,NAME)VALUES(new.id,new.account);
END$$;
在行级触发器代码中,可以使用old和new访问到该行的旧数据和新数据,old和new是对应表的行记录类型变量。
MySQL引擎
数据库引擎用于存储、处理和保护数据的核心服务。利用数据库引擎可控制访问权限并且快速处理事务,从而满足企业内大多数需要大量数据的应用程序要求。
存储引擎主要有:
MyIsam、InnoDB、Memory、Blackhole等。
mysql数据库默认引擎是InnoDB
InnoDB是一个事务型的存储引擎,有行级锁和外键约束,支持全文检索(索引),它的设计目标是处理大容量数据库系统,MySQL运行时InnoDB会在内存中建立缓冲池,用于缓冲数据和索引;支持主键自增,不存储表的总行数。
索引
为什么要有索引呢?
假设某张表中有10万条数据,这100万条数据在硬盘上是存储在数据页上的,一页数据大小为16K,100万条数据需要很多数据页,现在如果要查询id = 8900条数据信息。MySQL需要全表扫描来找到id = 8900 的数据,也就是从“数据页1”来查询,对于大量数据查询起来是非常慢的。
什么是索引
索引就是一个排好序的快速查找的数据结构。
数据库除存储数据本身之外,还维护着一个满足特定查找算法的数据结构,这些数据结构以某种方式指向数据,这要就可以在这些数据结构的基础上实现高级查找算法,这种数据结构就是索引。
左边是数据表,一共两列七行记录,最左边表示数据记录的地址,为了加快Col2的查找速度,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个执行对应数据记录物理地址的指针,这样就可以运用二叉查找在一定的复杂度内获取到响应数据,而快速的检索出符合条件的记录。
索引的原理类似于查字典,查询书籍等。本质都是不断地缩小数据范围筛选出想要的结果。
索引的优势
提高数据检索的效率,降低数据库的IO成本
通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗
索引的劣势
索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引列也是要占用磁盘内存的。
索引提高查询速度的同时也会降低更新表的速度,例如进行INSERT、UPDATE和DELETE因为更新时,MySQL不仅要保证数据,还要保证一下,索引文件,每次更新添加了索引列的字段,都会调整因为更新带来的键值变化后的索引信息。
索引类型
FULLTEXT:全文索引,一般用于查找文本中的关键字,而不是直接比较是否相等,多在char、varchar、text 等数据类型中创建全文索引。全文索引主要是用来解决like模糊匹配效率低的问题
HASH:哈希索引,多用于等值查询,时间复杂度为o(1),效率非常高,但是不支持排序、范围查询和模糊查询等
BTREE:B+数索引,InnoDB存储引擎默认的索引,支持排序,分组,范围查询,模糊查询等,并且性能稳定。
RTREE:空间数据索引,多用于地理数据的存储,优势在于支持范围查找。
索引种类
主键索引:设定为主键后数据库会自动创建索引;
ALTER TABLE 表名 add PRIMARY KEY 表名(列名);
单列索引:一个索引值包含一个列,一个表可以有多个单列索引
CREATE INDEX 索引名 ON 表名(列名);
唯一索引:索引列的值必须唯一,允许为null
CREATE UNIQUE INDEX 索引名 ON 表名(列名);
组合索引:一个索引包含多个列,在数据库操作期间,符合索引比单值左印所需要的的开销更小(对于相同的多个列建立索引)
CREATE INDEX 索引名 ON 表名(列 1,列 2…);
组合索引最左前缀原则
例如某个表中有a、b、c三列,a、b为组合索引,那么使用时需要满足最左侧索引原则。在使用组合索引的列作为条件时,必须出现最左侧列作为条件,否则组合索引不生效。
-- 列如
select * from table where a=’’and b=’’-- 索引生效
select * from table where b=’’and a=’’-- 索引生效
select * from table where a=’’and c=’’-- 索引生效
select * from table where b=’’and c=’’-- 索引不生效
索引创建原则
需要创建索引
- 主键自动创建唯一索引
- 频繁作为查询条件的字段应该设置为索引,就是where后面的语句
- 查询中与其他表关联的字段,外键关系建立索引
- 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
- 分组中的字段需要建立索引
不要创建索引
-
表记录太少
-
经常增删改的表:索引提高了查询的速度,同时会降低更新表的速度,如对表进行INSERT、UPDATE、DELETE,因为更新表时,MySQL不仅要存储数据,还要更新索引文件
-
where条件里用不到的字段不创建索引
-
数据重复且分布平均的表字段
B树和B+树的区别
数据库为什么使用B+树而不是B树
聚簇索引和非聚簇索引
聚簇索引
找到了索引就找到了需要的数据,那么这个索引就是聚簇索引,所以主键就是聚簇索引。
非聚簇索引
索引的存储和数据是分离的,也就是说找到了索引但是没有找到数据,需要根据索引上的值再次回表查询,非聚簇索引也叫辅助索引。
一个例子:
CREATE TABLE student (
id BIGINT,
NO VARCHAR (20),
NAME VARCHAR (20),
PRIMARY KEY (`id`),
UNIQUE KEY `idx_no` (`no`)
)
创建一个学生表,做三种查询:
SELECT * FROM student WHERE id = 1
直接根据主键查询获取所有字段数据,此时主键是聚簇索引,因为主键对应的索引叶子节点存储了id=1的所有字段的值。
SELECT NO,NAME FROM student WHERE NO = 123
根据编号查询,编号本身是一个唯一索引,但查询的列包含学生编号和学生姓名,当命中编号索引时,该索引的节点的数据存储的是主键ID,需要根据主键ID重新查询一次,所以这种查询下编号no不是聚簇索引。
SELECT NO FROM student WHERE NO = 123
根据编号查询编号,?这有啥好差的?要的。可能是验证数据库中是否存在该编号,这种查询命中编号索引时,直接返回编号,因为需要的数据就是该索引,不需要回查,这种场景下no就是聚簇索引。
数据库事务
MySQL事务处理主要有两种方法
-
用BEGIN、ROLLBACK、COMMIT来实现
BEGIN:开启一个事务
ROLLBACK:事务回滚
COMMIT:事务确认
-
直接用set来改变MySQL的自动提交模式:
set global autocommit = 0;-- 禁止自动提交 set global autocommit = 1;-- 开启自动提交
查看autocommit模式:
show global variables like 'autocommit';
事务隔离级别
为什么要有隔离级别?
MySQL是一个服务器/客户端软件架构,也就是说,多个客户端连接服务器后,可以同时在不同的会话(一个客户端与服务器连接成功后就叫一个会话)中对服务器进行操作,输入各种语句,这些语句可以作为事务的一部分进行处理。不同的会话可以同时发送请求,也就是说服务器可能同时在处理多个事务,这样可能会导致不同的事务同时访问相同的数据。因为一个事务具有隔离性,当一个事务提交后其他事务才能执行继续访问这个数据。这样对性能影响很大,所以设计数据库时就提出来各种隔离级别,来最大限度的提升系统并发处理事务的能力。
查看隔离级别:
SELECT @@global.transaction_isolation,@@transaction_isolation;
mysql数据库提供了四种隔离级别,实际开发根据不同场景选择不容的隔离级别,除了串行化其他级别都存在某种问题。
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
原数据users表:
id | name | age |
---|---|---|
1 | jim | 20 |
设置隔离级别为未提交读(read uncommitted):
同时开启两事务A、B,B事务在A事务还没有提交的时候读取A中修改的数据。
事务A:
BEGIN;
UPDATE users SET age = 18 WHERE id = 1 -- 1
ROLLBACK -- 3
COMMIT; -- 4
事务B:
BEGIN;
SELECT age FROM users WHERE id = 1 -- 2
COMMIT;-- 5
/*
结果: 18
数据库表 age = 20
*/
B读取可A未提交的数据,A事务中发生了回滚,数据库中age并没有被修改,所以B事务读取到的数据是错的,所以称之为脏读。
设置提交读隔离级别(read committed):
同时开启两个事务A、B,B事务在A事务提交后才能读取到A中修改的结果。
A事务未提交时,B事务获取到的仍然是事务开启前的结果。
A事务:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN;
UPDATE users SET age = 18 WHERE id = 1-- 1
COMMIT;-- 3
B 事务:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN;
SELECT age FROM users WHERE id = 1 -- 2 结果 20
SELECT age FROM users WHERE id = 1 -- 4 结果 18
COMMIT;-- 5
提交读隔离级别可以解决脏读问题,但是B事务中两次查询的结果不一致,称为不可重复读。
设置可重复读隔离级别(repeatable read):
MySQL默认的隔离级别。同一个事务中多次读取相同的数据返回的结果是一样的。其避免了脏读和不可重复读问题,但是除InnoDB 外幻读依然存在。
事务A:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN;
UPDATE users SET age = 18 WHERE id = 1 -- 2
COMMIT; -- 3
事务B:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN;
SELECT age FROM users WHERE id = 1 -- 1 结果18
SELECT age FROM users WHERE id = 1 -- 4 结果18
COMMIT; -- 4
可重复读,可能会产生幻读问题,幻读就是B事务查询了users 表,发现只有id=1的用户想要添加一个id = 2的用户,但是A事务快一步添加,B事务在添加的时候就会显示重复的主键,可是B事务查询明明没有id = 2的数据。
设置串行化(serializable)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE
解决幻读问题:
读写加锁,B事务如果未提交,A事务是无法insert的,必须等待B事务提交;
事务A:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN;
INSERT INTO users(id,NAME,age) VALUE (3 ,"ldl",18)-- 2 执行后一直在等待状态,直到B事务提交.
COMMIT;
事务B:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN;
SELECT * FROM users -- 1
COMMIT;
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
read uncommitted | 可能 | 可能 | 可能 |
read committed | 不可能 | 可能 | 可能 |
repeatable read | 不可能 | 不可能 | 可能 |
serializable | 不可能 | 不可能 | 不可能 |
事务实现的原理
MySQL中日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种日志,redolog(重做日志)和undolog(回滚日志)。其中redolog用于保存事务的持久性;undolog则是事务原子性和隔离性实现的基础。
原子性实现原理
原子性实现的关键,是当事务发生回滚的时候,能撤销所有已经成功执行的SQL语句。InnoDB实现回滚靠的是undolog:当事务对数据库中的数据进行修改时,InnoDB会生成对应的undolog;如果事务执行失败或者是执行ROLLBACK,导致事务需要回滚时,就可以利用undolog中的数据来回滚到修改之前的样子。
undolog属于逻辑日志,它记录的是SQL执行的相关信息。当发生回滚时,InnoDB会根据undolog的内容做与之前相反的工作:对于每个insert,回滚时执行delete,每个delete执行insert,每个update执行一个相反的update,把数据改回去。
持久性实现原理
redolog叫做重做日志,是保证实物持久性的重要机制。当mysql服务器意外崩溃或者宕机后,保证已经提交的事务,确定持久化到磁盘中的一种措施。
InnoDB是以页为单位来管理存储空间的,任何的增删改查操作最终都会操作完整的一个页,会将整页的数据加载到buffer pool中,然后对需要修改的记录进行修改,修改完毕后不会立即刷新到磁盘中,而且仅仅修改了一条记录,刷新一个完整的数据页的话过于浪费了。但是如果不刷新的话,数据此时还在内存中,如果此时系统崩溃最终数据会丢失。因此引入了redolog,也就是说,修改完成之后不立即刷新,而是记录一条日志,日志内容就是记录那个页面,多少偏移量,什么数据发生了什么改变。这样即使系统崩溃,在恢复后也可以根据日志进行数据恢复。另外,redolog是循环写入固定的文件,是顺序写入磁盘的。
总结:redolog就是记录在操作之后,记录哪个页面,多少偏移量,什么数据发生了怎样的变化。
隔离级别实现原理MVCC
MVCC是什么?
多版本并发控制(MVCC,Multi-Version Concurrent Control),是MySQL提高性能的一种方式,配合undolog和版本链,让不同事务的读写、写读操作可以并发执行,减少锁的使用,从而提升系统的性能。
MVCC的目的是让读写,写读操作并发执行.
MVCC使得数据库读不会对数据加锁,普通的select请求不会加锁,提高了数据库的并发处理能力。借助MVCC,数据库可以实现READ COMMITTED,REPEATABLE READ等隔离级别。
事务D的两次查询在不同的隔离级别结果是不同的。
读未提交:name = 张二三 ;name = 张三三
读已提交:name = 张三 ;name = 张二三
可重复读: name = 张三; name = 张三
MVCC是通过在每行记录后面保存两个隐藏的列来实现的。一个保存行的事务ID(TRX_ID),一个保存了行的回滚指ROLL_PT).
trx_id:每次对某行记录执行改动时,都会把对应事务id赋值给trx——id隐藏列。
roll_pt:每次对记录进行改动时,都会把旧版本写到undolog日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到修改前的信息。
对该记录每次更新后,都会将旧值放到一条 undolog 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pt 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务 id,这个信息很重要。
ReadView 是什么
ReadView是“快照读”SQL执行时MVCC提取数据的依据,是一个数据结构,包含4个字段
m_ids:当前活跃的事务编号集合
min_trx_id:最小活跃事务编号
max_trx_id:预分配事务编号,当前最大事务编号+1
creator_trx_id:ReadView创建者的事务编号
读已提交(RC):
可重复读(RR):
读已提交级别: 称为当前读,当每个事物每次读取时,会生成一个 readVew,读取
的是最新数据.
可重复读级别: 称为快照读,当一个事务第一次查询时,会生成一个 readView,第二次查询时仍会从当前 readView 中读数据.
锁机制
行锁、间隙锁、表锁
按照粒度,锁可以分为行锁、间隙锁和表锁。间隙锁位于行锁和表锁之间。表锁在操作数据时会锁定整张表,并发性能差;行锁只锁定需要操作的数据,并发性能好,但是加锁本身就需要消耗资源(获取锁、检查锁、释放锁等都需要耗费资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL中不同存储引擎支持的锁是不一样的,MyIsam支持表锁,InnoDB同时支持表锁和行锁。
行锁
MySQL中粒度最小的一种锁,表示只针对当前操作的行进行加锁。行锁能大大减少数据库操作的冲突。其锁的粒度小,但是加锁的开销大
特点:开销大;加锁慢;会出现死锁;锁定力度小,发生冲突概率低;并发度也高。
间隙锁
锁的是一个区间,当我们用范围条件而不是相等条件检索数据,InnoDB 会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙",InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key 锁)。 例如:
select * from student where id>2 and id<5
/*
数据库中只有id为1和2的数据
即使数据库中没哟id = 3 的数据,其他线程想要添加id为3的数据也是不可以的,因为2到5的数据已经被锁定.
*/
表锁
表级锁是 MySQL 中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使用的MYISAM 与 INNODB 都支持表级锁定。表级锁定分为表共享锁与表排他锁。
特点:
开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低.
共享锁(s)
共享锁又称为读锁。允许一个事务去读一行,组织其他事务获取相同数据的排他锁。若事务T对数据A加上S锁,则事务T可以读取A但是不能修改A,其他事务只能对A加S锁,不能加X锁,直到T释放A上的S锁。
排他锁(X)
排他锁又称为写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务 T 对数据对象 A 加上 X 锁,事务 T可以读 A 也可以修改 A,其他事务不能再对 A 加任何锁,直到 T 释放 A 上的锁。update,delete,insert 都会自动给涉及到的数据加上排他锁,select 语句默认不会加任何锁类型。
SQL优化
-
尽量不要使用select * ,而是使用具体字段。可能用到覆盖索引,减少回表,提高查询效率。
-
避免在where字句中使用or来链接条件,使用union all吧两个SQL结果合并。使用or可能会使索引失效,从而全表扫描。
-
尽量用数值代替字符串类型。如:主键用int类型,性别用0/1.在查询和链接时会逐个比较字符串的每个字符;数值类型只需要比较一次就可以了。
-
对于查询的优化,尽量避免全表扫描,首先应该考虑在where以及order by涉及到的列上建立索引。
-
尽量避免索引失效
5.1 在where字句中对字段进行null值判断,否则将会导致引擎放弃使用索引进而全表扫描,可以在num上设置默认值0,5.2 避免在where字句中使用or来链接条件,将会导致引擎无法使用索引进而全表扫描,使用union all吧两个SQL结果合并。
5.3 in和not in 也要谨慎使用,例如in(1,2,3)这种连续的数值,能用between就不要用in,between 1 and 3
5.4 模糊查询也将导致全表扫描。
5.5尽量避免在where字句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:: select id from t where substring(name,1,3)='abc
-
inner join、left join、right join优先使用inner join
三种连接如果结果相同,优先使用 inner join
inner join 内连接,只保留两张表中完全匹配的结果集;
left join 会返回左表所有的行,即使在右表中没有匹配的记录;
right join 会返回右表所有的行,即使在左表中没有匹配的记录;
-
使用group by 时,建议先过滤在分组。
-
清空表时,优先使用truncate
truncate table 比 delete 速度快,且使用的系统和事务日志资源少.
delete 语句每次删除一行,并在事务日志中为所删除的每行记录一项。truncate
table 通过释放存储表数据所用的数据页来删除数据.
-
表链接不宜太多。
-
不建议在索引上使用内置函数。