Home | 简体中文 | 繁体中文 | 杂文 | Github | 知乎专栏 | 51CTO学院 | CSDN程序员研修院 | OSChina 博客 | 腾讯云社区 | 阿里云栖社区 | Facebook | Linkedin | Youtube | 打赏(Donations) | About
知乎专栏多维度架构

4.2. 关系型数据库设计

MySQL 数据库设计案例

4.2.1. 数据字典

我不建议使用传统的《数据字典》,我的做法是E-R图加数据库注释

注释伴随表,视图,触发器,过程等等,便于维护

4.2.2. 用户帐号表

用户帐号或通行证系统设计,下面以我的数库为例讲解。

我一般使用两个表 passport,profile 完成网站会员系统。

首先说说passport表,你也要以使用user或member等等命名,这个表设计尽可能地简单,不要使用过多字段。仅保存登录所必须用到的字段,如user,password,nickname,email... 登录帐号和密码做复合索引。

然后是profile表,这个表与passport是1:1关系,保存用户详细信息

这样设计可以保证海量用户登录时的速度。

		
+----------+
| user     |
|----------|
|id        | <---+
|username  |     |
|password  |     |
|nickname  |     |
|status    |     |
+----------+     |
                1:1
+----------+     |
| profile  |     |
|----------|     |
|user_id   | o---+
|name      |
|sex       |
|address   |
|telphone  |
|status    |
+----------+
		
		

4.2.2.1. 用户注册键盘跟踪表设计

该表的功能是,防止用户注册过程中流逝,记录已经填写的数据。

			
CREATE TABLE `signup_keyloggers` (
	`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '唯一ID',
	`cookie` VARCHAR(32) NOT NULL COMMENT 'cookie id',
	`type` ENUM('baidu','google') NOT NULL COMMENT '推广账号类型',
	`field` ENUM('Name','Mobile','Email') NOT NULL COMMENT '字段名',
	`value` VARCHAR(50) NOT NULL COMMENT '值',
	`status` ENUM('New','Sent','Ignored','Called','Processed') NOT NULL DEFAULT 'New' COMMENT '状态',
	`operator` VARCHAR(10) NOT NULL COMMENT '操作人',
	`ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	`mtime` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '状态修改时间',
	PRIMARY KEY (`id`),
	UNIQUE INDEX `unique_index` (`type`, `cookie`, `field`, `value`)
)
COMMENT='用户注册键盘记录器'
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
			
			

当用户注册成功会根据cookie id 删除该表中的数据。

当数据被记录后,客服就可以对客户回访,并修改状态status,忽略 Ignored,邮件发送Sent, 电话回访Called等等

4.2.3. 分类表设计

4.2.3.1. 树形分类表

			
 +-----------+
 | category  |
 |-----------|
 |id         | <---+
 |title      |     |
 |description|    1:n
 |status     |     |
 |parent_id  | o---+
 +-----------+
			
			
		
CREATE TABLE `category` (
	`id` SMALLINT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(10) NOT NULL,
	`description` VARCHAR(255) NULL,
	`status` ENUM('enable','desable') NOT NULL DEFAULT 'enable',
	`parent_id` SMALLINT(10) UNSIGNED NOT NULL DEFAULT '0',
	PRIMARY KEY (`id`),
	CONSTRAINT `FK1` FOREIGN KEY (`parent_id`) REFERENCES `category` (`id`)
)
COMMENT='goods category'
ENGINE=InnoDB
ROW_FORMAT=DEFAULT
		
			

4.2.3.2. 多对多分类

多对多分类,主要用于满足,一个产品/文章属于多个分类的需求。

		
      +------------+
      | category   |
      |------------|
 +--> |id          | <---+
 |    |title       |     |     +----------------------+
1:n   |description |    1:n    | categroy_has_product |
 |    |status      |     |     +----------------------+
 +--o |parent_id   |     |     | id                   |
      +------------+     +---o | category_id          |
                         +---o | product_id           |
      +------------+     |     +----------------------+
      | product    |    1:n
      +------------+     |
      |id          | <---+
      |price       |
      |quantity    |
      |...         |
      |status      |
      +------------+
		
			

4.2.3.3. 快速检索子分类设计

上面我刚刚讲过怎样实现“不限子树的分类树”,我们可以实现不限层次的无线分类表。

			
 +-----------+
 | category  |
 |-----------|
 |id         | <---+
 |title      |     |
 |description|    1:n
 |status     |     |
 |parent_id  | o---+
 +-----------+
			
			

问题出来了,当我需要读取一个分类(任意分类)下的所有子分类,怎样实现,很多人会说用“递归”。 当然“递归”可是现实我们的需求,在几百个分类的项目中,使用递归也不是不可以的,但是当数量非常庞大时怎么办?

当然有更好的解决方案,请看下面

			
 +-----------+
 | category  |
 |-----------|
 |id         | <---+
 |title      |     |
 |description|    1:n
 |status     |     |
 |parent_id  | o---+
 |path       |
 +-----------+
			
			
			
+-------------------------------------------------------------------------+
| category                                                                |
+----+-----------+-----------------------+--------+-----------+-----------+
| id | name      | description           | status | parent_id | path      |
+----+-----------+-----------------------+--------+-----------+-----------+
|  1 | 中国    | 中华人民共和家                                    | Y      |      NULL | 1/        |
|  4 | 广东省 | 广东省                                                      | Y      |         1 | 1/4       |
|  5 | 深圳市 | NULL                      | Y      |         4 | 1/4/5     |
|  6 | 宝安区 | NULL                      | Y      |         5 | 1/4/5/6   |
|  7 | 龙华镇 | NULL                      | Y      |         6 | 1/4/5/6/7 |
+----+-----------+-----------------------+--------+-----------+-----------+
			
			
			
CREATE TABLE `category` (
	`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '分类ID',
	`name` VARCHAR(50) NOT NULL COMMENT '分类名称',
	`description` VARCHAR(200) NULL DEFAULT NULL COMMENT '分类描述',
	`status` ENUM('Y','N') NOT NULL DEFAULT 'Y' COMMENT '分类状态有继承性',
	`parent_id` INT(10) NULL DEFAULT '1' COMMENT '分类父ID',
	`path` VARCHAR(255) NOT NULL COMMENT '分类递归路径索引',
	INDEX `PK` (`id`),
	INDEX `relation` (`id`, `parent_id`),
	INDEX `FK_category_category` (`parent_id`),
	INDEX `path` (`path`)
)
COMMENT='分类表'
ENGINE=InnoDB
ROW_FORMAT=DEFAULT
AUTO_INCREMENT=0

insert into category(`name`,`description`,`status`,`parent_id`,`path`) values('中国','中华人民共和家','Y',null,'1/')
			
			
			
ALTER TABLE `category`
	ADD CONSTRAINT `FK_category_category` FOREIGN KEY (`parent_id`) REFERENCES `category` (`id`)
			
			

抽取广东子树

			
select * from category where path like '1/4%';
			
			
			
mysql> select * from category where path like '1/4%';
+----+-----------+-------------+--------+-----------+-----------+
| id | name      | description | status | parent_id | path      |
+----+-----------+-------------+--------+-----------+-----------+
|  4 | 广东省 | 广东省   | Y      |         1 | 1/4       |
|  5 | 深圳市 | NULL        | Y      |         4 | 1/4/5     |
|  6 | 宝安区 | NULL        | Y      |         5 | 1/4/5/6   |
|  7 | 龙华镇 | NULL        | Y      |         6 | 1/4/5/6/7 |
+----+-----------+-------------+--------+-----------+-----------+
4 rows in set (0.00 sec)
			
			

4.2.3.4. 计算节点数量

			
DROP TABLE IF EXISTS `test`;
CREATE TABLE IF NOT EXISTS `test` (
  `id` int(11) DEFAULT NULL,
  `pid` int(11) DEFAULT NULL,
  `name` char(50) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `test` (`id`, `pid`, `name`) VALUES
	(1, 0, 'A'),
	(2, 1, 'B'),
	(3, 1, 'C'),
	(4, 0, 'D'),
	(5, 0, 'E'),
	(6, 5, 'F');

select (select t2.name from test t2 where t2.id=t1.pid) as name, count(pid) as sum from test t1 where t1.pid <> 0 group by t1.pid;
			
			

统计所有节点包括数量为零的

				select t1.name, (select count(t2.name) from test t2 where t2.pid=t1.id) as sum from test t1
			

4.2.3.5. Example

例 4.1. identity_card 身份证归属地表

					CREATE TABLE `identity_card` (
					`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '唯一主键',
					`pid` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '父ID',
					`path` VARCHAR(50) NOT NULL COMMENT '路径',
					`number` VARCHAR(18) NOT NULL COMMENT '身份证号码段',
					`zone` VARCHAR(50) NOT NULL COMMENT '行政区域',
					`status` ENUM('Y','N') NOT NULL DEFAULT 'N' COMMENT '状态',
					`modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建与修改时间',
					PRIMARY KEY (`id`),
					INDEX `FK_identity_card_identity_card` (`pid`),
					INDEX `path` (`path`),
					INDEX `number` (`number`),
					CONSTRAINT `FK_identity_card_identity_card` FOREIGN KEY (`pid`) REFERENCES `identity_card` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
					)
					COMMENT='identity card number'
					COLLATE='utf8_general_ci'
					ENGINE=InnoDB;
				
					"id" "pid" "path" "number" "zone" "status" "modified"
					"1012" "1" "1.1012" "330000" "浙江省" "Y" "2012-05-16 17:18:14"
					"1041" "1012" "1.1012.1041" "330300" "温州市" "Y" "2012-05-16 17:44:18"
					"1052" "1041" "1.1012.1041.1052" "330381" "瑞安市" "Y" "2012-05-16 17:44:25"
					"1367" "1" "1.1367" "360000" "江西省" "Y" "2012-05-16 16:57:23"
					"1451" "1367" "1.1367.1451" "360900" "宜春市" "Y" "2012-05-16 17:44:58"
					"1990" "1" "1.1990" "430000" "湖南省" "Y" "2012-05-16 16:50:50"
					"1991" "1990" "1.1990.1991" "430100" "长沙市" "Y" "2012-05-16 16:50:54"
					"2124" "1990" "1.1990.2124" "431300" "娄底市" "Y" "2012-05-16 16:54:45"
				

4.2.4. 文章表设计

看具体情况,拆分表,可按“日”,“月”,“年”等等

		
      +-----------+
      | category  |
      |-----------|
  +-->|id         | <---+
  |   |title      |     |
  |   |description|    1:n
  |   |status     |     |
  |   |parent_id  | o---+
  |   +-----------+
  |
 1:n
  |
  |   +-----------------+            +------------------+
  |   | article_2008_01 |            | feedback_2008_01 |
  |   |-----------------|            |------------------|
  |   |id               |<--1:n--+   |id                |
  |   |title            |        |   |title             |
  |   |content          |        |   |content           |
  |   |datetime         |        |   |datetime          |
  |   |status           |        |   |status            |
  +--o|category_id      |        +--o|news_id           |
  +--o|user_id          |        +-->|user_id           |
  |   +-----------------+        |   +------------------+
  |                              |
 1:n  +----------+     +---1:n---+
  |   | user     |     |
  |   |----------|     |
  +-->|id        | <---+
      |user      |
      |passwd    |
      |nickname  |
      |status    |
      +----------+
		
		

4.2.4.1. 分区表设计

分区表可以通过表空间,等等技术实现,优点是解决了Union查询问题,保证了数据的一致性。

		
      +-----------+
      | category  |
      |-----------|
  +-->|id         | <---+
  |   |title      |     |
  |   |description|    1:n
  |   |status     |     |
  |   |parent_id  | o---+
  |   +-----------+
  |
 1:n
  |
  |   +-----------------+            +-----------------+
  |   | article         |            | feedback        |
  |   |-----------------|            |-----------------|
  |   |id               |<--1:n--+   |id               |
  |   |title            |        |   |title            |
  |   |content          |        |   |content          |
  |   |datetime         |        |   |datetime         |
  |   |status           |        |   |status           |
  +--o|category_id      |        +--o|news_id          |
  +--o|user_id          |        +-->|user_id          |
  |   +-----------------+        |   +-----------------+
  |   | 2007,2008,2009  |        |   | 2007,2008,2009  |
  |   +-----------------+        |   +-----------------+
  |                              |
 1:n  +----------+     +---1:n---+
  |   | user     |     |
  |   |----------|     |
  +-->|id        | <---+
      |user      |
      |passwd    |
      |nickname  |
      |status    |
      +----------+
		
			

4.2.4.2. Title性能优化

显示title前20个汉字并在后尾添加省略号。

4.2.5. 评论表

		
+----------+
| user     |
|----------|
|id        | <---+
|user      |     |
|passwd    |     |
|nickname  |     |
|status    |     |
+----------+     |
                1:n
+-----------+    |      +-----------+
| feedback  |    |      | news      |
|-----------|    |      |-----------|
|id         |    |  +-->|id         |
|title      |    |  |   |title      |
|content    |    |  |   |content    |
|datetime   |    | 1:n  |datetime   |
|status     |    |  |   |status     |
|user_id    |o---+  |   |user_id    |
|news_id    |o------+   +-----------+
+-----------+
		
		

4.2.6. 记录点击率,阅读次数,及评分表

		
+--------------+             +--------------+
| article      |             | article_rank |
|--------------|             |--------------|
|id            | <---1:1---o |article_id    |
|title         |             |click         |
|content       |             |read          |
|datetime      |             |score         |
|status        |             |...           |
|category_id   |             |...           |
|user_id       |             |...           |
+--------------+             +--------------+
		
		

4.2.7. 产品属性表

4.2.7.1. 简单实现

		
+------------+           +--------------------------+           +-----------------------+
| product    |           | product_attribute        |           |product_attribute_key  |
+------------+           +--------------------------+           +-----------------------+
|id          | <--1:1--o |product_id                |     +---> |id                     |
|price       |           |product_attribute_key_id  | o---+     |name                   |
|quantity    |           |product_attribute_value_id| o---+     +-----------------------+
|...         |           +--------------------------+     |     +-----------------------+
|category_id |                                           1:n    |product_attribute_value|
+------------+                                            |     +-----------------------+
                                                          +---> |id                     |
                                                                |name                   |
                                                                +-----------------------+
		
			

4.2.7.2. 实现属性组管理

product attribute group

		
+------------+           +--------------------------+           +--------------------------+           +-----------------------+
| category   |           | product_attribute_group  |           | product_attribute        |           |product_attribute_key  |
+------------+           +--------------------------+           +--------------------------+           +-----------------------+
|id          |     +---> |id                        | <--1:n--o |product_attribute_group_id|     +---> |id                     |
|title       |     |     |name                      |           |product_attribute_key_id  | o---+     |name                   |
|description |    1:1    |status                    |           |product_attribute_value_id| o---+     +-----------------------+
|status      |     |     +--------------------------+           +--------------------------+     |     +-----------------------+
|parent_id   |     |                                                                            1:n    |product_attribute_value|
|default_pag | o---+                                                                             |     +-----------------------+
+------------+                                                                                   +---> |id                     |
                                                                                                       |name                   |
                                                                                                       +-----------------------+
		
			

4.2.7.3. 可编辑属表

product attribute group

		    
    +------------+           +------------------+           +--------------------------+           +---------------------------------+
    | category   |           | attribute_group  |           | group_has_attribute      |           |attribute_key                    |
    +------------+           +------------------+           +--------------------------+           +---------------------------------+
 +->|id          |     +-->  |id                | <--1:n--o |attribute_group_id        |     +-+-> |id                               |
 |  |title       |     |     |name              |           |attribute_key_id          | o---+ |   |name                             |
 |  |description |    1:1    |status            |           |                          |       |   |type enum('Bool','List','Input') |
 |  |status      |     |     +------------------+           +--------------------------+       |   |default array()                  |
 |  |parent_id   |     |                                                                       |   +---------------------------------+
 |  |default_pag | o---+                                                                       |
 |  +------------+                                                                             |
1:n                                                                                            |
 |  +-------------+             +--------------------------+                                   |
 |  | product     |             | product_attribute        |                                   |
 |  +-------------+             +--------------------------+                                   |
 |  |id           |         +-> |product_id                |                                   |
 |  |price        |         |   |attribute_key_id          | o---------------1:n---------------+
 |  |quantity     |         |   |attribute_value           |
 |  |...          |         |   +--------------------------+
 +-o|category_id  |         |
    |attr_group_id| <--1:n--o
    +-------------+
		    
			
				+--------------------------------------------------+
				| product_attribute_key |
				+--------------------------------------------------+
				| 1 | color | list | red,green,blue |
				| 2 | sex | bool | Female,Male |
				| 3 | qty | input| '' |
				+--------------------------------------------------+
			

4.2.8. 商品库存表

		
+------------+              +----------------+               +------------------+
| product    |              | product_store  |               | user_order       |
+------------+              +----------------+               +------------------+
|id          | <--+         |id              | <---+         |id                |
|price       |    +--1:1--o |product_id      |     |         |user_id           |
|quantity    |              |sn              |     +--1:n--o |product_store_id  |
|...         |              |status          |               |                  |
|category_id |              +----------------+               +------------------+
+------------+
		
		

product 是产品表总表,product_store每个产品一条记录,同时将sn编号对应到物理产品,这时记录库存需要

			select count(id) from product_store where product_id='xxxxx' and status = 'sell'
		

商品销售

			begin;
			select id from product_store where status = 'sale' and product_id='xxxxx' for update;
			insert into user_order(user_id,product_store_id) values('xxxxxx','xxxxx');
			update product_store set status = 'sold' where status = 'sale' and product_id='xxxxx';
			commit;
		

售出的商品与用户的订单项一一对应的。

注意上面,这里使用了排它锁与事务处理,防止一个商品卖给两个人。

根据上面的思路我们可以将商品属性与product_store表进行一对一匹配,这样每个商品都有它自己的商品属性,甚至价格也可以移到product_store表中,例如不同颜色售价不同。

4.2.9. 国际化语言表

		
      +-----------+             +---------------+
      | category  |    .---+    | category_lang |
      |-----------|   /    |    +---------------+
  +-->|id         | <---+  +--o |category_id    |
  |   |title      |     |       |language_id    | o---+
  |   |description|    1:n      |name           |     |      +-------------+
  |   |status     |     |       +---------------+     .      | language    |
  |   |parent_id  | o---+                              \     +-------------+
  |   +-----------+                                     >--> |id           |
 1:n                                                   /     |lang         |
  |   +------------+                                  '      |status       |
  |   | product    |                                  |      +-------------+
  |   +------------+           +--------------+       |
  |   |id          | <---+     | product_lang |       |
  |   |price       |     |     +--------------+       |
  |   |quantity    |     +---o |product_id    |       |
  |   |...         |           |language_id   | o-----+
  +-o |category_id |           |name          |
      +------------+           +--------------+
		
		

4.2.10. Workflow

		

   +------------+     +---------------+     +-----------+
   | user       |     | role_has_user |     | role      |
   +------------+     +---------------+     +-----------+
   |id          |o-+  |id             |  +->|id         |<-+
   |node_id     |  +->|user_id        |  |  |name       |  |
   |up_id       |     |role_id        |o-+  |description|  |
   +------------+	  +---------------+     +-----------+  |
		                                                   |
   +----------------+     +------------+                   |
   | workflow       |     | job        |                   |
   +----------------+     +------------+                   |
+->|id              |  +->|id          |                   |
|  |job_id          |o-+  |name        |                   |
+-o|up_id           |     |role_id     |o------------------+
   |                |     |description |
   +----------------+	  +------------+

		
		

4.2.11. 内容版本控制

主表

			CREATE TABLE `article` (
			`article_id` MEDIUMINT(8) UNSIGNED NOT NULL AUTO_INCREMENT,
			`cat_id` SMALLINT(5) NOT NULL DEFAULT '0',
			`title` VARCHAR(150) NOT NULL DEFAULT '',
			`content` LONGTEXT NOT NULL,
			`author` VARCHAR(30) NOT NULL DEFAULT '',
			`keywords` VARCHAR(255) NOT NULL DEFAULT '',
			PRIMARY KEY (`article_id`),
			INDEX `cat_id` (`cat_id`)
			)
			ENGINE=MyISAM
			ROW_FORMAT=DEFAULT
			AUTO_INCREMENT=1
		

本版控制表,用于记录每次变动

			CREATE TABLE `article_history` (
			`id` MEDIUMINT(8) UNSIGNED NOT NULL AUTO_INCREMENT,
			`article_id` MEDIUMINT(8) UNSIGNED NOT NULL,
			`cat_id` SMALLINT(5) NOT NULL DEFAULT '0',
			`title` VARCHAR(150) NOT NULL DEFAULT '',
			`content` LONGTEXT NOT NULL,
			`author` VARCHAR(30) NOT NULL DEFAULT '',
			`keywords` VARCHAR(255) NOT NULL DEFAULT '',
			PRIMARY KEY (`id`),
			INDEX `article_id` (`article_id`)
			)
			ENGINE=MyISAM
			ROW_FORMAT=DEFAULT
			AUTO_INCREMENT=1
		

版本控制触发器

			DROP TRIGGER article_history;

			DELIMITER //
			CREATE TRIGGER article_history BEFORE update ON article FOR EACH ROW
			BEGIN
			INSERT INTO article_history SELECT * FROM article WHERE article_id = OLD.article_id;
			END; //
			DELIMITER;
		

4.2.12. logging 日志表的设计

			CREATE TABLE `logging` (
			`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
			`tag` ENUM('unknow','www','user','admin') NOT NULL DEFAULT 'unknow' COMMENT '日志标签',
			`time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '产生时间',
			`facility` ENUM('bank','unionpay','sms','email') NOT NULL COMMENT '类别',
			`priority` ENUM('info','warning','error','critical','exception','debug') NOT NULL COMMENT '级别',
			`message` VARCHAR(512) NOT NULL COMMENT '内容',
			PRIMARY KEY (`id`)
			)
			COMMENT='日志表'
			COLLATE='utf8_general_ci'
			ENGINE=InnoDB
			AUTO_INCREMENT=2;
		

分区日志表

		
delimiter $$

CREATE TABLE `logging` (
  `tag` enum('unknow','login','info','admin','cron','manual') NOT NULL DEFAULT 'unknow' COMMENT '日志标签',
  `asctime` datetime NOT NULL COMMENT '产生时间',
  `facility` enum('account','bank','unionpay','sms','email','unknow') NOT NULL DEFAULT 'unknow' COMMENT '类别',
  `priority` enum('info','warning','error','critical','exception','debug') NOT NULL DEFAULT 'debug' COMMENT '级别',
  `message` varchar(512) NOT NULL COMMENT '内容',
  `operator` varchar(50) NOT NULL DEFAULT 'computer' COMMENT '操作者'
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE (YEAR(asctime))
SUBPARTITION BY HASH (MONTH(asctime))
(PARTITION p0 VALUES LESS THAN (1990) ENGINE = InnoDB,
 PARTITION p1 VALUES LESS THAN (2000) ENGINE = InnoDB,
 PARTITION p2 VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */$$
		
		

分表+分区,每年分表一次,每个分区中保存一个月的数据

			delimiter $$

			CREATE TABLE `logging_2013` (
			`tag` enum('unknow','login','info','admin','cron','manual') NOT NULL DEFAULT 'unknow' COMMENT '日志标签',
			`asctime` datetime NOT NULL COMMENT '产生时间',
			`facility` enum('account','bank','unionpay','sms','email','unknow') NOT NULL DEFAULT 'unknow' COMMENT '类别',
			`priority` enum('info','warning','error','critical','exception','debug') NOT NULL DEFAULT 'debug' COMMENT '级别',
			`message` varchar(512) NOT NULL COMMENT '内容',
			`operator` varchar(50) NOT NULL DEFAULT 'computer' COMMENT '操作者'
			) ENGINE=InnoDB DEFAULT CHARSET=utf8
			/*!50100 PARTITION BY LIST (MONTH(asctime))
			SUBPARTITION BY KEY (facility)
			(PARTITION part0 VALUES IN (1) ENGINE = InnoDB,
			PARTITION part1 VALUES IN (2) ENGINE = InnoDB,
			PARTITION part2 VALUES IN (3) ENGINE = InnoDB,
			PARTITION part3 VALUES IN (4) ENGINE = InnoDB,
			PARTITION part4 VALUES IN (5) ENGINE = InnoDB,
			PARTITION part5 VALUES IN (6) ENGINE = InnoDB,
			PARTITION part6 VALUES IN (7) ENGINE = InnoDB,
			PARTITION part7 VALUES IN (8) ENGINE = InnoDB,
			PARTITION part8 VALUES IN (9) ENGINE = InnoDB,
			PARTITION part9 VALUES IN (10) ENGINE = InnoDB,
			PARTITION part10 VALUES IN (11) ENGINE = InnoDB,
			PARTITION part11 VALUES IN (12) ENGINE = InnoDB) */$$
		

命名分区

			delimiter $$

			CREATE TABLE `logging_2012` (
			`tag` enum('unknow','login','info','admin','cron','manual') NOT NULL DEFAULT 'unknow' COMMENT '日志标签',
			`asctime` datetime NOT NULL COMMENT '产生时间',
			`facility` enum('account','bank','unionpay','sms','email','unknow') NOT NULL DEFAULT 'unknow' COMMENT '类别',
			`priority` enum('info','warning','error','critical','exception','debug') NOT NULL DEFAULT 'debug' COMMENT '级别',
			`message` varchar(512) NOT NULL COMMENT '内容',
			`operator` varchar(50) NOT NULL DEFAULT 'computer' COMMENT '操作者'
			) ENGINE=InnoDB DEFAULT CHARSET=utf8
			/*!50100 PARTITION BY LIST (MONTH(asctime))
			SUBPARTITION BY KEY (facility)
			(PARTITION January VALUES IN (1) ENGINE = InnoDB,
			PARTITION February VALUES IN (2) ENGINE = InnoDB,
			PARTITION March VALUES IN (3) ENGINE = InnoDB,
			PARTITION April VALUES IN (4) ENGINE = InnoDB,
			PARTITION May VALUES IN (5) ENGINE = InnoDB,
			PARTITION June VALUES IN (6) ENGINE = InnoDB,
			PARTITION July VALUES IN (7) ENGINE = InnoDB,
			PARTITION August VALUES IN (8) ENGINE = InnoDB,
			PARTITION September VALUES IN (9) ENGINE = InnoDB,
			PARTITION October VALUES IN (10) ENGINE = InnoDB,
			PARTITION November VALUES IN (11) ENGINE = InnoDB,
			PARTITION December VALUES IN (12) ENGINE = InnoDB) */$$
		

4.2.13. uuid 替代传统序列 id

		
DROP TABLE IF EXISTS `uuid_test`;
CREATE TABLE IF NOT EXISTS `uuid_test` (
  `uuid` varchar(36) NOT NULL,
  `name` varchar(20) NOT NULL,
  PRIMARY KEY (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='uuid 测试';
		
		

插入触发器

		
DROP TRIGGER IF EXISTS `uuid_test_insert`;
SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE='';
DELIMITER //
CREATE TRIGGER `uuid_test_insert` BEFORE INSERT ON `uuid_test` FOR EACH ROW BEGIN
	IF new.uuid is null or new.uuid = '' or length(new.uuid) != 36 THEN
		set new.uuid=uuid();
	END IF;
END//
DELIMITER ;
SET SQL_MODE=@OLDTMP_SQL_MODE;
		
		

ID放撰改触发器

		
DROP TRIGGER IF EXISTS `uuid_test_update`;
SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE='';
DELIMITER //
CREATE TRIGGER `uuid_test_update` BEFORE UPDATE ON `uuid_test` FOR EACH ROW BEGIN
	set new.uuid = old.uuid;
END//
DELIMITER ;
SET SQL_MODE=@OLDTMP_SQL_MODE;
		
		

4.2.14. 动态配置表

很多时候我们需要使用数据库存储配置项,由于各种原因我们无法使用配置文件来完成,例如在一个有很多节点集群环境中使用文件配置文件时非常不方便。

		
DROP TABLE IF EXISTS `config`;
CREATE TABLE IF NOT EXISTS `config` (
  `key` varchar(50) NOT NULL,
  `value` varchar(50) NOT NULL,
  `operator` varchar(50) NOT NULL DEFAULT 'dba',
  `mtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='网站动态配置文件';


INSERT INTO `config` (`key`, `value`, `operator`, `mtime`) VALUES
	('cache.apc', 'ON', 'dba', '2013-07-18 16:17:13'),
	('cache.file.path', '/tmp', 'dba', '2013-07-18 16:17:57'),
	('cache.redis', 'YES', 'dba', '2013-07-18 16:17:22'),
	('payment.alipay.status', 'Enabled', 'dba', '2013-07-18 16:15:15'),
	('payment.alipay.url', 'http://xx.comx.com', 'dba', '2013-07-18 16:16:38'),
	('payment.yeepay.status', 'Enabled', 'dba', '2013-07-18 16:15:17'),
	('payment.99bill.status', 'Enabled', 'dba', '2013-07-18 16:15:10'),
	('payment.zqpay.status', 'Disabled', 'dba', '2013-07-18 16:15:20');
		
		

配置项key的写法很讲究

			单个配置
			database.host=localhost
			database.user=user
			database.pass=pass

			多个配置
			database.1.host=localhost
			database.1.user=user
			database.1.pass=pass
			database.1.status=1

			database.2.host=localhost
			database.2.user=user
			database.2.pass=pass
			database.2.status=1

			优化配置项,例如:payment.alipay.status 可以这样优化
			payment.status.alipay
			payment.status.yeepay
		

这样做的目的是为了更好的使用like进行查询

			select `key`,`value` from config where `key` like 'payment.status.%';
			select `key`,`value` from config where `key` like 'database.?.status';
		

4.2.14.1. 配置表历史记录

我有一个表,里面只有固定行数的行记录,这些数据就是配置参数,我们将配置文件保存在数据库中,因为需要做负载均衡而不能使用文件配置文件。

有这样一个需求,这个记录每次修改都要保存历史记录,用于审计等等。我是这样设计该表的

				CREATE TABLE `config_fee` (
				`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
				`level` INT(11) NULL DEFAULT NULL COMMENT '层级',
				`type` ENUM('Deposit','Withdrawing') NOT NULL DEFAULT 'Withdrawing' COMMENT '类型,存款,取款',
				`min_fee` FLOAT(10,2) NOT NULL COMMENT '最低手续费',
				`max_fee` FLOAT(10,2) NOT NULL COMMENT '最高手续费',
				`ratio` FLOAT(10,2) NOT NULL COMMENT '手续费比例',
				`operator` VARCHAR(10) NOT NULL COMMENT '操作者',
				`status` ENUM('Current','Trash') NOT NULL DEFAULT 'Current',
				`ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
				`mtime` TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
				PRIMARY KEY (`id`)
				)
				COMMENT='手续费管理'
				COLLATE='utf8_general_ci'
				ENGINE=InnoDB;
			

数据记录的形态

			
mysql> select type,operator,status,ctime,mtime from config_mtf_fee;
+---------+----------+---------+---------------------+---------------------+
| type    | operator | status  | ctime               | mtime               |
+---------+----------+---------+---------------------+---------------------+
| Deposit | 141      | Trash   | 2014-08-28 11:10:17 | 2014-08-28 11:10:57 |
| Deposit | 141      | Trash   | 2014-08-28 11:10:17 | 2014-08-28 11:10:57 |
| Deposit | 141      | Trash   | 2014-08-28 11:10:17 | 2014-08-28 11:10:57 |
| Deposit | 141      | Trash   | 2014-08-28 11:10:17 | 2014-08-28 11:10:57 |
| Deposit | 324      | Current | 2014-08-28 11:10:54 | 2014-08-28 11:10:59 |
+---------+----------+---------+---------------------+---------------------+
2 rows in set (0.00 sec)
			
			

如上图所示,状态 Current 是当前记录,而Trash是废弃的历史记录。

每次修改数据,首先将Current改为Trash,然后插入一条新数据状态为Current,我们只会使用最后一条状态为current的数据。

4.2.15. 验证码

用户注册,登陆等等需要验证码,下面的方案是,请求验证码生成一个随机验证码,存在code中,identity可以存储来源IP/手机号码/Cookie等等用户校验, 5分钟内如果没有被使用就会删除,如果5分钟内被使用也会删除。type 可以存放www,user,bbs,admin.....等等,将所有验证码放在captcha表中统一管理。

这个方案主要是考虑没有memcache/redis/apc/xcache等等缓存环境下的解决方案,你也可以将下表改造一下,增加ttl字段用于存放生存时间,而不是采用5分钟一刀切的方案。

			CREATE TABLE `captcha` (
			`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
			`type` ENUM('user','admin') NOT NULL DEFAULT 'admin' COMMENT '验证码类型',
			`identity` VARCHAR(32) NOT NULL COMMENT '唯一身份识别md5摘要',
			`code` VARCHAR(6) NOT NULL COMMENT '验证码',
			`ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
			PRIMARY KEY (`id`),
			UNIQUE INDEX `code` (`code`),
			UNIQUE INDEX `identity` (`identity`)
			)
			COMMENT='验证码'
			COLLATE='utf8_general_ci'
			ENGINE=InnoDB;
		
		
CREATE EVENT `captcha` ON SCHEDULE
		EVERY 5 MINUTE STARTS '2013-07-08 16:27:03'
	ON COMPLETION PRESERVE
	ENABLE
	COMMENT ''
	DO BEGIN
	delete from captcha where type='myid' and ctime < DATE_ADD(now(), INTERVAL -5 MINUTE);
END
		
		

4.2.16. 手机归属地数据库表

members_location 表与 members 表是一对一关系,该表只负责存储归属地信息

			DROP TABLE IF EXISTS `members_location`;
			CREATE TABLE IF NOT EXISTS `members_location` (
			`id` int(10) unsigned NOT NULL,
			`province` varchar(50) NOT NULL,
			`city` varchar(50) NOT NULL,
			`ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
			PRIMARY KEY (`id`),
			KEY `province` (`province`),
			KEY `city` (`city`),
			CONSTRAINT `FK_members_location_members` FOREIGN KEY (`id`) REFERENCES `members` (`id`)
			) ENGINE=InnoDB DEFAULT CHARSET=utf8;
		

当某些用户符合条件需要查询归属地是,只要将其插入到 members_mobile 表即可。该表使用黑洞引擎并不会存储手机号码,所以明文手机号码安全得到了保障。

			DROP TABLE IF EXISTS `members_mobile`;
			CREATE TABLE IF NOT EXISTS `members_mobile` (
			`id` int(10) NOT NULL,
			`number` varchar(11) NOT NULL
			) ENGINE=BLACKHOLE DEFAULT CHARSET=utf8;
		

当有数据进入到 members_mobile 时出发器 members_mobile_insert 会工作,去 mobile_location 表中查询归属地后保存在 members_location 表中

			DROP TRIGGER IF EXISTS `members_mobile_insert`;
			SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE='';
			DELIMITER //
			CREATE TRIGGER `members_mobile_insert` BEFORE INSERT ON `members_mobile` FOR EACH ROW BEGIN
			insert into members_location(id,province,city) select NEW.id,mobile_location.province,mobile_location.city from mobile_location where mobile_location.id = md5(LEFT(NEW.number, 7));
			END//
			DELIMITER ;
			SET SQL_MODE=@OLDTMP_SQL_MODE;
		

mobile_location 是存储手机号段与归属地信息的数据库

			DROP TABLE IF EXISTS `mobile_location`;
			CREATE TABLE IF NOT EXISTS `mobile_location` (
			`id` varchar(50) NOT NULL,
			`province` varchar(50) DEFAULT NULL,
			`city` varchar(50) DEFAULT NULL,
			PRIMARY KEY (`id`)
			) ENGINE=InnoDB DEFAULT CHARSET=utf8;
		

4.2.17. 数据检查

4.2.17.1. 身份证校验

该函数能够检查身份证号码是否正确

			
CREATE DEFINER=`neo`@`%` FUNCTION `check_id_number`(`idnumber` CHAR(18))
	RETURNS enum('true','false')
	LANGUAGE SQL
	NOT DETERMINISTIC
	NO SQL
	SQL SECURITY DEFINER
	COMMENT ''
BEGIN
DECLARE status ENUM('true','false') default 'false';
DECLARE verify CHAR(1);
DECLARE sigma INT;
DECLARE remainder INT;

IF length(idnumber) = 18 THEN
	set sigma = cast(substring(idnumber,1,1) as UNSIGNED) * 7
		+cast(substring(idnumber,2,1) as UNSIGNED) * 9
		+cast(substring(idnumber,3,1) as UNSIGNED) * 10
		+cast(substring(idnumber,4,1) as UNSIGNED) * 5
		+cast(substring(idnumber,5,1) as UNSIGNED) * 8
		+cast(substring(idnumber,6,1) as UNSIGNED) * 4
		+cast(substring(idnumber,7,1) as UNSIGNED) * 2
		+cast(substring(idnumber,8,1) as UNSIGNED) * 1
		+cast(substring(idnumber,9,1) as UNSIGNED) * 6
		+cast(substring(idnumber,10,1) as UNSIGNED) * 3
		+cast(substring(idnumber,11,1) as UNSIGNED) * 7
		+cast(substring(idnumber,12,1) as UNSIGNED) * 9
		+cast(substring(idnumber,13,1) as UNSIGNED) * 10
		+cast(substring(idnumber,14,1) as UNSIGNED) * 5
		+cast(substring(idnumber,15,1) as UNSIGNED) * 8
		+cast(substring(idnumber,16,1) as UNSIGNED) * 4
		+cast(substring(idnumber,17,1) as UNSIGNED) * 2;
	set remainder = MOD(sigma,11);
	set verify = (case remainder
		when 0 then '1' when 1 then '0' when 2 then 'X' when 3 then '9'
		when 4 then '8' when 5 then '7' when 6 then '6' when 7 then '5'
		when 8 then '4' when 9 then '3' when 10 then '2' else '/' end
	);

END IF;

IF right(idnumber,1) = verify THEN
	set status = 'true';
END IF;

RETURN status;

END
			
			

首先我们使用正确身份证号码进行测试,返回true

			
mysql> select check_id_number('330702198003090915');
+---------------------------------------+
| check_id_number('330702198003090915') |
+---------------------------------------+
| true                                  |
+---------------------------------------+
1 row in set (0.01 sec)
			
			

长度不符合18位直接返回false.

			
mysql> select check_id_number('33070219800309');
+-----------------------------------+
| check_id_number('33070219800309') |
+-----------------------------------+
| false                             |
+-----------------------------------+
1 row in set (0.00 sec)	

mysql> select check_id_number('33070219800309091457889');
+--------------------------------------------+
| check_id_number('33070219800309091457889') |
+--------------------------------------------+
| false                                      |
+--------------------------------------------+
1 row in set, 1 warning (0.00 sec)
			
			

随便改译为数,校验失败返回 false

			
mysql> select check_id_number('330702198003090914');
+---------------------------------------+
| check_id_number('330702198003090914') |
+---------------------------------------+
| false                                 |
+---------------------------------------+
1 row in set (0.00 sec)					
			
			

4.2.18. 创建与修改时间

ctime 为创建时间, mtime 是修改时间默认CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

			CREATE TABLE `trades_platform` (
			`id` INT(10) UNSIGNED NULL DEFAULT NULL,
			`type` ENUM('A','B','C') NOT NULL,
			`login` VARCHAR(10) NOT NULL,
			`password` VARCHAR(10) NOT NULL,
			`ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
			`mtime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
			)
			COMMENT='平台'
			ENGINE=InnoDB;
		

我们还可以让mtime默认为NULL,这样更节省存储空间。

			`ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
			`mtime` TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
		

4.2.19. 在线用户表

该表的功能是显示在线用户,控制多点登陆,防止异常退出

		
CREATE TABLE IF NOT EXISTS `employees_online` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(20) NOT NULL COMMENT 'User ID',
  `ipaddr` varchar(15) NOT NULL COMMENT 'IP Address',
  `expire` datetime NOT NULL COMMENT 'Expire time',
  `status` enum('Login','Logout','Offline') NOT NULL DEFAULT 'Login' COMMENT 'Current Status',
  `message` varchar(255) NOT NULL COMMENT 'Leave a message',
  `ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Created Time',
  `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Modified Time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='show listing of last logged in users';

DELIMITER //
CREATE DEFINER=`dba`@`192.168.%` EVENT `employees_online` ON SCHEDULE EVERY 15 MINUTE STARTS '2014-08-22 10:33:24' ON COMPLETION NOT PRESERVE ENABLE COMMENT 'Employees Online Logging' DO BEGIN
	update employees_online set `status` = 'Offline' where expire < now() and ctime > DATE_ADD(now(), INTERVAL -15 MINUTE);
END//
DELIMITER ;

SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_ENGINE_SUBSTITUTION';
DELIMITER //
CREATE TRIGGER `employees_online_before_delete` BEFORE DELETE ON `employees_online` FOR EACH ROW BEGIN
	SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Permission denied', MYSQL_ERRNO = 1001;
END//
DELIMITER ;
SET SQL_MODE=@OLDTMP_SQL_MODE;

SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_ENGINE_SUBSTITUTION';
DELIMITER //
CREATE TRIGGER `employees_online_before_update` BEFORE UPDATE ON `employees_online` FOR EACH ROW BEGIN
	SET NEW.`id` = OLD.id;
	SET NEW.`username` = OLD.username;
	SET NEW.`ipaddr` = OLD.ipaddr;
	SET NEW.`message` = OLD.message;
	SET NEW.`ctime` = OLD.ctime;
END//
DELIMITER ;
SET SQL_MODE=@OLDTMP_SQL_MODE;	
		
		

登陆日志将永久保存,防止数据被删除由触发器employees_online_before_delete负责

防止有人撰改登陆信息,由触发器employees_online_before_update负责,主要是防止撰改登陆名与IP地址,这样讲不能从其他电脑登陆,必须用户Logout才能在其他电脑登陆。

4.2.20. HTML TO Text

实现 PHP strip_tags 函数的功能。

		
CREATE DEFINER=`dba`@`%` FUNCTION `strip_tags`(`$str` TEXT)
RETURNS text CHARSET utf8
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
    DECLARE $start, $end INT DEFAULT 1;
    LOOP
        SET $start = LOCATE("<", $str, $start);
        IF (!$start) THEN RETURN $str; END IF;
        SET $end = LOCATE(">", $str, $start);
        IF (!$end) THEN SET $end = $start; END IF;
        SET $str = INSERT($str, $start, $end - $start + 1, "");
    END LOOP;
END		
		
		
		
mysql> select strip_tags('<span><i>hello</i> <b>world</b>!!! <br /><a href="//www.netkiller/">netkiller</a>');
+----------------------------------------------------------------------+
| strip_tags('<span>hel<b>lo <a href="world">wo<>rld</a> <<x>again<.') |
+----------------------------------------------------------------------+
| hello world again.                                                   |
+----------------------------------------------------------------------+
1 row in set

mysql> select strip_tags('<span style="color:red"><i>hello</i> <b id="world" >world</b>!!! <br /><a class="home" href="//www.netkiller/">netkiller</a><span>') as TEXT;
+--------------------------+
| TEXT                     |
+--------------------------+
| hello world!!! netkiller |
+--------------------------+
1 row in set (0.00 sec)
		
		

4.2.21. SNS 数据库设计

这里讲解SNS交友社区的数据库设计与实现

我们要实现下面几个功能

  1. 朋友之间的关系,多对多关系
  2. 朋友之间的维度,如3度4度....
  3. 朋友的查找
		
CREATE DATABASE `sns` /*!40100 COLLATE 'utf8_general_ci' */
		
		

4.2.21.1. people 表

people 是存储人,你可以用为user,member都可以

			
CREATE TABLE `people` (
	`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(50) NOT NULL,
	PRIMARY KEY (`id`)
)
COMMENT='Social Network Site - Six Degrees of Separation - http://www.netkiller.cn'
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
			
			

存储具体的这人

4.2.21.2. firend 表

这个表的功能主要是维持朋友之间的关系网,这里使用了多对多方式并且使用外键防止产生脏数据。

			
CREATE TABLE `friend` (
	`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`people_id` INT(10) UNSIGNED NOT NULL,
	`friend_id` INT(10) UNSIGNED NOT NULL,
	`ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
	PRIMARY KEY (`id`),
	UNIQUE INDEX `unique` (`people_id`, `friend_id`),
	INDEX `FK_firend_people` (`people_id`),
	INDEX `FK_firend_people_2` (`friend_id`),
	CONSTRAINT `FK_firend_people` FOREIGN KEY (`people_id`) REFERENCES `people` (`id`),
	CONSTRAINT `FK_firend_people_2` FOREIGN KEY (`friend_id`) REFERENCES `people` (`id`)
)
COMMENT='Social Network Site - Six Degrees of Separation - http://www.netkiller.cn'
COLLATE='utf8_general_ci'
ENGINE=InnoDB;

			
			

4.2.21.3. 演示

首先初始化用户数据

			
INSERT INTO `people` (`id`, `name`) VALUES
	(1, 'Neo'),
	(2, 'Luke'),
	(3, 'Jack'),
	(4, 'Joey'),
	(5, 'Jam'),
	(6, 'John');
			
			

建立朋友之间的关系

			
INSERT INTO `friend` (`id`, `people_id`, `friend_id`) VALUES
	(1, 1, 2),
	(2, 1, 3),
	(3, 1, 4),
	(4, 1, 5),
	(5, 1, 6),
	(6, 2, 1),
	(7, 2, 3);
			
			

现在就可以查找你的朋友了

			
select people.* from friend, people where friend.people_id = 1 and friend.friend_id = people.id;
			
			

查找朋友的朋友就比较麻烦了,必须使用递归方法,一层一层查下去,反复执行SQL效率是很低的,所以我们准备了第三张表。

4.2.21.4. network 表

关系网表,主要功能是弥补firend表,用于快速检索(在不使用递归的情况下)

			
CREATE TABLE `network` (
	`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`people_id` INT(10) UNSIGNED NOT NULL,
	`following_id` INT(10) UNSIGNED NOT NULL,
	`friend_id` INT(10) UNSIGNED NULL DEFAULT NULL,
	`degrees` VARCHAR(250) NOT NULL,
	`ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
	PRIMARY KEY (`id`),
	UNIQUE INDEX `unique` (`people_id`, `friend_id`, `following_id`),
	INDEX `FK_firend_people` (`people_id`),
	INDEX `FK_firend_people_2` (`friend_id`),
	INDEX `FK_friend_people_following_id` (`following_id`),
	CONSTRAINT `FK_firend_people` FOREIGN KEY (`people_id`) REFERENCES `people` (`id`),
	CONSTRAINT `FK_friend_people_following_id` FOREIGN KEY (`following_id`) REFERENCES `people` (`id`),
	CONSTRAINT `FK_friend_people_friend_id` FOREIGN KEY (`friend_id`) REFERENCES `people` (`id`)
)
COMMENT='Social Network Site - Six Degrees of Separation - http://www.netkiller.cn'
COLLATE='utf8_general_ci'
ENGINE=InnoDB;

			
			

following 一个朋友, Neo following Jam

			
INSERT INTO `people` (`id`, `name`) VALUES
	(1, 'Neo'),
	(2, 'Luke'),
	(3, 'Jack'),
	(4, 'Joey'),
	(5, 'Jam'),
	(6, 'John');

INSERT INTO `network` (`people_id`, `following_id`, `friend_id`, `degrees`) VALUES ( 1, 5, NULL, '1.5');
			
			

之前Neo已经 following Jam,接下来查找Jam的朋友,现在Neo following John, John 是 Jam 的朋友,friend_id = NULL 表示 Jam 尚未有朋友

			
select * from network where people_id=1 and friend_id = 5;

INSERT INTO `sns`.`network` (`people_id`, `following_id`, `friend_id`, `degrees`) VALUES ('1', '6', '5', '1.5.6');
			
			

Neo following Joey, Joey 是 Luke 的朋友, 所以 Luke可能是 Neo的朋友

			
INSERT INTO `sns`.`network` (`people_id`, `following_id`, `friend_id`, `degrees`) VALUES ('1', '4', '2', '1.2.4');
			
			

查询不同维度下的所有好友,查询出的用户ID需要处理。

			
select * from network where people_id=1 and degrees like "1.%";
select * from network where people_id=1 and degrees like "1.2%";
select * from network where people_id=1 and degrees like "1.2.%";
			
			

至此社区管理网就建立起来了

上面的例子演示了 people_id=1 即 Neo 的关系网

4.2.22. TINYINT(1) 还是 ENUM( 'true' , 'false')

使用枚举:

为什么使用枚举:

  1. 枚举在插入时会做数据检查,不允许枚举之外的定义写入字段,故不会出现脏数据
  2. 枚举可读性更高 TINYINT(1) 需要维护数据字典,查询需要连表才能读到它的定义
  3. 数据更紧凑,节省存储空间
  4. 缺点是修改枚举定义,每次都需要变更数据库结构

什么时候使用TINYINT(1):

  1. 数据超过 10 条
  2. 缺点 TINYINT(1) 使用外键才能实现约束
		
		
		
		

4.2.23. PostgreSQL 所特有数据库设计

PostgreSQL 数据库 ORDBMS / OODBMS 等特有属性

对象相关数据库管理系统(ORDBMS Object – Oriented Relative DBMS)

4.2.23.1. 国家地区表的设计

			
 +-----------+
 | city      |
 |-----------|
 |id         | <---+
 |name       |     |
 |description|    1:n
 |status     |     |
 |parent_id  | o---+
 +-----------+
			
			

例 4.2. 递归查询实例 city 表

定义结构

	    	
CREATE TABLE city
(
  id serial NOT NULL,
  name character varying,
  parent_id integer,
  status boolean,
  CONSTRAINT city_pkey PRIMARY KEY (id),
  CONSTRAINT city_parent_id_fkey FOREIGN KEY (parent_id)
      REFERENCES city (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
  OIDS=FALSE
);
ALTER TABLE city
  OWNER TO sys;
	    	
	    	

插入数据

	    	
INSERT INTO city (id, name, parent_id, status) VALUES (1, '广东', NULL, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (2, '湖南', NULL, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (3, '深圳', 1, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (4, '东莞', 1, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (5, '福田', 3, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (6, '南山', 3, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (7, '宝安', 3, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (8, '西乡', 7, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (9, '福永', 7, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (10, '龙华', 7, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (11, '长沙', 2, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (12, '湘潭', 2, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (13, '常德', 2, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (14, '桃源', 13, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (15, '汉寿', 13, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (16, '黑龙江', NULL, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (17, '伊春', 16, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (18, '哈尔滨', 16, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (19, '齐齐哈尔', 16, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (20, '牡丹江', 16, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (21, '佳木斯', 16, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (22, '民治', 10, NULL);
INSERT INTO city (id, name, parent_id, status) VALUES (23, '上塘', 10, NULL);
	    	
	    	

查询

	    	
WITH RECURSIVE path(id, name, path, idpath, parent_id, status) AS (
  SELECT id, name, '/' || name , '/' || id , parent_id, status FROM city WHERE parent_id is null
  UNION
  SELECT
    city.id,
    city.name,
    parentpath.path ||
      CASE parentpath.path
	WHEN '/' THEN ''
	ELSE '/'
      END || city.name,
    parentpath.idpath ||
     CASE parentpath.idpath
	WHEN '/' THEN ''
	ELSE '/'
      END || city.id,
    city.parent_id, city.status
  FROM city, path as parentpath
  WHERE city.parent_id = parentpath.id
)

SELECT * FROM path;
	    	
	    	

结果输出

	    	
 id |   name   |           path            |    idpath    | parent_id | status
----+----------+---------------------------+--------------+-----------+--------
  1 | 广东     | /广东                     | /1           |           |
  2 | 湖南     | /湖南                     | /2           |           |
 16 | 黑龙江   | /黑龙江                   | /16          |           |
  3 | 深圳     | /广东/深圳                | /1/3         |         1 |
  4 | 东莞     | /广东/东莞                | /1/4         |         1 |
 11 | 长沙     | /湖南/长沙                | /2/11        |         2 |
 12 | 湘潭     | /湖南/湘潭                | /2/12        |         2 |
 13 | 常德     | /湖南/常德                | /2/13        |         2 |
 17 | 伊春     | /黑龙江/伊春              | /16/17       |        16 |
 18 | 哈尔滨   | /黑龙江/哈尔滨            | /16/18       |        16 |
 19 | 齐齐哈尔 | /黑龙江/齐齐哈尔          | /16/19       |        16 |
 20 | 牡丹江   | /黑龙江/牡丹江            | /16/20       |        16 |
 21 | 佳木斯   | /黑龙江/佳木斯            | /16/21       |        16 |
  5 | 福田     | /广东/深圳/福田           | /1/3/5       |         3 |
  6 | 南山     | /广东/深圳/南山           | /1/3/6       |         3 |
  7 | 宝安     | /广东/深圳/宝安           | /1/3/7       |         3 |
 14 | 桃源     | /湖南/常德/桃源           | /2/13/14     |        13 |
 15 | 汉寿     | /湖南/常德/汉寿           | /2/13/15     |        13 |
  8 | 西乡     | /广东/深圳/宝安/西乡      | /1/3/7/8     |         7 |
  9 | 福永     | /广东/深圳/宝安/福永      | /1/3/7/9     |         7 |
 10 | 龙华     | /广东/深圳/宝安/龙华      | /1/3/7/10    |         7 |
 22 | 民治     | /广东/深圳/宝安/龙华/民治 | /1/3/7/10/22 |        10 |
 23 | 上塘     | /广东/深圳/宝安/龙华/上塘 | /1/3/7/10/23 |        10 |
(23 rows)
	    	
	    	

4.2.23.2. 话题讨论表的设计

例 4.3. 话题讨论表的设计

http://justcramer.com/2010/05/30/scaling-threaded-comments-on-django-at-disqus/

	        
create table comments (
    id SERIAL PRIMARY KEY,
    message VARCHAR,
    author VARCHAR,
    parent_id INTEGER REFERENCES comments(id)
);
insert into comments (message, author, parent_id)
    values ('This thread is really cool!', 'David', NULL), ('Ya David, we love it!', 'Jason', 1), ('I agree David!', 'Daniel', 1), ('gift Jason', 'Anton', 2),
    ('Very interesting post!', 'thedz', NULL), ('You sir, are wrong', 'Chris', 5), ('Agreed', 'G', 5), ('Fo sho, Yall', 'Mac', 5);
			
	        
			
WITH RECURSIVE cte (id, message, author, path, parent_id, depth)  AS (
    SELECT  id,
        message,
        author,
        array[id] AS path,
        parent_id,
        1 AS depth
    FROM    comments
    WHERE   parent_id IS NULL

    UNION ALL

    SELECT  comments.id,
        comments.message,
        comments.author,
        cte.path || comments.id,
        comments.parent_id,
        cte.depth + 1 AS depth
    FROM    comments
    JOIN cte ON comments.parent_id = cte.id
    )
    SELECT id, message, author, path, depth FROM cte ORDER BY path;
	    	
			

输出结果

 id |           message           | author |  path   | depth
----+-----------------------------+--------+---------+-------
  1 | This thread is really cool! | David  | {1}     |     1
  2 | Ya David, we love it!       | Jason  | {1,2}   |     2
  4 | gift Jason                  | Anton  | {1,2,4} |     3
  3 | I agree David!              | Daniel | {1,3}   |     2
  5 | Very interesting post!      | thedz  | {5}     |     1
  6 | You sir, are wrong          | Chris  | {5,6}   |     2
  7 | Agreed                      | G      | {5,7}   |     2
  8 | Fo sho, Yall                | Mac    | {5,8}   |     2
(8 rows)
		    

4.2.23.3. 账户表/余额表/消费储蓄表

此表适用于购物车等金钱来往账面等等。

		
-- Table: account

-- DROP TABLE account;

CREATE TABLE account
(
  id integer NOT NULL DEFAULT nextval('trade_id_seq'::regclass),
  no character varying(10) NOT NULL, -- 账号
  balance money NOT NULL DEFAULT 0.00, -- 余额
  datetime timestamp without time zone NOT NULL DEFAULT (now())::timestamp(0) without time zone,
  CONSTRAINT account_pkey PRIMARY KEY (id)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE account
  OWNER TO dba;
COMMENT ON COLUMN account.no IS '账号';
COMMENT ON COLUMN account.balance IS '余额';


-- Index: account_no_idx

-- DROP INDEX account_no_idx;

CREATE INDEX account_no_idx
  ON account
  USING btree
  (no COLLATE pg_catalog."default");
		
		
		

账户结余计算

		
select acc.*, (select sum(balance)+acc.balance from account as ac where ac.id < acc.id) as profit from account as acc;

test=# select acc.*, (select sum(balance)+acc.balance from account as ac where ac.id < acc.id) as profit from account as acc;
 id |  no  | balance  |      datetime       | profit
----+------+----------+---------------------+---------
  1 | 1000 |    $0.00 | 2013-10-09 10:51:10 |
  2 | 1000 |   $12.60 | 2013-10-09 10:51:22 |  $12.60
  4 | 1000 |   $16.80 | 2013-10-09 10:51:42 |  $29.40
  5 | 1000 |  $100.00 | 2013-10-09 10:51:49 | $129.40
  6 | 1000 |  $200.00 | 2013-10-09 10:56:35 | $329.40
  7 | 1000 |   $50.45 | 2013-10-09 10:57:23 | $379.85
  8 | 1000 |   $75.50 | 2013-10-09 10:57:31 | $455.35
  9 | 1000 |  -$55.30 | 2013-10-09 10:59:28 | $400.05
 10 | 1000 | -$200.00 | 2013-10-09 10:59:44 | $200.05
(9 rows)
				
		

4.2.24. Sharding

Sharding是近几年提出的概念,可以做分表,分库切割,通过hash值定位。但都存在一个问题,数据连续性,索引无法跨表。

Oracle 在8.x中就支持分区功能,MySQL在5.1.x中也是闲类似功能,PostgreSQL 因存储结构设计的较好,基本不需要做分区。

4.2.24.1. horizontal

		
ALTER TABLE `goods`  DROP INDEX `goods_sn_2`;
ALTER TABLE goods PARTITION BY RANGE (goods_id) (
    PARTITION p0 VALUES LESS THAN (10000),
    PARTITION p1 VALUES LESS THAN (20000),
    PARTITION p2 VALUES LESS THAN (30000),
    PARTITION p3 VALUES LESS THAN (40000),
    PARTITION p4 VALUES LESS THAN MAXVALUE
);

ALTER TABLE goods PARTITION BY HASH(goods_id) PARTITIONS 10;

ALTER TABLE goods  PARTITION BY KEY (is_on_sale) PARTITIONS 2;

ALTER TABLE goods PARTITION BY HASH(YEAR(FROM_UNIXTIME(add_time))) PARTITIONS 4;
		
		

4.2.24.2. vertical

4.2.24.3. 新闻数据库分表案例

这里我通过一个新闻网站为例,解决分表的问题

避免开发中经常拼接表,我采用一个一劳永逸的方法,建立一个 news 表使用黑洞引擎,然后通过出发器将数据分流到匹配的表中。同时采用uuid替代数字序列,可以保证未来数年不会出现ID用尽。

		
CREATE TABLE IF NOT EXISTS `news` (
  `uuid` varchar(36) NOT NULL COMMENT '唯一ID',
  `title` varchar(50) NOT NULL COMMENT '新闻标题',
  `body` text NOT NULL COMMENT '新闻正文',
  `ctime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间',
  `mtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `atime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '访问时间',
  PRIMARY KEY (`uuid`)
) ENGINE=BLACKHOLE DEFAULT CHARSET=utf8;
		
		

该表仅仅用于举例,结构比较简单。接下来创建年份分表,你也可以每个月一个表,根据你的许下灵活调整。表结构与上面的news表相同,注意 ENGINE=InnoDB。

		
CREATE TABLE IF NOT EXISTS `news_2012` (
  `uuid` varchar(36) NOT NULL COMMENT '唯一ID',
  `title` varchar(50) NOT NULL COMMENT '新闻标题',
  `body` text NOT NULL COMMENT '新闻正文',
  `ctime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间',
  `mtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `atime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '访问时间',
  PRIMARY KEY (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='news 表';

CREATE TABLE IF NOT EXISTS `news_2013` (
  `uuid` varchar(36) NOT NULL COMMENT '唯一ID',
  `title` varchar(50) NOT NULL COMMENT '新闻标题',
  `body` text NOT NULL COMMENT '新闻正文',
  `ctime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间',
  `mtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `atime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '访问时间',
  PRIMARY KEY (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='news 表';
		
		

uuid 索引表,主要的功能是通过uuid查询出该记录在那张表中。更好的方案是将数据放入solr中处理,包括标题与内容搜索等等。

		
CREATE TABLE `news_index` (
	`uuid` VARCHAR(36) NOT NULL,
	`tbl_name` VARCHAR(10) NOT NULL,
	PRIMARY KEY (`uuid`)
)
COMMENT='news uuid 索引表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB;

		
		

news_insert 过程,用于向目标表中插入数据,可以单独call 但不建议。因为insert 远比 call 更通用,要考虑移植性与通用性

		
DELIMITER //
CREATE DEFINER=`neo`@`%` PROCEDURE `news_insert`(IN `uuid` vARCHAR(36), IN `title` VARCHAR(50), IN `body` TEXT, IN `ctime` TIMESTAMP)
BEGIN
	if year(ctime) = '2012' then
		insert into news_2012(uuid,title,body,ctime) values(uuid,title, body, ctime);
	end if;
	if year(ctime) = '2013' then
		insert into news_2013(uuid,title,body,ctime) values(uuid,title, body, ctime);
	end if;
	insert into news_index values(uuid, year(ctime));
END//
DELIMITER ;
		
		

插入触发器,负责获取 uuid 然后调用存储过程

		
SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE='';
DELIMITER //
CREATE TRIGGER `news_before_insert` BEFORE INSERT ON `news` FOR EACH ROW BEGIN
	IF new.uuid is null or new.uuid = '' or length(new.uuid) != 36 THEN
		set new.uuid=uuid();
	END IF;
	call news_insert(new.uuid,new.title,new.body,new.ctime);
END//
DELIMITER ;
SET SQL_MODE=@OLDTMP_SQL_MODE;
		
		

这个触发器用户保护表中的 uuid 值不被修改。

		
SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE='';
DELIMITER //
CREATE TRIGGER `news_before_update` BEFORE UPDATE ON `news_2013` FOR EACH ROW BEGIN
	set new.uuid = old.uuid;
END//
DELIMITER ;
SET SQL_MODE=@OLDTMP_SQL_MODE;
		
		

4.2.25. MySQL 大数据操作注意事项

http://netkiller.github.io/journal/mysql.parallel.html

4.2.25.1. 关于 delete

delete from mytable 必死无疑,你需要分批删除,尽量缩小每个批次删除的记录数,delete 是可以并行执行的,你可以同时运行多个删除操作

手工分批删除

通过 where 条件和 limit 限制条数,缩小删除结果集,分多笔执行,手工删除。

		
mysql> show processlist;
+--------+-----------------+---------------------+-----------+---------+-------+-----------------------------+--------------------------------------------------------+
| Id     | User            | Host                | db        | Command | Time  | State                       | Info                                                   |
+--------+-----------------+---------------------+-----------+---------+-------+-----------------------------+--------------------------------------------------------+
|      1 | event_scheduler | localhost           | NULL      | Daemon  |    52 | Waiting for next activation | NULL                                                   |
| 115986 | dba             | localhost           | example   | Query   |     0 | NULL                        | show processlist                                       |
| 117446 | dba             | localhost           | example   | Query   |    20 | updating                    | delete from mytable where OPEN_TIME like '2011.11.28%' |
| 117525 | dba             | localhost           | example   | Query   |     2 | updating                    | delete from mytable where OPEN_TIME like '2011.12.02%' |
| 117526 | dba             | localhost           | example   | Query   |    49 | updating                    | delete from mytable where OPEN_TIME like '2011.12.12%' |
| 117527 | dba             | localhost           | example   | Query   |     6 | updating                    | delete from mytable where OPEN_TIME like '2011.12.21%' |
| 117528 | dba             | localhost           | example   | Query   |    64 | updating                    | delete from mytable where OPEN_TIME like '2011.12.30%' |
| 117546 | dba             | localhost           | example   | Query   |    33 | updating                    | delete from mytable where OPEN_TIME like '2011.11.10%' |
+--------+-----------------+---------------------+-----------+---------+-------+-----------------------------+--------------------------------------------------------+
23 rows in set (0.00 sec)
		
			
自动定时执行,分批删除数据

需求:数据需要删除100万条数据,直接删除会对系统造成压力,导致主库阻塞。

解决方案:分批删除,使用 event 定时执行删除 SQL 直到删完位置。

开启 EVENT

		
set global event_scheduler = on;		
		
			

准备数据备份、删除和回撤所需的三条SQL

			
备份 SQL:
CREATE TABLE mytable_2022_7_30 SELECT * FROM mytable;

删除 SQL:
DELETE FROM mytable LIMIT 1; 

回撤 SQL:
INSERT INTO mytable SELECT * FROM mytable_2022_7_30;	
			
			

通过 where 缩小结果集

			
备份 SQL:
CREATE TABLE mytable_2022_7_30 SELECT * FROM mytable where level = 'info';

删除 SQL:
DELETE FROM mytable where level = 'info' LIMIT 1; 

回撤 SQL:
INSERT INTO mytable SELECT * FROM mytable_2022_7_30;
			
			

上面条SQL在测试环境验证无误后,编写定时删除 EVENT

			
DELIMITER $$
CREATE EVENT IF NOT EXISTS event_delete
ON SCHEDULE EVERY 1 SECOND ON COMPLETION PRESERVE 
DO BEGIN
	DECLARE num integer;

	SELECT  COUNT(*) INTO num FROM mytable;
    
	IF num > 0 THEN
		delete from mytable limit 1;
		insert into logs(ctime) values(now());
	END IF;
    
END$$
			
			

EVENT 在测试环境运行无误才能在生产环境执行。

通过 limit 调整每次删除的数量,为了观察 EVENT 执行状态,这里会插入一条执行日志。

数据删除完毕之后回收 EVENT

			
DROP EVENT event_delete;
			
			

4.2.25.2. 关于 update

在电商领域常常遇到一个问题“调价”,经常需要调整一批商品的价格, 程序猿一条语句搞定有没有?

		
update goods set price=price+10 where category_id = xxx
		
		

在开发,测试环境是可以通过测试的,一旦部署到生产环境,必死无疑

4.2.25.3. 关于创建索引

大表创建索引需要很久的时间,通常要经历 manage keys 与 copy to tmp table 的过程

		
mysql> show processlist;
+--------+-----------------+---------------------+----------+---------+-------+-----------------------------+------------------------------------------------------------------+
| Id     | User            | Host                | db       | Command | Time  | State                       | Info                                                             |
+--------+-----------------+---------------------+----------+---------+-------+-----------------------------+------------------------------------------------------------------+
|      1 | event_scheduler | localhost           | NULL     | Daemon  |    47 | Waiting for next activation | NULL                                                             |
| 115986 | dba             | localhost           | example  | Query   |     0 | NULL                        | show processlist                                                 |
| 118814 | dba             | 192.168.6.20:50459  | example  | Query   |     8 | copy to tmp table           | ALTER TABLE `mytable` ADD INDEX `modifiy_time` (`MODIFY_TIME`)   |
+--------+-----------------+---------------------+----------+---------+-------+-----------------------------+------------------------------------------------------------------+
17 rows in set (0.00 sec)
		
		

删除索引,也需要经理 copy to tmp table 过程,漫长的等待

		
mysql> show processlist;
+--------+-----------------+---------------------+--------------+---------+-------+-----------------------------+-------------------------------------------------+
| Id     | User            | Host                | db           | Command | Time  | State                       | Info                                            |
+--------+-----------------+---------------------+--------------+---------+-------+-----------------------------+-------------------------------------------------+
|      1 | event_scheduler | localhost           | NULL         | Daemon  |    11 | Waiting for next activation | NULL                                            |
| 115986 | dba             | localhost           | example      | Query   |     0 | NULL                        | show processlist                                |
| 118814 | dba             | 192.168.6.20:50459  | example      | Query   |     4 | copy to tmp table           | ALTER TABLE `mytable`	DROP INDEX `modifiy_time` |
+--------+-----------------+---------------------+--------------+---------+-------+-----------------------------+-------------------------------------------------+
17 rows in set (0.00 sec)
		
		

所以数据设计要深思熟虑,做到提前未雨绸缪,不要亡羊补牢

4.2.25.4. 关于 OPTIMIZE

OPTIMIZE 的操作是将当前表复制到临时表操作后再删除当前表,最后将临时表改名

		
mysql> show processlist;
+--------+-----------------+---------------------+---------------------------+---------+-------+-----------------------------+--------------------------+
| Id     | User            | Host                | db                        | Command | Time  | State                       | Info                     |
+--------+-----------------+---------------------+---------------------------+---------+-------+-----------------------------+--------------------------+
|      1 | event_scheduler | localhost           | NULL                      | Daemon  |    14 | Waiting for next activation | NULL                     |
| 115835 | dba             | 192.168.6.20:49664  | example                   | Query   |     9 | copy to tmp table           | OPTIMIZE TABLE `mytable` |
| 115986 | dba             | localhost           | example                   | Query   |     0 | NULL                        | show processlist         |
+--------+-----------------+---------------------+---------------------------+---------+-------+-----------------------------+--------------------------+
17 rows in set (0.00 sec)
		
		

4.2.25.5. 关于切换引擎

转换ENGINE从MyISAM到InnoDB会经历creating table然后copy to tmp table在修改表名几个阶段,过程非常缓慢

		
mysql> show processlist;
+------+-----------------+---------------------+---------+---------+-------+-----------------------------+------------------------------------------+
| Id   | User            | Host                | db      | Command | Time  | State                       | Info                                     |
+------+-----------------+---------------------+---------+---------+-------+-----------------------------+------------------------------------------+
|    1 | event_scheduler | localhost           | NULL    | Daemon  |    10 | Waiting for next activation | NULL                                     |
| 3167 | dba             | 192.168.6.20:56723  | example | Query   |     2 | creating table              | ALTER TABLE `mytable`	ENGINE=InnoDB   |
| 3172 | dba             | localhost           | example | Query   |     0 | NULL                        | show processlist                         |
+------+-----------------+---------------------+---------+---------+-------+-----------------------------+------------------------------------------+
18 rows in set (0.00 sec)
		
		

copy to tmp table 过程

		
mysql> show processlist;
+------+-----------------+---------------------+---------+---------+-------+-----------------------------+------------------------------------------+
| Id   | User            | Host                | db      | Command | Time  | State                       | Info                                     |
+------+-----------------+---------------------+---------+---------+-------+-----------------------------+------------------------------------------+
|    1 | event_scheduler | localhost           | NULL    | Daemon  |    21 | Waiting for next activation | NULL                                     |
| 3167 | dba             | 192.168.6.20:56723  | example | Query   |    13 | copy to tmp table           | ALTER TABLE `mytable`	ENGINE=InnoDB   |
| 3172 | dba             | localhost           | example | Query   |     0 | NULL                        | show processlist                         |
+------+-----------------+---------------------+---------+---------+-------+-----------------------------+------------------------------------------+
18 rows in set (0.00 sec)
		
		

此时我们查看mysql data目录会看到临时表文件

		
# ll /var/lib/mysql/hx9999_real_history/
		
-rw-rw---- 1 mysql mysql      9522 May 16 17:17 #sql-c2f_c5f.frm
-rw-rw---- 1 mysql mysql        48 May 16 17:17 #sql-c2f_c5f.par
-rw-rw---- 1 mysql mysql 637534208 May 16 17:29 #sql-c2f_c5f#P#p0.ibd
-rw-rw---- 1 mysql mysql    180224 May 16 17:17 #sql-c2f_c5f#P#p1.ibd
-rw-rw---- 1 mysql mysql    180224 May 16 17:17 #sql-c2f_c5f#P#p2.ibd
-rw-rw---- 1 mysql mysql    180224 May 16 17:17 #sql-c2f_c5f#P#p3.ibd
-rw-rw---- 1 mysql mysql    180224 May 16 17:17 #sql-c2f_c5f#P#p4.ibd
-rw-rw---- 1 mysql mysql    180224 May 16 17:17 #sql-c2f_c5f#P#p5.ibd
-rw-rw---- 1 mysql mysql    180224 May 16 17:17 #sql-c2f_c5f#P#p6.ibd
-rw-rw---- 1 mysql mysql    180224 May 16 17:17 #sql-c2f_c5f#P#p7.ibd		
		
		

4.2.25.6. 确保SELECT不被受阻

使用各种手段保证select操作不被受阻,只要select一直可以查询网站前端就能提供80%的功能,一旦select受阻一切都是浮云。

保证 select 操作优先于其他操作

		
UPDATE [LOW_PRIORITY] [IGNORE] tbl_name  
SET col_name1=expr1 [, col_name2=expr2 ...]  
[WHERE where_definition]  
[ORDER BY ...]  
[LIMIT row_count]		
		
		

update的时候增加 LOW_PRIORITY 参数,可以降低更新语句的优先级。

my.cnf

		
[mysqld]		
low_priority_updates=1
		
		

或者启动是添加--low-priority-updates参数

全局开启

		
SET @@global.low_priority_updates = 1;		
		
		

适用于本次会话连接

		
SET @@session.low_priority_updates = 1;
		
		

使用 limit 限制更新记录的数量

		
update mytable set status='Y' where status='N' limit 1000;		
		
		

4.2.25.7. 记录操作者

		
update mytable set status='Y',update_date=now(),op_user='neo' where status='N';