Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

データベースのパフォーマンスを低下させる一般的なRailsイディオム

RailsのActiveRecordを初めて見たときのことを覚えています。それは啓示でした。これは2005年にさかのぼり、PHPアプリのSQLクエリを手動でコーディングしていました。突然、データベースの使用は退屈な雑用から簡単になり、あえて言うと楽しいものになりました。

...それから私はパフォーマンスの問題に気づき始めました。

ActiveRecord自体は遅くはありませんでした。実際に実行されているクエリに注意を払うのをやめました。そして、Rails CRUDアプリで使用される最も慣用的なデータベースクエリのいくつかは、デフォルトでは、より大きなデータセットへのスケールアップが非常に不十分であることがわかりました。

この記事では、最大の原因の3つについて説明します。ただし、最初に、DBクエリが適切に拡張されるかどうかを判断する方法について説明しましょう。

パフォーマンスの測定

データセットが十分に小さい場合、すべてのDBクエリはパフォーマンスが高くなります。したがって、実際にパフォーマンスの感触をつかむには、実稼働サイズのデータ​​ベースに対してベンチマークを行う必要があります。この例では、faultsというテーブルを使用します。 約22,000件のレコードがあります。

postgresを使用しています。 postgresでは、パフォーマンスを測定する方法は、explainを使用することです。 。例:

# explain (analyze) select * from faults where id = 1;
                                     QUERY PLAN
--------------------------------------------------------------------------------------------------
 Index Scan using faults_pkey on faults  (cost=0.29..8.30 rows=1 width=1855) (actual time=0.556..0.556 rows=0 loops=1)
   Index Cond: (id = 1)
 Total runtime: 0.626 ms

これは、クエリ(cost=0.29..8.30 rows=1 width=1855)を実行するための推定コストの両方を示しています。 そしてそれを実行するのにかかった実際の時間(actual time=0.556..0.556 rows=0 loops=1)

より読みやすい形式が必要な場合は、postgresに結果をYAMLで出力するように依頼できます。

# explain (analyze, format yaml) select * from faults where id = 1;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Index Scan"         +
     Scan Direction: "Forward"       +
     Index Name: "faults_pkey"       +
     Relation Name: "faults"         +
     Alias: "faults"                 +
     Startup Cost: 0.29              +
     Total Cost: 8.30                +
     Plan Rows: 1                    +
     Plan Width: 1855                +
     Actual Startup Time: 0.008      +
     Actual Total Time: 0.008        +
     Actual Rows: 0                  +
     Actual Loops: 1                 +
     Index Cond: "(id = 1)"          +
     Rows Removed by Index Recheck: 0+
   Triggers:                         +
   Total Runtime: 0.036
(1 row)

今のところ、「PlanRows」と「ActualRows」にのみ焦点を当てます。

  • 計画行 最悪の場合、クエリに応答するためにDBがループする必要のある行数
  • 実際の行 クエリを実行したとき、DBは実際に何行をループしましたか?

上記のように「PlanRows」が1の場合、クエリはおそらく適切にスケーリングされます。 「PlanRows」がデータベース内の行数と等しい場合、クエリは「全表スキャン」を実行し、適切にスケーリングされないことを意味します。

クエリのパフォーマンスを測定する方法がわかったので、いくつかの一般的なレールのイディオムを見て、それらがどのように積み重なるかを見てみましょう。

カウント

Railsビューでこのようなコードを見るのは本当に一般的です:

Total Faults <%= Fault.count %>

その結果、SQLは次のようになります。

select count(*) from faults;

explainにプラグインしてみましょう 何が起こるか見てみましょう。

# explain (analyze, format yaml) select count(*) from faults;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Aggregate"          +
     Strategy: "Plain"               +
     Startup Cost: 1840.31           +
     Total Cost: 1840.32             +
     Plan Rows: 1                    +
     Plan Width: 0                   +
     Actual Startup Time: 24.477     +
     Actual Total Time: 24.477       +
     Actual Rows: 1                  +
     Actual Loops: 1                 +
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Parent Relationship: "Outer"+
         Relation Name: "faults"     +
         Alias: "faults"             +
         Startup Cost: 0.00          +
         Total Cost: 1784.65         +
         Plan Rows: 22265            +
         Plan Width: 0               +
         Actual Startup Time: 0.311  +
         Actual Total Time: 22.839   +
         Actual Rows: 22265          +
         Actual Loops: 1             +
   Triggers:                         +
   Total Runtime: 24.555
(1 row)

うわー!単純なカウントクエリは、22,265行(テーブル全体)をループしています。 postgresでは、カウントは常にレコードセット全体をループします。

whereを追加すると、レコードセットのサイズを小さくできます。 クエリの条件。要件によっては、パフォーマンスが許容できる範囲でサイズが十分に小さくなる場合があります。

この問題を回避する他の唯一の方法は、カウント値をキャッシュすることです。 Railsは、次のように設定すると、これを実行できます。

belongs_to :project, :counter_cache => true

クエリがレコードを返すかどうかを確認するときに、別の方法を利用できます。 Users.count > 0の代わりに 、Users.exists?を試してください 。結果のクエリは、はるかにパフォーマンスが高くなります。 (これを私に指摘してくれた読者のGerry Shawに感謝します。)

並べ替え

インデックスページ。ほとんどすべてのアプリには少なくとも1つあります。データベースから最新の20レコードをプルし、それらを表示します。もっと簡単なことは何ですか?

レコードをロードするコードは、次のようになります。

@faults = Fault.order(created_at: :desc)

そのためのSQLは次のようになります:

select * from faults order by created_at desc;

それでは分析しましょう:

# explain (analyze, format yaml) select * from faults order by created_at desc;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Sort"               +
     Startup Cost: 39162.46          +
     Total Cost: 39218.12            +
     Plan Rows: 22265                +
     Plan Width: 1855                +
     Actual Startup Time: 75.928     +
     Actual Total Time: 86.460       +
     Actual Rows: 22265              +
     Actual Loops: 1                 +
     Sort Key:                       +
       - "created_at"                +
     Sort Method: "external merge"   +
     Sort Space Used: 10752          +
     Sort Space Type: "Disk"         +
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Parent Relationship: "Outer"+
         Relation Name: "faults"     +
         Alias: "faults"             +
         Startup Cost: 0.00          +
         Total Cost: 1784.65         +
         Plan Rows: 22265            +
         Plan Width: 1855            +
         Actual Startup Time: 0.004  +
         Actual Total Time: 4.653    +
         Actual Rows: 22265          +
         Actual Loops: 1             +
   Triggers:                         +
   Total Runtime: 102.288
(1 row)

ここでは、このクエリを実行するたびに、DBが22,265行すべてを並べ替えていることがわかります。ブエノはありません!

デフォルトでは、SQLのすべての「orderby」句により、レコードセットがリアルタイムですぐに並べ替えられます。キャッシュはありません。あなたを救う魔法はありません。

解決策は、インデックスを使用することです。このような単純なケースでは、created_at列にソートされたインデックスを追加すると、クエリがかなり高速化されます。

Railsの移行では、次のように記述できます:

class AddIndexToFaultCreatedAt < ActiveRecord::Migration
  def change
    add_index(:faults, :created_at)
  end
end

次のSQLを実行します:

CREATE INDEX index_faults_on_created_at ON faults USING btree (created_at);

最後に、(created_at) ソート順を指定します。デフォルトでは昇順です。

ここで、並べ替えクエリを再実行すると、並べ替え手順が含まれていないことがわかります。事前に並べ替えられたデータをインデックスから読み取るだけです。

# explain (analyze, format yaml) select * from faults order by created_at desc;
                  QUERY PLAN
----------------------------------------------
 - Plan:                                     +
     Node Type: "Index Scan"                 +
     Scan Direction: "Backward"              +
     Index Name: "index_faults_on_created_at"+
     Relation Name: "faults"                 +
     Alias: "faults"                         +
     Startup Cost: 0.29                      +
     Total Cost: 5288.04                     +
     Plan Rows: 22265                        +
     Plan Width: 1855                        +
     Actual Startup Time: 0.023              +
     Actual Total Time: 8.778                +
     Actual Rows: 22265                      +
     Actual Loops: 1                         +
   Triggers:                                 +
   Total Runtime: 10.080
(1 row)

複数の列で並べ替える場合は、複数の列で並べ替えられるインデックスも作成する必要があります。 Railsの移行では次のようになります。

add_index(:faults, [:priority, :created_at], order: {priority: :asc, created_at: :desc)

より複雑なクエリを実行し始めるときは、explainを実行することをお勧めします。 。早くそして頻繁にそれをしなさい。クエリを簡単に変更したため、postgresがインデックスを使用して並べ替えることができなくなっている場合があります。

制限とオフセット

インデックスページには、データベース内のすべてのアイテムを含めることはほとんどありません。代わりに、一度に10、30、または50個のアイテムのみを表示してページをめくります。これを行う最も一般的な方法は、limitを使用することです。 およびoffset 一緒。 Railsでは次のようになります:

Fault.limit(10).offset(100)

これにより、次のようなSQLが生成されます。

select * from faults limit 10 offset 100;

ここで、explainを実行すると、奇妙なことがわかります。スキャンされる行数は110で、制限にオフセットを加えたものに等しくなります。

# explain (analyze, format yaml) select * from faults limit 10 offset 100;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Limit"              +
     ...
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Actual Rows: 110            +
         ...

オフセットを10,000に変更すると、スキャンされる行数が10010に跳ね上がり、クエリが64倍遅くなることがわかります。

# explain (analyze, format yaml) select * from faults limit 10 offset 10000;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Limit"              +
     ...
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Actual Rows: 10010          +
         ...

これは不穏な結論につながります。ページを開くと、後のページの読み込みが前のページよりも遅くなります。上記の例で1ページあたり100アイテムを想定すると、100ページは1ページより13倍遅くなります。

それで、あなたは何をしますか?

率直に言って、私は完璧な解決策を見つけることができませんでした。まず、データセットのサイズを縮小できるかどうかを確認することから始めます。これにより、最初に数百または数千のページを用意する必要がなくなりました。

レコードセットを減らすことができない場合、最善の策は、offset/limitをwhere句に置き換えることです。

# You could use a date range
Fault.where("created_at > ? and created_at < ?", 100.days.ago, 101.days.ago)

# ...or even an id range
Fault.where("id > ? and id < ?", 100, 200)
結論

この記事で、dbクエリで発生する可能性のあるパフォーマンスの問題を見つけるためにpostgresのexplain関数を実際に利用する必要があることを確信していただければ幸いです。最も単純なクエリでもパフォーマンスに大きな問題が発生する可能性があるため、確認することをお勧めします。 :)


  1. Excel で自動的に更新されるデータベースを作成する方法

    この記事では、作成する方法について説明します データベース エクセルで 更新 自動的に 4つの便利な方法の助けを借りて。これは、動的データを操作する際に必要な重要な機能です。データベースが別のソースからのデータに依存している場合、ソース データに応じた自動更新は非常に重要です。メソッドに慣れるために、例を見てみましょう。 自動的に更新されるデータベースを作成するための 4 つの便利な方法 エクセルで 1. Web からデータを抽出して、Excel で自動的に更新されるデータベースを作成する タスク :抽出 アメリカ、ニューヨークの 14 日間の天気予報 ウェブ から Excel デー

  2. Google Chrome の新しい拡張機能プランが広告ブロッカーを無効にする可能性がある

    Google は、Chrome ブラウザのエクスペリエンスを強化する予定です。ただし、これはさまざまな拡張機能の開発者を刺激しました。この計画が実行されれば、Chrome のパフォーマンスとセキュリティが向上しますが、オンライン ブラウジング中に広告や悪意のあるリンクをブロックするように設計された拡張機能も機能しなくなります。 問題が発生するのはどの拡張機能ですか? Google の提案は、トラッカー ブロッカー Ghostery、uBlock Origin - オープン ソース広告ブロッカー、NoScript - JavaScript ソフトウェア ブロッカー、パスワード マネージャー