mysql cookbook00011
11.0 引言
一个序列是在需要的时候按照顺序生成的一组整数。在mysql中有如下几个主题为其生成序列:
AUNTO_INCRMENT列是为一些行生成序列的机制 例如:用户编号
对于很多程序而言,仅仅生成序列值是不够的,也需要判断刚刚插入数据库的一行记录的序列值 例如:刚刚提交的表单数据回显,
需要把刚刚插入的序列值存入到另外一个表中
针对于由于表中的某些记录被删除而造成的空洞,以及为什么要使用再序列技术
- 使用一个AUTO_INCREMENT 列来创建复合序列
创建了一个复合组建,但是其中包含一个AUTO_INCREMAENT列,那么就使用这种方法。
例如:主题公告牌,按照主题分类,再按AUTO_INCREMENT 生成编号
处理这种情况要十分小心,存在于在多张中都存在AUTO_INCREMENT字段的来执行一条SQL语句
序列也可以用作计数器,例如:对于你在网站上提供广告服务,你可能在每次点击数增加一次计数值
这个主题中建议了几种为生成只读序列查询结果进行编号的方法。
11.1 创建一个序列列并生成序列值
AUTO_INCREMENT 背后的机制大致为:当你向表中插入一个值时,MYSQL生成序列中的下一个值,并且赋给新行中。
对于不同的存储引擎处理AUTO_INCREMENT的方式不一样:如果你显示的把ID设置为非NULL,你可能得到如下两种结果:
1. 如果你插入的值已经在表中出现过,并且数据库不能接受重复,报:Duplicate entry 值1 for key 值2
2. 没出现过,插入新值,但是这个值比现有的计数器的下一个值大,这个表的计数器被重设为该值加一,那么原本从5开始的,
但是你插入的是10 那么下一个插入的就变成了11而对于6-10的值就间断了
11.2 为序列列选择数据类型
对于一个句子:CREATE TABLE TAB1 ( ID INT UNSINGED NOT NULL AUTO_INCREMENT PRIMARY KEY(ID)
从这个句子中可以得到如下信息:
AUTO_INCREMENT :告诉MYSQL 它应该为该列值生成连续的数字
INT :INT 是一个基本数据类型,你并不一定使用他,还有TINYINT SMALLINT MEDIUMINT INT BIGINT 等,AUTO_INCREMENT只针对整数
UNSINGED :避免负值,这不是AUTO_INCREMENT所必须的;原因有2:1. 因为在序列仅由正整数组成,2. 范围减半:TINYINT在有符号位的时候
范围-128-127但是有效序列:1-127;对于无符号位的为:1-255所以范围缩短了一半
注意:假如你使用了负数作为序列号,那么在重新序列化的时候MYSQL会重新把他们编制为整数,那么到时候就惨了
NOT NULL :AUTO_INCREMENT列不能包含NULL值,对于AUTO_INCREMNET列插入NULL其实就是告诉MYSQL帮起生成序列值
PRIMARY KEY :AUTO_INCREMENT列必须被索引化,因为使用AUTO_INCRMENT的目的就是提供一个唯一的序列;UNIQUE 也是可以的
注意:当你创建一长包含AUTO_INCREMENT时,你要考虑使用的引擎是什么;因为引擎会影响一些操作:删除键是否可重用,是否可设置初始值等
11.3 序列生成的行删除的效果
这个问题关于你使用的引擎和删除的是那一行?
下面的操作针对于如下表:
mysql> select * from insect;
+----+-------------------+------------+------------+
| id | name | date | origin |
+----+-------------------+------------+------------+
| 1 | millipede | 2006-09-10 | driveway |
| 2 | housefly | 2006-09-10 | kitchen |
| 3 | grasshopper | 2006-09-10 | front yard |
| 4 | stink bug | 2006-09-10 | front yard |
| 5 | cabbage butterfly | 2006-09-10 | garden |
| 6 | ant | 2006-09-10 | back yard |
| 7 | millbug | 2006-09-10 | under rock |
| 8 | ant | 2006-09-10 | back yard |
+----+-------------------+------------+------------+
要求:把不是昆虫、重复的昆虫删除掉。从上面的图中你会发现millbug ant millipedes不是昆虫类的所以删除
mysql> select * from insect;
+----+-------------------+------------+------------+
| id | name | date | origin |
+----+-------------------+------------+------------+
| 2 | housefly | 2006-09-10 | kitchen |
| 3 | grasshopper | 2006-09-10 | front yard |
| 4 | stink bug | 2006-09-10 | front yard |
| 5 | cabbage butterfly | 2006-09-10 | garden |
| 6 | ant | 2006-09-10 | back yard |
+----+-------------------+------------+------------+
5 rows in set (0.00 sec)
对于删除1的行时,就会存在一个空洞1,因为MYSQL不会去补充一个序列的空洞,对于新插入的值没有影响,
然而7,8行也没有了那么这是删除了序列 的顶端的值,这种情况就需要看你所使用的存储引擎了:
1. 对于BDB表,接下来的序列值通常就是本列中当前最大值加1.如果你删除行值为最顶端值,那么该值会被重复的使用。
2. 对于INNODB/MYISAM序列值不会被重复使用,所以你严格要求单调递增那么就选择INNODB/MYISAM
假如觉得重复使用序列引擎不好使,通过ALTER TABLE tab1 ENGINE=MYISAM/INNODB
查询表信息:SHOW CREATE TABLE tab1 ;SELECT ENGINE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='数据库名'
AND TABLE_NAME='表名';
清除表的信息:TRUNCATE TABLE tab1;
11.4 查询序列值
SELECT LAST_INSERT_ID语句获取插入列的值
1. 使用LAST_INSERT_ID()获取AUTO_INCREMENT值
使用一种直观(但是可能错误)方式获取该值:SELECT MAX(id) FROM insect;之所以这样的查询可能会错误是因为:多线程、多事务
当你执行查询前另外一个客户端向其中插入一条数据ID+1了那么查询就出问题,除非使用一个事务和锁定表,但是,MYSQL提供了
LAST_INSERT_ID()函数可以更加简洁的获取正确的ID值。返回本次连接服务器之后,最新创建AUTO_INCREMENT值,不受其他客户端影响
问题:其他客户端能改变LAST_INSERT_ID()函数的返回值吗?
不会的,因为LAST_INSERT_ID()函数的返回值基于服务器的每个客户端连接,这特性很重要,因为避免了不同用户之间的干扰。
2. 使用API提供的函数来获取AUTO_INCREMENT值
JAVA:
Statement s = conn.createStatement();
s.executeUpdate();
long seq = ((com.mysql.jdbc.Statement)s).getLastInsertID();
s.close();
//注意:因为getLastInsertID()函数是驱动定义的,你实际是通过Statement对象转变为
com.mysql.jdbc.Statement类型对象来使用的。
//如果使用的是PrepareStatement,那么就转变为com.mysql.jdbc.prepareStatement类型对象
3. 客户端和服务器端获取序列值的比较
服务器端和客户端的所有方法,都要求生成和获取AUTO_INCREMENT操作在同一个MYSQL连接内。如果你生成了一个AUTO_INCREMENT值,
然后断开连接你重新建立连接查询刚刚生成的值,你会得到0;在一个特定的连接内AUTO_INCRMENT的值会比连接本身更长:
1. 你执行了生成AUTO_INCREMENT语句以后,这个值对于LAST_INCREMENT_ID()操作一直有效的,就算你执行了其他语句,直到你
重新生成AUTO_INCREMENT。
2. 客户端序列值的有效性与每一条语句相关,而不仅仅是有生成AUTO_INCREMENT值的语句决定的。当你在执行完INSERT语句又执行
了其他的语句那么AUTO_INCREMENT的值可能会改变。
11.5 对一个已有的序列进行重新计数
对于再序列化的理由如下:
1. 美化:出于这种理由是被不建议的。
2. 性能:再序列化的诱惑可能是,能清除空洞更加紧凑,并且是的MYSQL能够更快的执行语句,
这是不正确的;因为在进行再序列化时他会锁住表,反而带来负面的影响。
3. 空间用完:一个序列的上限取决于它的数据类型。如果一个AUTO_INCREMENT序列达到了所使用的上限那么我想这还算一个合理的理由。
再序列化其实也很容易:就是先删除该列,在添加该列。
1. mysql> alter table insect drop id;
Query OK, 5 rows affected (0.45 sec)
Records: 5 Duplicates: 0 Warnings: 0
mysql> alter table insect add id int unsigned not null auto_increment first ,
-> add primary key(id);
Query OK, 5 rows affected (0.44 sec)
2. 单独的使用两个alter语句会产生两个操作时间间隔内,所操作列会从表中消失。这可能会给在这段时间内访问该表的其他客户端带来问题
为了解决这个问题:ALTER TABLE insect DROP ID ,ADD ID UNSIGNED NOT NULL AUTO_INCREMENT FIRST;
MYSQL允许在一个ALTER TABLE进行多个操作(这不是所有的系统都支持的,然而需要注意的是多个操作不是简单的两个ALTER语句结合)
区别在于这样(使用一个ALTER语句)不需要重新建立设定主键:除非在ALTER TABLE句中所有的语句执行结束后,源表中的主键列消失了,
否则MYSQL不会删除主键。
11.6 扩展序列列的取值范围
通过改变表的
结构来扩大取值范围而不是表的
内容:
1. 如果序列的数据类型是有符号的,那么就把其变成无符号的:
ALTER TABLE insect MODIFY id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT.
2. 如果某一列已经是UNSIGNED并且不是最大的整数类型,把列类型变成更大的数据类型可以扩大取值范围:
ALTER TABLE insect MODIFY id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT
11.7 序列顶部数值的再次使用
问题:你从一个表中删除顶部一些行,你能再次使用删除的值,而不需要进行该列的再序列吗?
答案:是的,通过修改序列计数器,MYSQL将从当前表中最大值+1开始生成新的列值
例如:1-100 你删除了90-100那么告诉MYSQL数据库以现在的最大序列+1开始生成序列值。这是BDB的默认行为,而对于MYISAM/INNODB来说:
ALTER TABLE insect AUTO_INCRMENT = 1;//把MYSQL重置为可利用的最小值
注意:如果一个序列中间有断层,你可以使用ALTER TABLE 重新计数,但是这样做并不会出去中间断层;只有通过再重新序列化才可以去除中间层。
11.8 确保各行按照给定顺序重编号
需要按照某列中特定顺序来进行排列各行
完成该功能有第一种方式:
1. 创建克隆表:mysql> create table insect_c select * from insect where 0;
2. 使用INSERT INTO...SELECT 从源表中复制行,复制除序列之外的列,使用ORDER BY 子句指定拷贝的顺序
insert into insect_c(name,date,origin) select name,date,origin from insect order by origin
3. 删除源表,并且重命名新表的名字为源表名字
mysql> drop table insect;
mysql> rename table insect_c to insect;
4. 如果是一个很大的MYISAM表。并且含有多个索引,创建新表时不定义删除AUTO_INCREMENT列之外的索引。然后再将数据
复制到新表,然后定义索引
mysql> insert into insect_c(name,date,origin) select name,date,origin from insect;
完成该功能有第二种方式:
1. 创建一个包含源表中除AUTO_INCREMENT列之外的列的新表
mysql> create table insect_c select name,date,origin from insect where 0;
2. 使用INSERT INTO ..SELECT 语句把非AUTO_INCREMENT列复制到新表中
mysql> insert into insect_c(name,date,origin) select name,date,origin from insect;
3. 在源表中删除所有的行,并且设置序列计数器
mysql> truncate table insect
mysql> set last_insert_id=1;
4. 把信息从新表中复制到源表中,使用 ORDER BY语句把按照希望顺序的数据赋值到源表中,MYISAM、innodb自动将序列值
赋给AUTO_INCREMENT列
mysql> insert into insect(name,date,origin) select * from insect_c;
11.9 从某个特定值开始一个序列
在创建表的时候同时指定AUTO_INCREMENT=N就可以从具体的哪个值开始了,这是只针对于MYISAM/INNODB来说的
CREATE TABLE tab1(id INT NOT MULL UNSIGNED AUTO_INCREMENT PRIMARY KEY ) AUTO_INCREMENT=N
对于其他的存储引擎来说可以使用技巧:先插入一个假数据为N-1 再重新插入数据 最后把假数据删除
CREATE TABLE tab2( id INT NOT NULL UNSIGNED AUTO_INCREMENT PRIMARY KEY ) ENGINE=BDB;
INSERT INTO tab2 values(N-1);
INSERT INTO tab2 values(null);
...
DELETE tab2 WHERE id = N-1;
注意:如果你使用了TRUNCATE TABLE 清空一张表,那么序列起点可能重置为1 甚至对那些不重用序列值的存储引擎也是如此,这种情况下
如果不希望序列从1开始那么就指定序列计数器的值
11.10 序列化一张未序列的表
插入新的列:alter table tab1 add id int not null unsigned auto_increment
first/after col1,add primary key(id);
可以指定插入是在哪里的前面或者后面通过first/after关键字
11.11 使用AUTO_INCREMENT 栏来创建多重序列
把AUTO_INCREMENT 列和其他列链接起来,使他们都是同一个索引的一部分
mysql> show create table insect\G;
*************************** 1. row ***************************
Table: insect
Create Table: CREATE TABLE `insect` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(30) NOT NULL,
`date` date NOT NULL,
`origin` varchar(30) NOT NULL,
PRIMARY KEY (`id`,`name`)
) ENGINE=MyISAM AUTO_INCREMENT=6 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql>alter table insect_bug add id int unsigned not null auto_increment, -> add primary key(name,id); ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key mysql> alter table insect_bug engine=myisam -> ; Query OK, 0 rows affected (0.55 sec) Records: 0 Duplicates: 0 Warnings: 0
mysql>alter table insect_bug -> add id int unsigned not null auto_increment first, -> add primary key(name,id); Query OK, 0 rows affected (0.16 sec)通过这段代码你会发现对于引擎为INNODB来说是不能通过上面的方式进行修改的,对于MYISAM可以 |
当使用AUTO_INCREMENT栏创建多重序列时现象很奇葩:
mysql> insert into insect(name,date,origin) select name,date,origin from insect; Query OK, 5 rows affected (0.02 sec) Records: 5 Duplicates: 0 Warnings: 0
mysql> select * from insect order by name; +----+-------------------+------------+------------+ | id | name | date | origin | +----+-------------------+------------+------------+ | 1 | ant | 2006-09-10 | back yard | | 6 | ant | 2006-09-10 | back yard | | 4 | cabbage butterfly | 2006-09-10 | garden | | 9 | cabbage butterfly | 2006-09-10 | garden | | 2 | grasshopper | 2006-09-10 | front yard | | 7 | grasshopper | 2006-09-10 | front yard | | 5 | housefly | 2006-09-10 | kitchen | | 10 | housefly | 2006-09-10 | kitchen | | 3 | stink bug | 2006-09-10 | front yard | | 8 | stink bug | 2006-09-10 | front yard | +----+-------------------+------------+------------+ 10 rows in set (0.03 sec) |
mysql> show create table insect\G *************************** 1. row *************************** Table: insect Create Table: CREATE TABLE `insect` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL, `date` date NOT NULL, `origin` varchar(30) NOT NULL, PRIMARY KEY (`id`,`name`) ) ENGINE=MyISAM AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) |
mysql> insert into insect_bug values(null,'ant','2006-01-01','back yard'); Query OK, 1 row affected (0.00 sec)
mysql> select * from insect_bug; +----+-------------------+------------+------------+ | id | name | date | origin | +----+-------------------+------------+------------+ | 1 | ant | 2006-09-10 | back yard | | 1 | grasshopper | 2006-09-10 | front yard | | 1 | stink bug | 2006-09-10 | front yard | | 1 | cabbage butterfly | 2006-09-10 | garden | | 1 | housefly | 2006-09-10 | kitchen | | 2 | ant | 2006-09-10 | back yard | | 2 | grasshopper | 2006-09-10 | front yard | | 2 | stink bug | 2006-09-10 | front yard | | 2 | cabbage butterfly | 2006-09-10 | garden | | 2 | housefly | 2006-09-10 | kitchen | | 3 | ant | 2006-01-01 | back yard | +----+-------------------+------------+------------+ 11 rows in set (0.00 sec) |
| insect_bug | CREATE TABLE `insect_bug` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL, `date` date NOT NULL, `origin` varchar(30) NOT NULL, PRIMARY KEY (`name`,`id`) ) ENGINE=MyISAM AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 | +------------+---------------------------------------------------------- |
右边是表的结构你会发现他们基本上是没有什么区别的,唯独区别在于他们创建的主键一个是name,id 而另外一个是id,name
他们插入数据时,显示的效果不一样;所以实现下面这种的优先把不是自动增长列设为主键列。
mysql> insert into insect_bug values(null,'ant','2006-01-01','back yard'); Query OK, 1 row affected (0.00 sec)
mysql> select * from insect_bug; +----+-------------------+------------+------------+ | id | name | date | origin | +----+-------------------+------------+------------+ | 1 | ant | 2006-09-10 | back yard | | 1 | grasshopper | 2006-09-10 | front yard | | 1 | stink bug | 2006-09-10 | front yard | | 1 | cabbage butterfly | 2006-09-10 | garden | | 1 | housefly | 2006-09-10 | kitchen | | 2 | ant | 2006-09-10 | back yard | | 2 | grasshopper | 2006-09-10 | front yard | | 2 | stink bug | 2006-09-10 | front yard | | 2 | cabbage butterfly | 2006-09-10 | garden | | 2 | housefly | 2006-09-10 | kitchen | | 3 | ant | 2006-01-01 | back yard | +----+-------------------+------------+------------+ 11 rows in set (0.00 sec)你会发现我设置了AUTO_INCREMENT=20但是他增长还是从3开始 mysql> alter table insect_bug engine=innodb; ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key mysql> |
其实下面的注意事项在上面已经进行了演示了:
1. CREATE TABLE 语句定义的索引列的顺序是无关紧要的,真正有关心的是索引定义中指定每一列的顺序。AUTO_INCREMENT列必须在最后指定
,否则走合序列的机制就不能正常工作
2. 一个PRIMARY KEY 不能包含有NULL值,但是对UNIQUE可以如果作为索引的非AUTO_INCREMENT列中可能含有NULL,那么你应该创建一个
UNNIQUE索引而非是PRIMARY KEY.
推广:对于多列索引,最后一列是AUTO_INCREMENT列,MYSQL为每个由非AUTO_INCREMENT列组成的唯一组合生成一个独立的序列。
MYSQL的多列机制能够比单列的逻辑更加的容易使用.
原则:你可能以某种方式来考虑处理某些值(以单个字符串的方式处理ID),但是这并不意味着在数据库中你要以同样的方式来处理这些值。
也就是说显示给用户看的处理数据方式和数据库处理数据方式可以不一样。
11.12 管理多重并发AUTO_INCREMENT数值
对于你正在处理多个含有AUTO_INCREMENT的表之间数据时,你会发现很难分清楚到底LAST_INSERT_ID是哪些值
通过下面的三种方式来解决:
1. 在SQL层面上,把生成的AUTO_INCREMENT值保存在一个用户定义的变量中:
INSERT INTO tab1 VALUES(NULL);
SET @temp_var = LAST_INSERT_ID();
2. 在API的层面上,把AUTO_INCREMENT值保存在编程语言的变量中。
3. 使用某些技术API能够使你保持独立的客户端AUTO_INCREMENT值。PrepareStatement,Statement;
11.13使用AUTO_INCREMENT值将表进行关联
当多表关联而且两个表都存在AUTO_INCREMNT的情况时要特别注意,当你主表中插入一值,LAST_INSERT_ID = 主表的最大序列值
但是你插入从表中时,因为从表也存在AUTO_INCREMENT所以也会生成序列,然而LAST_INSERT_ID的值就会被从表的值所覆盖。这
就出现了问题:
订单<-->货品详情
通过设置中间变量来处理:在插入主表的时候,通过变量来记住主表的最大序列值。在从表插入时引用该值
11.14 将序列生成器用作计数器
讨论:对于这样一个问题——假如你需要统计书籍在网上的点击率,这样的一张表该怎么样来设计?
解决:由于他们并不关心书籍的其他信息只是对于书籍的点击率感兴趣,那么这张表的设计应该是这样的:
CREATE TABLE booksales(title VARCHAR(60) NOT NULL,copies INT NOT NULL PRIMARY KEY);
下面两种方式来向表中插入数据:
第一种:
1. 首先初始化表:INSERT INTO booksales values('bookName',0);
2. 更新表的书被点击的次数:UPDATE booksales SET copies = cppies +1 WHERE title='bookName';
第二种:
INSERT INTO booksales (title,copies) VALUE('bookName',1) ON DUPLICATE KEY UPDATE copies= copies+1;
那么对于这样的数据该怎样的到某本书的销售量呢?
SELECT copies FROM bookName WHERE title='bookName';
假如你写出列这种代码那就存在的比较严重的问题了,假如在你查询之前,另外的客户端点击了你要查的书籍那么查询就是错误的。
前面也有讲过这种类似的情况,通过加锁或绑定在同一个事务当中等;还有是通过下面的方式。
对于MYSQL提供了一种基于LAST_INSERT_ID()解决方法。如果你把LAST_INSERT_ID()作为表达式参数来调用
,MYSQL会把它当作AUTO_INCREMENT值来处理。为了在booksales中应用这一特性,代码如下:
INSERT INTO booksales VALUES('bookName',LAST_INSERT_ID(1)) ON DUPLICATE KEY
UPDATE copies=LAST_INSERT_ID(copies+1);
这个语句字计数值初始化和更新中使用了LAST_INSERT_ID(expr)子句。使用LAST_INSERT_ID()作为表达式参数,MYSQL会将该表示作为
AUTO_INCREMENT值来处理。然后你可以使用无参数构造函数来调用:
SELECT LAST_INSERT_ID();
在JAVA 中也是可以类似这样写的,这里就不再写了》。。
在通过LAST_INSERT_ID(expr)来生成序列与AUTO_INCREMENT序列相比,有些不同的属性:
1. AUTO_INCREMENT 值每次增加1,然而LAST_INSERT_ID(expr)生成的计数值可以增加你指定的任意值。可以10,20,30 等等
2. 序列起始值可以是任意数值,甚至是负数。也可以使用一个负数。也可以使用一个负数作为增量生成一个递减序列(不要定义UNSIGNED)
3. 只需要简单的把计数器值设置为你所想要的值,就可以重置计数器。假设你想告诉消费者当月的销售情况,而不是所有的销售记录
(你想显示一条消息:您是本月第N位客户)UPDATE booksales SET copies=0;)
4. 不是所有的情况下,用LAST_INSERT_ID(expr)生成的值都能使用客户端查询方法获取,你可以在UPDATE或者INSERT语句后使用
获取它,但是在SET语句之后不能。SET @VAR = LAST_INSERT_ID(40);获取的值不一定是40了
11.15创建循环序列
使用DIVISION 和 MODULO操作符生成循环元素
有些序列生成问题要求序列值能够循环使用。假设为产品进行编号而且为了产品出现问题而进行:12产品包装一盒,6盒包装一箱,这种情况下
产品编号为单品编号+盒编号+一批编号。可以通过下面的方式一:
unit = unit + 1;
if(unit > 12){
unit = 1;
box = box + 1;
}
if(box > 6){
box = 1;
case = case + 1;
}
保存新的箱子、盒子、产品编号。上面确实可以,但是下面的更加简便:
通过产品一个编号,通过AUTO_INCREMENT方式,然后为箱子、盒子、单品赋予编号。公式如下:
unit_num = ((seq-1)%12)+1;
box_num = (((seq-1)/12)%6)+1;
case_num = ((seq-1)/12*6)+1;