如何处理耗时的 SQL?

Posted

技术标签:

【中文标题】如何处理耗时的 SQL?【英文标题】:How can I handle the time consuming SQL? 【发布时间】:2010-08-25 10:29:18 【问题描述】:

我们有一个包含 600 万条记录的表,然后我们有一个 SQL 需要大约 7 分钟来查询结果。我认为 SQL 不能再优化了。

查询时间导致我们的weblogic抛出最大阻塞线程异常。

有什么建议可以让我处理这个问题吗?

以下是查询,但我很难更改它,

SELECT * FROM  table1 
WHERE trim(StudentID) IN ('354354','0') 
AND concat(concat(substr(table1.LogDate,7,10),'/'),substr(table1.LogDate,1,5)) 
       BETWEEN '2009/02/02' AND '2009/03/02' 
AND TerminalType='1' 
AND RecStatus='0' ORDER BY StudentID, LogDate DESC, LogTime

但是,我知道使用字符串来比较日期很耗时,但是在我无法更改表结构之前有人写道...

LogDate被定义为一个字符串,格式是mm/dd/yyyy,所以我们需要在...和...之间进行substring和concat,我觉得这里很难优化。

【问题讨论】:

在不知道您的索引或表结构等的情况下,我们无法评论问题是否是 SQL 问题。 在日期列上使用 concat() 和 substr() 对我来说没有任何意义,并且肯定会阻止使用任何索引。另外,为什么要修剪() StudentID 列?如果是PK的话,听起来就很诡异。 你能展示一下这张桌子的样子吗?即列类型是什么? 【参考方案1】:

这个查询很有可能正在执行全文件扫描,因为您的 WHERE 条件不太可能利用任何索引。

LogDate 是日期字段还是文本字段?如果是日期字段,则不要执行 substr 和 concat。只需说“'2009-02-02' 和 '2009-02-03' 之间的 LogDate 或任何日期范围。如果它被定义为文本字段,您应该认真考虑将其重新定义为日期字段。(如果您的日期真的是文本并且写成 mm/dd/yyyy 那么如果日期跨度超过一年,那么您的 ORDER BY ... LOGDATE DESC 将不会给出有用的结果。)

StudentID 是否需要修整?在将数据放入数据库之前清理数据要好得多,然后每次检索时都尝试清理它。

如果将 LogDate 定义为日期,并且您可以在输入时修剪 studentid,然后在一个或两个字段上创建索引,查询时间应该会大幅下降。

或者,如果您想要一个快速而肮脏的解决方案,请在“trim(studentid)”上创建一个索引。

如果这没有帮助,请向我们提供有关您的表格布局和索引的更多信息。

【讨论】:

oracle 会自动确定那些“And”子句的执行顺序吗?由于 RecStatus='0' 会过滤掉大部分数据 简短回答:基本上,是的。更长的答案:Oracle(或任何 SQL 引擎)有一个“优化器”,它构建一个“查询计划”,这是它将用来满足查询的逻辑。基本上,这意味着决定读取和连接表的顺序——这里的一个有争议的问题是这个查询只使用一个表——以及使用什么索引。与其说它测试条件的顺序是什么,不如说它能够通过索引满足什么条件,以及它必须逐条读取并保留或丢弃什么条件。 (续...) 如果 RecStatus 测试可以消除大部分不需要的记录,那么您应该在 RecStatus 上创建一个索引。通常每个查询每个表只能使用一个索引,因此如果您通常只需要某个 StudentID 内的某个 RecStatus,那么您应该在 (StudentID, RecStatus) 上创建一个索引。 (Oracle 也有位图索引,您可以在每个查询中使用多个索引,但这变得越来越复杂。)【参考方案2】:
SELECT * ... WHERE trim(StudentID) IN ('354354','0')

如果这是正常构造,那么您需要一个function based index。因为没有它你会强制数据库服务器执行全表扫描。

根据经验,您应该尽可能避免在WHERE 子句中使用函数。 trim(StundentID)substr(table1.LogDate,7,10) 阻止数据库服务器使用任何索引或对查询应用任何优化。尝试尽可能多地使用本机数据类型,例如DATE 而不是 VARCHAR 用于 LogDateStudentID 也应该在客户端软件中正确管理,例如修剪INSERT/UPDATE之前的数据。

【讨论】:

【参考方案3】:

如果你的数据库支持它,你可能想试试materialized view。

如果没有,可能值得考虑自己实现类似的东西,通过安排一个运行查询的计划作业,该查询执行昂贵的修剪和连接并使用结果刷新表,以便您可以针对更好的查询运行表,避免昂贵的东西。或者使用触发器来维护这样的表。

【讨论】:

【参考方案4】:

但是查询时间导致我们的weblogic抛出最大阻塞线程异常。

如果查询需要 7 分钟且无法加快,您必须停止实时运行此查询。您可以更改您的应用程序以查询您定期刷新的缓存结果表吗?

作为在此之前的紧急权宜之计,您可以实现一个闩锁(在 Java 中),它一次只允许一个线程执行此查询。第二个线程将立即因错误而失败(而不是使整个系统停机)。这可能不会让这个查询的用户满意,但至少它保护了其他所有人。

我更新了查询,你能给我一些建议吗?

这些字符串操作使索引几乎不可能。你确定你至少不能摆脱“修剪”吗?实际数据中真的有多余的空格吗?如果是这样,您可以将范围缩小到一个 student_id,这应该会加快速度。

您希望在 (student_id, log_date) 上有一个复合索引,希望复杂的 log_date 条件仍然可以使用索引范围扫描来解决(对于给定的学生 id)。

【讨论】:

【参考方案5】:

如果没有关于您正在执行哪种查询以及是否使用索引的任何进一步信息,很难提供任何具体信息。

但这里有一些一般提示。

    确保对经常过滤/排序的列使用索引。 如果只是某个查询太慢,也许您可​​以通过在数据库更改时自动生成结果来阻止自己执行该查询。例如,您通常可以将计数存储在某处,而不是 count()

尝试通过在将数据插入表之前/同时自动调用trim() 来从查询中删除trim()。这样您就可以简单地使用索引来查找StudentID

此外,date 过滤器应该可以在您的数据库中本地使用。在不知道哪个数据库可能更困难的情况下,但这样的事情应该可以工作:LogDate BETWEEN '2009-02-02' AND '2009-02-02'

如果您还在所有这些列上添加索引(即StudentIDLogDateTerminalTypeRecStatusEmployeeID,那么它应该是闪电般的速度。

【讨论】:

如果您无法在执行插入的应用程序中修剪数据,您可以使用 on insert/update 触发器强制 TRIM 修改 new.StudentId。但 Id 列似乎不太可能是字符串。 oracle 会自动确定那些“And”子句的执行顺序吗? @MemoryLeak:Oracle 理解如何执行BETWEEN 子句将毫无问题。剩下的,如果你只有AND 子句,那么顺序就无关紧要了。必须满足所有条件。 我只是觉得“和”的不同执行顺序会有不同的表现。 @MemoryLeak:应该没什么区别。无论过滤器的顺序如何,查询规划器都会自动选择执行查询的最佳方式。【参考方案6】:

在不知道您使用的是什么数据库以及您的表结构是什么的情况下,很难提出任何改进建议,但可以通过使用索引、提示等来改进查询。

在您的查询中,以下部分 concat(concat(substr(table1.LogDate,7,10),'/'), substr(table1.LogDate,1,5)) BETWEEN '2009/02/02' AND '2009/02/02'

太搞笑了。 在“2009/02/02”和“2009/02/02”之间 ??伙计,你想做什么?

你能把你的表结构贴在这里吗?

无论如何,600 万条记录并不是什么大事。

【讨论】:

+1 for the BETWEEN on strings ;) 我很想看到这段代码处理 Gazillionember 2010 的第 7 届。【参考方案7】:

很多人告诉你,你的问题出在日期字段上。您肯定需要将日期从字符串字段更改为本机日期类型。如果它是以这种确切方式在您的应用程序中使用的遗留字段 - 您仍然可以创建一个基于函数的to_date(logdate, 'DD/MM/YYYY') 索引,将您的“字符串”日期转换为“日期”日期,并允许快速已经提到的@ 987654322@搜索而不修改你的表数据。

这应该会加快速度。

【讨论】:

【参考方案8】:

根据您提供的少量信息,我的预感是以下子句为我们提供了线索:

     ... WHERE trim(StudentID) IN ('354354','0') 

如果您有大量学生身份不明的记录(即 studentID=0),studentID 上的索引将非常不平衡。

在 600 万条记录中,有多少条 studentId=0?

【讨论】:

另外,学生证是数字还是文本?查看查询中的引号,它似乎被定义为文本列。如果是整数,去掉引号。【参考方案9】:

您的主要问题是您的查询将所有内容都视为字符串。

如果 LogDate 是没有时间组件的日期,您需要类似以下的内容

SELECT * FROM  table1 
WHERE  StudentID  IN (:SearchStudentId,0) 
AND  table1.LogDate = :SearchDate
AND TerminalType='1' 
AND RecStatus='0' 
ORDER BY EmployeeID, LogDate DESC, LogTime

如果 LogDate 有时间组件,而 SearchDate 没有时间组件,那么类似这样。 (.99999 会将时间设置为午夜前 1 秒)

SELECT * FROM  table1 
WHERE  StudentID  IN (:SearchStudentId,:StudentId0) 
AND  table1.LogDate BETWEEN :SearchDate AND :SearchDate+0.99999 
AND TerminalType='1' 
AND RecStatus='0' 
ORDER BY EmployeeID, LogDate DESC, LogTime

注意在调用之间更改的参数使用绑定变量。它不会使查询更快,但它是“最佳实践”。

根据您的调用语言,您可能需要添加 TO_DATE 等,以将传入的绑定变量转换为 Date 类型。

【讨论】:

【参考方案10】:

如果 StudentID 是 char(通常是使用 trim() 的原因),您可以通过填充变量而不是修剪字段来获得更好的性能,如下所示(假设 StudentID 是 char(10)):

StudentID IN (lpad('354354',10),lpad('0',10))

这将允许使用 StudentID 上的索引(如果存在)。

【讨论】:

以上是关于如何处理耗时的 SQL?的主要内容,如果未能解决你的问题,请参考以下文章

Oracle 如何处理给定的 SQL 语句

如何处理 SQL 代理作业错误?

如何处理 SQL 中的长比较

#yyds干货盘点# 如何处理消息丢失问题?

如何处理以保留 Sql 关键字命名的表列?

SQL如何处理英文和中文分开