| 知乎专栏 |
用户帐号或通行证系统设计,下面以我的数库为例讲解。
我一般使用两个表 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 |
+----------+
该表的功能是,防止用户注册过程中流逝,记录已经填写的数据。
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等等
+-----------+ | 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
多对多分类,主要用于满足,一个产品/文章属于多个分类的需求。
+------------+
| 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 |
+------------+
上面我刚刚讲过怎样实现“不限子树的分类树”,我们可以实现不限层次的无线分类表。
+-----------+ | 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)
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.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"
看具体情况,拆分表,可按“日”,“月”,“年”等等
+-----------+
| 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 |
+----------+
分区表可以通过表空间,等等技术实现,优点是解决了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 |
+----------+
+----------+
| 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------+ +-----------+
+-----------+
+--------------+ +--------------+ | article | | article_rank | |--------------| |--------------| |id | <---1:1---o |article_id | |title | |click | |content | |read | |datetime | |score | |status | |... | |category_id | |... | |user_id | |... | +--------------+ +--------------+
+------------+ +--------------------------+ +-----------------------+
| 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 |
+-----------------------+
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 |
+-----------------------+
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| '' | +--------------------------------------------------+
+------------+ +----------------+ +------------------+ | 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表中,例如不同颜色售价不同。
+-----------+ +---------------+
| 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 |
+------------+ +--------------+
+------------+ +---------------+ +-----------+ | 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 | +----------------+ +------------+
主表
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;
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) */$$
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;
很多时候我们需要使用数据库存储配置项,由于各种原因我们无法使用配置文件来完成,例如在一个有很多节点集群环境中使用文件配置文件时非常不方便。
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';
我有一个表,里面只有固定行数的行记录,这些数据就是配置参数,我们将配置文件保存在数据库中,因为需要做负载均衡而不能使用文件配置文件。
有这样一个需求,这个记录每次修改都要保存历史记录,用于审计等等。我是这样设计该表的
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的数据。
用户注册,登陆等等需要验证码,下面的方案是,请求验证码生成一个随机验证码,存在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
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;
该函数能够检查身份证号码是否正确
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)
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,
该表的功能是显示在线用户,控制多点登陆,防止异常退出
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才能在其他电脑登陆。
实现 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)
这里讲解SNS交友社区的数据库设计与实现
我们要实现下面几个功能
CREATE DATABASE `sns` /*!40100 COLLATE 'utf8_general_ci' */
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;
存储具体的这人
这个表的功能主要是维持朋友之间的关系网,这里使用了多对多方式并且使用外键防止产生脏数据。
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;
首先初始化用户数据
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效率是很低的,所以我们准备了第三张表。
关系网表,主要功能是弥补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 的关系网