6

以下の8つの異なるSQLの質問に対処するための最良の方法は何ですか。

データベーススキーマ、Railsモデルでの表現方法、およびデータベースから取得する必要のあるデータに関する7つの質問を下に配置しました。私が答えた質問もあれば、最善の解決策がわからない質問もあります。

質問#7は、他のすべての質問への回答を変更する可能性があるため、カーブボールです。

基準

  1. n+1クエリは必要ありません。複数のクエリは問題ありませんが、返されるすべての行に追加のクエリが必要な場合、スケーラブルではありません。
  2. SQLが独自に実行できる結果をフィルタリングするために後処理を必要としないでください。たとえば、5番目の答えは、すべての学生をデータストアから取得してから、コースのない学生を削除することではありません。
  3. オブジェクトのカウントを取得しても、別のSQLクエリがトリガーされることはありません。
  4. SQLでデータを集約できる場合は、非正規化によってデータベース列を追加する必要はありません。
  5. MongoDBやCouchDBなどのNOSQLソリューションは、以下のすべての質問に答えるのに適していますか?

データベーススキーマ

学生
-------
ID
名前

コース
-----
ID
名前
学年

登録
----------
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

解決

カウンターキャッシュはここでうまく機能します。

クラスAddCounters<ActiveRecord:: Migration
  デフアップ
    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
    終わり
    Course.reset_column_information
    Course.all.each do | c |
      Course.update_counters c.id、:students_count => c.students.length
    終わり
  終わり

  デフダウン
    remove_column:students、:courses_count
    remove_column:courses、:students_count
  終わり
終わり

ActiveRecord

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

4)少なくとも3つの11年生のコース、1つ以上の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

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では関連付け)に2回参加することが含まれます。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)顧客は、学生がシステムに表示されるときはいつでも、受講している最高学年レベルのコースを示す番号を学生の横に表示すると述べています。たとえば、スージーが9年生の理科コースと10年生の数学コースを受講している場合、スージーの横に「10」を表示します。

解決

すべての学生レコードについてデータベースにクエリを実行することは受け入れられません。100人の学生を表示するページには、100個のクエリが必要です。この時点で、「最高レベルのコース」のフラグを学生テーブルに配置して、データベースを非正規化したいと思います。これが私の最善の行動ですか?最初からリレーショナルデータベース以外の別のデータストアを使用する方が良いでしょうか?

顧客が任意のデータをバッジとして表示するように要求したと想像してください:最高学年レベル、受講した数学コースの数、数学、科学、歴史をすべて一緒に受講する場合のゴールドバッジなど。 これらの各ケースが非正規化の要求である場合データベースの?非正規化されたデータは、正規化されたデータと同じリレーショナルデータベースに保持する必要がありますか?

4

2 に答える 2

3

まず、データベーススキーマは問題ないと思います。これらのユースケースは非常に一般的であるため、これらのユースケースに基づいて非正規化することはしません。

次に、永続性、ビジネスロジック、およびレポートを区別する方法を学ぶ必要があります。ActiveRecordは、基本的な永続性とビジネスロジックのカプセル化に適しています。CRUDを処理し、アプリケーションの多くのロジックをモデルに組み込むことができます。ただし、あなたが話しているロジックの多くは、レポートのように聞こえます。特に#6です。このようなある種のクエリロジックでは、生のSQLが最善の策になることを受け入れる必要があります。実装したキャッシュカウンターは、アクティブなレコードとモデルに慣れている場合に役立つと思いますが、これらのソリューションのいくつかで行ったように、ほとんどの場合、プレーンSQLにドロップする必要があります。一般に、レポートにはストレートSQLが必要です。

正規化されたデータベースは、優れたアプリケーション設計に不可欠です。これは、OLTPトランザクションおよびビジネスロジック用にコードをクリーンにするために非常に重要です。SQLでいくつかの結合を行う必要があるという理由だけで非正規化しないでください。それがSQLが得意なことです。非正規化によって行うのは、永続性とOLTPロジックを遅くして難しくするという犠牲を払って、レポートロジックの一部をより速く簡単にすることだけです。

だから私はあなたの正規化されたデータベースを維持することから始めます。関連するテーブルに参加する必要がある場合は、通常のSQLを使用せずに、activerecordのincludeメソッドを使用してこれを行うことができます。結合に基づくカウントなどを行うには、プレーンSQLを使用する必要があります。

最終的に、データベースが非常に大きくなり、大量のデータが含まれる場合、すべての結合を実行する必要があるため、レポートの速度が低下します。これはFINEです。その時点で、すぐに、正規化されたデータベースから毎時、毎晩、毎週などを更新できる、非正規化された別のレポートデータベースの作成を検討し始めます。次に、レポートロジックを移動して、結合を行わずにレポートデータベースにクエリを実行します。ただし、この方法で開始する必要はありません。見返りがわからないまま、余分な複雑さと費用が発生しているだけです。おそらく、結合を使用したレポートSQLは、インデックスを使用して非正規化することなく無期限に機能します。時期尚早に最適化しないでください。

nosqlも必ずしも答えではないと思います。私がほとんど知らないことから、NoSQLは特定のユースケースでうまく機能します。アプリケーションのユースケースとスキーマは、リレーショナルデータベースに適しているようです。

全体として、raw sql(arel / activerecordではない)と実装したカウンターの組み合わせは問題ないと思います。

于 2012-09-04T21:43:37.393 に答える
1

現在、同じ問題に直面しています。私の調査によると、それを回避する方法がいくつかあります。

まず第一に、どのアプリケーションもこれらの問題に遭遇すると思います。基本的な考え方は、正規化された方法でデータをモデル化することです。これは、大量のデータがあり、データが複数のテーブルにまたがっている場合、本質的に遅くて面倒になります。

私が思いついた最良のアプローチは次のとおりです。

  1. 取り組んでいる現実世界のものにできるだけ近い問題をモデル化する
  2. 必要に応じて正規化する

これら 2 つは、アプリケーションに多くの柔軟性を与え、多くの便利なメソッドを提供するだけでなく、私が答えようとしているほとんどの質問を解決するはずです。

必要なものを取得するために多数の結合を行う必要があり、必要なものを簡単に取得するにはテーブルを非正規化する必要があると感じたら、次のことを検討します。

SQL ビュー: これらは事前に定義された SQL ステートメント (結合など) で、モデルをリンクできます。一般に、これは ActiveRecord http://hashrocket.com/blog/posts/sql-views-and-activerecordを介してクエリを実行するよりもはるかに高速です。

集約テーブル: 1 つ以上の集約テーブルを作成し、delayed_job、resque などを使用して非同期的に更新します。これらの集計は、たとえば 1 日に 1 回更新でき、モデルはそれを直接クエリできます。これはある種の非正規化テーブルであることに注意してください。

Couchbase (NOSQL) これはまだ使っていませんが、とても面白そうです。 http://couchbaseonrails.com/understand

于 2014-05-01T20:23:14.050 に答える