Home | 简体中文 | 繁体中文 | 杂文 | Github | 知乎专栏 | 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) 使用外键才能实现约束