rust nom 实现一个简单的sql解析器

2023-10-26

祝福

过年期间,新型冠状病毒肺炎闹的动静不小,不过相信国家可以妥善处理的。
祝大家平安健康!中国加油!武汉加油!

前言

上篇文章中,简单的描述了nom一些常用的解析器、组合器。结尾说要实现一个简单的sql解析器,但是年底事情比较多,想着忙完回家,趁着过年补上,结果回到家里就天天睡觉,战果也很明显,胖了不少…
不过该欠下的坑,还是要补的。

分析

sql解析器是一个相对比较复杂的程序,我们是为了通过例子更加了解nom的使用方法,所以我们只是实现select语句的部分功能。

实现约定:
1、只实现到from table,where、group by、order by、having等不实现
2、字段只实现常规的table.column、字符串"abc"还有子查询,函数、算术表达式等不实现
3、table只实现常规的情况,子查询、关联等方式不实现
4、实现alias
5、除了子查询,不会再出现()包裹的情况,子查询必须()包裹切必须有别名
6、因为字段中出现多个的处理方式,表的多个情况就不再实现

设定输入sql语句字符串的生命周期为'a

字段

按照实现约定,字段有三种类型:常规、字符串、子查询,因此字段是一个枚举。
针对常规格式,格式为[ table.]column [[as] alias

#[derive(Debug, PartialEq, Eq)]
struct Field<'a> {
    own: Option<&'a str>, 	// 所属,table.column中的table
    name: &'a str, 					// 字段  table.column中的column
    alias: Option<&'a str>	// 别名  as alias中的alias
}

impl <'a> Field<'a> {
    fn new(name: &'a str) -> Field {
        Field {
            own: None,
            name,
            alias: None
        }
    }

    fn new_own(own: Option<&'a str>, name: &'a str) -> Self {
        Field {
            own,
            name,
            alias: None
        }
    }

    fn new_all(own: Option<&'a str>, name: &'a str, alias: Option<&'a str>) -> Self {
        Field {
            own,
            name,
            alias
        }
    }
}

针对字符串格式,格式为"str" [[as] alias]

#[derive(Debug, PartialEq, Eq)]
struct StringField<'a> {
    string: &'a str,						// 字符串值, "str" as s中的str
    alias: Option<&'a str>		// 别名  as alias 中的alias
}

impl <'a> StringField<'a> {
    fn new(string: &'a str) -> Self {
        StringField {
            string,
            alias: None
        }
    }
}

针对子查询,格式(sql) as alias

#[derive(Debug, Eq, PartialEq)]
struct SubSelect<'a> {
    parser: Box<SelectStatement<'a>>,
    alias: Option<&'a str>
}

impl <'a> SubSelect<'a> {
    fn new(parser: SelectStatement<'a>, alias: &'a str) -> Self {
        SubSelect {
            parser: Box::new(parser),
            alias: Some(alias)
        }
    }
}

此时三种情况的对象都已经声明,可以设定字段的枚举

#[derive(Debug, PartialEq, Eq)]
enum FieldEnum<'a> {
    Normal(Field<'a>),
    SubSelect(SubSelect<'a>),
    String(StringField<'a>)
}

按照实现约定,只有常规的格式 table [[as] alias],不过也按照多种情况来设定

#[derive(Debug, PartialEq, Eq)]
enum TableEnum<'a> {
    Normal(Table<'a>)
//    SubSelect(SubSelect<'a>)
}

#[derive(Debug, PartialEq, Eq)]
struct Table<'a> {
    name: &'a str,
    alias: Option<&'a str>
}

impl <'a> Table<'a> {
    fn new(name: &'a str) -> Self {
        Table {
            name,
            alias: None
        }
    }

    fn new_all(name: &'a str, alias: Option<&'a str>) -> Self {
        Table {
            name,
            alias
        }
    }
}

查询语句

只考虑字段和表,且字段和表为多个
但实现时,表不再考虑多个情况

#[derive(Debug, Eq, PartialEq)]
struct SelectStatement<'a> {
    field: Vec<FieldEnum<'a>>,
    table: Vec<TableEnum<'a>>
}

编码

nom是一个流式的文本解析器,从做到右挨个的进行匹配,所以需要先设定关键字,当遇到这些关键字的时候,代表进入下一个解析步骤。
全局设置关键字,使用lazy_static
Cargo.toml

[dependencies]
lazy_static = "1.4"

需要使用到宏
main.rs 或 lib.rs中

#[macro_use]
extern crate lazy_static;

关键字

lazy_static! {
    static ref KEY_WORDS: HashSet<&'static str> = {
        let mut s = HashSet::<&'static str>::new();

        s.insert("select");
        s.insert("from");
        s.insert("where");
        s.insert("group by");
        s.insert("order by");
        s.insert("as");

        s
    };
}

字符规则

sql语句中,一般情况下,字段、表、函数以及存储过程的名称字符规则为英文字符、数字和下划线。
设定is_sql_alphanumeric函数用来判断过来的每一个字符是否在这个规则之内

use nom::character::is_alphanumeric;

fn is_sql_alphanumeric(chr: char) -> bool {
    is_alphanumeric(chr as u8) || chr == '_'
}

然后设定sql_alphanumeric函数用来将输入的字符串进行匹配,当匹配到不在字符规则之内的字符时返回,且匹配出来的内容不能是关键字
例如:
输入abc,abc
经过sql_alphanumeric处理得到输出:Ok((",abc", “abc”))
作用和tag(“abc”)一样,只是tag(“abc”)只能匹配()内部的abc,而字段、表名这种的情况比较多,所以自己写一个类似的tag来进行处理

fn sql_alphanumeric(input: &str) -> IResult<&str, &str> {
    let clone = input.clone();
    let splits: Vec<&str> = clone.split(' ').collect();

    if splits.len() > 0 {
        let first_word = splits[0];
		// order by 或 group by匹配结束,返回
        if (first_word == "group" || first_word == "order") && splits.len() > 1 {
            let second_word = splits[1];
            if second_word == "by" {
                return Err(Err::Error(ParseError::from_error_kind(input, ErrorKind::IsNot)));
            }
        } else {
        	// 关键字匹配结束,返回
            if KEY_WORDS.contains(first_word) {
                return Err(Err::Error(ParseError::from_error_kind(input, ErrorKind::IsNot)));
            }
        }
    }

    input.split_at_position_complete(|item| !is_sql_alphanumeric(item))
}

除此之外还需要一个遇到不匹配的就抛出Err::Error错误的sql_alphanumeric1函数,用来给many或opt组合器进行使用

fn sql_alphanumeric1(input: &str) -> IResult<&str, &str> {
    let clone = input.clone();
    let splits: Vec<&str> = clone.split(' ').collect();

    if splits.len() > 0 {
        let first_word = splits[0];

        if (first_word == "group" || first_word == "order") && splits.len() > 1 {
            let second_word = splits[1];
            if second_word == "by" {
                return Err(Err::Error(ParseError::from_error_kind(input, ErrorKind::IsNot)));
            }
        } else {
            if KEY_WORDS.contains(first_word) {
                return Err(Err::Error(ParseError::from_error_kind(input, ErrorKind::IsNot)));
            }
        }
    }

    input.split_at_position1_complete(|item| !is_sql_alphanumeric(item), ErrorKind::IsNot)
}

sql_alphanumeric函数与sql_alphanumeric1函数的区别在于

input.split_at_position_complete(...)      // 这个不抛错
input.split_at_position1_complete(...)   // 这个抛错

alias

别名,格式:[[as] alias]
可能存在,也可能不存在,因此调用者需要使用opt()组合器
作为别名的解析函数,需要在别名不存在的时候抛出Err::Error错误,因此需要使用上面的sql_alphanumeric1函数
且as可能也不存在

/// 转换as语句
fn alias_parser(input: &str) -> IResult<&str, &str> {
    let (input, _) = if input.starts_with("as ") {
    	// 假如input为as alias
        let (input, _) = tag("as")(input)?;
        // 此时input为 _alias  _代表空格
        let (input, _) = space1(input)?;
        // 此时input为alias
        (input, ())
    } else {
    	// 假如input为alias或_alias  _代表空格
        let (input, _) = space0(input)?;
        // 此时input为alias
        (input, ())
    };
    // 假如input为alias  处理后input为""  alias为alias
    // 假如input为, table 处理后,会抛出Err::Error错误,经过opt处理会得到None
    let (input, alias) = sql_alphanumeric1(input)?;
    let (input, _) = space0(input)?;
    Ok((input, alias))
}

字段

字段需要进行三种,先分开进行处理,然后用一个总的调度进行匹配进行哪一种的处理。

常规格式的字段处理

格式为 [own.]column [[as] alias]
其中别名的处理函数已经实现,使用opt调用即可
假如own存在的话,那么一定会有一个. 此时需要tuple进行组合处理,tuple((sql_alphanumeric, char(’.’))),own又可以不存在,所以需要opt进行处理

fn normal_field_parser(input: &str) -> IResult<&str, Field> {
	// 假如input为table.column
	// 处理后得input为column,t为table
    let (input, t) = opt(tuple((sql_alphanumeric, char('.'))))(input)?;
    // 此步处理后input为空,field为column
    let (input, field) = sql_alphanumeric(input)?;
    let (input, _) = space0(input)?;
    // 此步处理后alias为None
    let (input, alias) = opt(alias_parser)(input)?;
    let (input, _) = opt(char(','))(input)?;

	// 组合获得常规字段对象
    let mut field = Field::new(field);
    if t.is_some() {
        let t = t.unwrap();
        field.own = Some(t.0);
    }
    if alias.is_some() {
        field.alias = Some(alias.unwrap());
    }

    Ok((input, field))
}

字符串格式字段处理

格式为:“str” [[as] alias]
sql中的字符串的起始终止字符一般为"或’,但具体在书写时使用的是哪种不是很清楚,因此需要将此字符传入,这样的话函数的声明与返回值就和之前的模式不一样了

fn string_field_parser(c: char) -> impl Fn(&str) -> IResult<&str, StringField>{}
/// 调用时
string_field_parser('"')(input)

使用字符串无可避免的会碰到转义字符的问题,因此需要使用escaped组合器,sql中的转移字符一般为\

fn string_field_parser(c: char) -> impl Fn(&str) -> IResult<&str, StringField> {
    move |input: &str| {
    	// 假如input为"abc\"",处理后input为abc\""
        let (input, _) = char(c)(input)?;
        let s = c.to_string() +"\\";
        // 此步处理后,input为",string为abc"
        let (input, string) = escaped(sql_alphanumeric1, '\\',
                                      one_of(s.as_str()))(input)?;
        // 此步处理后input为空,string为abc"
        let (input, _) = char(c)(input)?;
        let (input, _) = space1(input)?;
        let (input, alias) = opt(alias_parser)(input)?;

        let mut field = StringField::new(string);
        if alias.is_some() {
            field.alias = Some(alias.unwrap());
        }

        Ok((input, field))
    }
}

子查询处理

格式(subselect) [[as] alias]
子查询相对来说简单一些,将subselect直接调用sql解析函数即可,在此处我们声明sql解析函数为

fn sql_parser(input: &str) -> IResult<&str, SelectStatement>{}

那么子查询的处理函数为

fn sub_parser(input: &str) -> IResult<&str, SubSelect> {
    let (input, _) = char('(')(input)?;
    let (input, _) = space0(input)?;
    let (input, ss) = sql_parser(input)?;
    let (input, _) = space0(input)?;
    let (input, _) = char(')')(input)?;
    let (input, _) = space0(input)?;
    let (input, alias) = alias_parser(input)?;

    let ss = SubSelect::new(ss, alias);

    Ok((input, ss))
}

字段处理汇总

我们以开头字符进行判断,虽然草率,但我们是为了体验nom的使用方法,所以不要在意这些细节。
以"或’开头为字符串格式的
以(开头为子查询格式的
其他的就是常规的

fn field_parser(input: &str) -> IResult<&str, FieldEnum> {
    let first_char = &input.chars().next();
    if first_char.is_none() {
        return Err(Err::Error(ParseError::from_error_kind(input, ErrorKind::NoneOf)));
    }
    let first_char = first_char.as_ref().unwrap().to_owned();

    // 拿第一个字符进行比对,看到底是什么类型
    let (input, fe) = if first_char == '\'' || first_char == '"' {
        let (input, sf) = string_field_parser(first_char)(input)?;
        (input, FieldEnum::String(sf))
    } else if first_char == '(' {
        let (input, ss) = sub_parser(input)?;
        (input, FieldEnum::SubSelect(ss))
    } else {
        let (input, nf) = normal_field_parser(input)?;
        (input, FieldEnum::Normal(nf))
    };
    let (input, _) = opt(char(','))(input)?;

    let (input, _) = space0(input)?;

    Ok((input, fe))
}

表的实现逻辑思路与字段的非常相似,就不再多加阐述

整个查询语句

整个的查询语句以select开头,然后进行字段的处理,字段一般会有多个,所以使用many0进行循环解析,直到碰到了from
from之后是table的解析,table也存在多个,但逻辑与字段类似,就略过,只认为拥有一个table来处理

fn sql_parser(input: &str) -> IResult<&str, SelectStatement>{
    let (input, _) = tag("select")(input)?;
    let (input, _) = space1(input)?;
    let (input, fes) = many0(field_parser)(input)?;
    let (input, _) = tag("from")(input)?;
    let (input, _) = space1(input)?;
    let (input, ts) = sql_alphanumeric(input)?;
    let (input, _) = space0(input)?;
    let (input, alias) = opt(alias_parser)(input)?;

    let t = Table::new_all(ts, alias);
    let te = TableEnum::Normal(t);

    let ss = SelectStatement {
        field: fes,
        table: vec![te]
    };

    Ok((input, ss))
}

结尾

这样一个非常非常简单潦草的sql解析器就实现了,虽然简单潦草,但基本上覆盖了nom大部分的使用方法,配合文档基本上可以实现多数功能的开发,比如实现一个html DOM树的解析。
本文的代码地址直接看#[test]中的代码进行测试即可

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

rust nom 实现一个简单的sql解析器 的相关文章

  • 如何将存储过程中的值返回到 EF

    我试图通过 EF 调用存储过程并从存储过程中检索返回值 我用过this https stackoverflow com questions 6861737 executesqlcommand with output parameter an
  • SQL Server:复制表中的列

    将表中的列中的所有值复制到同一表中的另一列的最简单方法是什么 使用单个语句 如果列具有相同的数据类型 UPDATE
  • Linq 到自定义 SQL

    好的 我有一个带有巨大表的数据库 超过 100 万条记录和 50 多个列 我知道它不是最佳的 但它是我必须处理的 所以我需要运行限制返回数据量的查询 现在我的问题是这样的 我有一些运行并返回数据的自定义查询 用户可以通过选择将生成谓词模板并
  • 如何更新 SQL Server 中 ntext 列中的 XML 字符串?

    有一个包含 2 列的 SQL 表 ID int 和值 ntext 值行中包含各种 xml 字符串 ID Value 1
  • 将数据从 MS SQL 导入 MySQL

    我想从 MS SQL Server 导入数据 通过某种正则表达式运行它以过滤掉内容 然后将其导入 MySQL 然后 对于每个查询 我希望显示来自第三个数据库的相关图像 明智地导入和链接 最简单的方法是什么 谢谢 澄清 它是一个 PHP 应用
  • count 和 groupby 在一个查询中一起使用

    以下查询正在获取页面上的一些产品信息 这很好 但我也想以文本形式显示它出现的产品编号 但是 我使用了groupby但我也想用count on pro id SELECT FROM cart WHERE session id SESSION
  • 如何在 Doctrine 中使用 andWhere 和 orWhere ?

    WHERE a 1 AND b 1 Or b 2 AND c 1 OR c 2 我怎样才能在教义中做到这一点 q gt where a 1 q gt andWhere b 1 q gt orWhere b 2 q gt andWhere c
  • 如何对主索引重新编号

    我有一个简单的 MySQL 表 主索引 id 不是一一编号的 1 31 35 100 等 我希望它们的编号如 1 2 3 4 请告诉我该怎么做 我还想指出的是 我知道该操作可能产生的后果 但我只是想整理一下表格 我同意其他方法也可以 但我只
  • MySQL:查询中周数的周日期范围

    我有一个看起来像这样的数据库表 id clock info 1 1262556754 some info 2 1262556230 some other info 3 1262556988 and another 4 1262555678
  • SQL:将一个表中的所有记录插入到另一表中,而不指定列

    我想将备份表 foo bk 中的所有记录插入到 foot 表中 而不指定特定的列 如果我尝试这个查询 INSERT INTO foo SELECT FROM foo bk 我会收到错误 插入错误 列名称或提供的值的数量与表定义不匹配 是否可
  • 如何在 SQL 中存储目标(例如 RPG Quest)

    今天有人问我他们应该如何将任务目标存储在 SQL 数据库中 在这种情况下 请考虑角色扮演游戏 目标可能包括以下一些内容 发现 地点 杀死 n MOB 类型 获取 对象 的 n 个 实现 技能组 中的 技能 你在角色扮演游戏中获得的所有其他东
  • MYSQL 查询 WHERE IN 与 OR

    我开发了一个使用 OR 查询的系统 SELECT FROM tableA JOIN tableB ON idA idB WHERE idA 1 OR idA 2 OR idA 3 OR idA 4 OR idA 5 OR idA 100 与
  • 如何查询多个链接服务器?

    链接一些 SQL Server 2008 服务器 实例后 我想对这些服务器进行更通用的查询 我知道我必须像这样指定查询的命运 select from SRV INSTANCE dbname dbo foo 但是 我会针对多个链接服务器运行此
  • Azure COSMOS DB 如何查询数组中的内容

    如何进行查询以获取文档数组 roles 中包含某些内容的文档 我想获取以下文档 其中 Trainer 是数组中的元素 enabled true profilePicture null roles Trainer Client SELECT
  • 使用 SqlDataReader.IsDBNull 时使用列名

    我已经得到了从 SQL DB 读取数据的代码 我不知道应该如何编辑它 以便我可以使用原始列名称而不是列索引 string query SELECT FROM zajezd WHERE event thisrow AND year klien
  • 实体框架 - 查询可为空列时出现问题

    我在从具有可为空的tinyint 列的表中查询数据时遇到问题 问题似乎是查询生成为 AND CAST Extent1 PositionEffect AS int p linq 3 gt p linq 3 NULL 如果我手动运行该查询 它不
  • Mysql获取特定表的最后一个id

    我必须从特定的插入表中获取最后的插入 ID 可以说我有这个代码 INSERT INTO blahblah test1 test 2 VALUES test1 test2 INSERT INTO blahblah2 test1 test 2
  • 按每月时间为用户标记标签

    数据源 User ID Visit Date 1 2020 01 01 12 29 15 1 2020 01 02 12 30 11 1 2020 04 01 12 31 01 2 2020 05 01 12 31 14 Problem 我
  • 如何修改actix-web中间件中的请求数据? [复制]

    这个问题在这里已经有答案了 是否有推荐的方法来修改 actix web 上收到的请求 我正在寻找将数据添加到请求对象并使其可供下游中间件和处理程序处理的方法 The 中间件文档 https actix rs docs middleware
  • 选定的非聚合值必须是关联组的一部分

    我在 Teradata 中有两个表 Table A 和 Table B 它们之间是 LEFT JOIN 之后我将创建 SELECT 语句 其中包含两个表中的属性 SELECT attribute 1 attribute 2 attribut

随机推荐

  • Spark-RDD编程

    Spark在进行计算的时候通常会包含以下几个步骤 创建SparkContext上下文对象 使用SparkContext加载数据创建RDD RDD的转换算子transfotmations RDD的行动算子actions RDD的缓存和持久化
  • 反射获取字段的值与非空校验

    获取指定字段的值 通过字段对应的get方法 public Object getFieldValueByName1 String fieldName Object obj try String firstLetter fieldName su
  • 《Semi-Supervised Semantic Segmentation with Cross-Consistency Training》 2020CVPR 论文阅读

    在这项工作中 作者首先观察到 对于语义分割 低密度区域在隐藏表示中比在输入中更明显 作者提出了交叉一致性训练 其中预测的不变性是施加不同的扰动在编码器输出上 Cross Consistency Training 该模型包含一个共享的enco
  • SQL千万级大数据量查询优化

    转发自 https blog csdn net long690276759 article details 79571421 spm 1001 2014 3001 5506 防止查询资料找不到来源 很详细 1 对查询进行优化 应尽量避免全表
  • c++中istringstream及ostringstream超详细说明

    文章目录 1 stringbuf类介绍 1 1 stringbuf类构造函数 1 2 str函数 2 istringstream类 2 1 rdbuf函数 2 2 swap函数 3 ostringstream类和stringstream类
  • whisper

    Robust Speech Recognition via Large Scale Weak Supervision 介绍 大规模弱监督的训练 先前的方法都是通过大量的无监督学习训练 无监督的数据容易收集 所以通过大量无监督的学习可以训练出
  • Node中的事件循环

    Node中的事件循环 Node的底层语言是libuv 使用v8引擎解析js脚本 libuv负责调用接口API 将不同的任务交给不同的线程处理 再将处理结果交给v8引擎 v8引擎再将处理结果发送给用户 Node中任务的执行顺序 timers定
  • Mybatis-Plus:Service接口

    目录 IService接口 1 写实体类 2 写mapper接口 3 写service接口 4 写service接口的实现类 IService自带方法 1 save 2 SaveOrUpdate 3 Remove 4 Update 5 Ge
  • xss-domcobble绕过XSSfilter

    目录 DOM破坏的原理 例题 多层标签 HTMLCollection 一些常见的标签的关系 三层标签如何获取 例题 DOM破坏的原理 DOMClobber是一种攻击技术 它利用了DOM 文档对象模型 的特性来破坏或修改网页的结构和功能 DO
  • WDK李宏毅学习笔记第十五周01_Conditional Generation by Conditional

    Conditional Generation by GAN 文章目录 Conditional Generation by GAN 摘要 1 Supervised Conditional GAN 1 1 目的 1 2 做法 1 3 Discr
  • 把一个base64编码的图片绘制到canvas (canvas的图片在转成dataurl)

    把一个base64编码的图片绘制到canvas 需要引入jquery
  • SpringBoot中ThreadPoolTaskExecutor的使用

    文章目录 1 配置自己的线程池 2 使用 2 1 在Service层使用 2 2 多线程中使用事务的写法 2 3 方法内多线程 2 3 1 错误写法 2 3 2 正确写法 一 2 3 2 正确写法 二 2 3 3 正确写法 三 3 线程池与
  • mysql的相关技术说明_MySQL 系统架构 说明

    说明 本文转自 简朝阳 MySQL ACE 的 MySQL性能调优与架构设计 一 逻辑模块组成 总的来说 MySQL 可以看成是二层架构 第一层我们通常叫做SQL Layer 在MySQL 数据库系统处理底层数据之前的所有工作都是在这一层完
  • 计算机熔断与服务降级,Hystrix---服务熔断和服务降级

    一 服务熔断 防止服务雪崩 作用在服务提供者 服务熔断 熔断机制是应对雪崩效应的一种微服务链路保护机制 当扇出链路的某个微服务不可用或者响应时间太长时 会进行服务的降级 进而熔断该节点微服务的调用 快速返回 错误 的响应信息 当检测到该节点
  • Java多线程——Lock

    Lock 从JDK 5 0开始 Java提供了更强大的线程同步机制 通过显式定义同步锁对象来实现同步 同步锁使用Lock对象充当 java util concurrent locks Lock接口是控制多个线程对共享资源进行访问的工具 锁提
  • Java的静态绑定与动态绑定

    我们可以对思考一个问题 JVM是如何知道调用的是哪个类的方法源代码 这里面到底有什么内幕呢 这篇文章我们就将揭露JVM方法调用的静态 static binding 和动态绑定机制 auto binding 理解这两个绑定之前 我们不妨先理解
  • Vue + Springboot 前后端分离项目实践:项目简介及教程

    专栏目录 持续更新 Vue js Spring Boot 前后端分离项目实践 一 项目简介Vue js Spring Boot 前后端分离项目实践 二 搭建 Vue js 项目Vue js Spring Boot 前后端分离项目实践 三 前
  • Visual Studio 2015 + cmake编译QT5程序

    概述 由于QT的集成开发环境QTCreate 在代码调试功能上远不及Visual Studio方便 因此 在Windows平台 可以使用Visual Studio来开发调试QT程序 本文章就主要介绍下 如何使用CMAKE编译QT5程序 并使
  • 【Unity】SafeArea适配大小

    通过使用SafeArea 修改stretch适配类型的UI画布的Top偏移 适应安卓异型屏幕
  • rust nom 实现一个简单的sql解析器

    rust nom 实现一个简单的sql解析器 祝福 前言 分析 字段 表 查询语句 编码 关键字 字符规则 alias 字段 常规格式的字段处理 字符串格式字段处理 子查询处理 字段处理汇总 表 整个查询语句 结尾 祝福 过年期间 新型冠状