写有效率的SQL查询(IV)

本文主要介绍写SQL的另外两个误区:
1、 存储过程中使用局部变量而不使用参数变量(就是存储过程输入参数)做where条件
2、 查询条件中类型不匹配

这两种错误都是非常非常容易犯且非常发指的错误,特别是2,太多次见过了。
 
 一、关于存储过程使用局部变量,我们举例说明。
有这么一张表
存储过程:
create proc test
(
    @id int
)
as
select * from charge where charge_no > @id
 
那么 exec test 99998 ,执行计划为:

请注意上图中的估计行数。
 
但是如果把存储过程修改为:
alter proc test
(
    @id int
)
as
declare @local int
set @local = @id
select * from charge where charge_no > @local
 
再次观察 exec test 99998 的查询计划:

请再次注意估计行数,现在是30000了。而我们都知道,修改前存储过程和修改后的输出结果集都没有任何变化,为2。
由于charge_no是聚集索引,而我们的查询条件是where charge_no > XXX,不论SQLServer估计行数有多大,伊都会使用相同的clustered index seek查找到XXX,然后直接顺序遍历基础表剩下的叶节点。
但是,若charge_no是非聚集索引,由于估计结果集行数大小由两行变成了总行数的百分之三十(使用局部变量做查询条件,这种where AAA > BBB,SQLServer无法估计结果集大小,所以它使用默认估计值:30%),nonclustered index seek变成nonclustered index scan(SQL2k5中若不是覆盖查询,会是clustered index scan),这是巨大的性能损耗,必须避免。
 
在这里顺带着再次强调另外一个问题:缓存的查询计划可能会强力的伤害性能。为了更详细的说明它,我们把存储过程test改为:
alter proc test
(
    @id int
)
as
select * from charge where charge_no > @id
 
然后看看执行计划 exec test 99998( 见上面的图,不重复贴了 ) 。再来看看 exec test 1 的执行计划:

我们可以注意到,尽管真实的结果集变动非常巨大,但是查询计划还是完全不变,SQLServer在使用缓存。这种情况在使用聚集索引时不会让查询变得更糟,但是使用非聚集索引就会差上十万八千里,IO开销会差上n个数量级(n取决于真实的结果集)。
所以如果你的查询由于输入参数的不同,选择性变动剧烈,最好在创建存储过程的时候使用 WITH RECOMPILE 选项。即:
create proc test
(
    @id int
)
with recompile
as
select * from charge where charge_no > @id
 
OK,但并不是所有的情况下在查询条件中使用局部变量都有问题。如果查询条件中涉及的索引,SQLServer发现伊的分布密度非常小(比如一个identity(1,1)列或者一个unique),那么在where AAA = XXX的情况下,SQLServer仍然会认为结果集相对总行数很小,而选择index seek类的查询计划。
 
二、  查询条件中的类型不匹配。
    所谓的类型不匹配是说,查询条件where AAA = @var,列AAA的定义和@var不同。例如,AAA是varchar(64),@var是bigint。这种情况下,非常有可能让本来是index seek的运算变成index scan,在大数据量表中,性能差距会非常明显。

    从我的经验来看,并不是所有的隐式转换都会带来这样的问题。但是这样的问题大量的存在,并且在分析性能瓶颈、做索引调优时,会给你带来极大的困扰。必须分析缓存中查询计划对应的原始语句,看那玩意属于慢性自杀。
我们写SQL一般都会类型匹配,但是通过应用程序就非常容易出错。比方说一个表有个MobileNo字段用来存储手机号码,表中是varchar。但是应用程序你这么写:
SqlConnection conn = ...;
           SqlCommand cmd = new SqlCommand("select * from Users where MobileNo = @mo", conn);
           cmd.Parameters.Add(new SqlParameter("@mo", 13511223344));
           SqlDataReader reader = cmd.ExecuteReader();
            //....
那么你挂了……
 
    到现在为止,我没有看到任何资料说哪种形式的隐式转换会让SQL无法判定结果集大小或者可以不去爬整棵索引树。所以我的建议是,使用最强类型去匹配查询列。查询列是啥,就写啥。是varchar(64)就别简单的new SqlParamerer(“@mo”, “13511223344”),要精确指定它的类型、长度。这样做有另外一个好处,偶将在下一篇blog——比较拼SQL、参数化SQL、使用存储过程执行DB指令的优劣时说明(btw:我相信那是一个好坑:))。

 原文地址


==加个总结=====================================
1、存储过程中,能不使用本地变量就不使用,尽可能的使用参数变量(也就是输入参数)。如果不得不使用本地变量,那也得只用在分布密度足够小的索引上使用。
2、写查询条件时,应该尽可能的使类型匹配。使用诸如SqlCommand执行DB指令时,一定要让输入参数从类型到长度严格匹配相应的列。尽管DB端不是所有的隐式转换都会引起性能损耗。
================================================


       稍微提一句,在msdn中SQL Server Database Engine>Troubleshooting the Database Engine > Troubleshooting Queries下有一篇《Troubleshooting Poor Query Performance: Constant Folding and Expression Evaluation During Cardinality Estimation》,尽管说的粗糙无比外带模棱两可,但还是推荐一读。
<think>好的,我现在需要帮用户解决如何用Python解析PL脚本并检测其中的SQL语句是否包含'select *'的问题。首先,我得理解用户的需求。用户可能是在进行代码审查或安全检测,希望自动化检查PL/SQL脚本中是否存在使用'select *'的情况,因为这种法可能存在潜在的风险,比如字段变更导致的问题或者性能问题。 接下来,我得考虑如何解析PL脚本中的SQL语句。PL/SQL代码通常包含在BEGIN...END块中,里面可能混合了SQL语句和其他逻辑。直接使用字符串匹配可能会误判,比如注释中的'select *'或者字符串中的内容。所以需要更精确的方法。 首先,想到的是用正则表达式来提取SQL语句。不过,PL/SQL的结构比较复杂,需要找到所有可能的SELECT语句。可能需要编一个正则表达式来匹配SELECT开头,后面跟着任意字符直到分号或者另一个SQL关键字,比如FROM。但需要考虑多行的情况,以及大小不敏感的问题。 然后,解析步骤可能需要分阶段:先读取PL脚本文件,逐行处理,或者一次性读取整个内容。然后使用正则表达式找出所有SELECT语句,再检查这些语句中是否包含'select *'。需要注意的是,用户可能使用了不同的法,比如SELECT *中间有空格或者换行,或者有注释干扰,比如SELECT /*注释*/ * FROM table。 另外,用户提到的PL脚本可能指的是PL/SQL,也可能是其他类型的存储过程脚本,比如PostgreSQL的PL/pgSQL。不过根据引用[1]和[2],用户可能是在PostgreSQL环境下工作,但问题中的PL脚本更可能指PL/SQL,比如Oracle的存储过程。不过不管哪种,解析方法可能类似。 接下来,需要考虑如何避免误判。例如,字符串中的'select *'不应该被检测到,注释中的内容也需要排除。这时候可能需要一个更复杂的解析器,或者使用AST(抽象语法树)来分析。但Python中是否有合适的PL/SQL解析库呢?可能没有现成的,所以可能需要用正则表达式结合一些处理逻辑。 可能的步骤是: 1. 读取PL脚本文件内容。 2. 使用正则表达式提取所有SQL语句中的SELECT部分。 3. 对每个提取的SELECT语句检查是否有'select *',忽略大小和可能的空格。 4. 输出检测结果,包括行号或位置。 关于正则表达式,可以尝试匹配以SELECT开头,后面跟着任意字符,直到FROM或者其他子句,但需要注意贪婪匹配和非贪婪匹配的问题。例如,模式可能是r'(?i)select\s+\*',其中(?i)表示忽略大小,\s+匹配一个或多个空格,但可能不够,因为可能有换行或注释。 另外,PL脚本中的SQL语句可能用分号结束,或者在动态SQL中,比如EXECUTE IMMEDIATE 'SELECT * FROM ...',这种情况也需要考虑进去。这时候需要处理字符串中的内容,比如单引号内的部分。 这可能变得复杂,因为动态SQL中的字符串可能包含SELECT语句,这时候需要正确解析字符串的内容。例如,在PL/SQL中可能有: v_sql := 'SELECT * FROM employees WHERE id = 1'; 这时候需要解析v_sql这个变量赋值中的字符串内容。所以可能需要先提取所有字符串,然后检查其中的内容是否为SELECT语句。 或者,可能需要处理整个脚本,忽略注释,并提取所有可能的SQL字符串,无论是静态还是动态的。 这可能比较复杂,但用户的需求可能只需要检查所有出现的SELECT语句,不管是动态还是静态的。所以可能需要分步骤处理: 1. 去除注释:PL/SQL中的注释有--和/* */两种形式,需要先去除这些注释,避免里面的内容被误判。 2. 处理字符串:将字符串中的内容提取出来,或者替换掉,避免字符串中的'select *'被误认为实际的SQL语句。例如,在PL/SQL中,字符串可能被单引号包围,并且单引号用两个单引号转义。所以需要正确识别字符串,并将其内容暂时保存或排除。 3. 提取SQL语句:在剩下的代码中查找SELECT语句,包括动态SQL中的语句。 这样可能需要编一个状态机来处理代码中的注释和字符串,然后提取SQL部分。 不过对于Python来说,这样的处理可能需要较多的代码。但用户可能希望一个简单的方法,所以可能需要权衡准确性和实现难度。 例如,一个简化的方法可能是: - 读取整个文件内容,转换为小。 - 使用正则表达式查找所有出现的'select *',但需要考虑前面是否有注释,或者是否是字符串中的内容。 不过这种方法可能会有很多误报,比如字符串中的内容或者注释中的例子。 另一种方法是使用Python的sqlparse库,这个库用于解析SQL语句,可以分割多个SQL语句并分析其中的结构。但sqlparse主要用于标准的SQL,可能对PL/SQL中的特定结构处理不够好,不过对于提取SELECT语句可能足够。 例如,可以先将整个PL脚本内容传递给sqlparse.parse(),然后遍历其中的语句,检查是否有包含'select *'的部分。但需要注意,PL脚本中的SQL可能嵌入在PL代码中,比如在EXECUTE IMMEDIATE后的字符串中,这时候需要先提取这些字符串中的内容,再解析。 可能步骤: 1. 使用正则表达式提取PL脚本中所有字符串(单引号包围的部分),特别是动态SQL中的字符串。 2. 对这些字符串内容使用sqlparse解析,检查是否有SELECT语句中包含'*'。 或者,直接在整个脚本中查找所有'select'关键字,然后检查后面是否有'*',但需要考虑格式问题,比如select后跟换行符或者多个空格,以及是否在注释或字符串中。 总结可能的解决方案: 方案一:使用正则表达式匹配所有出现的'select\s+\*',忽略大小,并排除注释和字符串中的情况。 方案二:使用sqlparse库解析提取出的SQL语句,但需要先处理PL脚本中的动态SQL部分。 方案三:结合处理注释和字符串,然后进行正则匹配。 根据用户的需求,可能方案一更简单,但准确率可能不够;方案三更准确但实现复杂。 现在需要给出一个Python脚本的示例,所以可能需要实现一个基本版本,处理注释和字符串,然后查找'select *'。 首先,处理注释: - 对于行注释--,可以逐行处理,去掉--后面的内容。 - 对于块注释/* ... */,需要用正则表达式替换掉。 其次,处理字符串中的内容,可以用正则表达式找到所有单引号包围的部分,并暂时替换为占位符,或者标记这些区域,避免在解析时误判。 例如,可以将字符串内容提取出来保存,剩下的代码部分进行检测,同时在提取的字符串内容中也检查是否有SELECT *,如果是动态SQL的话。 不过这可能比较复杂,用户可能只需要检查所有出现的'select *',不管是否在字符串或注释中。但用户可能更关注实际执行的SQL,所以需要排除注释,但需要包含动态SQL中的字符串。 所以可能的步骤: 1. 预处理:去除注释。 2. 提取所有字符串内容,并检查其中的SQL语句。 3. 在剩下的代码中查找静态SQL语句(比如在PL/SQL中直接的SELECT语句)。 但如何区分静态和动态SQL?例如,在Oracle PL/SQL中,静态SQL是直接在BEGIN块中的,而动态SQL是拼成字符串的。 不过用户的需求是解析PL脚本中的SQL语句,不管静态还是动态,所以可能需要检查所有可能的SQL字符串。 综上,可能的Python脚本步骤如下: 1. 读取PL脚本文件内容。 2. 去除注释(行注释和块注释)。 3. 提取所有字符串内容,并检查其中是否包含'select *'。 4. 在非字符串的代码部分查找'select *'。 但如何正确去除注释并处理字符串?可能需要使用正则表达式来处理这些部分。 例如,处理块注释的正则表达式:/\*.*?\*/,使用re.DOTALL标志匹配跨行。 处理行注释:--.*$ 处理字符串:'(''|[^'])*',即匹配单引号包围的内容,其中两个单引号是转义。 不过处理字符串的正则表达式可能比较复杂,可以使用re.finditer来找到所有字符串的位置,并记录这些区域,然后在处理代码时跳过这些区域,或者在处理时替换掉字符串,然后再进行检测。 或者,可以分步骤处理: - 先处理块注释,替换为空。 - 处理行注释,替换为空。 - 处理字符串,替换为空或标记。 然后,在剩下的文本中查找'select *',同时也在字符串内容中查找。 例如,代码可能如下: import re def detect_select_star(pl_script): # 去除块注释 pl_script = re.sub(r'/\*.*?\*/', '', pl_script, flags=re.DOTALL) # 去除行注释 pl_script = re.sub(r'--.*$', '', pl_script, flags=re.MULTILINE) # 提取所有字符串内容 strings = re.findall(r"'(''|[^'])*'", pl_script) # 检查字符串中的SELECT * for s in strings: if re.search(r'\bselect\s+\*\b', s, re.IGNORECASE): print(f"发现动态SQL中的'SELECT *':{s}") # 将字符串替换为空,避免重复检测 pl_script_no_strings = re.sub(r"'(''|[^'])*'", '', pl_script) # 在非字符串部分查找SELECT * matches = re.finditer(r'\bselect\s+\*\b', pl_script_no_strings, re.IGNORECASE) for match in matches: print(f"发现静态SQL中的'SELECT *',位置:{match.start()}") # 还可以输出行号等信息,需要更复杂的处理 但这个方法可能不够准确,比如在字符串中如果有转义的情况,或者跨行字符串。例如,PL/SQL中的字符串可以跨行,如: v_sql := 'SELECT * FROM employees'; 这时候正则表达式可能无法正确匹配,因为单引号没有闭合,导致错误。因此可能需要更复杂的字符串处理。 另外,在静态SQL中,比如在PL/SQL的游标声明中,可能有: CURSOR c1 IS SELECT * FROM employees; 这时候需要检测到这种情况。 另外,用户可能需要检测到所有形式的SELECT *,无论是否有别名或其他修饰,例如SELECT t.*,但用户的问题中特别提到的是'select *',所以可能只需要精确匹配这种情况,或者包括SELECT t.*的情况? 用户的需求是检查是否存在'select *',所以可能只需要匹配SELECT后面跟着的*,可能有空格或者换行。例如,SELECT\n* FROM或者SELECT*FROM(无空格),但通常会有空格。 所以正则表达式可能需要更灵活,比如r'(?i)\bselect\s+\*',但这可能无法匹配SELECT*FROM这种情况。所以可能需要使用r'(?i)\bselect\b\s*\*',其中\s*表示零或多个空格。 另外,可能要考虑是否在关键词之后有其他内容,比如SELECT * FROM,或者SELECT * INTO等。 综上,调整正则表达式为更准确的模式:r'(?i)\bselect\b\s*\*',这样可以匹配SELECT后面有任意数量空格的*。 另外,需要处理多行的情况,所以可能需要使用re.DOTALL标志,或者在处理时将换行符替换为空格。 或者,将整个文本转换为单行处理,但可能影响其他部分的匹配。 此外,代码中可能需要处理大小,因此使用re.IGNORECASE标志。 另外,用户可能需要获取具体的行号,以便定位问题。这时候需要逐行处理,或者在处理整个文本时记录行号。 例如,可以先将文本按行分割,逐行处理注释和字符串,记录每行的内容,然后检查每行是否有匹配。 这可能更复杂,但能准确得到行号。 可能的优化步骤: 1. 按行读取文件,逐行处理。 2. 对于每行,先去除行注释(--之后的内容)。 3. 处理块注释,可能需要跨行处理,所以需要跟踪块注释的状态。 4. 处理字符串,同样需要跟踪字符串是否开始,跨行字符串的情况。 这可能需要状态机的实现: - 初始化状态:不在块注释,不在字符串。 - 逐行处理,对于每行: a. 如果不在块注释中: i. 处理行注释:将该行分为两部分,注释前和注释后,只保留注释前部分。 ii. 检查是否有块注释开始/*,处理块注释的开始和结束,分割出非注释部分。 iii. 处理字符串,检查单引号,分割字符串内外的内容。 iv. 在非字符串、非注释的部分检查'select *'。 v. 记录字符串中的内容,并在其中检查是否有'select *'。 b. 如果在块注释中,则跳过该行直到找到*/。 这可能需要较多的代码,但能更准确地处理各种情况。 例如,可以编一个函数来处理这些状态,并收集所有非注释、非字符串的代码部分,以及字符串中的内容,然后分别检查。 不过由于时间限制,可能需要给出一个简化的示例,同时说明其局限性。 因此,在回答中,可以提供一个基于正则表达式的初步解决方案,并指出其局限性,比如无法处理跨行字符串或复杂注释的情况,但对于大多数简单的情况有效。 示例代码可能如下: import re def check_select_star(filename): with open(filename, 'r') as f: content = f.read() # 预处理:去除块注释 content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) # 预处理:去除行注释 content = re.sub(r'--.*', '', content, flags=re.MULTILINE) # 查找所有SELECT * 模式,忽略大小 pattern = re.compile(r'\bselect\s+\*\b', re.IGNORECASE) matches = pattern.finditer(content) found = False for match in matches: start = match.start() # 计算行号 line_num = content.count('\n', 0, start) + 1 print(f"检测到 'SELECT *' 在行号: {line_num}") found = True # 检查动态SQL中的字符串 string_pattern = re.compile(r"'(''|[^'])*'", re.IGNORECASE) strings = string_pattern.findall(content) for s in strings: if re.search(r'\bselect\s+\*\b', s, re.IGNORECASE): print(f"在动态SQL字符串中发现 'SELECT *': {s}") found = True if not found: print("未检测到 'SELECT *' 的使用") # 示例调用 check_select_star('example.plsql') 不过,这个代码可能无法处理跨行字符串和块注释中的内容,比如: 动态SQL可能成: v_sql := 'SELECT * FROM table'; 这时候正则表达式可能无法正确匹配字符串,因为单引号未闭合。因此,可能需要更复杂的字符串处理,比如逐字符分析,跟踪单引号的状态。 但为了简化,可以假设字符串不跨行,或者使用更复杂的正则表达式处理多行字符串。例如,修改字符串的正则表达式为r"'(''|\\'|[^'])*'",但这在PL/SQL中是否正确呢?PL/SQL中的字符串用两个单引号转义,所以正则表达式应为r"'(''|[^'])*'"。但跨行的话,可能需要用re.DOTALL标志,但单引号内的内容可能包含换行符。 例如,修改字符串的正则表达式为: r"'((?:''|[^']|\n)*)'" 但可能需要用更复杂的处理方式,或者分步骤处理。 此外,检测动态SQL中的SELECT *可能需要单独处理字符串中的内容。 综上,回答中需要提供一个Python脚本示例,使用正则表达式处理基本的注释和字符串,并检测SELECT *的出现,同时说明该方法的局限性,建议在复杂情况下使用专业的解析器或更严谨的处理方法。</think>要使用Python解析PL脚本并检测SQL语句中的`SELECT *`用法,可以采用以下分步实现方法: ### 分步解决方案 1. **预处理脚本内容** - 移除块注释`/*...*/` - 移除行注释`--` - 提取所有字符串内容用于动态SQL检测 2. **静态SQL检测** - 在非注释、非字符串区域检测`SELECT *`模式 3. **动态SQL检测** - 分析字符串内容中的SQL语句 ### Python实现代码 ```python import re def check_select_star(filename): with open(filename, 'r', encoding='utf-8') as f: content = f.read() # 移除块注释(支持跨行) content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) # 移除行注释 content = re.sub(r'--.*$', '', content, flags=re.MULTILINE) # 检测静态SQL中的SELECT * static_pattern = re.compile( r'\bselect\s+\*', re.IGNORECASE | re.DOTALL ) static_matches = static_pattern.finditer(content) # 检测动态SQL中的SELECT * string_pattern = re.compile( r"'((?:''|\\'|[^']|\n)*')", re.IGNORECASE | re.DOTALL ) dynamic_matches = [] for match in string_pattern.finditer(content): sql_str = match.group(1) if re.search(r'\bselect\s+\*', sql_str, re.IGNORECASE): dynamic_matches.append(match) # 输出结果 found = False lines = content.split('\n') # 静态SQL检测结果 for match in static_matches: start = match.start() line_num = content.count('\n', 0, start) + 1 print(f"静态SQL检测:行号 {line_num} - 包含 SELECT *") found = True # 动态SQL检测结果 for match in dynamic_matches: start = match.start() line_num = content.count('\n', 0, start) + 1 print(f"动态SQL检测:行号 {line_num} - 字符串内包含 SELECT *") found = True if not found: print("未发现 SELECT * 用法") # 示例调用 check_select_star("sample.plsql") ``` ### 代码说明 1. **注释处理** 使用正则表达式移除PL/SQL中的注释,避免误判注释内容中的`SELECT *`[^4]。 2. **字符串提取** 通过正则表达式`'((?:''|\\'|[^']|\n)*')`匹配包含转义单引号的字符串,支持跨行字符串检测。 3. **模式检测** - 静态SQL:直接检测非注释区域的`SELECT *` - 动态SQL:分析字符串内容中的SQL模式 ### 局限性说明 1. 无法解析复杂嵌套语法结构 2. 可能误判包含`SELECT *`的注释或文本内容 3. 建议结合专业SQL解析器(如`sqlparse`库)提升准确性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值