【问题标题】:What makes this query so inefficient in Rails 3是什么让这个查询在 Rails 3 中如此低效
【发布时间】:2013-09-14 22:15:32
【问题描述】:

我一直在为同样的问题苦苦挣扎——在 Rails 中执行高效查询。我目前正在尝试对具有 500,000 条记录的模型执行查询,然后提取一些有关返回结果的描述性统计信息。

概述: 我想提取一些符合一组标准的产品。然后我想...

  • 计算记录数(如果没有我想禁止某些操作)
  • 确定匹配记录的最高和最低价格并计算落在特定范围内的商品数量

就目前而言,这组命令比我希望的要长得多(在我的台式计算机上本地运行 26000 毫秒)并且涉及 8 或 9 个活动记录操作,每个操作大约需要 3000 毫秒

是我做错了什么导致处理速度如此缓慢吗?任何建议都会很棒

我的控制器中的代码是:

    filteredmatchingproducts = Allproduct.select("id, product_name, price")
    .where('product_name LIKE ? 
    OR (product_name LIKE ? AND product_name NOT LIKE ? AND product_name NOT LIKE ?       AND product_name NOT LIKE ? AND product_name NOT LIKE ? AND product_name NOT LIKE ?) 
    OR product_name LIKE ? OR product_name LIKE ? OR product_name LIKE ? OR product_name LIKE ? OR (product_name LIKE ? AND product_name NOT LIKE ?) OR product_name LIKE ?', 
    '%Bike Box', '%Bike Bag%', '%Pannier%', '%Shopper%', '%Shoulder%', '%Shopping%', '%Backpack%' , '%Wheel Bag%', '%Bike sack%', '%Wheel cover%', '%Wheel case%', '%Bike case%', '%Wahoo%', '%Bicycle Travel Case%')
    .order('price ASC')

    @selected_products = filteredmatchingproducts.paginate(:page => params[:page])  

    @productsfound = filteredmatchingproducts.count
    @min_price = filteredmatchingproducts.first
    @max_price = filteredmatchingproducts.last

    @price_range = @max_price.price - @min_price.price

    @max_pricerange1 = @min_price.price + @price_range/4
    @max_pricerange2 = @min_price.price + @price_range/2
    @max_pricerange3 = @min_price.price + 3*@price_range/4
    @max_pricerange4 = @max_price.price 

    if @min_price == nil
    #don't do anything - just avoid error
    else

    @restricted_products_pricerange1 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?', 0 , @max_pricerange1).count
    @restricted_products_pricerange2 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?', @max_pricerange1 + 0.01 , @max_pricerange2).count
    @restricted_products_pricerange3 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?',  @max_pricerange2 + 0.01 , @max_pricerange3).count
    @restricted_products_pricerange4 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?',  @max_pricerange3 + 0.01 , @max_pricerange4).count
    end

编辑 为了清楚起见,我的基本问题是 - 为什么每个查询都需要在大型 Allproduct 数据库上执行,是否没有办法对前一个查询的结果执行后一个查询(即使用过滤匹配产品本身不重新计算它适用于每个查询)?在其他编程语言中,我习惯于记住变量并对这些记住的值执行操作,而不是在执行操作之前再次计算它们——这不是 Rails 中的心态吗?

【问题讨论】:

  • 有机会迁移到包含聚合窗口函数的数据库吗? (我只是猜测 SQLite 没有)。所有这一切都可以通过 PostgreSQL 的单个查询来实现,并且对于如此多的记录,您应该尝试将其推送到数据库中。

标签: ruby-on-rails ruby-on-rails-3 sqlite activerecord


【解决方案1】:

您共享的代码 sn-p 有太多问题。也许最重要的是,这不是特定于 Rails 的优化问题,而是数据库结构和优化问题。

您正在使用“like”查询,两边都有与号 (%),这会导致 SQLLite 中的线性搜索时间,因为无法应用索引。理想情况下,您不应该使用“Like”来应用搜索,而是应该定义一个 product_categories 表,该表在 AllProducts 表中作为 product_category_id 引用,并在其上定义一个索引。

要初始化@products_found、@min_price 和@max_price 变量,您可以执行以下操作:

filteredmatchingproductlist = filteredmatchingproducts.to_a
@productsfound = filteredmatchingproductlist.count
@min_price = filteredmatchingproductlist.first
@max_price = filteredmatchingproductlist.last

这将避免在您对 Array 而不是 ActiveRecord::Relation 执行这些操作时为它们触发单独的查询。

由于结果已排序,您可以对 filtersmatchingproductlist 数组应用良好的旧二进制搜索,并计算计数以获得与代码最后四行相同的结果:

@restricted_products_pricerange1 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?', 0 , @max_pricerange1).count
@restricted_products_pricerange2 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?', @max_pricerange1 + 0.01 , @max_pricerange2).count
@restricted_products_pricerange3 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?',  @max_pricerange2 + 0.01 , @max_pricerange3).count
@restricted_products_pricerange4 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?',  @max_pricerange3 + 0.01 , @max_pricerange4).count

最后,如果您确实需要计数和全文搜索,最好集成 Sphinx 或 Solr 等搜索引擎。查看http://pat.github.io/thinking-sphinx/searching.html 作为如何实现它的参考。

【讨论】:

  • 请注意,索引可以应用于某些 RDBMS 中的“%abc%”。
  • 哪个 RDBMS?据我所知,Oracle、Postgres、SQLlite 和 MySQL 对此类查询执行全表扫描。
  • Oracle 至少可以运行快速的全索引扫描
  • 谢谢。我已经调整了我的答案,现在只关注 SQLLite。
【解决方案2】:

product_name 字段是什么?看来您可以使用 act_as_taggable gem (https://github.com/mbleigh/acts-as-taggable-on)。 LIKE 语句导致数据库检查每条记录是否匹配,并且非常繁重。当您有 500k 条记录时,需要一段时间。

【讨论】:

  • 感谢您的建议,我会研究一下这个想法。我主要关心的是为什么查询必须运行这么多次(每次需要 3000 毫秒,这将包括你提到的缓慢) - 有没有办法存储结果 filteredmatchingproducts 而不必回去拉一切每次都离开Allproduct
  • 一般来说 LIKE 语句并不完全正确,尽管它可能特别适用于 SQLite。问题在于 LIKE 语句的选择性不是很强,因此完全扫描表而不是使用索引更有效。例如,在 RDBMS 市场更复杂的一端,Oracle 可以对表进行采样以估计满足完整条件集的行的比例,然后选择全表扫描或快速全索引扫描来检索行。
【解决方案3】:

如果您处理的只是价格,您应该继续处理一系列价格,而不是 ActiveRecord::Relation。所以试试类似的东西:

filteredmatchingproducts = (...).map(&:price)

然后对该数组执行所有操作。此外,尽可能尝试批量加载大型请求,然后尽可能维护自己的计数等。这将避免应用程序一次占用所有内存并减慢速度:

http://guides.rubyonrails.org/active_record_querying.html#retrieving-multiple-objects-in-batches

【讨论】:

    【解决方案4】:

    它执行这么多查询的原因是因为您要求它执行大量查询。 (此外,所有LIKEs 都会使事情变慢。)这是您的代码,在每个查询之前添加了注释(总共 8 个)。

    filteredmatchingproducts = Allproduct.select("id, product_name, price")
    .where('product_name LIKE ? 
    OR (product_name LIKE ? AND product_name NOT LIKE ? AND product_name NOT LIKE ?       AND product_name NOT LIKE ? AND product_name NOT LIKE ? AND product_name NOT LIKE ?) 
    OR product_name LIKE ? OR product_name LIKE ? OR product_name LIKE ? OR product_name LIKE ? OR (product_name LIKE ? AND product_name NOT LIKE ?) OR product_name LIKE ?', 
    '%Bike Box', '%Bike Bag%', '%Pannier%', '%Shopper%', '%Shoulder%', '%Shopping%', '%Backpack%' , '%Wheel Bag%', '%Bike sack%', '%Wheel cover%', '%Wheel case%', '%Bike case%', '%Wahoo%', '%Bicycle Travel Case%')
    .order('price ASC')
    
    #!!!! this is a query "select ... offset x, limit y"
    @selected_products = filteredmatchingproducts.paginate(:page => params[:page])  
    
    #!!!! this is a query "select count ..."
    @productsfound = filteredmatchingproducts.count
    #!!!! this is a query "select ... order id asc, limit 1"
    @min_price = filteredmatchingproducts.first
    #!!!! this is a query "select ... order id desc, limit 1"
    @max_price = filteredmatchingproducts.last
    
    @price_range = @max_price.price - @min_price.price
    
    @max_pricerange1 = @min_price.price + @price_range/4
    @max_pricerange2 = @min_price.price + @price_range/2
    @max_pricerange3 = @min_price.price + 3*@price_range/4
    @max_pricerange4 = @max_price.price 
    
    if @min_price == nil
    #don't do anything - just avoid error
    else
    
    #!!!! this is a query "select ... where price BETWEEN X and Y"
    @restricted_products_pricerange1 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?', 0 , @max_pricerange1).count
    #!!!! this is a query "select ... where price BETWEEN X and Y"
    @restricted_products_pricerange2 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?', @max_pricerange1 + 0.01 , @max_pricerange2).count
    #!!!! this is a query "select ... where price BETWEEN X and Y"
    @restricted_products_pricerange3 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?',  @max_pricerange2 + 0.01 , @max_pricerange3).count
    #!!!! this is a query "select ... where price BETWEEN X and Y"
    @restricted_products_pricerange4 = filteredmatchingproducts.select("price").where('price BETWEEN ? and ?',  @max_pricerange3 + 0.01 , @max_pricerange4).count
    end
    

    【讨论】:

    • 杰里米,感谢您的回复。完全同意您的评论,我当然希望按照您的说明执行八个查询 - 但我真正的问题是,为什么每个查询都需要在 Allproducts 模型上执行 - 有没有办法执行后面的查询改为过滤匹配产品(即记住先前查询的结果)。我认为我的问题的症结在于我是否可以使用不同的变量类型,这意味着您不需要为 8 个查询中的每一个返回庞大的数据库?
    猜你喜欢
    • 2012-06-18
    • 1970-01-01
    • 1970-01-01
    • 2020-06-06
    • 1970-01-01
    • 2021-09-11
    • 2012-01-15
    • 1970-01-01
    • 2016-04-28
    相关资源
    最近更新 更多