5

問題

大量のレコード (約 1000) を読み書きする必要があります。以下の例では、1000 レコードを書き込むのに 20 分もかかり、それらを読み取るのに 12 秒もかかります (「読み取り」テストを実行するときは、行をコメントアウトしますdo create_notes())。

起源

これは完全な例です (ビルドして実行します)。出力をコンソールに出力するだけです (ブラウザには出力しません)。

type User.t =
  { id : int
  ; notes : list(int) // a list of note ids
  }

type Note.t =
  { id : int
  ; uid : int // id of the user this note belongs to
  ; content : string
  }

db /user : intmap(User.t)
db /note : intmap(Note.t)

get_notes(uid:int) : list(Note.t) =
  noteids = /user[uid]/notes
  List.fold(
    (h,acc -> 
      match ?/note[h] with
      | {none} -> acc
      | {some = note} -> [note|acc]
    ), noteids, [])

create_user() =
  match ?/user[0] with
  | {none} -> /user[0] <- {id=0 notes=[]}
  | _ -> void

create_note() =
  key = Db.fresh_key(@/note)
  do /note[key] <- {id = key uid = 0 content = "note"}
  noteids = /user[0]/notes
  /user[0]/notes <- [key|noteids]

create_notes() =
  repeat(1000, create_note)

page() =
  do create_user()
  do create_notes()
  do Debug.alert("{get_notes(0)}")
  <>Notes</>

server = one_page_server("Notes", page)

もう一つ

また、トランザクションを介してメモを取得しようとしました (以下を参照)。Db.transaction が適切なツールのように見えますが、それをうまく採用する方法が見つかりませんでした。get_notes_via_transactionこのメソッドは とまったく同じくらい遅いことがわかりましたget_notes

get_notes_via_transaction(uid:int) : list(Note.t) =
  result = Db.transaction( ->
    noteids = /user[uid]/notes
    List.fold(
      (h,acc -> 
        match ?/note[h] with
        | {none} -> acc
        | {some = note} -> [note|acc]
      ), noteids, [])
  )
  match result with
  | {none} -> []
  |~{some} -> some

ご協力いただきありがとうございます。

編集:詳細

役立つかもしれない少しの追加情報:

さらにテストを重ねた結果、最初の 100 レコードの書き込みに 5 秒しかかからないことがわかりました。各レコードは、前のレコードよりも書き込みに時間がかかります。500 番目のレコードでは、各レコードの書き込みに 5 秒かかります。

プログラムを中断して (プログラムが遅く感じ始めたときに) 再度開始すると (データベースをクリアせずに)、中断したときと同じ (遅い) ペースでレコードが書き込まれます。

これで解決に近づくでしょうか?

4

1 に答える 1

3

ニック、これはおそらくあなたが望んでいた答えではありませんが、ここにあります:

  1. この種のパフォーマンス実験では、フレームワークを変更することをお勧めします。たとえば、クライアントをまったく使用しないなどです。create_node関数のコードを次のように置き換えます。

    counter = Reference.create(0)
    create_note() =
      key = Db.fresh_key(@/note)
      do /note[key] <- {id = key uid = 0 content = "note"}
      noteids = /user[0]/notes
      do Reference.update(counter, _ + 1)
      do /user[0]/notes <- [key|noteids]
      cntr = Reference.get(counter)
      do if mod(cntr, 100) == 0 then
           Log.info("notes", "{cntr} notes created")
         else
           void
      void
    
    import stdlib.profiler
    
    create_notes() =
      repeat(1000, -> P.execute(create_note, "create_note"))
    
    P = Server_profiler
    
    _ =
      do P.init()
      do create_user()
      do create_notes()
      do P.execute(-> get_notes(0), "get_notes(0)")
      P.summarize()
    
  2. 100回の挿入ごとにプリンターの中間タイミングを使用すると、挿入時間が線形ではなく、挿入されたアイテムの数に対して2乗であることがすぐにわかります。これは、/user[0]/notes <- [key|noteids]明らかにリスト全体が再度書き込まれるリストの更新によるものです。私の知る限り、それを回避するための最適化がありましたが、私が間違っているか、何らかの理由でここでは機能しません.

  3. 前述の最適化は別として、Opa でこのデータをモデル化するためのより良いアプローチは、次のプログラムのようにセットを使用することです。

    type Note.t =
    { id : int
    ; uid : int // id of the user this note belongs to
    ; content : string
    }
    
    db /user_notes[{user_id; note_id}] : { user_id : int; note_id : int }
    db /note : intmap(Note.t)
    
    get_notes(uid:int) : list(Note.t) =
      add_note(acc : list(Note.t), user_note) =
        note = /note[user_note.note_id]
        [note | acc]
      noteids = /user_notes[{user_id=uid}] : dbset({user_id:int; note_id:int})
      DbSet.fold(noteids, [], add_note)
    
    counter = Reference.create(0)
    
    create_note() =
      key = Db.fresh_key(@/note)
      do /note[key] <- {id = key uid = 0 content = "note"}
      do DbVirtual.write(@/user_notes[{user_id=0}], {note_id = key})
      do Reference.update(counter, _ + 1)
      cntr = Reference.get(counter)
      do if mod(cntr, 100) == 0 then
           Log.info("notes", "{cntr} notes created")
         else
           void
      void
    
    import stdlib.profiler
    
    create_notes() =
      repeat(1000, -> Server_profiler.execute(create_note, "create_note"))
    
    _ =
      do Server_profiler.init()
      do create_notes()
      do Server_profiler.execute(-> get_notes(0), "get_notes(0)")
      Server_profiler.summarize()
    

    ここで、データベースへの入力に約 2 秒かかるように設定します。残念ながら、この機能は非常に実験的であるため、文書化されていません。実際に、この例で爆発的に爆発します。

  4. 残念ながら、(3) と (4) を改善する予定はありません。業界標準の社内 DB ソリューションを提供することはあまり現実的ではないことがわかったからです。したがって、現時点では、Opa と既存の No-SQL データベースとの緊密な統合に全力を注いでいます。今後数週間で、それについての良いニュースをお届けできることを願っています。

私たちのチームからこの問題についてもっと学ぶように努めます。何か間違っていることがわかった場合は修正します。

于 2011-10-26T12:09:12.887 に答える