ActiveRecord - 非规范化案例研究

时间:2012-09-04 21:21:46

标签: ruby-on-rails activerecord denormalization

处理以下8个不同SQL问题的最佳方法是什么。

我已经在数据库模式下面放置了它,它在我的Rails模型中如何表示,以及我需要从数据库中获取的数据的七个问题。我已回答了一些问题,其他问题我不确定最佳解决方案。

问题#7是一个曲线球,因为它可能会改变所有其他问题的答案。

标准

  1. 不应要求n + 1个查询。多个查询都可以,但如果返回的每一行都需要一个额外的查询,那么它就不可扩展了。
  2. 不应该要求后处理来过滤SQL可以自己完成的结果。例如,第五个答案不应该是从数据存储中拉出所有学生,然后删除那些没有课程的学生。
  3. 检索对象的计数不应该触发另一个SQL查询。
  4. 如果SQL允许我聚合数据
  5. ,则不必通过非规范化添加数据库列
  6. NOSQL解决方案(例如MongoDB或CouchDB)是否更适合回答以下所有问题?
  7. 数据库架构

    Students
    -------
    ID
    Name
    
    Courses
    -----
    ID
    Name
    Grade
    
    Enrollments
    ----------
    ID
    Student_ID
    Course_ID
    

    ActiveRecord模型

    
    class Course < ActiveRecord::Base
      has_many :enrollments
      has_many :students, :through=>:enrollments
    end
    class Enrollment < ActiveRecord::Base
      belongs_to :student
      belongs_to :course
    end
    class Student < ActiveRecord::Base
      has_many :enrollments
      has_many :courses, :through => :enrollments
    end
    

    问题

    1)检索9年级数学课程的所有学生

    SQL

    
    SELECT s.* FROM Students s
    LEFT JOIN Enrollments e on e.student_id = s.id
    LEFT JOIN Courses c on e.course_id = c.id
    WHERE c.grade = 9 AND c.name = 'Math'
    

    解决方案

    这个很简单。 ActiveRecord处理好这个

    
    c = Course.where(:grade=>9).where(:name=>'Math').first
    c.students
    

    2)检索约翰

    所有课程

    SQL

    
    SELECT c.* FROM Courses c
    LEFT JOIN Enrollments e on c.id = e.course_id
    LEFT JOIN Students s on e.student_id = s.id
    WHERE s.name = 'John'
    

    解决方案

    再次,简单。

    
    s = Student.where(:name=>'John').first
    s.courses
    

    3)检索所有9年级课程以及参加课程的学生人数(但不检索学生)

    SQL

    
    SELECT c.*, count(e.student_id) FROM Courses C
    LEFT JOIN Enrollments e on c.id = e.course_id
    WHERE c.grade = 9 GROUP BY c.id
    

    解决方案

    计数器缓存在这里可以很好地工作。

    class AddCounters < ActiveRecord::Migration
      def up
        add_column :students, :courses_count, :integer, :default=>0
        add_column :courses, :students_count, :integer, :default=>0
        Student.reset_column_information
        Student.all.each do |s|
          Student.update_counters s.id, :courses_count => s.courses.length
        end
        Course.reset_column_information
        Course.all.each do |c|
          Course.update_counters c.id, :students_count => c.students.length
        end
      end
    
      def down
        remove_column :students, :courses_count
        remove_column :courses, :students_count
      end
    end
    

    ActiveRecord的

    Course.where(:grade=>9).each do |c|
      puts "#{c.name} - #{c.students.size}"
    end
    

    4)检索所有至少参加过三门11年级课程,一门以上十年级课程,没有九年级课程的学生

    无解决方案

    不确定最佳解决方案。如果没有为每个学生的每个年级的课程数量保留一个计数器缓存,那么在SQL中这将非常麻烦。我可以添加一个钩子来自己更新这些信息。我不想拉所有学生和课程,并在后期处理中计算。

    慢速解决方案

    以下解决方案会产生大量查询。可能无法预加载课程。 (例如,学生来自该课程的协会)

    
    students = some_course.students
    matching_students = []
    students.each do |s|
      courses_9 = 0
      courses_10 = 0
      courses_11 = 0
      s.courses.each do |c|
        courses_9  += 1 if c.grade == 9
        courses_10 += 1 if c.grade == 10
        courses_11 += 1 if c.grade == 11
      end
      if courses_11 <= 3 && courses_10 > 1 && courses_9 == 0
        matching_students << s
      end
    end
    return matching_students
    

    5)检索所有参加多门数学课程的学生 查询)

    SQL

    
    SELECT s.*, count(e.course_id) as num_Courses FROM Students s
    INNER JOIN Enrollments e on s.id = e.student_id
    INNER JOIN Courses c on e.course_id = c.id AND c.name = 'Math'
    GROUP BY s.id HAVING num_Courses > 0
    

    或者

    
    SELECT DISTINCT s.* FROM Students s
    INNER JOIN Enrollments e_math_1 on e_math_1.student_id = s.id
    INNER JOIN Courses c_math_1 ON e_math_1.course_id = c_math_1.id AND c_math_1.name = 'Math'
    INNER JOIN Enrollments e_math_2 on e_math_2.student_id = s.id
    INNER JOIN Courses c_math_2 ON e_math_2.course_id = c_math_2.id AND c_math_2.name = 'Math'
    WHERE c_math_1.id != c_math_2.id
    

    无解决方案

    不确定最佳解决方案。棘手的部分是ActiveRecord(或NoSQL)解决方案无法检索所有学生,并且之后查看他们的课程,因为这样做太慢了。

    慢速解决方案

    
    students = SomeObject.students
    multiple_math_course_students = []
    students.each do |s|
      has_math_course = false
      add_student = false
      s.courses.each do |c|
        if c.name == 'Math'
          if has_math_course
            add_student = true
          else
            has_math_course = true
          end
        end
      end
      multiple_math_course_students << s if add_student
    end
    

    6)检索所有参加数学和科学课程的学生

    SQL

    
    SELECT s.* FROM Students s
    INNER JOIN Enrollments e_math on e_math.student_id = s.id
    INNER JOIN Courses c_math ON e_math.course_id = c_math.id
    INNER JOIN Enrollments e_science on e_science.student_id = s.id
    INNER JOIN Courses c_science on e_science.course_id = c_science.id WHERE c_math.name = 'Math' AND c_science.name = 'Science'
    

    无解决方案

    这涉及两次加入同一个表(或在Rails中,关联)。有没有办法用ActiveRecord的AREL包装器顺利完成这项工作?你可以为科学课和数学课建立一个单独的关联,允许你对每个课进行单独的操作,但这不适用于下面#7的情况。

    慢速解决方案

    
    students = SomeObject.students
    math_and_science_students = []
    students.each do |s|
      has_math_course = false
      has_science_course = false
      s.courses.each do |c|
        has_math_course = true if c.name == 'Math'
        has_science_course = true if c.name == 'Science'
      end
      math_and_science_students << s if has_math_course && has_science_course
    end
    

    7)客户表示,只要学生在系统中出现,就会在学生旁边显示一个数字,显示他们正在学习的最高年级课程。例如,如果Suzie正在修读9年级的科学课程和10年级的数学课程,那么在Suzie旁边显示“10”。

    解决方案

    查询每个学生记录的数据库是不可接受的。显示100名学生的页面需要100个查询。此时,我希望通过在学生表中使用“最高级别课程”标记来对数据库进行非规范化。这是我最好的行动方案吗?从一开始就使用除关系数据库之外的其他数据存储会更好吗?

    想象一下,客户要求将任意数据显示为徽章:最高等级,数学课程数量,如果将数学,科学和历史全部放在一起的金徽章等等。这些情况中的每一个都应该是对数据库进行非规范化的调用吗?非规范化数据应该与标准化数据保存在同一个关系数据库中吗?

2 个答案:

答案 0 :(得分:3)

首先,我认为您的数据库架构很好。我不会根据这些用例去标准化,因为它们很常见。

其次,您必须学会区分持久性,业务逻辑和报告。 ActiveRecord适用于基本持久性和封装业务逻辑。它处理CRUD的东西,并允许您将大量的应用程序逻辑放在模型中。但是,你所谈论的很多逻辑听起来都像是报道,特别是#6。您将不得不接受这样的查询逻辑,原始SQL将是您最好的选择。我认为你已经实现的缓存计数器可以帮助你保持活跃的记录和模型,如果你在那里更舒服,但很可能你必须放弃到简单的SQL,因为你已经为这些解决方案做了几个。报告通常需要直接sql。

规范化数据库对良好的应用程序设计至关重要。对于使OLTP事务和业务逻辑清理代码非常重要。不要因为你必须在sql中做一些连接而非规范化。这就是sql擅长的。通过非规范化所要做的就是使你的一些报告逻辑变得更快更容易,这使得你的持久性和OLTP逻辑越来越慢。

所以我会开始保持你的规范化数据库。如果你需要加入一个相关的表,你通常可以使用activerecord的include方法来执行此操作,而无需使用常规的SQL。要做一些基于连接的计数,你必须使用普通的sql。

最终,如果您的数据库变得非常大并且包含大量数据,那么您的报告将因为您必须执行的所有联接而变慢。这可以。在那一点上,不久之后,开始考虑建立一个非规范化的单独报告数据库,您可以从规范化数据库中每小时,每晚,每周等更新。然后移动报告逻辑以查询报告数据库,而无需进行连接。但是,没有必要以这种方式开始。你只需承担额外的复杂性和费用而不确定收益。也许带有连接的报告sql将无限期地工作,而不使用索引进行非规范化。不要过早优化。

我不认为nosql也不一定是答案。据我所知,NoSQL适用于特定用例。您的应用程序的用例和模式似乎适合关系数据库。

总的来说,我认为原始sql(不是arel / activerecord)和你实现的计数器的组合都很好。

答案 1 :(得分:1)

我此刻遇到了同样的问题。根据我的研究,有几种方法可以解决它。

首先,我相信任何应用程序都会遇到这些问题。基本思想是我们以规范化的方式对数据进行建模,当有大量数据和数据跨越多个表时,这种方式本身变得缓慢而繁琐。

我能够提出的最佳方法如下:

  1. 将问题建模为接近您正在处理的现实世界
  2. 根据需要进行标准化
  3. 这两个应该为应用程序提供很大的灵活性,并提供许多便利方法以及解决我试图回答的大部分问题

    一旦我需要做一堆连接来获得我需要的东西,我觉得我应该对表格进行非规范化以便轻松达到我需要的东西,我考虑以下内容:

    SQL视图: 这些是预定义的sql语句,例如,我可以将模型链接到的语句。 通常,这比通过ActiveRecord查询更快 http://hashrocket.com/blog/posts/sql-views-and-activerecord

    聚合表: 创建一个或多个聚合表并使用delayed_job异步更新,例如resque。 例如,这些聚合可以每天更新一次,模型可以直接查询。 请注意,这是某种非规范化表。

    Couchbase(NOSQL) 我还没有用过这个,但它看起来很有趣。 http://couchbaseonrails.com/understand