「完zheng」技术大牛成长课,从0到1带你手写一个数据库系统

65 阅读7分钟

技术大牛成长课,从0到1带你手写一个数据库系统

看讠果∪RL:www lexuecode com

如何设计好数据库关键接口

  1. 参考之前的表、字段表命名规则 。表命名规则:库名+业务名+表名;字段命名规则要尽量做到一套系统中基本一致,不能出现在这个表中是stationType,在其它表中是type。

  2. 字段类型、长度要保持一致。字段长度要预估好,可以设置长一点。之前项目中出现过type长度为1,突然有一天报错的了,原来新需求增加了type刚好超过字段长度。

  3. 创建必要的索引、约束。比如主外键索引,没有加基本上关联查询都很慢,而且还很难找出问题原因,后面又是一个艰难的优化过程;唯一约束,根据业务场景要创建好,避免在程序层面没有控制好的情况下,数据库也没有控制好。

  4. 新创建表通常会有主键、创建时间、创建人、修改时间、修改人、状态(为了数据安全绝大多数删除都是逻辑删除)

  5. 基本遵守“三范式”。

  • 确保每列的原子性,每列或者属性值都不可再分的的最小数据单元。举例,用户表:id,用户名,性别,年龄。。。每个字段都是最小的单元,不能再细分。

  • 确保表上的每列都和主键相关。比如用户表,角色表,用户表是不能保存用户角色的,一张表通常只记录一类信息。

  • 确保每列都和主键直接相关 。主要是Armstrong公理(从已知的一些函数依赖,可以推导出另外一些函数依赖,这就需要一系列推理规则,这些规则常被称作“Armstrong 公理”。)比如,用户表,用户类型表,类型不能存在于‘用户表’。

  1. 可以做适当字段冗余。

  2. 关于业务拓展方面设计,比如订单主表(有业务类型,业务id-子业务的id)、订单主表详情、订单各业务的子表;会员表、会员拓展表、第三方用户表。

  3. 表、字段加上注释。类型、状态代表什么意思要写清楚。

技术大牛成长课,从0到1带你手写一个数据库系统 - SQL语法解析器

什么词法解析?

如何理解词法解析呢?词法解析我们可以这么来进行理解,在启动词法解析任务时,它将从左到右把字符一个个的读取并加载到解析程序里面,然后对字节流进行扫描,接着根据构词规则识别字符并切割成一个个的词条,切词的规则是遇到空格进行分割,遇到分号时结束词法解析。比如一个简单的SQL如下所示:

SQL示例

SELECT name FROM tab;
1.

通过词法解析后,结果如下所示:

3.1.2 什么是语法解析?

如何理解语法解析呢?语法解析我们可以这么来进行理解,在启动语法解析任务时,语法分析的任务会在词法分析的结果上将词条序列组合成不同语法短句,组成的语法短句将与相应的语法规则进行适配,若适配成功则生成对应的抽象语法树,否则报会抛出语法错误异常。比如如下SQL语句:

SQL示例

复制

SELECT name FROM tab WHERE id=1001;
1.

约定规则如下:

上表中,红色的内容通常表示终结符,它们一般是大写的关键字或者符号等,小写的内容是非终结符,一般用作规则的命名,比如字段、表名等。具体AST数据结构如下图所示:

3.1.3 什么是语义解析?

如何理解语义解析呢?语义解析我们可以这么来进行理解,语义分析的任务是对语法解析得到的抽象语法树进行有效的校验,比如字段、字段类型、函数、表等进行检查。比如如下语句:

SQL示例

SELECT name FROM tab WHERE id=1001;
1.

上述SQL语句,语义分析任务会做如下检查:

  • SQL语句中表名是否存在;
  • 字段name是否存在于表tab中;
  • WHERE条件中的id字段类型是否可以与1001进行比较操作。

上述检查结束后,语义解析会生成对应的表达式供优化器去使用。

技术大牛成长课,从0到1带你手写一个数据库系统 - 实现一个SQL语法解析器

步骤一:定义词法规则文件

CommonLexerRules.g4

// 定义词法规则
lexer grammar CommonLexerRules;
//////// 定义词法
// 匹配ID
ID 
    : [a-zA-Z]+ ;
// 匹配INT
INT    : [0-9]+    ;
// 匹配换行符
NEWLINE: '\n'('\r'?);
// 跳过空格、跳格、换行符
WS     : [ \t\n\r]+ -> skip;
//////// 运算符
DIV:'/';
MUL:'*';
ADD:'+';
SUB:'-';
EQU:'=';
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.

步骤二:定义语法规则文件(LibExpr.g4)

LibExpr.g4
// 定于语法规则
grammar LibExpr;
// 导入词法规则
import CommonLexerRules;
// 词法根
prog:stat+ EOF?;
// 定义声明
stat:expr (NEWLINE)?         # printExpr
| ID '=' expr (NEWLINE)? # assign
| NEWLINE                # blank
;
// 定义表达式
expr:expr op=('*'|'/') expr # MulDiv
|expr op=('+'|'-') expr # AddSub
|'(' expr ')'           # Parens
|ID                     # Id
|INT                    # Int
;
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.

步骤三:编译生成文件

如果是Maven工程,这里在pom文件中添加如下依赖:

ANTLR依赖JAR

复制

<dependencies>
    <dependency>
        <groupId>org.antlr</groupId>
        <artifactId>antlr4</artifactId>
        <version>4.9.3</version>
    </dependency>
    <dependency>
        <groupId>org.antlr</groupId>
        <artifactId>antlr4-runtime</artifactId>
        <version>4.9.3</version>
    </dependency>
</dependencies>
1.2.3.4.5.6.7.8.9.10.11.12.

然后,执行Maven编译命令即可:

Maven编译命令

复制

mvn generate-sources
1.

步骤四:编写简单的示例代码

待预算的示例文本:

示例文本

1+2
1+24
1+24-5
1+2*4-5+20/5
(1+2)*4
1.2.3.4.5.

加减乘除逻辑类:

逻辑实现类

package com.vivo.learn.sql;
import java.util.HashMap;
import java.util.Map;
/**
 * 重写访问器规则,实现数据计算功能
 * 目标:
 *     1+2 => 1+2=3
 *     1+2*4 => 1+2*4=9
 *     1+2*4-5 => 1+2*4-5=4
 *     1+2*4-5+20/5 => 1+2*4-5+20/5=8
 *     (1+2)*4 => (1+2)*4=12
 */
public class LibExprVisitorImpl extends LibExprBaseVisitor<Integer> {
    // 定义数据
    Map<String,Integer> data = new HashMap<String,Integer>();
// expr (NEWLINE)?         # printExpr
@Override
public Integer visitPrintExpr(LibExprParser.PrintExprContext ctx) {
    System.out.println(ctx.expr().getText()+"="+visit(ctx.expr()));
    return visit(ctx.expr());
}
// ID '=' expr (NEWLINE)? # assign
@Override
public Integer visitAssign(LibExprParser.AssignContext ctx) {
    // 获取id
    String id = ctx.ID().getText();
    // // 获取value
    int value = Integer.valueOf(visit(ctx.expr()));
    // 缓存ID数据
    data.put(id,value);

    // 打印日志
    System.out.println(id+"="+value);
    return value;
}
// NEWLINE                # blank
@Override
public Integer visitBlank(LibExprParser.BlankContext ctx) {
    return 0;
}
// expr op=('*'|'/') expr # MulDiv
@Override
public Integer visitMulDiv(LibExprParser.MulDivContext ctx) {
    // 左侧数字
    int left = Integer.valueOf(visit(ctx.expr(0)));
    // 右侧数字
    int right = Integer.valueOf(visit(ctx.expr(1)));
    // 操作符号
    int opType = ctx.op.getType();
    // 调试
    // System.out.println("visitMulDiv>>>>> left:"+left+",opType:"+opType+",right:"+right);
    // 判断是否为乘法
    if(LibExprParser.MUL==opType){
        return left*right;
    }
    // 判断是否为除法
    return left/right;
}
// expr op=('+'|'-') expr # AddSub
@Override
public Integer visitAddSub(LibExprParser.AddSubContext ctx) {
    // 获取值和符号

    // 左侧数字
    int left = Integer.valueOf(visit(ctx.expr(0)));
    // 右侧数字
    int right = Integer.valueOf(visit(ctx.expr(1)));
    // 操作符号
    int opType = ctx.op.getType();
    // 调试
    // System.out.println("visitAddSub>>>>> left:"+left+",opType:"+opType+",right:"+right);
    // 判断是否为加法
    if(LibExprParser.ADD==opType){
        return left+right;
    }
    // 判断是否为减法
    return left-right;
}
// '(' expr ')'           # Parens
@Override
public Integer visitParens(LibExprParser.ParensContext ctx) {
    // 递归下调
    return visit(ctx.expr());
}
// ID                     # Id
@Override
public Integer visitId(LibExprParser.IdContext ctx) {
    // 获取id
    String id = ctx.ID().getText();
    // 判断ID是否被定义
    if(data.containsKey(id)){
        // System.out.println("visitId>>>>> id:"+id+",value:"+data.get(id));
        return data.get(id);
    }
    return 0;
}
// INT                    # Int
@Override
public Integer visitInt(LibExprParser.IntContext ctx) {
    // System.out.println("visitInt>>>>> int:"+ctx.INT().getText());
    return Integer.valueOf(ctx.INT().getText());
}
}
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.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.102.103.

Main函数打印输出结果类:

package com.vivo.learn.sql;
import org.antlr.v4.runtime.tree.ParseTree;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.antlr.v4.runtime.*;
/**
• 打印语法树
*/
public class TestLibExprPrint {
// 打印语法树 input -> lexer -> tokens -> parser -> tree -> print
public static void main(String args[]){
printTree("E:\smartloli\hadoop\sql-parser-example\src\main\resources\testCase.txt");
}
/**• 打印语法树 input -> lexer -> token -> parser -> tree
• @param fileName
*/
private static void printTree(String fileName){
// 定义输入流
ANTLRInputStream input = null;
// 判断文件名是否为空,若不为空,则读取文件内容,若为空,则读取输入流
if(fileName!=null){
try{
input = new ANTLRFileStream(fileName);
}catch(FileNotFoundException fnfe){
System.out.println("文件不存在,请检查后重试!");
}catch(IOException ioe){
System.out.println("文件读取异常,请检查后重试!");
}
}else{
try{
input = new ANTLRInputStream(System.in);
}catch(FileNotFoundException fnfe){
System.out.println("文件不存在,请检查后重试!");}catch(IOException ioe){

     System.out.println("文件读取异常,请检查后重试!");
 }
}
// 定义词法规则分析器
LibExprLexer lexer = new LibExprLexer(input);
// 生成通用字符流
CommonTokenStream tokens = new CommonTokenStream(lexer);
// 语法解析
LibExprParser parser = new LibExprParser(tokens);
// 生成语法树
ParseTree tree = parser.prog();
// 打印语法树
// System.out.println(tree.toStringTree(parser));
// 生命访问器
LibExprVisitorImpl visitor = new LibExprVisitorImpl();
visitor.visit(tree);
}
}