sql >> データベース >  >> RDS >> Database

高速ローカルストレージを求めて

    最近、大量のデータをディスクに高速かつ頻繁に転送する必要のある機能の開発に携わりました。さらに、このデータは時々ディスクから読み取られることになっています。したがって、私はこのデータを保存する場所、方法、および手段を見つける運命にありました。この記事では、タスクを簡単に確認し、このタスクを完了するためのソリューションを調査して比較します。

    タスクのコンテキスト :私は、相対データベース開発用のツール(SQL Server、MySQL、Oracle)を開発するチームで働いています。ツールの範囲には、スタンドアロンツールと、MSSSMSのアドインの両方が含まれます。

    タスク :IDEの次の起動時にIDEを閉じるときに開いたドキュメントを復元します。

    ユースケース :どのドキュメントが保存され、どのドキュメントが保存されなかったかを考えずに、オフィスを離れる前にIDEをすばやく閉じる。 IDEの次の起動時に、閉じた時点と同じ環境を取得して作業を続行する必要があります。作業のすべての結果は、無秩序に閉鎖された瞬間に保存する必要があります。プログラムまたはオペレーティングシステムのクラッシュ中、または電源オフ中。

    タスク分析 :同様の機能がWebブラウザにあります。ただし、ブラウザは約100個の記号で構成されるURLのみを保存します。この場合、ドキュメントのコンテンツ全体を保存する必要があります。したがって、ユーザーのドキュメントを保存および保存する場所が必要です。さらに、ユーザーが他の言語とは異なる方法でSQLを操作する場合もあります。たとえば、1000行を超える長さのC#クラスを作成した場合、それはほとんど受け入れられません。一方、SQLユニバースには、10〜20行のクエリとともに、巨大なデータベースダンプが存在します。このようなダンプはほとんど編集できません。つまり、ユーザーは編集内容を安全に保つことを望んでいます。

    ストレージの要件:

    1. 軽量の組み込みソリューションである必要があります。
    2. 書き込み速度が速い必要があります。
    3. マルチプロセッシングアクセスのオプションが必要です。同期オブジェクトを使用してアクセスを確保できるため、この要件は重要ではありませんが、それでも、このオプションがあると便利です。

    候補者

    最初の候補はかなり不器用です。つまり、すべてをAppDataのどこかのフォルダーに保存します。

    2番目の候補は明らかです–組み込みデータベースの標準であるSQLite。非常に堅実で人気のある候補者。

    3番目の候補はLiteDBデータベースです。これは、Googleでの「.net用の組み込みデータベース」クエリの最初の結果です。

    一目で

    ファイルシステム。ファイルはファイルであり、メンテナンスと適切な命名が必要です。ファイルの内容に加えて、プロパティの小さなセット(ディスク上の元のパス、接続文字列、それが開かれたIDEのバージョン)を保存する必要があります。つまり、1つのドキュメントに対して2つのファイルを作成するか、プロパティをコンテンツから分離する形式を考案する必要があります。

    SQLiteは古典的なリレーショナルデータベースです。データベースは、ディスク上の1つのファイルで表されます。このファイルはデータベーススキーマにバインドされています。その後、SQL手段を使用してファイルを操作する必要があります。プロパティまたはコンテンツを個別に使用する必要がある場合に備えて、プロパティ用とコンテンツ用の2つのテーブルを作成できます。

    LiteDBは非リレーショナルデータベースです。 SQLiteと同様に、データベースは単一のファイルで表されます。完全にС#で書かれています。魅力的な使い方が簡単です。ライブラリにオブジェクトを渡すだけで、シリアル化は独自の方法で実行されます。

    パフォーマンステスト

    コードを提供する前に、一般的な概念を説明し、比較結果を提供したいと思います。

    一般的な概念は、大量の小さなファイルをデータベースに書き込む速度、平均的な量の平均的なファイル、および少量の大きなファイルを比較することです。平均的なファイルの場合はほとんど実際の場合に近いですが、小さいファイルと大きいファイルの場合は境界の場合であり、これも考慮に入れる必要があります。

    標準のバッファサイズのFileStreamを使用して、コンテンツをファイルに書き込んでいました。

    SQLiteには1つのニュアンスがありました。すべてのドキュメントコンテンツ(前述のとおり、非常に大きくなる可能性があります)を1つのデータベースセルに入れることはできませんでした。重要なのは、最適化の目的で、ドキュメントのテキストを1行ずつ保存することです。つまり、テキストを1つのセルに入れるには、すべてのドキュメントを1つの行に入れる必要があります。これにより、使用される操作メモリの量が2倍になります。問題の反対側は、データベースからデータを読み取るときに明らかになります。そのため、SQLiteには別のテーブルがあり、データは行ごとに保存され、データは外部キーを使用してファイルプロパティのみを含むテーブルにリンクされていました。さらに、ログを記録せずに1つのトランザクション内で、オフ同期モードでバッチデータ挿入(一度に数千行)を使用してデータベースを高速化することができました。

    LiteDBは、プロパティの中にListを持つオブジェクトを受け取り、ライブラリはそれを独自にディスクに保存しました。
    テストアプリケーションの開発中に、私はLiteDBを好むことを理解しました。重要なのは、SQLiteのテストコードは120行以上かかるのに対し、LiteDbの同じ問題を解決するコードは20行しかかからないということです。

    テストデータの生成

    FileStrings.cs

    internal class FileStrings {
    
           private static readonly Random random = new Random();
    
           public List Strings {
               get;
               set;
           } = new List();
    
           public int SomeInfo {
               get;
               set;
           }
    
           public FileStrings() {
           }
    
           public FileStrings(int id, int minLines, decimal lineIncrement) {
    
               SomeInfo = id;
               int lines = minLines + (int)(id * lineIncrement);
               for (int i = 0; i < lines; i++) {
    
                   Strings.Add(GetString());
               }
           }
    
           private string GetString() {
    
               int length = 250;
               StringBuilder builder = new StringBuilder(length);
               for (int i = 0; i < length; i++) {                builder.Append(random.Next((int)'a', (int)'z'));            }            return builder.ToString();        }    } Program.cs            List files = Enumerable.Range(1, NUM_FILES + 1)              .Select(f => new FileStrings(f, MIN_NUM_LINES, (MAX_NUM_LINES - MIN_NUM_LINES) / (decimal)NUM_FILES))
                 .ToList();
    

    SQLite

    private static void SaveToDb(List files) {
    
         using (var connection = new SQLiteConnection()) {
           connection.ConnectionString = @"Data Source=data\database.db;FailIfMissing=False;";
           connection.Open();
           var command = connection.CreateCommand();
           command.CommandText = @"CREATE TABLE files
    (
       id INTEGER PRIMARY KEY,
       file_name TEXT
    );
    CREATE TABLE strings
    (
       id INTEGER PRIMARY KEY,
       string TEXT,
       file_id INTEGER,
       line_number INTEGER
    );
    CREATE UNIQUE INDEX strings_file_id_line_number_uindex ON strings(file_id,line_number);
    PRAGMA synchronous = OFF;
    PRAGMA journal_mode = OFF";
           command.ExecuteNonQuery();
    
           var insertFilecommand = connection.CreateCommand();
           insertFilecommand.CommandText = "INSERT INTO files(file_name) VALUES(?); SELECT  last_insert_rowid();";
           insertFilecommand.Parameters.Add(insertFilecommand.CreateParameter());
           insertFilecommand.Prepare();
    
           var insertLineCommand = connection.CreateCommand();
           insertLineCommand.CommandText = "INSERT INTO strings(string, file_id, line_number) VALUES(?, ?, ?);";
           insertLineCommand.Parameters.Add(insertLineCommand.CreateParameter());
           insertLineCommand.Parameters.Add(insertLineCommand.CreateParameter());
           insertLineCommand.Parameters.Add(insertLineCommand.CreateParameter());
           insertLineCommand.Prepare();
    
           foreach (var item in files) {
             using (var tr = connection.BeginTransaction()) {
               SaveToDb(item, insertFilecommand, insertLineCommand);
               tr.Commit();
             }
           }
         }
       }
    
       private static void SaveToDb(FileStrings item, SQLiteCommand insertFileCommand, SQLiteCommand insertLinesCommand) {
    
         string fileName = Path.Combine("data", item.SomeInfo + ".sql");
    
         insertFileCommand.Parameters[0].Value = fileName;
    
         var fileId = insertFileCommand.ExecuteScalar();
    
         int lineIndex = 0;
         foreach (var line in item.Strings) {
    
           insertLinesCommand.Parameters[0].Value = line;
           insertLinesCommand.Parameters[1].Value = fileId;
           insertLinesCommand.Parameters[2].Value = lineIndex++;
           insertLinesCommand.ExecuteNonQuery();
         }
       }

    LiteDB

    private static void SaveToNoSql(List item) {
    
               using (var db = new LiteDatabase("data\\litedb.db")) {
                   var data = db.GetCollection("files");
                   data.EnsureIndex(f => f.SomeInfo);
                   data.Insert(item);
               }
           }
    

    次の表は、テストコードを数回実行した場合の平均結果を示しています。変更中、統計的な偏差はほとんど認識できませんでした。

    この比較でLiteDBが勝ったことには驚きませんでした。しかし、私はファイルに対するLiteDBの勝利にショックを受けました。ライブラリリポジトリを簡単に調べたところ、ディスクへの非常に細心の注意を払って実装されたページ書き込みが見つかりましたが、これはそこで使用されている多くのパフォーマンストリックの1つにすぎないと確信しています。もう1つ指摘したいのは、フォルダ内のファイルの量が非常に多くなると、ファイルシステムへのアクセス速度が速く低下することです。

    機能の開発にLiteDBを選択しましたが、この選択について後悔することはほとんどありません。重要なのは、ライブラリはすべてのC#向けにネイティブで記述されていることです。何かが明確でない場合は、いつでもソースコードを参照できます。

    短所

    上記のLiteDBの長所と競合他社との比較に加えて、開発中に短所に気づき始めました。これらの短所のほとんどは、図書館の「若者」によって説明できます。 「標準」シナリオの境界をわずかに超えてライブラリの使用を開始したところ、いくつかの問題(#419、#420、#483、#496)が見つかりました。ライブラリの作成者は質問に非常に迅速に回答し、ほとんどの問題はすぐに解決されました。現在、残っているタスクは1つだけです(クローズステータスと混同しないでください)。これは競争力のあるアクセスの問題です。非常に厄介な競合状態がライブラリのどこかに隠れているようです。このバグは非常に独創的な方法で受け継がれました(このテーマについては別の記事を書くつもりです)。
    また、きちんとした編集者と視聴者がいないことにも言及したいと思います。 LiteDBShellがありますが、これは真のコンソールファン専用です。

    概要

    LiteDB上に大きくて重要な機能を構築しました。現在、このライブラリも使用する別の大きな機能に取り組んでいます。インプロセスデータベースをお探しの方は、LiteDBと、それがタスクのコンテキストでどのように証明されるかに注意を払うことをお勧めします。ご存知のように、1つのタスクで何かがうまくいったとしても、必ずしもそうとは限らないからです。別のタスクを実行します。


    1. データベースコーナー:Mysqlストレージエンジンの初心者向けガイド

    2. foreach%dopar%+ RPostgreSQL

    3. MySQL:行をコピーする方法ですが、いくつかのフィールドを変更しますか?

    4. LINQ to Entitiesは、メソッド'System.String ToString()'メソッドを認識せず、このメソッドをストア式に変換できません