SQL 基本概念、数据类型、CRUD、索引、外键

SQL数据库(关系型数据库)

关系型数据库是由二维表及其之间的联系所组成的一个数据组织
表是相关数据条目的集合,它由许多列和行组成。其中列又称为字段,行又称为一条记录

图形化界面
  • SQL Server Management Studio
    用于访问、配置、管理和开发 SQL Server 的集成环境。
连接到服务器
  • Navicat
    通常用于MySql(以下基于5.7安装包)
使用SSH+常规

DBMS - 数据库管理系统(Database Management System)

数据库管理系统是一种可以访问数据库中数据的计算机程序。
DBMS 使我们有能力在数据库中提取、修改或者存贮信息。

什么是SQL (Structured Query Language)

SQL(结构化查询语言),是一门 ANSI 的标准计算机语言,用来访问和操作数据库系统。
SQL可作用于RDBMS(关系型数据库服务系统),如 My SQL, SQL Server等。不同的 SQL 数据库程序拥有SQL 标准之外的私有扩展

架构演进
SQL数据库架构演进
  1. 一个应用服务器配一个关系型数据库,每次读写数据库。
  2. 流量增大,服务器出现瓶颈,增加Nginx做负载均衡
  3. 流量继续增大,数据库性能出现瓶颈,做读写分离,主从库之间通过binlog同步数据
  4. 流量再次增大,做分库分表,对表做垂直拆分,对库做水平拆分。
    以分库为例:扩出两台数据库,以一定的单号(例如交易单号),以一定的规则(例如取模),交易单号对2取模为0的丢到数据库1去,交易单号对2取模为1的丢到数据库2去,写数据库的流量均分到两台数据库上。一般分库分表会使用Shard的方式,通过一个中间件,便于连接管理、数据监控且客户端无需感知数据库ip

常用数据类型

  • 数字 int float
  • 日期和时间 datetime date time
  • 字符串
    char 定长 最长8000个字符
    varchar 不定长 最长8000个字符
    text 不定长 最长2,147,483,647个字符
  • 二进制 binary varbinary image
  • json(mysql5.7.8 开始支持)

SQL语法

  1. SQL 对大小写不敏感
  2. SQL 使用单引号来环绕文本值(大部分数据库系统也接受双引号)。如果是数值,则不要使用引号。
  3. 表名、字段名等,为防止和预设字段重名,可使用反引号,如:`my_table`.`id`

可以把 SQL 分为两个部分:数据操作语言 (DML)(数据本身的操作,如数据的增删改查) 和 数据定义语言 (DDL)(数据结构的增删改查,如修改库表结构、定义索引等)

SELECT 查询

查看数据库版本

select version();

注意,MySQL5.7之后直接是8

查看数据

如 FROM 后只有单个表,可省略表名.

SELECT 表名.列名1 , 表名.列名2 FROM 表名
SELECT 表名.* FROM 表名
多表联查

FROM 后跟多个表,此时应配合where在两表间建立规则,否则会产生笛卡尔积(如A有m行,B有n行,输出m×n行)

SELECT * FROM 表1名,表2名 WHERE 表1.列名 = 表2.列名
嵌套查询(子查询)

每次查询的结果其实是一个临时表,可以基于其进行二次查询
如返回结果是一行一列的一个值,称为标量子查询,可充当一个变量,甚至当一个列名。如返回结果是n行一列的结构,称为值列表,可配合innot in等使用。
在SELECT、WHERE、FROM中均可以使用子查询

SELECT (SELECT name FROM com WHERE id = comid) cname
FROM emp 
WHERE name = '小张';

SELECT name
FROM com
WHERE id in (SELECT comid FROM emp WHERE name='小张');

SELECT com.name
FROM (SELECT comid cid FROM emp WHERE name = '小张') t01, com
WHERE t01.cid = com.id;
去重 DISTINCT

可以认为是GROUP BY的一种特殊情况
当运用在多列时,使用所有列的组合来确定结果集中行的唯一性。

SELECT DISTINCT 列名称 FROM 表名称
SELECT DISTINCT 列名称1,列名称2 FROM 表名称
条件 WHERE (配合运算符使用)
SELECT 列名称 FROM 表名称 WHERE 列 运算符 值
SELECT * FROM 表名称 WHERE 列 LIKE '%lond%' //查询所有带lond的列
SELECT * FROM 表名称 WHERE 列 NOT IN ('Adams','Carter')
SELECT * FROM 表名称 WHERE 列 NOT BETWEEN 1 AND 5
SELECT * from 表名称 WHERE 列 > all (单列子查询)
逻辑运算符(只能跟子查询) 功能
all 子查询中全部
any 子查询中任一
some 同 all
条件运算符 功能
= 等于
<> 或 != 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于
NOT 配合BETWEEN,IN,LIKE等使用。注意NOT IN后的内容里不可有null值,否则返回空
BETWEEN 在某个范围内
EXISTS 存在
UNIQUE 无重复项
IN 允许查询多个值,如("A","B","C")或一个表。注意,当IN后跟表时,表的列数应当与之前的变量数相同,如WHERE (A, B) in (SELECT id, name from table)
LIKE 配合通配符搜索某种模式(类似正则)
通配符 % 替代一个或多个字符
通配符 _ 仅替代一个字符
通配符 [ABC] 字符列中的任何单一字符
通配符 [^ABC] 不在字符列中的任何单一字符
  • null 的运算规则比较特别
    如果null参与算术运算,则该算术表达式的值为null。(例如:+,-,*,/ )
    如果null参与比较运算,则结果可视为false。(例如:>=,<=,<> )
    如果null参与聚集运算,通常为null的行不统计在内。
    如果在not in 右侧的子查询中有null,且存在左侧内容,则返回0而非左侧结果。
select 1 not in (1,2,3,null);
0
  • AND 和 OR 运算符
SELECT * FROM Persons WHERE FirstName='Thomas' AND LastName='Carter'
SELECT * FROM Persons WHERE firstname='Thomas' OR lastname='Carter'
SELECT * FROM Persons WHERE (FirstName='Thomas' OR FirstName='William')
AND LastName='Carter'
  • AS 别名
SELECT 列名称 AS 别名 FROM 表名称
SELECT 列名称 FROM 表名称 AS 别名 
SELECT po.OrderID, p.Name FROM Persons AS p, Product_Orders AS po WHERE p.Name=po.OwnerName
-- 会互相遍历拼接两张表的内容
  • limit 指定条数 (mysql语法)
SELECT 列名称 FROM 表名称 limit 数字
  • top 指定条数/百分比 (sqlserver语法)
SELECT TOP 数字 列名称 FROM 表名称
SELECT TOP 数字 PERCENT 列名称 FROM 表名称
SELECT INTO

用于复制并创建新表。(MySQL不支持该语法)

SELECT 列名 INTO 新表名 (IN 其他数据库名) FROM 旧表名

或批量赋值查询结果。

SELECT 列名1,列名2 into 变量1,变量2 FROM 表名
ORDER BY 排序

默认为升序排列(或显式使用ASC),需要降序时使用DESC

SELECT * FROM Persons ORDER BY Age DESC, Name ASC
  • 自定义排序
# 张三,小明排序后,其余姓名按照出生日期排序
SELECT * from name_info ORDER BY FIELD(name,"张三","小明"),date_birth

# 根据目标字段在传入的str中的顺序排序
SELECT * from table_name WHERE LOCATE(substr, str, startPos)
SELECT * from name_info ORDER BY LOCATE(name,"张三,小明,小刘")

# 使用 case + when
SELECT name  from name_info 
order by 
case name 
when "张三" then 1 
when "小明" then 2 
when "小刘" then 3 
else 0 
end
SELECT * FROM `table` ORDER BY RAND() LIMIT 10
RANK 排名

MySQL8.0新增的窗口函数
如下即可对同一name的数据按分数获取排名序号

select name, score, rank() OVER (PARTITION BY name ORDER BY score asc) FROM users

低版本MySQL可以通过变量自增自行获取排名序号(最后一个rank为派生表alias)

select *, @rank := @rank + 1 rank from users,(select @rank := 0) rank; 
GROUP BY 分组(HAVING)

对结果集进行分组,并可通过 HAVING 对分组结果进行筛选
书写顺序同执行顺序:WHERE(筛选待分组的表)=> GROUP BY => HAVING(筛选分组后的结果)
其中 HAVING 语法基本与WHERE一致,但可以支持SUM函数

不建议使用未在GROUP BY表达式中,且非聚合函数的值,因容易产生误会:

  • 作为子查询时MySQL的自动优化策略,可能会导致预料之外的行为。此时可在外面额外封装一层SELECT保证结果正确。此处为一个典型问题
  • 在非MySQL5.7中,将同一组的数据里第一条数据的对应列值作为返回数据。如select `id`, MAX(`value`), `type` from `test` GROUP BY `type`;,返回的是该type下第一条数据的id,而非MAX(value)对应数据的id。
  • 在MySQL5.7中启用了严格模式(可通过select @@sql_mode;查看),sql_mode中ONLY_FULL_GROUP_BY默认为打开状态,因此会报错:this is incompatible with sql_mode=only_full_group_by
    可以用any_value函数处理该字段,此时返回结果和其他版本相同:
select any_value(`id`), MAX(`value`), `type` from `test` GROUP BY `type`;
  • 查询每个国家×性别的OrderPrice之和,并筛去为0的项:
SELECT Country,Sex,SUM(OrderPrice) FROM Orders
WHERE Country IS NOT NULL AND Sex IS NOT NULL 
GROUP BY Customer,Sex
HAVING SUM(OrderPrice)>0
  • 分组,并取出每组某值最大的一行:
    以下两种排序方法总是错误的(5.7直接报错,其他版本会拿到错误的结果)
-- 并不能找到每组拥有最大value的行,而是每组的第一行,并附带该组最大行的value
SELECT test.* , MAX(value) from test GROUP BY type;

-- 在上例的基础上,对GROUP BY输出的内容进行ORDER BY排序
SELECT test.* , MAX(value) from test GROUP BY type ORDER BY type;

以下两种方法各版本都能用,但数据量过大时效率较差

-- 不去重
SELECT *  FROM `test` 
WHERE
    ( `type`, `value` ) IN (
    SELECT
        `type`,
        `min_value` 
    FROM
        ( SELECT `type`, min( `value` ) AS `min_value` FROM `test` WHERE `value` IS NOT NULL GROUP BY `type` ) AS `t` 
    );
    
-- 去重
SELECT * FROM `test`
    JOIN ( SELECT * FROM ( SELECT * FROM test WHERE `value` IS NOT NULL ORDER BY `value` ASC LIMIT 99999 ) AS temp GROUP BY `type` ) AS `t` 
WHERE
    `test`.`id` = `t`.`id`;

在MySQL5.6之前,可显式指定排序方式,如未指定则采用原表当前排序

-- 显式指定排序方式(提供GROUP BY列的ASC或DESC指示符)
select * 
from  user_position 
group by userId desc

-- 采用临时表当前排序
SELECT * from (
    SELECT * from test ORDER BY value desc
) as temp
GROUP BY type;

在MySQL5.6以上(注意5.7还需要配合any_value),废除了显式排序,且隐式排序仅在有 limit 时才可用,否则总是以id正序排列后进行分组

-- 通过 limit 支持排序
SELECT * from (
    SELECT * from test ORDER BY value desc limit 99999
) as temp
GROUP BY type;

MySQL8还提供了新的窗口函数(分析函数) ROW_NUMBER() OVER()用于此功能,将表中的记录按字段 COLUMN1进行分组,按字段 COLUMN2 进行排序

ROW_NUMBER() OVER(PARTITION BY COLUMN1 ORDER BY COLUMN2)

SELECT
    *,
    row_number() over ( PARTITION BY `type` ORDER BY `value` DESC ) 
FROM
    `test`;
JOIN 连接(on)

根据多个表中的列之间的关系,返回一个新的临时表。满足关系的不同表字段,会出现在新表的同一行。

SELECT po.OrderID, p.Name FROM Persons AS p JOIN Product_Orders AS po ON p.Name = po.OwnerName
  • JOIN 或 INNER JOIN
    类似多表联查。
    当不配合on使用时,返回笛卡尔积。
    配合on使用时,只有匹配成功的行会展示。
  • LEFT JOIN
    必须有on
    左表的行即使未能根据on匹配成功,也会全部显示
  • RIGHT JOIN
    必须有on
    右表的行即使未能根据on匹配成功,也会全部显示
  • FULL JOIN (MySQL不支持)
    两表内容不管能否根据on匹配成功,都会全部展示。

on 和 where
先执行on生成新的临时表,再执行where对该临时表进行筛选
在 INNER JOIN 中,因每个表未匹配成功的行都不会展示,可以认为 on 和 where 的筛选效果相同,但 on 效率更高

UNION 和 UNION ALL

将多个表的结果合并到同个列中显示(各语句的列数必须相同)
UNION会自动去重,UNION ALL则显示所有

SELECT 列1(s) FROM 表1
UNION
SELECT 列2(s) FROM 表2

INSERT 新增
  • 向表中(指定列)插入一条新数据
    未指定列时即为所有列依次填入数据(除id外)
INSERT INTO 表 VALUES (值1, 值2,....)
INSERT INTO 表 (列1, 列2,...) VALUES (值1, 值2,....)
Update 修改
  • 更新数据
UPDATE 表 SET 列 = 新值 WHERE 列 = 某值
UPDATE Person SET Address = 'Zhongshan', City = 'Nanjing' WHERE LastName = 'Wilson'
Delete 删除
  • 删除语句
    不写where则删除表中所有行
DELETE FROM 表名称 WHERE 列名称 = 值
DELETE FROM table_name

json 类型数据

mysql 5.7.8 新增了 json 数据类型,该类型的数据具有一些特殊的语法
attributes字段:'{"ports": {"usb": 3, "hdmi": 1}'

  • 查询
SELECT
    *
FROM
    `e_store`.`products`
WHERE
    `category_id` = 1
AND JSON_EXTRACT(`attributes` , '$.ports.usb') > 0
AND `attributes` -> '$.ports.hdmi' > 0;
  • 更新
-- 新增字段
UPDATE `e_store`.`products`
SET `attributes` = JSON_INSERT(
    `attributes` ,
    '$.chipset' ,
    'Qualcomm'
)
WHERE
    `category_id` = 2;


-- 修改字段
UPDATE `e_store`.`products`
SET `attributes` = JSON_REPLACE(
    `attributes` ,
    '$.chipset' ,
    'Qualcomm Snapdragon'
)
WHERE
    `category_id` = 2;

-- 新增或修改字段
UPDATE `e_store`.`products`
SET `attributes` = JSON_SET(
    `attributes` ,
    '$.body_color' ,
    'red'
)
WHERE
    `category_id` = 1;
  • 删除
UPDATE `e_store`.`products`
SET `attributes` = JSON_REMOVE(`attributes` , '$.mount_type')
WHERE
    `category_id` = 3;

SHOW

查看库表、变量、配置等信息

show databases;
show tables;
show table status; # 表基础属性
show tables from database_name;
show database_name.tables;

show variables like '%log_bin%'; # binlog配置相关信息
show variables like 'general_log_file';
show variables like 'log_error';
show master logs; # binlog文件列表
show binary logs;
show binlog events; # binlog内容

CREATE DATABASE 创建库表

CREATE DATABASE 新数据库名

CREATE TABLE courses (
    course_id INT AUTO_INCREMENT PRIMARY KEY,
    course_name VARCHAR(50) NOT NULL,
    taken_date DATE,
    PRIMARY KEY (employee_id , course_id)//联合主键
);

ALTER TABLE

在已有的表中添加、修改或删除列。

ALTER TABLE 表名 ADD COLUMN 列名 数据类型
ALTER TABLE 表名 DROP COLUMN 列名
ALTER TABLE 表名 MODIFY COLUMN 列名 数据类型
常用数据类型
  • char(n) 固定长度字符串
  • varchar(size) 可变长度字符串
  • date 日期 精确到天
  • datetime2 日期 精确到100纳秒
  • bit 相当于布尔值(0 1 null)
  • int 整数
SQL约束

用于限制加入表的数据的类型。可以在CREATE TABLEALERT TABLE时进行设置,拼接在数据类型之后。

  • NOT NULL 禁止为null
  • UNIQUE 每条记录必须唯一
  • CHECK 用于限制列中的值的范围。
  • DEFAULT 用于向列中插入默认值
  • PRIMARY KEY 主键,每个表有且仅有一个主键,自动递增且不能为NULL
  • FOREIGN KEY 外键,指向另一个表中的主键
CREATE TABLE Orders
(
  Id_O int NOT NULL PRIMARY KEY,
  OrderNo int NOT NULL CHECK (OrderNo>0),
  City varchar(255) DEFAULT 'Sandnes',
  Id_P int FOREIGN KEY REFERENCES Persons(Id_P)
)
  • CREATE INDEX 或 CREATE UNIQUE INDEX
    用于在表内创建(唯一的)索引
    用户无法看到索引,它们只能被用来加速搜索/查询。
    更新一个包含索引的表需要比更新一个没有索引的表更多的时间,这是由于索引本身也需要更新。
CREATE INDEX 索引名 ON 表名 (列名(s)) 

DROP

删除索引/表/库

DROP TABLE 表名称
DROP DATABASE 数据库名称
TRUNCATE TABLE 表名称//只删内容不删表本身

IDENTITY / AUTO_INCREMENT

每条新增记录会自动获得递增的该值。通常为主键设置该值。
默认地,IDENTITY 的开始值是 1,每条新记录递增 1。
要规定 "P_Id" 列以 20 起始且递增 10,可以把 IDENTITY 改为 IDENTITY(20,10)

CREATE TABLE Persons
(
P_Id int PRIMARY KEY IDENTITY,
LastName varchar(255) NOT NULL
)

索引

索引用于提高where条件的查询效率,其本质上是一个包含了主键与索引字段的表。

  • 默认以主键作为索引字段创建非空唯一索引:主键索引,表里的数据也直接放在该表。其他索引称为二级索引
  • 只包含一个字段的索引称为单列索引,包含多个字段的索引称为联合索引
  • 唯一索引:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
原理

索引表的B+树会按照索引字段从小到大排列并分页,最终形成索引页(包含每个数据页的最小值记录)和多个数据页(包含主键和索引字段)。
通过where指定索引字段条件时,通过索引页找到对应数据页,再在数据页内部找到对应主键,最后通过回表找到该主键下其他字段的信息。

应优先选择区分度高的列作为索引,区分度为:count(distinct col)/count(*),即字段不重复的比例。
索引字段参与计算时无法利用索引,如from_unixtime(create_time) = ’2014-05-29’应改为create_time = unix_timestamp(’2014-05-29’)。

对于联合索引,排列时按照最左前缀匹配原则总是优先比较左侧字段,如相同,再比较下一个字段。因此制定联合索引时,因注意顺序。例如创建(a,b)联合索引后,如查询条件为b=4,则无法利用索引。但如条件为a=4,b=4则可以利用。

联合索引示例

弊端
  1. 索引表会占用存储空间
  2. 进行增删改时,也同时影响索引表(修改值、B+树重新分页等),降低增删改的效率
添加索引
普通索引 ALTER TABLE tableName ADD INDEX (columnName)
主键索引 ALTER TABLE `table_name` ADD PRIMARY (columnName)
唯一索引 ALTER TABLE `table_name` ADD UNIQUE (columnName)
组合索引 ALTER TABLE `table_name` ADD UNIQUE (columnName1,columnName2,columnName3)
全文索引 ALTER TABLE `table_name` ADD FULLTEXT (columnName)

主键与外键

  • 主键(primary key)
    能够唯一标识表中某一行的属性,是一种特殊的索引。一个表只能有一个主键,其值唯一且不能为 null。通常默认的主键为一个递增int值:id
  • 外键(foreign key)
    外键通常为另一个表中的主键,用于维护两表间的数据一致性。(也可以为当前表的主键,则称为自参照表
    外键可以为空值,即只要外键的每个非空值出现在指定的主键中即可
设置外键

在 CREATE TABLE 或 ALTER TABLE 时,可设置外键。
其中 ON UPDATEON DELETE 可设置当父表中,子表外键关联内容被修改或删除时,子表的对应操作:

  • RESTRICT:默认值。禁止该删除或更新行为。
  • NO ACTION:MySQL中同 RESTRICT。
  • CASCADE:父表update主键时,子表相应修改外键列的值;父表删除该行时,子表删除相应行。
  • SET NULL:父表delete、update的时候,子表会将关联记录的外键字段所在列设为null(注意子表外键字段不要设置 not null)
  • SET DEFAULT:(Innodb不支持)。父表有变更时,子表将外键列设置成一个默认的值
CREATE TABLE products(
   id int not null auto_increment primary key,
   prd_name varchar(355) not null,
   prd_price decimal,
   blog_id int not null,
   FOREIGN KEY (blog_id)
   REFERENCES blog(id)
   ON UPDATE CASCADE
   ON DELETE RESTRICT
)ENGINE=InnoDB;

ALTER TABLE products
ADD COLUMN blog_id_2 int not null;

ALTER TABLE products
ADD FOREIGN KEY (blog_id_2)
REFERENCES blog(id)
ON DELETE NO ACTION
ON UPDATE CASCADE;

其他

  • VIEW 视图是可视化的表
  • Date 具有一些内置的函数,不过一般用类似 >'2019-09-06 00:00:00' 的语法即可
  • NULL
    未赋值的可选列中的记录会以NULL值保存。
    无法使用比较运算符来测试 NULL 值,必须使用 IS NULLIS NOT NULL 操作符
  • ISNULL函数
    为了在计算中防止null值影响计算,通过该函数可以在值为null时用指定值进行替换计算
SELECT UnitPrice*ISNULL(UnitsNumber,0) FROM Products

实例展示

会员筛选

声明变量yesterday 、tomorrow、devicename
查询变量subKey
同时执行多个查询语句并插入一个表中
SELECT col1=(SELECT 语句),col2=(SELECT 语句)

declare @yesterday varchar(40),@tomorrow varchar(40),@devicename varchar(40)
set @yesterday = '2021-3-2'
set @tomorrow = '2021-3-4'
set @devicename = 'HG01'


declare @subKey varchar(40);
SELECT @subKey = [SubKey] from [TronCell.SensingStore.S.Orders].[dbo].[Devices] where TenantId = 5157 AND Name = @devicename


SELECT "SUBKEY"=(SELECT @subKey),
"今日新会员"=(SELECT COUNT(*) FROM [dbo].[TicketUseds] WHERE CreationTime > @yesterday AND CreationTime < @tomorrow AND subKey = @subKey AND MemberType like N'%新会员%'),
"今日老会员"=(SELECT COUNT(*) FROM [dbo].[TicketUseds] WHERE CreationTime > @yesterday AND CreationTime < @tomorrow AND subKey = @subKey AND MemberType = N'老会员'),
"今日会员"=(SELECT COUNT(*) FROM [dbo].[TicketUseds] WHERE CreationTime > @yesterday AND CreationTime < @tomorrow AND subKey = @subKey),
"累计新会员"=(SELECT COUNT(*) FROM [dbo].[TicketUseds] WHERE CreationTime < @tomorrow  AND subKey = @subKey AND MemberType = N'新会员'),
"累计老会员"=(SELECT COUNT(*) FROM [dbo].[TicketUseds] WHERE CreationTime < @tomorrow AND  subKey = @subKey AND MemberType = N'老会员'),
"累计会员"=(SELECT COUNT(*) FROM [dbo].[TicketUseds] WHERE CreationTime < @tomorrow AND subKey = @subKey )

组合使用join和group by

用户表user有display_name和mobile,接口调用记录表visit_log有mobile、created_time,现在想列出每个人每天接口调用次数

select user.display_name,date_format(visit_log.created_time,'%Y-%m-%d'),count(*) from user LEFT JOIN visit_log on visit_log.mobile = user.mobile group by user.display_name,visit_log.mobile,date_format(visit_log.created_time,'%Y-%m-%d')

组合使用update和group by

更新每个type中value最小的行,令其value + 1

-- 不去重
UPDATE `test` 
SET `value` = `value` + 1 
WHERE
    ( `type`, `value` ) IN (
    SELECT
        `type`,
        `min_value` 
    FROM
        ( SELECT `type`, min( `value` ) AS `min_value` FROM `test` WHERE `value` IS NOT NULL GROUP BY `type` ) AS `t` 
    );

-- 去重   
UPDATE `test`
JOIN ( SELECT * FROM ( SELECT * FROM test WHERE `value` IS NOT NULL ORDER BY `value` ASC LIMIT 99999 ) AS temp GROUP BY `type` ) AS `t` 
SET `test`.`value` = `test`.`value` + 1 
WHERE
    `test`.`id` = `t`.`id`;

你可能感兴趣的:(SQL 基本概念、数据类型、CRUD、索引、外键)