CREATE DATABASE test23; USE test23; CREATE TABLE class ( id INT(11) NOT NULL AUTO_INCREMENT, className VARCHAR(30) DEFAULT NULL, address VARCHAR(40) DEFAULT NULL, monitor INT NULL , PRIMARY KEY (id) ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; CREATE TABLE student ( id INT(11) NOT NULL AUTO_INCREMENT, stuno INT NOT NULL , name VARCHAR(20) DEFAULT NULL, age INT(3) DEFAULT NULL, classId INT(11) DEFAULT NULL, PRIMARY KEY (id) ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; set global log_bin_trust_function_creators=1; -- #随机产生字符串 DELIMITER // CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255) BEGIN DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ'; DECLARE return_str VARCHAR(255) DEFAULT ''; DECLARE i INT DEFAULT 0; WHILE i < n DO SET return_str = CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1)); SET i = i + 1; END WHILE; RETURN return_str; END // DELIMITER ; -- 随机产生班级编号 DELIMITER // CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11) BEGIN DECLARE i INT DEFAULT 0; SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ; RETURN i; END // DELIMITER ; -- 创建往stu表中插入数据的存储过程 DELIMITER // CREATE PROCEDURE insert_stu( START INT , max_num INT ) BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; REPEAT SET i = i + 1; INSERT INTO student (stuno, name ,age ,classId ) VALUES ((START+i),rand_string(6),rand_num(1,50),rand_num(1,1000)); UNTIL i = max_num END REPEAT; COMMIT; END // DELIMITER ; -- 创建往class表中插入数据的存储过程 DELIMITER // CREATE PROCEDURE insert_class( max_num INT ) BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; REPEAT SET i = i + 1; INSERT INTO class ( classname,address,monitor ) VALUES (rand_string(8),rand_string(10),rand_num(1,100000)); UNTIL i = max_num END REPEAT; COMMIT; END // DELIMITER ; CALL insert_class(10000); CALL insert_stu(100000,500000); -- 删除某表上的索引 DELIMITER // CREATE PROCEDURE proc_drop_index(dbname VARCHAR(200),tablename VARCHAR(200)) BEGIN DECLARE done INT DEFAULT 0; DECLARE ct INT DEFAULT 0; DECLARE _index VARCHAR(200) DEFAULT ''; DECLARE _cur CURSOR FOR SELECT index_name FROM information_schema.STATISTICS WHERE table_schema = dbname AND table_name = tablename AND seq_in_index = 1 AND index_name <>'PRIMARY' ; -- 每个游标必须使用不同的declare continue handler for not found set done=1来控制游标的结束 DECLARE CONTINUE HANDLER FOR NOT FOUND set done=2 ; -- 若没有数据返回,程序继续,并将变量done设为2 OPEN _cur; FETCH _cur INTO _index; WHILE _index<>'' DO SET @str = CONCAT("drop index " , _index , " on " , tablename ); PREPARE sql_str FROM @str ; EXECUTE sql_str; DEALLOCATE PREPARE sql_str; SET _index=''; FETCH _cur INTO _index; END WHILE; CLOSE _cur; END // DELIMITER ;
-- 全值匹配 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND name='abcd'; CREATE INDEX index_age ON student(age); EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND name='abcd'; CREATE INDEX index_age_classId ON student(age,classId); EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND name='abcd'; CREATE INDEX index_age_classId_name ON student(age,classId,name); EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND name='abcd'; -- 最佳左前缀法则 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND name='abcd'; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE classId=1 AND name='abcd'; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE classId=4 AND age=30 AND name='abcd'; -- 计算、函数、类型转换(自动或手动)导致索引失效 -- 函数导致索引失效 CREATE INDEX index_name ON student(name); EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name LIKE 'abc%'; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(name,3) = 'abc'; -- 计算导致索引失效 CREATE INDEX index_stuno ON student(stuno); EXPLAIN SELECT SQL_NO_CACHE id,stuno,name FROM student WHERE stuno+1 = 900001; EXPLAIN SELECT SQL_NO_CACHE id,stuno,name FROM student WHERE stuno = 90000; -- 类型转换导致索引失效 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name = 123; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name = '123'; -- 范围条件右边的列索引失效 CALL proc_drop_index('test23','student'); SHOW INDEX FROM student; CREATE INDEX index_age_classId_name ON student(age,classId,name); EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId>20 AND name='abc'; CREATE INDEX index_age_name_classId ON student(age,name,classId); EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND name='abc' AND classId>20; -- 不等于(!= 或者<>)索引失效 CREATE INDEX index_name ON student(name); EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name = 'abc'; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name <> 'abc'; -- IS NULL 可以使用索引,IS NOT NULL无法使用索引 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NOT NULL; -- LIKE以通配符%开头索引失效 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name LIKE 'ab%'; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name LIKE '%ab'; -- OR 前后存在非索引的列,索引失效 CALL proc_drop_index('test23','student'); SHOW INDEX FROM student; CREATE INDEX index_age ON student(age); EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=10 OR classId=100; CREATE INDEX index_classId ON student(classId); EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=10 OR classId=100;
USE test23; CREATE TABLE IF NOT EXISTS type( id INT(10) UNSIGNED NOT NULL auto_increment, card INT(10) UNSIGNED NOT NULL, PRIMARY KEY(id) ); CREATE TABLE IF NOT EXISTS book( bookId INT(10) UNSIGNED NOT NULL auto_increment, card INT(10) UNSIGNED NOT NULL, PRIMARY KEY(bookId) ); delimiter // CREATE PROCEDURE ready() BEGIN DECLARE i INT DEFAULT 0; WHILE i<20 DO INSERT INTO type(card) VALUES(FLOOR(1+(RAND()*20))); INSERT INTO book(card) VALUES(FLOOR(1+(RAND()*20))); SET i = i+1; END WHILE; END // delimiter ; CALL ready();
EXPLAIN SELECT SQL_NO_CACHE * FROM type LEFT JOIN book ON type.card = book.card; -- 添加索引 CREATE INDEX index_book_card ON book(card); EXPLAIN SELECT SQL_NO_CACHE * FROM type LEFT JOIN book ON type.card = book.card; CREATE INDEX index_type_card ON type(card); EXPLAIN SELECT SQL_NO_CACHE * FROM type LEFT JOIN book ON type.card = book.card; -- 删除被驱动表的索引 DROP INDEX index_book_card ON book; EXPLAIN SELECT SQL_NO_CACHE * FROM type LEFT JOIN book ON type.card = book.card;
DROP INDEX index_type_card ON type; EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card = book.card; -- 添加索引 CREATE INDEX index_book_card ON book(card); EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card = book.card; CREATE INDEX index_type_card ON type(card); -- 对于内连接来说,查询优化器可以决定谁作为驱动表,谁作为被驱动表出现的 EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card = book.card; -- 删除被驱动表的索引 DROP INDEX index_book_card ON book; -- 对于内连接来讲,如果表的连接条件中只能有一个字段有索引,则有索引的字段所在的表会被作为被驱动表出现 EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card = book.card; CREATE INDEX index_book_card ON book(card); EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card = book.card; -- 向驱动表添加数据 INSERT INTO type(card) VALUES(FLOOR(1+(RAND()*20))); -- 对于内连接来说,在两个表的连接条件都存在索引的情况下,会选择数据少的表作为驱动表 EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card = book.card;
join 方式连接多个表,本质就是各个表之间数据的循环匹配。MySQL 5.5 版本之前,MySQL 只支持一种关联方式,就是嵌套循环(Nested Loop Join)。如果关联表的数据量很大,则 join 关联的执行时间会非常长。在 MySQL 5.5 以后的版本中,MySQL 通过引入 BNLJ 算法来优化嵌套执行。
USE test23; CREATE TABLE a(f1 INT,f2 INT,INDEX (f1)); CREATE TABLE b(f1 INT,f2 INT); INSERT INTO a VALUES(1,1),(2,2),(3,3),(4,4),(5,5),(6,6); INSERT INTO b VALUES(3,3),(4,4),(5,5),(6,6),(7,7),(8,8); EXPLAIN SELECT * FROM a JOIN b ON a.f1 = b.f1 WHERE a.f2=b.f2; EXPLAIN SELECT * FROM a LEFT JOIN b ON a.f1=b.f1 WHERE a.f2=b.f2; EXPLAIN SELECT * FROM a LEFT JOIN b ON a.f1 = b.f1 AND a.f2 = b.f2;
算法相当简单,从表A中取出一条数据1,遍历B表,将匹配的数据放到 esult... 以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断。可以看到这种方式效率是非常低的,以上述表A数据100条,表B数据1000条计算,则 A*B=10万次。开销统计如下:
开销统计 | SNLJ |
---|---|
外表扫描次数 | 1 |
内标扫描次数 | A |
读取记录数 | A+A*B |
JOIN比较次数 | B*A |
回表读取记录次数 | 0 |
Index Nested-Loop Join 其优化的思路主要是为了减少内层表数据的匹配次数,所以要求被驱动表上必须有索引才行。通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录去进行比较,这样极大的减少了对内层表的匹配次数。驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,故MySQL优化器都倾向于使用记录数少的表作为驱动表(外表)如果被驱动表添加索引,效率是非常高的,但如果索引不是主键索引,所以还得进行一次回表查询。相比,被驱动表得索引是主键索引,效率会更高。
开销统计 | INLJ |
---|---|
外表扫描次数 | 1 |
内标扫描次数 | 0 |
读取记录数 | A+B(match) |
JOIN比较次数 | A*Index(Height) |
回表读取记录次数 | B(match)(if possible) |
Block Nested-Loop Join 不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数据列(大小受 join buffer 的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和join buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中多次计较合并成一次,降低了被驱动表的访问频率。
注意:
这里缓存的不只是关联表的列,select 后面的列也会缓存起来。
再一个有 N 个 join 关联的sql中会分配 N-1 个 join buffer。所以查询的时候尽量减少不必要的字段,可以让 join bufferzoo 那个可以存放更多的列。
开销统计 | BNLJ |
---|---|
外表扫描次数 | 1 |
内标扫描次数 | A*used_column_size/join_buffer_size+1 |
读取记录数 | A+B(Aused_column_size/join_buffer_size) |
JOIN比较次数 | B*A |
回表读取记录次数 | 0 |
Hash Join是做大数据集连接时的常用方式,优化器使用两个表中较小(相对较小)的表利用Join Key在内存中建立散列表,然后扫描较大的表并探测散列表,找出与Hash表匹配的行。这种方式适用于较小的表完全可以放于内存中的情况,这样总成本就是访问两个表的成本之和。在表很大的情况下,并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高I/O的性能。它能够很好的工作于没有索引的大表和并行查询的环境中,并提供最好的性能。Hash Join只能应用于等值连接,这是由Hash的特点决定的。
类别 | Nested Loop | Hash Join |
---|---|---|
使用条件 | 任何条件 | 等值连接 |
相关资源 | CPU、磁盘I/O | 内存、临时空间 |
特点 | 当由高选择性索引或进行限制性搜索时效率比较高,能够快速返回第一次的搜索结果 | 当缺乏索引或者索引条件模糊时,Hash Join比Nested Loop有效。在数据仓库环境下,如果表的记录数多,效率高 |
缺点 | 当索引丢失或者查询条件限制不够时,效果很低;当表的记录数多时,效率低 | 为建立哈希表,需要大量内存。第一次的结果返回较慢 |
MySQL 从 4.1 版本开始支持子查询,使用子查询可以进行 SELECT 语句的嵌套查询,即一个 SELECT 查询的结果作为另一个 SELECT 语句的条件。 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的 SQL 操作 。但是,子查询的执行效率不高。原因:
在 MySQ L中,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表,其速度比子查询要快 ,如果查询中使用索引的话,性能就会更好。
尽量不要使用 NOT IN 或者 NOT EXISTS,用 LEFT JOIN xxx ON xx WHERE xx IS NULL 替代
在 MySQL 中,支持两种排序方式,分别是 FileSort 和 Index 排序。
优化建议
USE test23; CALL proc_drop_index('test23', 'student'); CALL proc_drop_index('test23', 'class'); SHOW INDEX FROM student; SHOW INDEX FROM class; EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classId; EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classId LIMIT 10; CREATE INDEX index_age_classId_name ON student(age,classId,name); -- ORDER BY 时不使用 LIMIT 索引失效(需要回表操作) EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classId; -- ORDER BY 时不使用 LIMIT 索引可以使用(不需要回表操作) EXPLAIN SELECT SQL_NO_CACHE age,classId FROM student ORDER BY age,classId; -- 增加 LIMIT 条件,使用上索引 EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classId LIMIT 10; -- ORDER BY 时规则不一致,索引失效(顺序错,不索引;方向反,不索引) EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY classId,age; EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age ASC,classId DESC LIMIT 10; EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age DESC,classId DESC LIMIT 10;
排序的字段若如果不在索引上,曾filesort会有两种算法:双路排序和单路排序
MySQL 4.1之前的使用双路排序,两次扫描磁盘,最终得到数据,读取行指针和ORDER BY列,对它们进行排序看,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。从磁盘取非排序字段,在buffer进行排序,再从磁盘取其它字段。
单路排序,从磁盘读取查询需要的所有列,按照ORDER BY列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IIO变成了顺序IO。但是它会使用更多的空间,因为它把每一行都保存在内存中了。
在sort_buffer中,单路比多路多占用很多空间,因为单路是把所有字段都取出来,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再去sort_buffer容量大小,在排……从而多次I/O。单路本来向省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。
优化策略
索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那它不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引。覆盖索引是非聚簇复合索引的一种形式,它包括在查询里面的 SELECT、JOIN 和 WHERE 子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。简单说就是索引列 + 主键 包含 SELECT 到 FROM 之间查询的列
。
USE test23; CALL proc_drop_index('test23', 'student'); CREATE INDEX index_age_name ON student(age,name); EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age <> 20; EXPLAIN SELECT SQL_NO_CACHE age,name FROM student WHERE age <> 20; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name LIKE '%abc'; EXPLAIN SELECT SQL_NO_CACHE age,name FROM student WHERE name LIKE '%abc';
Index Condition Pushdown(ICP) 是 MySQL 5.6 中新特性,是一种在存储引擎使用索引过滤数据的优化方式。如果没有 ICP,存储引擎会遍历索引以定位基表中的行,并将它们返回给 MySQL 服务器,由 MySQL 服务器评估 WHERE 后面的条件是否保留行。启动 ICP 后,如果部分 WHERE 条件可以仅使用索引中的列进行筛选,则 MySQL 服务器会把这部分 WHERE 条件放到存储引擎筛选。然后,存储引擎通过使用索引条目来筛选数据,并且只有在满足这一条件时才能表中读取行。ICP 可以减少存储引擎必须访问基表的次数和 MySQL 服务器必须访问存储引擎的次数。但是,ICP 的加速效果取决于在存储引擎内通过 ICP 筛选掉的数据的比例。
默认情况下启动索引条件下推。可以通过设置系统变量 optimizer_swith 控制 iindex_condition_pushdown 。当使用索引条件下推是 EXPLAIN语句输出结果中 Extra 列结果内容显示为 Using index condition.
-- 打开索引下推 SET optimizer_switch = 'index_condition_pushdown=on'; -- 关闭索引下推 SET optimizer_switch = 'index_condition_pushdown=off';
USE test23; CREATE TABLE people( id INT NOT NULL auto_increment, zipcode VARCHAR(20) COLLATE utf8_bin DEFAULT NULL, firstname VARCHAR(20) COLLATE utf8_bin DEFAULT NULL, lastname VARCHAR(20) COLLATE utf8_bin DEFAULT NULL, address VARCHAR(50) COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY(id), KEY index_zipcode_lastname_firstname(zipcode,lastname,firstname) ) ENGINE = INNODB auto_increment = 5 DEFAULT CHARSET = utf8mb3 COLLATE = utf8_bin; INSERT INTO people VALUES('1','000001','三','张','北京市'), ('2','000002','四','李','上海市'), ('3','000003','五','王','广州市'), ('4','000004','六','赵','深圳'); EXPLAIN SELECT * FROM people WHERE zipcode = '000001' AND lastname LIKE '%张%' AND address LIKE '%北京%';
delimiter // CREATE PROCEDURE insert_people(max_num INT) BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; REPEAT SET i = i + 1; INSERT INTO people(zipcode,firstname,lastname,address) VALUES('000001','sakura','kinomoto','友枝町'); UNTIL i = max_num END REPEAT; COMMIT; END // delimiter ; CALL insert_people(1000000); -- 打开profiling工具 SET profiling = 1; -- 默认打开索引下推 SELECT * FROM people WHERE zipcode = '000001' AND lastname LIKE '%张%'; -- 不使用索引下推 SET optimizer_switch = 'index_condition_pushdown=off'; SELECT * FROM people WHERE zipcode = '000001' AND lastname LIKE '%张%'; SHOW profiles;
索引是个前提,其实选择与否还是要看表的大小,我们可以将选择的标准理解为小标驱动大表。在这种方式下效率是最高的。
SELECT * FROM A WHERE cc IN (SELECT cc FROM B); SELECT * FROM A WHERE EXISTS(SELECT cc FROM B WHERE B.cc = A.cc);
当 A 小于 B 时,用 EXISTS。因为 EXISTS 的实现,相当于外表循环,实现的逻辑类似于:
for i in A for j in B if j.cc == i.cc then ...
当 B 小于 A 时用 IN,因为实现的逻辑类似于:
for i in B for j in A if j.cc == i.cc then ...
哪个表小就用哪个表来驱动,A 表小就用 EXISTS,B 表小就用 IN。
前提:统计的具体字段非空
COUNT(*)
和 COUNT(1)
都是对所有结果进行 COUNT,COUNT(*)
和 COUNT(1)
本质上并没有什么区别(二者执行时间可能略有差别)。如果有 WHERE 子句,则是对所有符合筛选条件的数据进行统计;如果没有 WHERE 子句,则是对数据表的数据行数进行统计。
如果是 MyISAM 存储引擎,统计数据表的行数只需要 O(1) 的复杂度,这是因为每张 MyISAM 的数据表都有一个 meta 信息存储了 row_count 值,而一致性则由表级锁来保证。如果是 InnoDB 存储引擎 ,因为支持事务,采用行级锁和 MVCC 机制,所以无法像 MyISAM 的数据表一样,维护一个 row_count 变量,因此需要采用扫描全表,是O(N)的复杂度,进行循环 + 计数的方式来完成统计。
在 InnoDB 引擎中,如果采用 COUNT(具体字段)
来统计数据行数,要尽量采用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,明显会大于二级索引(非聚簇索引)。对于 COUNT(*)
和 COUNT(1)
来说,它们不需要查找具体的行,只是统计行数,系统会自动采用占用空间更小的二级索引来进行统计。如果有多个二级索引,会使用 key_len 小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引来进行统计。
在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表> 查询。原因:
针对的是会扫描全表的 SQL 语句,如果你可以确定结果集只有一条,那么加上 LIMIT 1 的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT 1 了。
只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。
COMMIT 所释放的资源: