阿里巴巴 的代码规范( 阿里规约 )主要包含 Java 、 C++ 、 Python 等编程语言的编码规范。最新版本可通过其 GitHub 仓库获取。

参考文章:

【阿里规约】阿里开发手册解读——命名规范篇

【阿里规约】阿里开发手册解读——代码格式篇

【阿里规约】阿里开发手册解读——数据库和ORM篇

命名规范

包名

  • 统一小写:例如商品库存包com.example.product.stock,而不是com.example.productStock

  • 分隔符间单词必须单语义:例如商品库存包com.example.product.stock,而不是com.example.product_stock、com.example.product-stock

参考

【强制】 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用 单数 形式,但是类名如果有复数含义,类名可以使用复数形式。
正例: 应用工具类包名为 com.alibaba.ai.util、类名为 MessageUtils(此规则参考 spring 的框架结构)

类名

普通类和方法

  • 驼峰命名:例如UserService,而不是User_service。
  • 禁止拼音英文混用;
  • 禁止下划线、美元符号起始。
  • 可读性比长度重要:命名尽可能短,但望文生义更重要
    • 可读性高(推荐):下面这些类既保证了望文生义,又保证了缩写
      • 商品描述类:DescriptionOfProduct 缩写为 ProductDesc
      • 应用程序配置类:ApplicationConfiguration 缩写为 AppConfig
      • 客户信息类:CustomerInformation 缩写为 CustInfo。
    • 可读性低(不推荐)
      • AbstractClass缩写命名成 AbsClass;
      • PurchaseProduct类缩写成PurProduct;

参考:

  • 【强制】 代码中的命名均不能以 下划线或美元符号 开始,也不能以 下划线或美元符号 结束。

    反例: name / name / $name / name / name$ / name

  • 【强制】 代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。

    说明: 正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,纯拼音命名方式更要避免采用。

    正例: renminbi / alibaba / taobao / youku / hangzhou 等国际通用的名称,可视同英文。

    反例: DaZhePromotion [打折] / getPingfenByName() [评分] / int 某变量 = 3

  • 【强制】 方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式。

    正例: localValue / getHttpMessage() / inputUserId

  • 【强制】 杜绝完全不规范的缩写,避免望文不知义。

    反例: AbstractClass缩写命名成 AbsClass;condition缩写命名成 condi,此类随意缩写严重降低了代码的可阅读性。

  • 【推荐】 为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。

    正例: 在 JDK 中,表达原子更新的类名为:AtomicReferenceFieldUpdater。

    反例: int a 的随意命名方式。

  • 【推荐】 在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。

    正例: startTime / workQueue / nameList / TERMINATED_THREAD_COUNT

    反例: startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD

测试类

命名:待测类名+Test 。

举例:用户测试类UserServiceTest,而不是TestUserService。

参考:

【强制】 抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;

​ 测试类命名以它要测试的类的名称开始,以 Test 结尾

抽象类

命名: Abstract+自定义类名。

举例:抽象支付方式类AbstractPaymentMethod/BasePaymentMethod,而不是AbstractPayment。

异常类

命名:自定义异常名+Exception。

例如商品找不到类ProductNotFoundException,而不是ProductNotFound。

枚举类

  • 类名:自定义类名+Enum。 例如ProcessStatusEnum
  • 成员名:跟常量一样
  • 纯大写+下划线:例如最大库存量MAX_STOCK_COUNT。
  • 可读性比长度重要:常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。例如最大库存量MAX_STOCK_COUNT而不是MAX_COUNT。

枚举类和常量类区别:

相同点:在Java中,枚举和常量都用于表示一组固定的值,但它们适用的场景有所不同。

不同点:

  • 枚举(Enum)适用于表示一组有限的可能取值,这些取值在程序中某个上下文中具有特殊意义。例如,表示一周的星期几或某个颜色的枚举类型。枚举类型可通过限定的值范围提供更好的类型安全性,并且可使用switch语句进行更清晰的代码编写。
  • 常量(Constant)适用于表示在程序中经常使用的不可变的值。常量一般定义为 final 类型,并且通常用于表示某个固定的数值或者字符串常量。常量的值在程序中是不可修改的,可以通过常量名直接使用,提高代码的可读性和可维护性,但是可能会降低类型安全性和表达能力。

总结来说,枚举适用于表示一组有限的、具有特殊意义的取值,常量适用于表示程序中经常使用的不可变的值。

【参考】 枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
说明: 枚举其实就是特殊的类,域成员均为常量,且构造方法被默认强制是私有。
正例: 枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON

接口和实现类

  • service和dao实现类以Impl结尾。注意不要用Imp结尾。例如UserServiceImpl而不是UserServiceImp
  • 能力型接口以-able结尾。如异常根类Throwable是可抛出的,实现类Exception和Error都可抛出。如Drawable接口和Circle实现类。

参考:接口和实现类的命名有两套规则

  1. 【强制】 对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用Impl 的后缀与接口区别。

    正例: CacheServiceImpl 实现 CacheService 接口。

  2. 【推荐】 如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形容词)。

    正例: AbstractTranslator 实现 Translatable 接口。

PO、DTO、VO

  • DO / BO / DTO / VO / AO / PO / UID 要大写,而非驼峰:例如UserDO,而不是UserDo。例如serialVersionUID,而不是serialVersionUid。
  • 禁止POJO后缀:POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。

扩展:PO、DTO、VO

  1. 数据对象:xxxDO,xxx 即为数据表名。
  2. DTO数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
  3. VO展示对象:xxxVO,xxx 一般为网页名称。
  4. POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。

参考
【强制】 类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。
正例: JavaServerlessPlatform / UserDO / XmlService / TcpUdpDeal / TaPromotion
反例: javaserverlessplatform / UserDo / XMLService / TCPUDPDeal / TAPromotion
【参考】 各层命名规约:
A) Service/DAO 层方法命名规约

  1. 获取单个对象的方法用 get 做前缀。
  2. 获取多个对象的方法用 list 做前缀,复数形式结尾如:listObjects。
  3. 获取统计值的方法用 count 做前缀。
  4. 插入的方法用 save/insert 做前缀。
  5. 删除的方法用 remove/delete 做前缀。
  6. 修改的方法用 update 做前缀。

用到设计模式的类

案例参考文章:设计模式

概述

  • 抽象类名以Abstarct/Base为前缀
  • 用到设计模式时要在命名中进行体现。

参考:
【推荐】 如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。
说明: 将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
正例: public class OrderFactory; public class LoginProxy; public class ResourceObserver;

案例一:工厂设计模式采购披萨

采购披萨工厂类:利用工厂方法模式创建披萨时,可以把抽象工厂类命名为AbstractOrderPizzaFactory类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 抽象工厂类,订购披萨类:
public abstract class AbstractOrderPizzaFactory{
// 构造方法不断根据用户输入披萨类型,调用创建披萨对象的抽象方法,实现加工披萨。
public void OrderPizza() {
Pizza pizza = null;
do {
pizza = createPizza(getType());
if (pizza == null) {
System.out.println("Failed to Order Pizza");
} else {
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}
} while (true);
}
public abstract Pizza createPizza(String orderType);
// ...
}
//具体工厂类,订购伦敦披萨类:
public class LDOrderPizza extends AbstractOrderPizzaFactory{
@Override
public Pizza createPizza(String orderType) {
Pizza pizza = null;
switch (orderType) {
case "cheese": pizza = new LDCheesePizza(); break;
case "pepper": pizza = new LDPepperPizza(); break;
default: break;
}
return pizza;
}
}

案例二: 观察者设计模式订阅天气

天气局接口和观察者类:WeatherSubject和天气观察者接口WeatherObserver

变量

普通变量

  • 驼峰命名:例如商品价格命名pruductPrice,而不是pruduct_price。
  • 禁止拼音英文混用;
  • 禁止下划线、美元符号起始。
  • 可读性比长度重要:命名尽可能短,但“望文生义”更重要
    • 可读性高(推荐):下面这些类
      • 最大值:maximumValue 缩写为 maxValue
      • 项目数量:numberOfItems 缩写为 numItems
      • 初始化计数器:initializeCounter 缩写为 initCounter
    • 可读性低(不推荐)
      • 数量:count缩写为cnt
      • 值:value缩写成val
      • 序号:index缩写成i
  • 类型名词放词尾:词尾是类型,让人能一眼看出这个变量是干什么的。例如:
    • 开始时间:startTime而不是startedAt。
    • 工作队列:workQueue而不是QueueOfWork。
    • 名字列表:nameList而不是listName。因为它是个列表。
    • 最大线程数:MAX_THREAD_COUNT而不是COUNT_MAX_THREAD 。它是数量而不是线程。

数组

中括号要直接跟在类型后面。

1
2
3
4
// 错误示例:中括号放在变量名后面,注释放在右边
String args[];
// 正确示例:中括号直接跟在类型后面,注释放在代码上面
String[] args;

参考:

【强制】 类型与中括号紧挨相连来表示数组。

正例: 定义整形数组 int[] arrayDemo;

反例: 在 main 参数中,使用 String args[]来定义

布尔型变量

实体类(PO类)里布尔型变量不能加is前缀,否则部分框架解析会引起序列化错误。同时MySQL表达“是否”的字段可以用is_xxx,只需<resultMap>设置从 is_xxx 到 xxx 的映射关系即可。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class User {
private Long id;
private String username;
// 逻辑删除字段
private boolean deleted;

// 其他属性、构造函数和方法

public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
}

为什么强制 boolean 类型变量不能使用 is 开头?

为了防止序列号失败。

  • lombok序列号失败:javaBeans规范boolean变量的getter方法是isXXX(),其他变量的getter方法是getXXX()。lombok遵循javaBeans规范,如果一个变量是boolean isSuccess;在注解@Data或@Getter生成getter方法的时候,它会生成isSuccess()方法,而不是isIsSucess()方法。这也是lombok的一个大坑。
  • rpc框架序列号失败:在一些rpc框架里面,当反向解析读取到isSuccess()方法的时候,rpc框架会“以为”其对应的属性值是success,而实际上其对应的属性值是isSuccess,导致属性值获取不到,从而抛出异常。

参考:
【强制】 POJO 类中布尔类型变量都不要加 is 前缀,否则部分框架解析会引起序列化错误。
说明: 在 MySQL 规约中的建表约定第一条,表达是与否的值采用 is_xxx 的命名方式,所以,需要在<resultMap> 设置从 is_xxx 到 xxx 的映射关系。
反例: 定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),RPC 框架在反向解析的时候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。

父子类变量

父子类变量命名:避免同名变量。例如父类有个name变量,子类想声明一个昵称变量,应另外声明nickname,而非再次声明name。

参考:
【强制】 避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可读性降低。
说明: 子类、父类成员变量名相同,即使是 public 类型的变量也是能够通过编译,而局部变量在同一方法内的不同代码块中同名也是合法的,但是要避免使用。对于非 setter/getter 的参数名称也要避免与成员变量名称相同。
反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ConfusingName {
private int age;
// 非 setter/getter 的参数名称,不允许与本类成员变量同名
public void getData(String alibaba) {
if (condition) {
final int money = 531;
// ...
}
for (int i = 0; i < 10; i++) {
// 在同一方法体中,不允许与其它代码块中的 money 命名相同
final int money = 615;
// ...
}
}
}
class Son extends ConfusingName {
// 不允许与父类的成员变量名称相同
private int sonAge;
}

局部变量

避免同一个方法的多个代码块中声明同名局部变量。例如下面方法的money变量不符合规范:

1
2
3
4
5
6
7
8
9
10
11
public void getData(String alibaba) {
if (condition) {
final int money = 531;
// ...
}
for (int i = 0; i < 10; i++) {
// 在同一方法体中,不允许与其它代码块中的 money 命名相同
final int money = 615;
// ...
}
}

常量

普通常量

  • 纯大写+下划线:例如最大库存量MAX_STOCK_COUNT。

  • 可读性比长度重要:常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。例如最大库存量MAX_STOCK_COUNT而不是MAX_COUNT。

  • 常量类细化:不要一个总的常量类维护所有常量,要按功能创建粒度小的常量类。例如电商平台商品模块不应该一个ProductStatusConstants维护所有商品常量,而应该细化成商品类型常量类ProductTypeConstants、商品价格范围常量类PriceRangeConstants、商品属性相关常量类ProductAttributeConstants等。

  • 枚举类:变量值只在固定几个值内变化时用枚举类。例如季节Season变量只会有春夏秋冬四种类型,可以定义成季节枚举类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public enum SeasonEnum {
    SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);

    private int seq;

    SeasonEnum(int seq) {
    this.seq = seq;
    }
    public int getSeq() {
    return seq;
    }
    }

参考
【强制】 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
正例: MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例: MAX_COUNT / EXPIRED_TIME
【推荐】 不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。
说明: 大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解和维护。
正例: 缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 ConfigConsts 下。
【推荐】 如果变量值仅在一个固定范围内变化用 enum 类型来定义。
说明: 如果存在名称之外的延伸属性应使用 enum 类型,下面正例中的数字就是延伸信息,表示一年中的第几个季节。

Long常量

大写L:以便于区分“L”和数字“1”。例如Long a=1L,而不是Long a=1l。

1
2
3
4
5
6
7
8
// 错误示例:
// a实际上表示 Long 型的 2,但容易混淆为数字 21。
Long a = 2l;
//正确示例:
// 使用大写 "L" 来明确表示 Long 型的 2。
Long a = 2L;
// 使用大写 "L" 来明确表示 long 型的 12345。
long b = 12345L;

参考:
【强制】 在 long 或者 Long 赋值时,数值后使用大写的 L,不能是小写的 l,小写容易跟数字 1 混淆,造成误解。
说明: Long a = 2l; 写的是数字的 21,还是 Long 型的 2。


代码格式

编码和换行符

规范:代码的编码统一使用UTF-8,换行符使用 Unix 格式,不要使用 Windows 格式。

换行符:

Unix格式:在Unix、Linux以及类Unix操作系统中,使用换行符\n来表示新行,这是常用的行结束符。

Windows格式:在Windows操作系统中,通常使用回车符和换行符\r\n(CR-LF,Carriage Return-Line Feed)来表示新行。

IDEA编码设置UTF-8:File → Setting → Editor → File Coding → Global Encoding;Project Encoding;Default encoding for properties files → 都选择 UTF-8

空格规范

保留字

规范:if/for/while/switch/do 等保留字与括号之间都必须加空格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (condition) {
// 代码逻辑
}
for (int i = 0; i < 10; i++) {
// 循环体
}
while (condition) {
// 循环体
}
switch (value) {
case 1:
// 情况1的代码
break;
default:
// 默认情况的代码
}

Java常见保留字

  1. 条件语句关键字:如if、else、switch、case、default。
  2. 循环控制关键字:如for、while、do、break、continue。
  3. 访问修饰符关键字:如public、private、protected。
  4. 数据类型关键字:如int、double、char、boolean。
  5. 类和对象关键字:如class、new、extends、implements。
  6. 异常处理关键字:如try、catch、throw、throws、finally。

二目、三目运算符

规范:任何二目、三目运算符的左右两边需要加一个空格。

1
2
int result = a + b;
boolean condition = (x > y) ? true : false;

java中常见的二目、三目运算符:

  • 二目运算符:+、-、*、/、%、=、==、>=、&&
  • 三目运算符:?:

缩进

规范:采用 4 个空格缩进。可以使用tab快捷键直接四个空格,禁止直接使用“tab”字符 。

IDEA 设置 tab 为 4 个空格时,请勿勾选 Use tab character;而在 eclipse 中,必须勾选 insert spaces for tabs。

IDEA设置 tab:File → Setting → Editor → Code Style → Tabs and Indents → 取消勾选Use tab character → Indent 设置4

注释

规范:注释双斜线后紧跟一个空格。

1
2
3
4
// 正例: 这是示例注释,请注意在双斜线之后有一个空格
String param = new String();
//反例: 这是示例注释,请注意在双斜线之后没空格
String param = new String();

强制转换

规范:强制转换时,右括号后无空格。

1
2
3
4
5
6
// 正例:
long first = 1000000000000L;
int second = (int)first + 2;
// 反例:
long first = 1000000000000L;
int second = (int) first + 2;

方法参数

规范:方法参数逗号后加一个空格。

1
2
3
4
5
6
// 正例:下例中实参的 args1,后边必须要有一个空格。
method(args1, args2, args3);

// 反例:下例中实参的 args1,后边没空格。
method(args1,args2,args3);
method(args1 ,args2 ,args3);

行数、字符数

单行字符数

规范:单行字符数不超过120个,超过时需要换行。

换行规则:

  1. 第2/3/4/..行相对第一行缩进 4 个空格。

  2. 运算符随下文一起换行。例如a + b,应该换行成a\n + b,而不是a + \nb

  3. 点符号与下文一起换行。例如dog.eat(),应该换行成dog\n.eat(),而不是dog.\neat()

  4. 方法的多个参数换行时,逗号不与下文一起换行。例如sum (1,2,3),应该换行成sum (1,2,\n3),而不是sum (1,2\n,3)

  5. 在括号前不换行。例如fun (a),应该换行成\nfun (a),而不是fun \n(a)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 正例:
StringBuilder sb = new StringBuilder();
// 超过 120 个字符的情况下,换行缩进 4 个空格,点号和方法名称一起换行
sb.append("Jack").append("Ma")...
.append("alibaba")...
.append("alibaba")...
.append("alibaba");

// 反例:
StringBuilder sb = new StringBuilder();
// 超过 120 个字符的情况下,不要在括号前换行
sb.append("Jack").append("Ma")...append
("alibaba");
// 参数很多的方法调用可能超过 120 个字符,不要在逗号前换行
method(args1, args2, args3, ...
, argsX);

方法行数

规范:单个方法的总行数不超过 80 行。

除注释之外的方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过80 行。

括号

大括号换行规则

规范:大括号内为空时无需换行。

1
2
3
4
5
6
7
8
9
10
11
12
// 正例
int number = 10;
// 大括号内为空,则不换行
if (number > 0) {}

// 反例
int number = 10;
if (number > 0)
{

}else{
}

规范:大括号内非空时:

  • 左大括号:前不换行,后换行
  • 右大括号:前换行,右换行(有else时不换行)
1
2
3
4
5
6
7
8
// 正例
int number = 10;
if (number > 0) {
System.out.println("这个数字是正数");
} else if(number < -3){
System.out.println("这个数字不是正数");
} else{}
System.out.println("...");

小括号规则

规范:小括号内侧不隔空格,外侧隔单空格。

1
2
3
4
5
6
7
8
// 正例:小括号内侧不隔空格,外侧隔单空格。
if (a == b)

// 反例:
// 小括号外侧没隔单空格。
if(a == b)
// 小括号内侧隔了空格
if ( a == b )

MySQL 规约

建表规约

命名:库名与应用名称尽量一致;

  • 大小写:MySQL表名不能有大写字母。因为MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。
  • 复数:不可使用复数。
  • 不可使用保留字:例如不能命名为add,from,set等。
  • 业务名称_表的作用:建议命名“业务名称_表的作用”,例如:
    • 用户信息表:user_info
    • 产品信息表:product_info
    • 客户订单关联表:customer_order_relation

字段

基础命名规范

  • 命名要慎重:字段名的修改代价很大,所以必须要慎重;
  • 大小写:MySQL字段名不能有大写字母。因为MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写,大小写混用会出问题。
  • 保留字:不可使用保留字。例如不能命名为add,from,set等。

基本规范

  • 注释:字段含义改变时,及时更新注释;
  • 合理冗余:多读少写、长度短、非唯一索引的字段可以冗余,以降低连表查询的次数。
  • 关联字段类型:要关联查询的两个字段,数据类型必须一致。如果不一致会导致索引失效,索引和索引失效场景具体可以参考顶部导航文章中的“MySQL高级篇”;
  • 分库分表依据:单表数据量五百万条数据,或者容量2GB
  • 三大必备字段:主键、创建时间、修改时间。即id, create_time(或者命名为gmt_create), update_time(或者命名为gmt_modified)

布尔型字段

  • 结构:is_xxx

  • 数据类型:unsigned tinyint

  • 值:1 表示是,0 表示否

  • 对应实体类变量:虽然数据库必须命名成is_xxx,但是该表对应的实体类成员变量不能命名为isXxx,否则会导致序列号失败。故需要在 resultMap 中进行字段与属性之间的映射。

《阿里规约》原文
【强制】 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint (1 表示是,0 表示否)。
说明: 任何字段如果为非负数,必须是 unsigned 。
注意: POJO 类中的任何布尔类型的变量,都不要加 is 前缀,所以,需要在< resultMap >设置从 is_xxx到 Xxx 的映射关系。数据库表示是与否的值,使用 tinyint 类型,坚持 is_xxx 的命名方式是为了明确其取值含义与取值范围。
正例: 表达逻辑删除的字段名 is_deleted ,1 表示删除,0 表示未删除。

小数

  • 类型:decimal。主要是为了防止丢失精度。

【强制】 小数类型为 decimal ,禁止使用 float 和 double 。
说明: 在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。

字符串

基本规范

  • 长度几乎固定字段:使用char类型。例如电话号、身份证字段类型char(11)即可,效率要比varchar(11)更高。因为实际存储时,varchar会根据实际输入的内容占用的长度进行存储,因此占用的存储空间是实际内容长度+可变长字段长度(当varchar使用长度≤255时使用一个字节记录,长度超出255时使用二个字节记录)。
  • 超长字段:长度超过 5000的超长字段,一律使用text类型,并将该字段独立出一个表。因为text、blog类型会导致索引失效;不使用varchar是因为varchar(5000)太长,建索引后非聚簇索引树过于占用磁盘空间。

【强制】 如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
【强制】 varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text ,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

varchar和char类型的区别、适用场景

长度:

  • char:固定长度的字符串
  • varchar:可变长度的字符串。

存储方式:

  • char:长度固定不可变,未存满的值会用空格填充到固定的长度。因此char类型字符串末尾无法存储空格,当然也不需要额外字节记录字符串长度。
  • varchar:varchar会使用1或2个额外字节记录字符串的长度。当列最大长度是255及以下时,varchar会使用一个字节记录可变长长度,最大长度255以上会使用两个字节记录可变长长度。因为varchar有记录长度,所以字符串末尾可以存储空格。

存储容量:

  • char:最多255个字符
  • varchar:理论上最多65535字节,最多65532个字符(当用utf-8编码存纯英文、且该表只有这一个字段时,字符串中的字符只占1个字节,能达到65532个字符)。但实际从性能考虑,超过5000长度时就不允许再用varchar,而是使用text类型。

各编码的占用长度

  • GBK编码:一个英文字符占一个字节,中文2字节,单字符最大可占用2个字节。
  • UTF-8编码:一个英文字符占一个字节,中文3字节,单字符最大可占用3个字节。
  • utf8mb4编码:一个英文字符占一个字节,中文3字节,单字符最大占4个字节(如emoji表情4字节)。

varchar(20) 是指字符串最大字节数是20,还是最大字符数是20

答案:取决于MySQL版本;4.0版本及以下,MySQL中varchar长度是按字节展示,如varchar(20),指的是20字节;5.0版本及以上,MySQL中varchar长度是按字符展示。如varchar(20),指的是20字符。

为什么varchar理论上最多字符数是65532

答案:因为MySQL行默认最大65535字节,varchar还需要1或2个字节维护可变长度,1个字节标识该列是否为NULL。

性能和空间

  • char:性能更好,每次更新时不用维护长度;但存在空间浪费的可能;
  • varchar:性能相对差一点,因为每次更新时要维护长度。如果更新后字符串变长后,原来的数据页正好存满,则需要耗费时间处理新字符串的存储;处理方式取决于存储引擎,例如MylSAM将行拆成多个片段存储,innoDB会分裂页。

适用场景:

  • char:存储长度几乎固定的字符串适用char类型。例如电话号、身份证字段类型char(11)即可,效率要比varchar(11)更高。因为实际存储时,varchar会根据实际输入的内容占用的长度进行存储,因此占用的存储空间是实际内容长度+可变长字段长度(当varchar使用长度≤255时使用一个字节记录,长度超出255时使用二个字节记录)。
  • varchar:长度几乎不固定、不超过2000字符的字符串。

varchar和text类型的区别、适用场景

存储方式

  • VARCHAR:可变长度的字符数据类型,它需要指定最大长度。实际存储时,会根据实际输入的内容占用的长度进行存储,因此占用的存储空间是实际内容长度加上一些额外的长度信息。
  • TEXT:TEXT也用于存储可变长度的字符数据,但它可以存储非常大的文本内容,通常可以存储几GB的数据。

索引和查询

  • VARCHAR:由于VARCHAR有固定的最大长度,可以建立更有效率的索引,同时在查询时会更快一些。
  • TEXT:对于较大的文本数据,使用TEXT类型可能会导致一些查询性能上的损失,因为文本数据的处理通常会比较耗费资源。

使用场景

  • VARCHAR:适用于长度可预期且不会太长的文本内容,比如姓名、地址等信息。
  • TEXT:适用于长度不确定或者非常长的文本内容,比如文章内容、评论等。

优缺点

  • VARCHAR:占用存储空间相对较小,适合存储较短字符串,支持索引,查询速度较快。但最大长度的限制可能会带来一些不便。
  • TEXT:可以存储非常大的文本内容,并且没有固定长度的限制,适合存储较长的文本数据。但是在查询和索引上可能会稍慢,而且在某些情况下,可能会消耗更多的存储空间。

外键/级联

禁用外键和级联。因为外键影响数据库的插入速度,每次插入时都要检查、更新外键;级联更新是强阻塞,也会影响性能。外键与级联更新适用于单机低并发的场景,不适合分布式、高并发集群的场景

  • 级联删除:创建外键时声明级联,则引用表删除数据时,被引用表也会级联删除这条数据。示例

    1
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id) ON DELETE CASCAD
  • 级联更新:创建外键时声明级联,则引用表更新数据时,被引用表也会级联更新这条数据。示例

    1
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id) ON UPDATE CASCAD

索引

命名规范

【强制】 主键索引名为 pk_ 字段名;唯一索引名为 uk _字段名 ; 普通索引名则为 idx _字段名。

说明: pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。

创建规范

  • 唯一索引:唯一特性字段必须创建唯一索引。一般不需要另外创建,因为创建唯一约束的时候会自动创建唯一索引。

  • 关联查询:被驱动表优先建索引,超过三个表禁止 join。

  • 字符串:

    • 模糊查询:禁止左模糊、全模糊索引。因为模糊查询会使索引失效,解决方案是使用ES等搜索引擎实现页面的搜索。
    • 索引长度:必须使用前缀索引。即字符串创建索引时必须指定索引长度,具体索引长度应该在区分度较高的前提下,索引长度越短越好。区分度=count(distinct left(列名, 索引长度))/count(*),即统计重复次数。
  • 排序:保持联合索引的有序性。例如搜索条件where a=? and b=? order by c;,则创建联合索引:a_b_c

  • 联合索引:区分度高的字段放左边。

  • 覆盖索引:使用覆盖索引防止回表;例如查询where a=? and b=? and c=?,则创建联合索引a_b_c,而不是a_b,因为走a_b_c的时候,直接在非聚簇索引树就能获取到所有要查询的字段,不需要回表查聚簇索引树。

  • 子查询优化深分页:正常情况下,深分页查询性能是很差的,例如我需要1w页第一条数据,那么就需要查出前1w条数据,性能很慢。用子查询可以优化深分页。

  • 深分页查询优化:需求是返回第1000000~1000010 的记录。如果直接limit 100000,10,将会先排序前十万条数据并回表,查询速度会非常慢,甚至会超时。

    • 主键有序的表根据主键排序,先过滤再排序:直接查上页最后记录之后的几个数据。

      1
      2
      3
      4
      # 自增。适用于app端和web端。由于不建议用自增策略(不安全、8.0才修复的ID回溯问题),所以此方法适用性不广。
      SELECT * FROM student WHERE id > 99999 LIMIT 10;
      # 雪花。x是上页最后一条记录的id。只适用于app端上下滑动分页时候必能拿到上页记录id。
      SELECT * FROM student WHERE id > #{x.id} LIMIT 10;
    • 主键不有序的表根据主键排序,先给主键分页,然后内连接原表:当前表内连接排序截取后的主键表,连接字段是主键。因为查主键是在聚簇索引树查,不用回表,排序和分页很快

      1
      SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 1000000,10) a WHERE t.id = a.id;
    • 主键有序的表根据非主键排序:得到上一页最后一条记录x(app端通过下拉翻页是肯定能获得上页最后记录的),那么目标页码的所有记录id都比x.id小(因为逆序,且排序依据其实是age,id,主键自增),目标页码的所有记录age都比x.age小或等于。

      1
      2
      3
      # 自增
      EXPLAIN SELECT * FROM student WHERE id<#{x.id} AND age>=#{x.age} ORDER BY age DESC LIMIT 10;
      # 雪花一个思路,只是x.id通过子查询获取
  • 命中索引的要求:SQL 性能优化目标至少达 range 级别(范围索引),要求是 ref 级别(非唯一索引),最好const(唯一索引)。

SQL语句

基本规范

  • 更新前要先查询:删除、更新前要先查询,避免误操作。
  • 禁用存储过程;
  • 禁用外键、级联。

查询字段

  • 禁用select *。原因:

    • 性能:多查询一些不需要的字段,性能差;

    • 失去覆盖索引的可能性:在命中联合索引时,查询的字段正好在非聚簇索引树中,就不需要回表了,而如果select *,则一定需要回表,影响性能。

    • 对比select 全部字段:即使需求是查询全部字段,也尽量用select 全部字段,而不用select *。原因:

      • 性能:select * 在系统解析的时候会多一步从系统表获取具体字段的步骤,因此会比select 全部字段多花时间,效率稍低。

      • 结果顺序:select 全部字段,查询的结果字段顺序可控;

    • 应用场景:某些特例也是可以用select *的,例如一些特定场景,在开发过程中(非生产环境),表结构、字段名频繁变化,可以暂时用select *

  • 查询数量:正确区分count(*)、count(1)、count(字段)

    • count(*):统计包括null的所有行数
  • count(1):统计包括null的第一列的行数。因为第一列在每一列都存在,所以等同于统计了所有行,并且不需要检查各行数据, 所以性能可能略高于count(*)

    • count(字段):统计不包括null的字段列的行数。例如学生表有100行,name列全是null,select count(name) from student查出的结果是0.
  • 求和:当某一列值全是null时,count(col)的值是0,sum(col)的值是null,所以求和时要防止空指针异常。

分页查询

先查数量再查询:分页查询前先查询count,如果count为0,则直接返回数据为null,不再分页查询,提高效率。


对象关系映射(ORM)

  • 禁用select *。

  • 布尔型字段:数据库表用is_xxx,实体类禁用isXxx

    • 结构:is_xxx

    • 数据类型:unsigned tinyint

    • 值:1 表示是,0 表示否

    • 对应实体类变量:虽然数据库必须命名成is_xxx,但是该表对应的实体类成员变量不能命名为isXxx,否则会导致序列号失败。所系需要在 resultMap 中进行字段与属性之间的映射。

  • 参数:使用#{},#param#,而不是${}。防止SQL注入。

  • 返回值:强制禁用Map,虽然少去了序列号的过程,性能会快一点,但是字段类型不可控。

  • 更新接口:更新时不要更新全部字段,尽量不要写一个参数为实体类的更新接口。一方面可以防止出错、另一方面可以提高性能、减少binlog存储(binlog是二进制日志文件,记录改不记录读,用于数据复制和数据恢复;在主从同步时用到)。

  • 不要滥用事务:事务要尽可能的控制粒度,使粒度尽可能的小,例如一些不必要的查询可以放在事务外部,以减少锁冲突、缩短连接时长,从而提高QPS(每秒发送的请求数)