This project is archived and is in readonly mode.

#1533 open
Ed Lebert

Preloading more than 1000 associated records causes ActiveRecord::StatementInvalid when using Oracle

Reported by Ed Lebert | December 8th, 2008 @ 07:28 PM | in 3.x

Rails often uses "in" sql clauses when preloading associations. Oracle has a 1000 term restriction on such clauses, so that you cannot say something like "WHERE parent_id IN (1,2,3,4 ... 1001)". One possible solution is to break it up like this:

"WHERE (parent_id IN (1,2,3,4,...) OR parent_id IN (1001,1002,...)".

It has been proposed that the adapters can supply their "in clause limit", and the abstract adapter can just default to return nil if it has no limit. But the oracle adapter can return 1000.

I searched to make sure this isn't a duplicate ticket first. Sorry if it is. This is my first rails bug.

Comments and changes to this ticket

  • Frederick Cheung

    Frederick Cheung December 8th, 2008 @ 08:19 PM

    I vaguely recall someone having the same issue, although I can't remember whether it was here or a post on one of the various mailing lists. This does seem reasonable, I suppose the only question I would ask would be is it preferable to do

    
    Select * from foos where id in ( ... ) or id in ( ... )
    

    or to do two queries, one for each chunk. I can't imagine that either would be very hard to do

  • Ed Lebert

    Ed Lebert December 8th, 2008 @ 08:28 PM

    I think the first option is best, but you're the expert.

    I used to just monkey patch ActiveRecord::AssociationPreload::ClassMethods::Find_assicoated_records. But another place where this popped up was a habtm relationship where activerecord performed a join query. I'm sure you know where this is in the code better than I do.

    However, another question is how far reaching will you make this fix? It seems like a lot of work to make this fix complete, as you'd have to at least put it anywhere in activerecord where there's an 'in' clause. Yikes. Also, what about when a user does a condition on @@@:conditions => ['column_name IN (?)', ids]@@@

    I hate oracle. But this is for a big corporate application the only database they allow is oracle. Unfortunately that is a reality for a lot of web developers.

  • Giorgio Gonnella

    Giorgio Gonnella December 17th, 2008 @ 11:57 AM

    I have this problem with Oracle too.

    Here I found a discussion about it: http://www.ruby-forum.com/topic/...

    The solution proposed there seems not to work in my case, however.

  • Frederick Cheung

    Frederick Cheung December 17th, 2008 @ 12:53 PM

    Personally I wouldn't worry about the case when the user does :conditions => {:foos => list_of_ids}. Users can work their way round that, whereas with the associations stuff you'd be stuck. I suspect that in the case with a big number of records you'd be better off triggering a fallback to the older joins based include

  • Giorgio Gonnella

    Giorgio Gonnella December 17th, 2008 @ 01:11 PM

    OK. The solution in the ruby-forum is of course incomplete, as patches only find_associated_records and not e.g. preload_belongs_to_association. (which is why is not working for me)

  • Giorgio Gonnella

    Giorgio Gonnella December 17th, 2008 @ 02:48 PM

    I "quick"-fixed it as in the following diff:

  • Giorgio Gonnella

    Giorgio Gonnella March 2nd, 2009 @ 05:08 PM

    Here is a patch (based on the rails-2.2 branch, can be easily adapted for the master branch)

  • Giorgio Gonnella

    Giorgio Gonnella March 2nd, 2009 @ 05:10 PM

    • Assigned user set to “Frederick Cheung”
  • Frederick Cheung

    Frederick Cheung March 2nd, 2009 @ 05:50 PM

    2 things:

    it looks to me like you are missing some parentheses in your sql - it will generate stuff that looks like

    id in (...) or id in (...) AND other conditions

    instead of

    (id in (...) or id in (...)) AND other conditions

    secondly, no tests :-)

  • Giorgio Gonnella

    Giorgio Gonnella March 2nd, 2009 @ 08:56 PM

    Hi Frederick, thank you for reviewing it.

    Reply to your objections:

    • missing parenthesis: of course a couple of parenthesis more around it can only be good, you're right, I will correct it

    • no tests: I should definitely prepare them. I probably have still too less experience in testing ActiveRecord features, but I will have a look on the tests suite you guys provide

  • CancelProfileIsBroken

    CancelProfileIsBroken August 5th, 2009 @ 02:20 PM

    • Tag changed from activerecord, associations, oracle, preload, preload_associaitons, sql to activerecord, associations, bugmash, oracle, preload, preload_associaitons, sql
  • Elad Meidar

    Elad Meidar August 9th, 2009 @ 10:45 PM

    -1 Not sure it's the right way to patch it, someone considered the performance issues with such a select? it's one of those things that are better left for the DBA to solve (that limit is configurable if i recall correctly).

    anyway, should not be patched in ActiveRecord, but rather on the OracleAdapter.

  • Elad Meidar

    Elad Meidar August 9th, 2009 @ 10:45 PM

    +1 verified on a sample app and a none-configured Oracle database with limit on.

  • Nick M

    Nick M January 15th, 2010 @ 09:20 PM

    • Tag changed from activerecord, associations, bugmash, oracle, preload, preload_associaitons, sql to activerecord, associations, bugmash, ora-01795, oracle, preload, preload_associaitons, sql

    From what I can tell, it's not possible to change this limit within Oracle. I also don't think that it's possible for the oracle adapter to cleanly fix this problem since it's Rails that generates all of this raw SQL. This means that any query that happens to be including more than 1000 associated records fails with this error: "OCIError: ORA-01795: maximum number of expressions in a list is 1000".

    The earlier patch fixes this problem, but I'm also unsure if there might be performance implications of constructing the query that way for all other databases. Although, personally, for queries loading 1000s of associations, I'm more concerned with getting actual results than performance.

    I'm also attaching an simpler monkey patch that surely has worse performance. The only real benefit is that you can drop this into config/initializers and it only overrides one method to fix things, and it's only triggered if there are more than 1000 items. The earlier patch is a better solution, but I didn't want to maintain fully patched copies of the various methods involved.

  • Nick M

    Nick M January 15th, 2010 @ 09:24 PM

    Oops, here's the simpler monkey patch that can be dropped in config/initializers

  • Steve

    Steve January 20th, 2010 @ 05:14 PM

    Hi Giorgio,

    I have the same issue and tried your solution and this is not working for me.
    

    Can you please upload your file /vendor/rails/activerecord/lib/active_record/association_preload.rb ?
    btw, I'm using rails 2.2

    Thanks

  • Rizwan Reza

    Rizwan Reza February 12th, 2010 @ 12:46 PM

    • Tag changed from activerecord, associations, bugmash, ora-01795, oracle, preload, preload_associaitons, sql to activerecord, associations, ora-01795, oracle, preload, preload_associaitons, sql
  • Student

    Student March 4th, 2010 @ 03:42 PM

    I've done a bit of checking. It seems that the database adapters are not shy about opening ActiveRecord::Base to make changes. (See, for instance vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb, and gems/activerecord-oracle-adapter-1.0.0.9250/lib/active_record/connection_adapters/oracle_adapter.rb).

    This indicates that the fix should probably be in the oracle adapter.

    Also, while it has been commented that the user should take care of :conditions => {:foos => list_of_ids} themselves, I have problems with this. If someone is using a plugin, the plugin might well generate the list. So they have to hack someone else's code. Furthermore, it is not a fix, but fixes that we need, as every instance would need to be corrected.

    Note that ActiveRecord::Base#quote_bound_value does not go through in_or_equals_for_ids, so the wrapper proposed is incomplete.

  • Student

    Student March 4th, 2010 @ 11:25 PM

    • Tag changed from activerecord, associations, ora-01795, oracle, preload, preload_associaitons, sql to activerecord, associations, ora-01795, oracle, patch, preload, preload_associaitons, sql

    Well, ... I lied. I have a fix which should cover all issues. The best solution, it would seem to me, is to adjust ActiveRecord::Base just a little to support the fix in the Oracle adapter:

    diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
    index cd67490..68dec91 100755
    --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1639,13 +1639,17 @@ module ActiveRecord #:nodoc:

             if value.respond_to?(:empty?) && value.empty?
               connection.quote(nil)
             else
    
    •        value.map { |v| connection.quote(v) }.join(',')
      
    •        join_quoted_values_for_condition(value.map{|v| connection.quote(v)})
           end
         else
           connection.quote(value)
         end
       end
      
    •  def join_quoted_values_for_condition(values) #:nodoc:
      
    •    values * ','
      
    •  end
      
      +
       def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
         unless expected == provided
           raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
      
      -- 1.6.0.4

    This allows a clean fix to the oracle adapter (enhanced)
    diff --git a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb b/lib/active_record/connection_adapters/oracle_enhance
    index e01b07d..f92167a 100644
    --- a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb @@ -107,6 +107,10 @@ module ActiveRecord

     end
     private :enhanced_write_lobs
    
    • def join_quoted_values_for_condition(values) #:nodoc:
    • values * ','
    • end + class << self # patch ORDER BY to work with LOBs def add_order_with_lobs!(sql, order, scope = :auto)
  • Student

    Student March 5th, 2010 @ 12:43 AM

    Uggh. Formatting problems AND I grabbed the wrong changes... (The patch above should be good)

    ---
    activerecord/lib/active_record/base.rb |    6 +++++-
    1 files changed, 5 insertions(+), 1 deletions(-)
    
    diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
    index cd67490..68dec91 100755
    --- a/activerecord/lib/active_record/base.rb
    +++ b/activerecord/lib/active_record/base.rb
    @@ -1639,13 +1639,17 @@ module ActiveRecord #:nodoc:
                if value.respond_to?(:empty?) && value.empty?
                  connection.quote(nil)
                else
    -              value.map { |v| connection.quote(v) }.join(',')
    +              join_quoted_values_for_condition(value.map{|v| connection.quote(v)})
                end
              else
                connection.quote(value)
              end
            end
    
    +        def join_quoted_values_for_condition(values) #:nodoc:
    +          values * ','
    +        end
    +
            def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
              unless expected == provided
                raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
    -- 
    1.6.0.4
    

    This supports the fix in the oracle adapter:

    ---
     .../connection_adapters/oracle_enhanced_adapter.rb |   14 ++++++++++++++
     1 files changed, 14 insertions(+), 0 deletions(-)
    
    diff --git a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb
    index e01b07d..5f62266 100644
    --- a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb
    +++ b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb
    @@ -107,6 +107,20 @@ module ActiveRecord
         end
         private :enhanced_write_lobs
         
    +    ORACLE_IN_LIMIT = 1000
    +
    +    def join_quoted_values_for_condition(values)
    +      return values * ',' unless values.length > ORACLE_IN_LIMIT
    +
    +      values.uniq!
    +      return values * ',' unless values.length > ORACLE_IN_LIMIT
    +
    +      quoted_chunks = values.in_groups_of(ORACLE_IN_LIMIT) do |chunk|
    +        "(SELECT * FROM TABLE(sys.odcinumberlist(#{chunk * ','})))"
    +      end
    +      quoted_chunks * " UNION "
    +    end
    +
         class << self
           # patch ORDER BY to work with LOBs
           def add_order_with_lobs!(sql, order, scope = :auto)
    -- 
    1.6.0.4
    
  • Student

    Student March 5th, 2010 @ 02:46 PM

    Well.. I found another place that needs to be touched...

    ---
     activerecord/lib/active_record/base.rb |    8 ++++++--
     1 files changed, 6 insertions(+), 2 deletions(-)
    
    diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
    index cd67490..563a645 100755
    --- a/activerecord/lib/active_record/base.rb
    +++ b/activerecord/lib/active_record/base.rb
    @@ -695,7 +695,7 @@ module ActiveRecord #:nodoc:
             }.join(", ")
     
             if id.is_a?(Array)
    -          ids_list = id.map {|i| quote_value(i)}.join(', ')
    +          ids_list = join_quoted_values_for_condition(id.map{|i| quote_value(i)})
               condition = "IN  (#{ids_list})"
             else
               condition = "= #{quote_value(id)}"
    @@ -1639,13 +1639,17 @@ module ActiveRecord #:nodoc:
                 if value.respond_to?(:empty?) && value.empty?
                   connection.quote(nil)
                 else
    -              value.map { |v| connection.quote(v) }.join(',')
    +              join_quoted_values_for_condition(value.map{|v| connection.quote(v)})
                 end
               else
                 connection.quote(value)
               end
             end
     
    +        def join_quoted_values_for_condition(values) #:nodoc:
    +          values * ','
    +        end
    +
             def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
               unless expected == provided
                 raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
    -- 
    1.6.0.4
    
  • Jeremy Kemper

    Jeremy Kemper March 5th, 2010 @ 04:32 PM

    • Milestone changed from 2.x to 2.3.6
    • State changed from “new” to “open”

    Needs a test case as well.

  • Student

    Student March 5th, 2010 @ 05:11 PM

    This patch is a nop for the rails core. Its purpose is to provide a hook that the Oracle adapter can use. Or do we need a test for join_quoted_values_for_condition ?

  • Jeremy Kemper

    Jeremy Kemper March 5th, 2010 @ 06:38 PM

    Ah, right. No test needed.

  • Rizwan Reza

    Rizwan Reza May 16th, 2010 @ 02:41 AM

    • Tag changed from activerecord, associations, ora-01795, oracle, patch, preload, preload_associaitons, sql to activerecord, associations, bugmash, ora-01795, oracle, patch, preload, preload_associaitons, sql
  • Jeremy Kemper

    Jeremy Kemper May 23rd, 2010 @ 05:54 PM

    • Milestone changed from 2.3.6 to 2.3.7
  • Jeremy Kemper

    Jeremy Kemper May 24th, 2010 @ 09:40 AM

    • Milestone changed from 2.3.7 to 2.3.8
  • Jeremy Kemper

    Jeremy Kemper May 25th, 2010 @ 11:45 PM

    • Milestone changed from 2.3.8 to 2.3.9
  • Jeremy Kemper

    Jeremy Kemper August 30th, 2010 @ 02:28 AM

    • Milestone changed from 2.3.9 to 2.3.10
    • Importance changed from “” to “”
  • Santiago Pastorino

    Santiago Pastorino February 2nd, 2011 @ 04:33 PM

    This issue has been automatically marked as stale because it has not been commented on for at least three months.

    The resources of the Rails core team are limited, and so we are asking for your help. If you can still reproduce this error on the 3-0-stable branch or on master, please reply with all of the information you have about it and add "[state:open]" to your comment. This will reopen the ticket for review. Likewise, if you feel that this is a very important feature for Rails to include, please reply with your explanation so we can consider it.

    Thank you for all your contributions, and we hope you will understand this step to focus our efforts where they are most helpful.

  • Santiago Pastorino

    Santiago Pastorino February 2nd, 2011 @ 04:33 PM

    • State changed from “open” to “stale”
  • Robert Tarrall

    Robert Tarrall March 1st, 2011 @ 10:58 PM

    • State changed from “stale” to “open”

    [state:open] This is still an issue in 3.0.4. For example, assuming User has_many :posts and Post has_one :content... this:

      User.where(:username=>'somebody').includes(:posts)
    

    works fine, but this:

      User.where(:username=>'somebody').includes(:posts => :content)
    

    will give us "ORA-01795: maximum number of expressions in a list is 1000" when the user has more than 1000 posts.

    The SQL generated looks like this:

      SELECT "contents".* FROM "contents"
        WHERE ("contents"."post_id" IN (ginormous list))
    

    which is extremely non-performant in Oracle. Ed's original suggestion of splitting the list of IDs should generate SQL which works in Oracle, but will be even worse performance-wise.

    If possible, we're much better off if the Content eager-load generates SQL using JOINs rather than "where ID in (x,y,z,...)":

      SELECT contents.* FROM contents
        INNER JOIN posts ON posts.id=contents.post_id
        INNER JOIN users ON users.id=posts.user_id
        WHERE users.nid=1
    
  • Santiago Pastorino

    Santiago Pastorino March 2nd, 2011 @ 02:50 PM

    • Milestone changed from 2.3.10 to 3.x
    • Assigned user changed from “Frederick Cheung” to “Aaron Patterson”
  • csnk

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

<h2 style="font-size: 14px">Tickets have moved to Github</h2>

The new ticket tracker is available at <a href="https://github.com/rails/rails/issues">https://github.com/rails/rails/issues</a>

Pages