实战案例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
这里我们要提取出 users 和 orders。正则怎么写?
#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(®ex, pattern, REG_EXTENDED | REG_ICASE) != 0) {
fprintf(stderr, "正则编译失败\n");
return;
}
const char *cursor = sql;
while (regexec(®ex, 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(®ex);
}
小提示:我在项目中遇到过表名带数据库前缀的情况,比如 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(®ex, pattern, REG_EXTENDED | REG_ICASE) != 0) return;
if (regexec(®ex, 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(®ex);
}
注意:如果SQL里有子查询,比如 SELECT (SELECT id FROM t) AS sub,上面的正则就会出问题。我曾经在这个坑里摔过一次——后来我改用递归解析,或者先去掉子查询再提取字段名。
3. 解析WHERE条件
WHERE条件相对好处理,因为它的结构比较固定:字段 运算符 值。但运算符可能是 =、>、LIKE、IN 等等。
我一般先提取WHERE后面的整个条件字符串,再逐条解析:
void parse_where_conditions(const char *sql) {
regex_t regex;
regmatch_t matches[2];
const char *pattern = "WHERE\\s+(.+)";
if (regcomp(®ex, pattern, REG_EXTENDED | REG_ICASE) != 0) return;
if (regexec(®ex, 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(®ex);
}
核心思路:WHERE条件的解析,说白了就是找到每个子条件里的字段名、运算符和值。我习惯用 ([a-zA-Z_][a-zA-Z0-9_]*)\\s*([=!<>]+|LIKE|IN|BETWEEN)\\s*(.+) 这个正则来匹配单个条件。
4. 解析JOIN语句
JOIN语句比前面几个复杂一点,因为它涉及两个表和一个关联条件。常见的JOIN类型有 INNER JOIN、LEFT JOIN、RIGHT 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(®ex, pattern, REG_EXTENDED | REG_ICASE) != 0) return;
const char *cursor = sql;
while (regexec(®ex, 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(®ex);
}
避坑指南:我曾经遇到过一个SQL,JOIN后面跟了子查询作为派生表,比如 JOIN (SELECT * FROM t) AS sub。这种情况正则就搞不定了,得用语法分析器。所以如果你的场景比较简单,正则够用;如果复杂,建议上lex/yacc。
知识体系总览
下面这张图,是我整理的本章节核心逻辑。你可以看到,SQL解析的四个任务,每个都有对应的正则策略和注意事项。
综合实战:一个完整的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