实战案例9:数据库SQL解析:提取表名、提取字段名、解析WHERE条件、解析JOIN语句

说实话,做C语言开发这么多年,我遇到过不少需要解析SQL的场景。比如早期做一个数据迁移工具,需要从几千行SQL脚本里批量提取表名和字段。那时候我第一反应是用字符串硬拆,结果被各种嵌套子查询、别名、JOIN搞得焦头烂额。后来我学乖了——正则表达式才是干这活儿的正确姿势。

今天我们就来聊聊,怎么用C语言的正则表达式,把一条SQL语句拆得明明白白。你想想看,一条SQL无非就是几个核心部分:表名、字段、WHERE条件、JOIN关系。我们一个一个来搞定。

1. 提取表名

表名通常出现在FROM后面,也可能出现在JOIN后面。我个人的习惯是,先匹配FROM关键字,再抓后面的表名。注意,表名可能带别名,也可能不带。

举个例子:

SELECT a.id, b.name FROM users AS a JOIN orders AS b ON a.id = b.user_id

这里我们要提取出 usersorders。正则怎么写?

#include <regex.h>
#include <stdio.h>
#include <string.h>

void extract_table_names(const char *sql) {
    regex_t regex;
    regmatch_t matches[3];
    // 匹配 FROM 或 JOIN 后面的表名(支持 AS 别名)
    const char *pattern = "(FROM|JOIN)\\s+([a-zA-Z_][a-zA-Z0-9_]*)";
    
    if (regcomp(&regex, pattern, REG_EXTENDED | REG_ICASE) != 0) {
        fprintf(stderr, "正则编译失败\n");
        return;
    }
    
    const char *cursor = sql;
    while (regexec(&regex, cursor, 3, matches, 0) == 0) {
        if (matches[2].rm_so != -1) {
            int start = matches[2].rm_so;
            int end = matches[2].rm_eo;
            int len = end - start;
            char table_name[64];
            strncpy(table_name, cursor + start, len);
            table_name[len] = '\0';
            printf("提取到表名: %s\n", table_name);
        }
        cursor += matches[0].rm_eo;
    }
    regfree(&regex);
}

小提示:我在项目中遇到过表名带数据库前缀的情况,比如 db.users。这时候正则要改成 ([a-zA-Z_][a-zA-Z0-9_]*\\.)?[a-zA-Z_][a-zA-Z0-9_]*,把点号也考虑进去。

2. 提取字段名

字段名在SELECT后面,用逗号分隔。但要注意,字段可能带表名前缀(比如 a.id),也可能有函数包裹(比如 COUNT(*))。

我建议先提取SELECT和FROM之间的内容,再逐个匹配字段名。正则可以这样写:

void extract_field_names(const char *sql) {
    regex_t regex;
    regmatch_t matches[2];
    // 匹配 SELECT 和 FROM 之间的内容
    const char *pattern = "SELECT\\s+(.+?)\\s+FROM";
    
    if (regcomp(&regex, pattern, REG_EXTENDED | REG_ICASE) != 0) return;
    
    if (regexec(&regex, sql, 2, matches, 0) == 0) {
        int start = matches[1].rm_so;
        int end = matches[1].rm_eo;
        int len = end - start;
        char fields_part[256];
        strncpy(fields_part, sql + start, len);
        fields_part[len] = '\0';
        
        // 现在按逗号拆分字段
        const char *delim = ",";
        char *token = strtok(fields_part, delim);
        while (token) {
            // 去掉前后空格
            while (*token == ' ') token++;
            char *endp = token + strlen(token) - 1;
            while (endp > token && *endp == ' ') endp--;
            *(endp + 1) = '\0';
            printf("提取到字段: %s\n", token);
            token = strtok(NULL, delim);
        }
    }
    regfree(&regex);
}

注意:如果SQL里有子查询,比如 SELECT (SELECT id FROM t) AS sub,上面的正则就会出问题。我曾经在这个坑里摔过一次——后来我改用递归解析,或者先去掉子查询再提取字段名。

3. 解析WHERE条件

WHERE条件相对好处理,因为它的结构比较固定:字段 运算符 值。但运算符可能是 =>LIKEIN 等等。

我一般先提取WHERE后面的整个条件字符串,再逐条解析:

void parse_where_conditions(const char *sql) {
    regex_t regex;
    regmatch_t matches[2];
    const char *pattern = "WHERE\\s+(.+)";
    
    if (regcomp(&regex, pattern, REG_EXTENDED | REG_ICASE) != 0) return;
    
    if (regexec(&regex, sql, 2, matches, 0) == 0) {
        int start = matches[1].rm_so;
        int end = matches[1].rm_eo;
        int len = end - start;
        char where_clause[512];
        strncpy(where_clause, sql + start, len);
        where_clause[len] = '\0';
        
        // 用 AND/OR 拆分条件
        const char *delim = "\\s+(AND|OR)\\s+";
        // 这里可以用正则进一步拆分,或者简单用 strstr 找 AND/OR
        printf("WHERE条件原文: %s\n", where_clause);
        
        // 提取每个条件中的字段名和运算符
        // 比如 "age > 18" 提取字段 age,运算符 >,值 18
    }
    regfree(&regex);
}

核心思路:WHERE条件的解析,说白了就是找到每个子条件里的字段名、运算符和值。我习惯用 ([a-zA-Z_][a-zA-Z0-9_]*)\\s*([=!<>]+|LIKE|IN|BETWEEN)\\s*(.+) 这个正则来匹配单个条件。

4. 解析JOIN语句

JOIN语句比前面几个复杂一点,因为它涉及两个表和一个关联条件。常见的JOIN类型有 INNER JOINLEFT JOINRIGHT JOIN 等。

我的做法是,先匹配JOIN关键字,再提取表名和ON条件:

void parse_join_statements(const char *sql) {
    regex_t regex;
    regmatch_t matches[4];
    // 匹配 JOIN 表名 ON 条件
    const char *pattern = "(LEFT|RIGHT|INNER|OUTER|CROSS|FULL)?\\s*JOIN\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s+ON\\s+(.+)";
    
    if (regcomp(&regex, pattern, REG_EXTENDED | REG_ICASE) != 0) return;
    
    const char *cursor = sql;
    while (regexec(&regex, cursor, 4, matches, 0) == 0) {
        // matches[1] 是 JOIN 类型(可能为空)
        // matches[2] 是表名
        // matches[3] 是 ON 条件
        if (matches[2].rm_so != -1) {
            int start = matches[2].rm_so;
            int end = matches[2].rm_eo;
            int len = end - start;
            char table[64];
            strncpy(table, cursor + start, len);
            table[len] = '\0';
            printf("JOIN表: %s\n", table);
        }
        if (matches[3].rm_so != -1) {
            int start = matches[3].rm_so;
            int end = matches[3].rm_eo;
            int len = end - start;
            char on_cond[256];
            strncpy(on_cond, cursor + start, len);
            on_cond[len] = '\0';
            printf("ON条件: %s\n", on_cond);
        }
        cursor += matches[0].rm_eo;
    }
    regfree(&regex);
}

避坑指南:我曾经遇到过一个SQL,JOIN后面跟了子查询作为派生表,比如 JOIN (SELECT * FROM t) AS sub。这种情况正则就搞不定了,得用语法分析器。所以如果你的场景比较简单,正则够用;如果复杂,建议上lex/yacc。

知识体系总览

下面这张图,是我整理的本章节核心逻辑。你可以看到,SQL解析的四个任务,每个都有对应的正则策略和注意事项。

SQL解析核心任务与正则策略 提取表名 FROM/JOIN 后匹配 支持 AS 别名 注意库名前缀 提取字段名 SELECT-FROM 之间 逗号分隔 警惕子查询 解析WHERE条件 提取条件字符串 AND/OR 拆分 匹配字段 运算符 值 解析JOIN语句 匹配JOIN类型 提取表名和ON条件 注意派生表 正则表达式核心模式 表名: (FROM|JOIN)\\s+([a-zA-Z_][a-zA-Z0-9_]*) 字段: SELECT\\s+(.+?)\\s+FROM ⚠ 复杂SQL(子查询、嵌套)建议用语法分析器 正则适合80%的常见场景,剩下的20%需要特殊处理

综合实战:一个完整的SQL解析器

把上面四个功能组合起来,就是一个简易的SQL解析器。我个人习惯把它们封装成一个函数,传入SQL字符串,输出解析结果:

typedef struct {
    char table_names[10][64];
    int table_count;
    char field_names[20][64];
    int field_count;
    char where_conditions[10][256];
    int where_count;
    char join_info[10][256];
    int join_count;
} SQLParseResult;

void parse_sql(const char *sql, SQLParseResult *result) {
    memset(result, 0, sizeof(SQLParseResult));
    extract_table_names(sql, result);
    extract_field_names(sql, result);
    parse_where_conditions(sql, result);
    parse_join_statements(sql, result);
}

总结一下:用正则解析SQL,核心是抓住每个关键字后面的结构规律。表名在FROM/JOIN后,字段在SELECT后,条件在WHERE后,JOIN关系在ON后。记住这四点,大部分SQL都能搞定。

嗯,这里要注意一点:正则不是万能的。如果你的SQL里出现了子查询、CTE、窗口函数这些高级特性,正则就会力不从心。那时候我建议你上真正的SQL解析器,比如libsqlite或者自己写一个递归下降解析器。不过对于日常的80%场景,正则已经足够用了。


公众号:蓝海资料掘金营,微信deep3321