C#程序实现将SQL Server的存储过程转换为PySpark代码
·
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Text.Json;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo;
class SqlServerProcToPySparkConverter
{
// -------------------------- 配置参数 --------------------------
private static string _sqlServerName = "localhost"; // SQL Server地址
private static string _databaseName = "YourDatabase"; // 目标数据库
private static string _extractedSqlFolder = "./ExtractedProcs"; // 提取的.sql文件保存路径
private static string _parsedJsonPath = "./ParsedProcs.json"; // 解析结果JSON路径
private static string _generatedPyPath = "./PySparkProcs.py"; // 生成的PySpark文件路径
// --------------------------------------------------------------
static void Main(string[] args)
{
Console.WriteLine("===== 开始SQL Server存储过程转PySpark流程 =====");
// 1. 从SQL Server提取存储过程到本地.sql文件
Console.WriteLine("\n1. 从SQL Server提取存储过程...");
bool extractSuccess = ExtractProcsFromSqlServer();
if (!extractSuccess) { Console.WriteLine("提取失败,流程终止"); return; }
// 2. 解析.sql文件中的存储过程,生成结构化JSON
Console.WriteLine("\n2. 解析存储过程生成JSON...");
var allParsedProcs = ParseProcsFromSqlFiles();
if (allParsedProcs.Count == 0) { Console.WriteLine("无有效存储过程,流程终止"); return; }
SaveParsedProcsToJson(allParsedProcs);
// 3. 基于JSON生成PySpark Python文件
Console.WriteLine("\n3. 生成PySpark代码...");
bool generateSuccess = GeneratePySparkCode(allParsedProcs);
if (!generateSuccess) { Console.WriteLine("代码生成失败,流程终止"); return; }
Console.WriteLine("\n===== 流程完成!生成文件路径:=====");
Console.WriteLine($"JSON解析结果:{_parsedJsonPath}");
Console.WriteLine($"PySpark代码:{_generatedPyPath}");
}
#region 功能1:从SQL Server提取存储过程(SMO)
/// <summary>
/// 使用SMO连接SQL Server,提取非系统存储过程到本地.sql文件
/// </summary>
private static bool ExtractProcsFromSqlServer()
{
try
{
// 创建输出目录
if (!Directory.Exists(_extractedSqlFolder))
Directory.CreateDirectory(_extractedSqlFolder);
// 连接SQL Server
ServerConnection conn = new ServerConnection(_sqlServerName);
Server sqlServer = new Server(conn);
Database targetDb = sqlServer.Databases[_databaseName];
if (targetDb == null)
{
Console.WriteLine($"❌ 未找到数据库:{_databaseName}");
return false;
}
// 提取所有非系统存储过程
var procs = targetDb.StoredProcedures.Cast<StoredProcedure>()
.Where(p => !p.IsSystemObject && !string.IsNullOrWhiteSpace(p.TextBody))
.ToList();
if (procs.Count == 0)
{
Console.WriteLine($"⚠️ 数据库 {_databaseName} 中无有效存储过程");
return false;
}
// 保存每个存储过程到.sql文件
foreach (var proc in procs)
{
string procFullName = $"{proc.Schema}.{proc.Name}";
string sqlFilePath = Path.Combine(_extractedSqlFolder, $"{proc.Schema}_{proc.Name}.sql");
File.WriteAllText(sqlFilePath, proc.TextBody);
Console.WriteLine($"✅ 提取:{procFullName} → {sqlFilePath}");
}
Console.WriteLine($"✅ 共提取 {procs.Count} 个存储过程");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"❌ 提取异常:{ex.Message}");
return false;
}
}
#endregion
#region 功能2:解析.sql文件生成结构化数据
/// <summary>
/// 读取提取的.sql文件,解析存储过程结构
/// </summary>
private static List<dynamic> ParseProcsFromSqlFiles()
{
var allParsedProcs = new List<dynamic>();
var sqlFiles = Directory.GetFiles(_extractedSqlFolder, "*.sql");
if (sqlFiles.Length == 0)
{
Console.WriteLine($"⚠️ {_extractedSqlFolder} 目录下无.sql文件");
return allParsedProcs;
}
foreach (var sqlFile in sqlFiles)
{
Console.WriteLine($"🔍 解析文件:{Path.GetFileName(sqlFile)}");
string sqlContent = File.ReadAllText(sqlFile);
var procBlocks = ExtractProcedureBlocks(sqlContent);
foreach (var block in procBlocks)
{
var parsedProc = ParseSingleProcedure(block);
allParsedProcs.Add(parsedProc);
Console.WriteLine($" ✅ 解析完成:{parsedProc.ProcedureName}");
}
}
return allParsedProcs;
}
/// <summary>
/// 提取单个.sql文件中的所有CREATE PROCEDURE块
/// </summary>
private static List<string> ExtractProcedureBlocks(string sqlContent)
{
var blocks = new List<string>();
var regex = new Regex(
@"CREATE\s+PROCEDURE\s+([\w\.]+)\s*([\s\S]*?)AS\s+BEGIN\s*([\s\S]*?)END\s*;",
RegexOptions.IgnoreCase | RegexOptions.Singleline);
foreach (Match m in regex.Matches(sqlContent))
{
if (m.Groups.Count >= 4)
{
string fullProcText = $"CREATE PROCEDURE {m.Groups[1].Value} {m.Groups[2].Value} AS BEGIN {m.Groups[3].Value} END;";
blocks.Add(fullProcText);
}
}
return blocks;
}
/// <summary>
/// 解析单个存储过程的完整结构(参数、变量、查询等)
/// </summary>
private static dynamic ParseSingleProcedure(string procText)
{
// 提取基础信息
string procName = ExtractProcedureName(procText);
var parameters = ParseParameters(procText);
var variables = ParseVariables(procText);
var queries = ParseSelectQueries(procText);
var dml = ParseDmlStatements(procText);
var tempTables = ParseTempTables(procText);
var tryCatch = ParseTryCatch(procText);
var cursors = ParseCursors(procText);
var dynamicSql = ParseDynamicSql(procText);
bool hasResultSet = HasSelectResultSet(procText);
// 整合结构化结果
return new
{
ProcedureName = procName,
RawText = procText,
Parameters = parameters,
Variables = variables,
SelectQueries = queries,
DmlStatements = dml,
TempTables = tempTables,
TryCatchBlocks = tryCatch,
Cursors = cursors,
DynamicSql = dynamicSql,
HasResultSet = hasResultSet
};
}
// -------------------------- 解析子模块 --------------------------
private static string ExtractProcedureName(string procText)
{
var match = Regex.Match(procText, @"CREATE\s+PROCEDURE\s+([\w\.]+)", RegexOptions.IgnoreCase);
return match.Success ? match.Groups[1].Value : "UnknownProc";
}
private static dynamic ParseParameters(string procText)
{
var input = new List<string>();
var output = new List<string>();
var regex = new Regex(@"@(\w+)\s+([\w\(\)]+)\s*(OUTPUT)?", RegexOptions.IgnoreCase);
foreach (Match m in regex.Matches(procText))
{
string param = $"@{m.Groups[1].Value} ({m.Groups[2].Value})";
if (m.Groups[3].Success) output.Add(param);
else input.Add(param);
}
return new { Input = input, Output = output };
}
private static List<dynamic> ParseVariables(string procText)
{
var vars = new List<dynamic>();
var regex = new Regex(@"DECLARE\s+@(\w+)\s+([\w\(\)]+)(?:\s*=\s*([^;]+))?", RegexOptions.IgnoreCase);
foreach (Match m in regex.Matches(procText))
{
vars.Add(new
{
Name = $"@{m.Groups[1].Value}",
Type = m.Groups[2].Value,
InitialValue = m.Groups[3]?.Value ?? "NULL"
});
}
return vars;
}
private static List<dynamic> ParseSelectQueries(string procText)
{
var queries = new List<dynamic>();
var regex = new Regex(@"SELECT\s+([\s\S]*?)\s+FROM\s+([\s\S]*?)(?:WHERE\s+([\s\S]*?))?(?:GROUP\s+BY\s+([\s\S]*?))?(?:ORDER\s+BY\s+([\s\S]*?))?;", RegexOptions.IgnoreCase);
foreach (Match m in regex.Matches(procText))
{
queries.Add(new
{
Fields = m.Groups[1].Value.Trim(),
From = m.Groups[2].Value.Trim(),
Where = m.Groups[3]?.Value?.Trim() ?? "",
GroupBy = m.Groups[4]?.Value?.Trim() ?? "",
OrderBy = m.Groups[5]?.Value?.Trim() ?? ""
});
}
return queries;
}
private static List<string> ParseDmlStatements(string procText)
{
var dml = new List<string>();
dml.AddRange(Regex.Matches(procText, @"INSERT\s+[\s\S]*?;", RegexOptions.IgnoreCase).Cast<Match>().Select(m => m.Value));
dml.AddRange(Regex.Matches(procText, @"UPDATE\s+[\s\S]*?;", RegexOptions.IgnoreCase).Cast<Match>().Select(m => m.Value));
dml.AddRange(Regex.Matches(procText, @"DELETE\s+[\s\S]*?;", RegexOptions.IgnoreCase).Cast<Match>().Select(m => m.Value));
return dml;
}
private static List<dynamic> ParseTempTables(string procText)
{
var temps = new List<dynamic>();
var regex = new Regex(@"CREATE\s+TABLE\s+#(\w+)\s*\(([\s\S]*?)\)", RegexOptions.IgnoreCase);
foreach (Match m in regex.Matches(procText))
{
temps.Add(new
{
Name = $"#{m.Groups[1].Value}",
Columns = m.Groups[2].Value.Trim()
});
}
return temps;
}
private static List<dynamic> ParseTryCatch(string procText)
{
var tryCatch = new List<dynamic>();
var tryRegex = new Regex(@"BEGIN\s+TRY\s*([\s\S]*?)\s*END\s+TRY", RegexOptions.IgnoreCase);
var catchRegex = new Regex(@"BEGIN\s+CATCH\s*([\s\S]*?)\s*END\s+CATCH", RegexOptions.IgnoreCase);
var tryBlocks = tryRegex.Matches(procText);
var catchBlocks = catchRegex.Matches(procText);
for (int i = 0; i < Math.Min(tryBlocks.Count, catchBlocks.Count); i++)
{
tryCatch.Add(new
{
TryBlock = tryBlocks[i].Groups[1].Value.Trim(),
CatchBlock = catchBlocks[i].Groups[1].Value.Trim()
});
}
return tryCatch;
}
private static List<dynamic> ParseCursors(string procText)
{
var cursors = new List<dynamic>();
var regex = new Regex(@"DECLARE\s+(\w+)\s+CURSOR\s+FOR\s+([\s\S]*?);", RegexOptions.IgnoreCase);
foreach (Match m in regex.Matches(procText))
{
cursors.Add(new
{
Name = m.Groups[1].Value,
Query = m.Groups[2].Value.Trim(),
PySparkNote = "使用DataFrame.filter/map替代游标逐行逻辑"
});
}
return cursors;
}
private static List<string> ParseDynamicSql(string procText)
{
return Regex.Matches(procText, @"EXEC\s+sp_executesql\s+[\s\S]*?;", RegexOptions.IgnoreCase)
.Cast<Match>().Select(m => m.Value).ToList();
}
private static bool HasSelectResultSet(string procText)
{
return Regex.IsMatch(procText, @"SELECT\s+[^@]", RegexOptions.IgnoreCase); // 排除变量赋值的SELECT
}
/// <summary>
/// 保存解析结果到JSON文件
/// </summary>
private static void SaveParsedProcsToJson(List<dynamic> parsedProcs)
{
string json = JsonSerializer.Serialize(parsedProcs, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_parsedJsonPath, json);
Console.WriteLine($"✅ JSON已保存到:{_parsedJsonPath}");
}
#endregion
#region 功能3:生成PySpark代码
/// <summary>
/// 基于解析的存储过程结构,生成PySpark函数代码
/// </summary>
private static bool GeneratePySparkCode(List<dynamic> parsedProcs)
{
try
{
// 构建PySpark代码模板
var pyBuilder = new System.Text.StringBuilder();
pyBuilder.AppendLine("\"\"\"");
pyBuilder.AppendLine($"自动生成的SQL Server存储过程转PySpark代码");
pyBuilder.AppendLine($"生成时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}");
pyBuilder.AppendLine($"包含存储过程数量:{parsedProcs.Count}");
pyBuilder.AppendLine("\"\"\"");
pyBuilder.AppendLine("from pyspark.sql import SparkSession");
pyBuilder.AppendLine("from pyspark.sql.functions import col, lit, expr, when");
pyBuilder.AppendLine("from pyspark.sql.types import *");
pyBuilder.AppendLine("import logging");
pyBuilder.AppendLine();
pyBuilder.AppendLine("# 初始化SparkSession(可根据实际场景调整)");
pyBuilder.AppendLine("def init_spark(app_name: str = \"ProcToPySpark\") -> SparkSession:");
pyBuilder.AppendLine(" return SparkSession.builder.appName(app_name).getOrCreate()");
pyBuilder.AppendLine();
// 为每个存储过程生成一个PySpark函数
foreach (var proc in parsedProcs)
{
string procName = proc.ProcedureName.Replace(".", "_"); // 处理架构名(如dbo.Proc1→dbo_Proc1)
var inputParams = proc.Parameters.Input.Select(p =>
{
string paramName = Regex.Match(p, @"@(\w+)").Groups[1].Value;
return $"{paramName}: str | int | float = None"; // 简化参数类型
});
// 1. 函数签名
pyBuilder.AppendLine($"def {procName}(");
pyBuilder.AppendLine($" spark: SparkSession,");
pyBuilder.AppendLine($" {string.Join(",\n ", inputParams)},");
pyBuilder.AppendLine($" logger: logging.Logger = None");
pyBuilder.AppendLine($") -> dict | None:");
pyBuilder.AppendLine($" \"\"\"");
pyBuilder.AppendLine($" 对应SQL Server存储过程:{proc.ProcedureName}");
pyBuilder.AppendLine($" 返回值:包含结果集(DataFrame)或执行状态的字典");
pyBuilder.AppendLine($" \"\"\"");
pyBuilder.AppendLine($" if logger is None:");
pyBuilder.AppendLine($" logger = logging.getLogger(__name__)");
pyBuilder.AppendLine();
// 2. 变量定义(映射T-SQL局部变量)
if (proc.Variables.Count > 0)
{
pyBuilder.AppendLine($" # 局部变量定义(映射T-SQL变量)");
foreach (var var in proc.Variables)
{
string pyVarName = var.Name.Substring(1); // 去掉@符号
string initValue = var.InitialValue.Equals("NULL", StringComparison.OrdinalIgnoreCase) ? "None" : var.InitialValue;
pyBuilder.AppendLine($" {pyVarName} = {initValue} # T-SQL类型:{var.Type}");
}
pyBuilder.AppendLine();
}
// 3. 临时表处理(映射为Spark临时视图或DataFrame)
if (proc.TempTables.Count > 0)
{
pyBuilder.AppendLine($" # 临时表处理(映射Spark临时视图)");
foreach (var temp in proc.TempTables)
{
string tempViewName = temp.Name.Substring(1); // 去掉#符号
pyBuilder.AppendLine($" # T-SQL临时表:{temp.Name}");
pyBuilder.AppendLine($" # 列定义:{temp.Columns}");
pyBuilder.AppendLine($" # 方案:1. 从查询创建 | 2. 定义Schema后插入");
pyBuilder.AppendLine($" # {tempViewName}_schema = StructType([");
pyBuilder.AppendLine($" # # 示例:StructField("col1", IntegerType(), nullable=True)");
pyBuilder.AppendLine($" # ])");
pyBuilder.AppendLine($" # {tempViewName}_df = spark.createDataFrame([], schema={tempViewName}_schema)");
pyBuilder.AppendLine($" # {tempViewName}_df.createOrReplaceTempView("{tempViewName}")");
pyBuilder.AppendLine();
}
}
// 4. 游标处理(提示用DataFrame集合操作替代)
if (proc.Cursors.Count > 0)
{
pyBuilder.AppendLine($" # 游标处理(PySpark推荐用集合操作替代逐行逻辑)");
foreach (var cursor in proc.Cursors)
{
pyBuilder.AppendLine($" # T-SQL游标:{cursor.Name},查询:{cursor.Query.Substring(0, Math.Min(50, cursor.Query.Length))}...");
pyBuilder.AppendLine($" # 替代方案:直接执行查询生成DataFrame,用filter/map/groupBy等操作处理");
pyBuilder.AppendLine($" # cursor_df = spark.sql("{cursor.Query.Replace(""", "\"")}")");
pyBuilder.AppendLine($" # 后续逻辑:cursor_df = cursor_df.filter(...).withColumn(...)");
pyBuilder.AppendLine();
}
}
// 5. 核心查询逻辑(映射SELECT为DataFrame/SQL)
if (proc.SelectQueries.Count > 0)
{
pyBuilder.AppendLine($" # 核心查询逻辑(T-SQL SELECT → PySpark DataFrame)");
for (int i = 0; i < proc.SelectQueries.Count; i++)
{
var query = proc.SelectQueries[i];
string dfName = $"result_df_{i + 1}";
pyBuilder.AppendLine($" # 查询{i + 1}:提取字段={query.Fields}, 来源={query.From}");
// 构建Spark SQL语句
string sparkSql = $"SELECT {query.Fields} FROM {query.From}";
if (!string.IsNullOrWhiteSpace(query.Where)) sparkSql += $" WHERE {query.Where}";
if (!string.IsNullOrWhiteSpace(query.GroupBy)) sparkSql += $" GROUP BY {query.GroupBy}";
if (!string.IsNullOrWhiteSpace(query.OrderBy)) sparkSql += $" ORDER BY {query.OrderBy}";
pyBuilder.AppendLine($" {dfName} = spark.sql("{sparkSql.Replace(""", "\"")}")");
pyBuilder.AppendLine($" logger.info(f"查询{i + 1}执行完成,返回行数:{dfName}.count()")");
pyBuilder.AppendLine();
}
}
// 6. DML操作(映射INSERT/UPDATE/DELETE为Spark操作)
if (proc.DmlStatements.Count > 0)
{
pyBuilder.AppendLine($" # DML操作(T-SQL → PySpark)");
foreach (var dml in proc.DmlStatements)
{
if (dml.StartsWith("INSERT", StringComparison.OrdinalIgnoreCase))
{
pyBuilder.AppendLine($" # INSERT操作:{dml.Substring(0, Math.Min(60, dml.Length))}...");
pyBuilder.AppendLine($" # 方案:1. 用df.write.saveAsTable(...) | 2. 执行Spark SQL INSERT");
pyBuilder.AppendLine($" # spark.sql("{dml.Replace(""", "\"")}")");
}
else if (dml.StartsWith("UPDATE", StringComparison.OrdinalIgnoreCase))
{
pyBuilder.AppendLine($" # UPDATE操作:{dml.Substring(0, Math.Min(60, dml.Length))}...");
pyBuilder.AppendLine($" # 方案:用withColumn更新字段后覆盖写入(注意分区表逻辑)");
pyBuilder.AppendLine($" # update_df = spark.table("表名").withColumn("列名", 新值).filter("条件")");
pyBuilder.AppendLine($" # update_df.write.mode("overwrite").saveAsTable("表名")");
}
else if (dml.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase))
{
pyBuilder.AppendLine($" # DELETE操作:{dml.Substring(0, Math.Min(60, dml.Length))}...");
pyBuilder.AppendLine($" # 方案:筛选保留数据后覆盖写入(避免全表删除)");
pyBuilder.AppendLine($" # keep_df = spark.table("表名").filter("NOT 删除条件")");
pyBuilder.AppendLine($" # keep_df.write.mode("overwrite").saveAsTable("表名")");
}
pyBuilder.AppendLine();
}
}
// 7. 异常处理(映射TRY/CATCH为Python try/except)
if (proc.TryCatchBlocks.Count > 0)
{
pyBuilder.AppendLine($" # 异常处理(映射T-SQL TRY/CATCH)");
pyBuilder.AppendLine($" try:");
pyBuilder.AppendLine($" # TRY块逻辑(已整合到上方查询/DML中)");
pyBuilder.AppendLine($" pass");
pyBuilder.AppendLine($" except Exception as e:");
pyBuilder.AppendLine($" logger.error(f"执行存储过程{proc.ProcedureName}失败:{str(e)}")");
pyBuilder.AppendLine($" # 可选:添加回滚逻辑(如删除临时视图)");
pyBuilder.AppendLine($" # spark.catalog.dropTempView("临时视图名")");
pyBuilder.AppendLine($" raise # 根据实际需求决定是否抛出异常");
pyBuilder.AppendLine();
}
// 8. 结果返回(根据是否有结果集返回DataFrame或状态)
pyBuilder.AppendLine($" # 结果返回");
if (proc.HasResultSet)
{
pyBuilder.AppendLine($" # 有结果集:返回最后一个DataFrame或结果字典");
pyBuilder.AppendLine($" return {{");
pyBuilder.AppendLine($" "procedure_name": "{proc.ProcedureName}",");
pyBuilder.AppendLine($" "status": "success",");
pyBuilder.AppendLine($" "last_result_df": {dfName if proc.SelectQueries.Count > 0 else "None"}");
pyBuilder.AppendLine($" }}");
}
else
{
pyBuilder.AppendLine($" # 无结果集:返回执行状态");
pyBuilder.AppendLine($" return {{");
pyBuilder.AppendLine($" "procedure_name": "{proc.ProcedureName}",");
pyBuilder.AppendLine($" "status": "success",");
pyBuilder.AppendLine($" "message": "DML操作执行完成"");
pyBuilder.AppendLine($" }}");
}
pyBuilder.AppendLine();
pyBuilder.AppendLine();
}
// 9. 测试调用示例
pyBuilder.AppendLine("# -------------------------- 测试调用示例 --------------------------");
pyBuilder.AppendLine("if name == "main":");
pyBuilder.AppendLine(" # 初始化日志和Spark");
pyBuilder.AppendLine(" logging.basicConfig(level=logging.INFO)");
pyBuilder.AppendLine(" spark = init_spark()");
pyBuilder.AppendLine(" logger = logging.getLogger(name)");
pyBuilder.AppendLine();
if (parsedProcs.Count > 0)
{
string testProcName = parsedProcs[0].ProcedureName.Replace(".", "_");
var testParams = parsedProcs[0].Parameters.Input.Select(p =>
{
string paramName = Regex.Match(p, @"@(\w+)").Groups[1].Value;
return $"{paramName}=123"; // 示例参数值
});
pyBuilder.AppendLine($" # 调用第一个存储过程示例");
pyBuilder.AppendLine($" result = {testProcName}(spark, {string.Join(", ", testParams)}, logger)");
pyBuilder.AppendLine($" logger.info(f"调用结果:{result}")");
}
pyBuilder.AppendLine(" spark.stop()");
// 保存PySpark代码到文件
File.WriteAllText(_generatedPyPath, pyBuilder.ToString());
return true;
}
catch (Exception ex)
{
Console.WriteLine($"❌ 生成PySpark代码异常:{ex.Message}");
return false;
}
}
#endregion
}
关键说明与使用步骤
-
环境准备
- 安装 .NET 6+ SDK(支持 C# 10+)
- 安装 SMO NuGet 包:
dotnet add package Microsoft.SqlServer.SqlManagementObjects - 确保目标机器可访问 SQL Server(开放 1433 端口,具备
VIEW DEFINITION权限)
-
配置修改
- 找到代码中
配置参数区域,修改:_sqlServerName:SQL Server 地址(如localhost或192.168.1.100\SQLEXPRESS)_databaseName:需要提取存储过程的数据库名(如SalesDB)- 输出路径(
_extractedSqlFolder/_parsedJsonPath/_generatedPyPath)可按需调整
- 找到代码中
-
运行流程
- 执行程序后,自动连接 SQL Server 提取非系统存储过程,保存为
.sql文件到./ExtractedProcs - 解析所有
.sql文件,生成包含存储过程结构的 JSON(ParsedProcs.json) - 基于 JSON 生成 PySpark 代码(
PySparkProcs.py),包含:- 每个存储过程对应的 PySpark 函数
- 变量/临时表/游标/DML 的等效实现方案
- 异常处理与日志记录
- 测试调用示例
- 执行程序后,自动连接 SQL Server 提取非系统存储过程,保存为
生成的 PySpark 代码特点
- 兼容性:适配 PySpark 3.x,使用 DataFrame API + Spark SQL 混合实现
- 可扩展性:每个模块(变量/查询/DML)都有注释说明,支持根据实际业务调整逻辑
- 安全性:避免动态 SQL 风险,推荐用参数化查询替代
sp_executesql - 易用性:包含
init_spark工具函数和测试调用示例,开箱即用。
更多推荐
所有评论(0)