ColdFusion 查询太慢

ColdFusion Query too slow

我在 cfloop 中有查询,这使得过程非常缓慢。有没有办法使这个查询更快?

<cfquery name="GetCheckRegister" datasource="myDB">
    SELECT * FROM CheckRegister, ExpenseType 
    Where PropertyID=10
    and ExpenseType.ExpenseTypeID=CheckRegister.ExpenseTypeID 
</cfquery>

<CFOUTPUT query=GetCheckRegister>
    <cfquery name="GetVendorName" datasource="myDB"> SELECT * FROM Vendors WHERE VendorID=#VendorID#</cfquery>
    <!--- I use the vendor name here --->

    <cfset local.CreditDate = "" />
  <cfquery name="getTenantTransactionDateFrom" dataSource="myDB">
    Select TenantTransactionDate as fromDate From TenantTransactions
    Where CheckRegisterID = #CheckRegisterID#
    Order By TenantTransactionDate Limit 1
  </cfquery>
  <cfquery name="getTenantTransactionDateTo" dataSource="myDB">
    Select TenantTransactionDate as ToDate From TenantTransactions
    Where CheckRegisterID = #CheckRegisterID#
    Order By TenantTransactionDate desc Limit 1
  </cfquery>
  <cfif getTenantTransactionDateFrom.fromDate neq "" AND getTenantTransactionDateTo.ToDate neq "">
    <cfif getTenantTransactionDateFrom.fromDate eq getTenantTransactionDateTo.ToDate>
      <cfset local.CreditDate = DateFormat(getTenantTransactionDateFrom.fromDate, 'mm/dd/yyyy') />
    <cfelse>
      <cfset local.CreditDate = DateFormat(getTenantTransactionDateFrom.fromDate, 'mm/dd/yyyy') & " - " & DateFormat(getTenantTransactionDateTo.ToDate, 'mm/dd/yyyy') />
    </cfif>
  </cfif>

  <!--- I use the local.CreditDate here  --->

  <!--- Here goes a table with the data --->
</CFOUTPUT>

cfoutput 就像一个循环。

我已经有很长时间没有进行任何 ColdFusion 开发了,但是一个常见的经验法则是不要在循环中调用查询。根据你在做什么,循环可以被认为是一个 RBAR(逐行痛苦)操作。

您实际上是在定义一个查询并遍历每条记录。对于每条记录,您正在执行三个额外的查询,也就是三个额外的数据库网络调用每条记录。在我看来,您有几个选择:

  1. 重写您的第一个查询以在每个查询中包含您需要的数据 记录检查。
  2. 让您的第一个查询保持原样,并创建在用户与记录交互时提供更多信息的功能,并以异步方式进行。像 "Show Credit Date" link 这样的东西出去并按需获取数据。
  3. 将循环中的查询合并为一个查询而不是两个 getTenantTransaction... 并查看性能是否有所提高。这将 RBAR 数据库调用从三个减少到两个。

如果满足以下条件,则使用主查询和循环处理数据可能会更快:

  • 将 SELECT 与 仅您需要的特定字段一起使用 ,以避免获取如此多的列(而不是 SELECT *),除非您使用所有字段:

SELECT VendorID, CheckRegisterId, ... FROM CheckRegister, ExpenseType ...

  • 在循环中使用 less 子查询,尝试将 table 加入主查询。例如,在主查询中使用供应商 table(如果可以加入此 table)

SELECT VendorID, CheckRegisterId, VendorName ... FROM CheckRegister, ExpenseType, Vendors ...

  • 最后,您可以估算进程的时间并检测性能问题:
    • ROWS = 主查询结果的行数
    • TIME_V = 使用有效的 VendorId
    • 获取 GetVendorName 结果的时间(毫秒)
    • TIME_TD1 = 使用有效的 CheckRegisterID
    • 获取 getTenantTransactionDateFrom 结果的时间(毫秒)
    • TIME_TD2 = 使用有效的 CheckRegisterID
    • 获取 getTenantTransactionDateTo 结果的时间(毫秒)
  • 然后,您可以使用TOTAL = ROWS * (TIME_V+ TIME_TD1 + TIME_TD2)计算结果时间。
  • 例如,如果 ROWS=10000,TIME_V = 30,TIME_TD1 = 15,TIME_TD2 = 15:RESULT = 10000 * (30 + 15 + 15) = 10000 * 60 = 600000 (ms) = 600 (sec) = 10 min

因此,对于 10000 行,循环 1 毫秒会导致流程增加 10 秒。

当主查询的结果行很多时,您需要尽量减少循环中每个元素的查询时间。每毫秒都会影响循环的性能。因此,您需要确保为循环中的每个查询过滤的每个字段都有正确的索引。

您总是希望避免循环查询。每当您查询数据库时,您都会进行往返(从服务器到数据库,再从数据库到服务器),这本质上很慢。

一种通用的方法是通过使用尽可能少的语句查询所有必需的信息来批量处理数据。将所有内容合并到一个语句中是最理想的,但这显然取决于您的 table 方案。如果您不能仅使用 SQL 解决它,您可以像这样转换您的查询:

GetCheckRegister...

(no loop)

<cfquery name="GetVendorName" datasource="rent">
    SELECT * FROM Vendors WHERE VendorID IN (#valueList(GetCheckRegister.VendorID)#)
</cfquery>

<cfquery name="getTenantTransactionDateFrom" dataSource="rent">
    Select TenantTransactionDate as fromDate From TenantTransactions
    Where CheckRegisterID IN (#valueList(GetCheckRegister.CheckRegisterID)#)
</cfquery>

etc.

valueList(query.column) returns 指定 column 值的逗号分隔列表。然后,此列表与 MySQL 的 IN (list) 选择器一起使用,以检索属于所有列出值的所有记录。

现在您将只对循环中的每个语句进行一次查询(总共 4 个查询,而不是 GetCheckRegister 中记录数的 4 倍)。但是所有记录都聚集在一起,因此您需要相应地匹配它们。为此,我们可以利用 ColdFusion 的 Query of Queries (QoQ),它允许您查询已检索到的数据。由于检索到的数据在内存中,因此访问起来很快。

GetCheckRegister, GetVendorName, getTenantTransactionDateFrom, getTenantTransactionDateTo etc.

<CFOUTPUT query="GetCheckRegister">

    <!--- query of queries --->
    <cfquery name="GetVendorNameSingle" dbType="query">
        SELECT * FROM [GetVendorName] WHERE VendorID = #GetCheckRegister.VendorID#
    </cfquery>

    etc.

</CFOUTPUT>

您基本上将真实查询移出了循环,而是使用 QoQ 查询循环中真实查询的结果。

尽管如此,通过在 MySQL 中分析它们来确保您的真实查询速度很快。 使用索引!

正如其他人所说,您应该摆脱循环并使用联接。查看您的内部循环,代码检索每个 CheckRegisterID 的最早和最晚日期。不要使用 LIMIT,而是使用 aggregate functions like MIN and MAX and GROUP BY CheckRegisterID. Then wrap that result in a derived query,这样您就可以将结果加入 CheckRegister ON id。

原始查询中的一些列没有限定范围,所以我做了一些猜测。有改进的余地,但类似的内容足以让您入门。

-- select only needed columns
SELECT cr.CheckRegisterID, ... other columns
FROM CheckRegister cr 
       INNER JOIN ExpenseType ex ON ex.ExpenseTypeID=cr.ExpenseTypeID 
       INNER JOIN Vendors v ON v.VendorID = cr.VendorID
       LEFT JOIN 
       (
            SELECT CheckRegisterID
              , MIN(TenantTransactionDate) AS MinDate
              , MAX(TenantTransactionDate) AS MaxDate
            FROM  TenantTransactions
            GROUP BY CheckRegisterID
       ) tt ON tt.CheckRegisterID = cr.CheckRegisterID

WHERE cr.PropertyID = 10

我强烈建议您阅读 JOIN,因为它们对任何 Web 应用程序都至关重要,IMO。

您应该在一个查询中获取所有数据,然后使用该数据输出您想要的内容。与一次获取数据并使用它相比,与数据库的多个连接几乎总是占用更多资源。要获得结果:

SQL Fiddle

初始架构设置:

CREATE TABLE CheckRegister ( checkRegisterID int, PropertyID int, VendorID int, ExpenseTypeID int ) ;
CREATE TABLE ExpenseType ( ExpenseTypeID int ) ;
CREATE TABLE Vendors ( VendorID int ) ;
CREATE TABLE TenantTransactions ( checkRegisterID int, TenantTransactionDate date, note varchar(20) );

INSERT INTO CheckRegister ( checkRegisterID, PropertyID, VendorID, ExpenseTypeID )
VALUES (1,10,1,1),(1,10,1,1),(1,10,2,1),(1,10,1,2),(1,5,1,1),(2,10,1,1),(2,5,1,1) 
;

INSERT INTO ExpenseType ( ExpenseTypeID ) VALUES (1), (2) ;
INSERT INTO Vendors ( VendorID ) VALUES (1), (2) ;

INSERT INTO TenantTransactions ( checkRegisterID, TenantTransactionDate, note )
VALUES 
    (1,'2018-01-01','start')
  , (1,'2018-01-02','another')
  , (1,'2018-01-03','another')
  , (1,'2018-01-04','stop')
  , (2,'2017-01-01','start')
  , (2,'2017-01-02','another')
  , (2,'2017-01-03','another')
  , (2,'2017-01-04','stop')
 ;

主查询:

SELECT cr.*
  , max(tt.TenantTransactionDate) AS startDate
  , min(tt.TenantTransactionDate) AS endDate
FROM CheckRegister cr 
INNER JOIN ExpenseType et ON cr.ExpenseTypeID = et.ExpenseTypeID
INNER JOIN Vendors v ON cr.vendorID = v.VendorID 
LEFT OUTER JOIN TenantTransactions tt ON cr.checkRegisterID = tt.CheckRegisterID
WHERE cr.PropertyID = 10
GROUP BY cr.CheckRegisterID, cr.PropertyID, cr.VendorID, cr.ExpenseTypeID

Results:

| checkRegisterID | PropertyID | VendorID | ExpenseTypeID |  startDate |    endDate |
|-----------------|------------|----------|---------------|------------|------------|
|               1 |         10 |        1 |             1 | 2018-01-04 | 2018-01-01 |
|               1 |         10 |        1 |             2 | 2018-01-04 | 2018-01-01 |
|               1 |         10 |        2 |             1 | 2018-01-04 | 2018-01-01 |
|               2 |         10 |        1 |             1 | 2017-01-04 | 2017-01-01 |

我只添加了 2 个支票登记簿,但 CheckRegisterID 1 有 2 个供应商和供应商 1 的 2 个费用类型。这看起来像是查询中的重复数据。如果您的数据不是这样设置的,您将不必在最终查询中担心它。

使用正确的 JOIN 语法获取您需要的相关数据。然后您可以聚合该数据以获得 fromDatetoDate。如果您的数据更复杂,您可能需要查看 Window 函数。 https://dev.mysql.com/doc/refman/8.0/en/window-functions.html

我不知道你的最终输出是什么样的,但上面的查询一次就给了你所有的查询数据。该数据的每一行都应该为您提供需要输出的内容,因此现在您只有一个查询可以循环。