ActiveRecord - 非规范化案例研究

Dav*_*ave 5 activerecord ruby-on-rails denormalization

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

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

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

标准

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

数据库架构

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
Run Code Online (Sandbox Code Playgroud)

问题

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'
Run Code Online (Sandbox Code Playgroud)

这个很简单.ActiveRecord处理得很好


c = Course.where(:grade=>9).where(:name=>'Math').first
c.students
Run Code Online (Sandbox Code Playgroud)

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'
Run Code Online (Sandbox Code Playgroud)

再一次,简单.


s = Student.where(:name=>'John').first
s.courses
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

Counter Cache在这里可以很好地工作.

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年级课程,一个以上10年级课程和没有9年级课程的学生

没有解决方案

不确定最佳解决方案.如果没有为每个学生的每个年级水平的课程数量保留计数器缓存,那么在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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

要么


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
Run Code Online (Sandbox Code Playgroud)

没有解决方案

不确定最佳解决方案.棘手的部分是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
Run Code Online (Sandbox Code Playgroud)

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'
Run Code Online (Sandbox Code Playgroud)

没有解决方案

这涉及到两次加入同一个表(或在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
Run Code Online (Sandbox Code Playgroud)

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

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

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

fre*_*gas 3

首先,我认为你的数据库架构很好。我不会根据这些用例进行反规范化,因为它们非常常见。

其次,你必须学会​​区分持久性、业务逻辑和报告。ActiveRecord 适合基本持久性和封装业务逻辑。它处理 CRUD 内容,并允许您将应用程序的大量逻辑放入模型中。然而,你所说的很多逻辑听起来都像报告,尤其是#6。您将不得不接受这样的事实:对于像这样的某种查询逻辑,原始 SQL 将是您的最佳选择。我认为您实现的缓存计数器可能会帮助您保持活动记录和模型(如果您在那里更舒服的话),但很可能您将不得不像您对其中几个解决方案所做的那样,不得不使用纯 sql。报告一般需要直接的sql。

规范化的数据库对于良好的应用程序设计至关重要。它对于使 OLTP 事务和业务逻辑的代码整洁非常重要。不要仅仅因为必须在 sql 中进行一些连接就进行非规范化。这就是sql所擅长的。通过非规范化,您要做的就是使某些报告逻辑更快、更容易,但代价是使持久性和 OLTP 逻辑变得更慢、更困难。

所以我会开始保留你的标准化数据库。如果您需要连接相关表,您通常可以使用 activerecord 的 include 方法来完成此操作,而无需求助于常规 sql。要执行诸如基于联接的计数之类的操作,您必须使用纯 sql。

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

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

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