Actian Zen を使用したデータの並べ替え

フラット ファイルは、データを格納する簡単な方法です。テキストとして簡単に表すことのできるデータの場合、このようなファイルの読み取りや書き込みにはほとんど労力がかかりません。このため、カンマ区切り値(CSV)ファイルなどのプレーン テキスト形式は、開発者にとって頼りになるソリューションです。しかし、フラット形式の使用は今後に影響を及ぼす可能性があります。その理由を理解するために、このチュートリアルでは最初に、CSV ファイルの情報を使用して作業します。その後、Zen データ ファイルの同じ情報を使用して作業します。

右側の「このページの内容」では当ページで扱うトピックを示しています。

CSV ファイルの読み取り

サンプルの CSV ファイルは、人物の姓、名、誕生日の一覧で構成されています。各フィールドはカンマで区切られており、各人のレコードは単独で 1 行にあります。少しですが、ファイルのサンプル行を次に示します。

Smith,John,1985-04-23
Quinton,James,1960-12-23
Doe,Jane,2000-01-19

ファイルのすべての行をメモリ内のリストに読み取ってから、そのリストをコンソールに書き戻します。現時点では、ファイルの解析は試みません。ファイルをメモリに読み取って、それを表示するだけです。

#include <iostream>
#include <fstream>
#include <vector>
using namespace std;
const int READ_BUFFER_SIZE = 1024;
int main()
{
     vector<string> fileLines;
     //Reading the file
     ifstream input("userList.csv");
     if (!input)
          return -1;
     char readBuffer[READ_BUFFER_SIZE];
     while (input) {
          input.getline(readBuffer, READ_BUFFER_SIZE);
          if(strlen(readBuffer))
               fileLines.push_back(readBuffer);
     }
     input.close();
     //Print the contents of the file  
     for (auto i = 0; i < fileLines.size(); ++i)
     {
          cout << fileLines[i] << endl;
     }
     return 0;
}

CSV データを文字列として並べ替える

次に、姓(last name)に基づいて最初の 5 つの要素を取得しましょう。最初の手順は、姓を基準にしてデータを並べ替えることです。このファイルで読み取ったデータは人物の姓で始まっているため、行全体で並べ替えを実行するだけで目的の効果を得られます。この並べ替えアルゴリズムを使用するには、リスト全体がメモリ内にある必要があることに留意してください。

void sortList(vector<string> & sourceList) 
{ 
    bool moreSortingNeeded = true; 
    while (moreSortingNeeded) { 
             moreSortingNeeded = false; 
             for (auto i = 0; i < sourceList.size()-1; ++i) 
             { 
                  std::string tempA = sourceList[i]; 
                  std::string tempB = sourceList[i + 1]; 
                  if (tempB < tempA) 
                  { 
                       sourceList[i] = tempB; 
                       sourceList[i + 1] = tempA; 
                       moreSortingNeeded = true; 
                  } 
             } 
      } 
}

CSV データをフィールドで並べ替える

データを並べ替えたら、並べ替えられたリストの最初の 5 つの要素を取り込むことで、姓に基づいて最初の 5 つの要素を見つけることができます。しかし、ほかの条件に基づいて最初の 5 つの要素を選択する場合には、もう少し作業が必要になります。各行のフィールドを分離する必要があります。CSV に数値または日付のデータがある場合は、それを解析および変換して、正しく並べ替えられるようにする必要があります。実証するために、代わりに名(first name)でデータを並べ替えましょう。名フィールドはレコードの先頭にないため、フィールドを解析する必要があります。人物の情報を保持するためのデータ構造は以下のとおりです。

struct Date {
     unsigned char Day;
     unsigned char Month;
     short int Year;
};
struct Employee {
     int ID;
     char FirstName[32];
     char LastName[32];
     Date DOB;
};

行から個々のフィールドにデータをコピーすることができます。文字列を区切り文字で分割された部分文字列に分離するために使用される、strtok_s(string tokenizer)という名前の C 関数があります。この関数を使用して、CSV の 1 行にあるフィールドおよび日付の部分を分離し、解析します。Employee 構造には以下の値が入力されます。

{
     Employee p = { 0 };
     char buffer[1024];
     char* pch;
     char* text = new char[line.size() + 1];
     strncpy_s(text,line.size()+1,  line.c_str(), line.size());
     char *lastName  = strtok_s(text, ",",&pch);
     char* firstName  = strtok_s(NULL, ",", &pch);
     char* dobString = strtok_s(NULL, ",", &pch);
     strncpy_s(p.FirstName, sizeof(p.FirstName), firstName, strlen(firstName));
     strncpy_s(p.LastName, sizeof(p.LastName), lastName, strlen(lastName));
     //////////
     char* year = strtok_s(dobString, "-", &pch);
     char* month = strtok_s(NULL, "-", &pch);
     char* day = strtok_s(NULL, "-", &pch);
     p.DOB.Month = stoi(month);
     p.DOB.Day = stoi(day);
     p.DOB.Year = stoi(year);
     return p;
}

厳密に型指定されたフィールドで CSV データを並べ替える

ここでは別の並べ替え方法が必要です。以下のコードは、以前に使用したアルゴリズムに基づいていますが、構造の厳密に型指定されたフィールドで機能するように書かれています。

void sortEmployeeList(vector<Employee>& sourceList)
{
     bool moreSortingNeeded = true;
     while (moreSortingNeeded)
     {
          moreSortingNeeded = false;
          for (auto i = 0; i < sourceList.size() - 1; ++i)
          {
               if (strncmp(sourceList[i].LastName, sourceList[i + 1].LastName, sizeof(Employee::LastName)) > 0)
               {
                    Employee temp = sourceList[i];
                    sourceList[i] = sourceList[i + 1];
                    sourceList[i + 1] = temp;
                    moreSortingNeeded = true;
               }
          }
     }
}

後で他の属性で検索したい場合は、コードを再び変更する必要があります。また、データ型を追加して作業を開始する場合は、解析の更新も必要です。

これは機能しますが、コードはより複雑になっています。他の条件で並べ替えたい場合は、コードにさらに変更を加える必要があります。また、並べ替えを実行するには、すべてのデータをメモリに読み込む必要があります。データ量が増えるにつれ、パフォーマンスの問題が生じ始める可能性があります。

Zen データ ファイルの読み取り

より良い方法を見てみましょう。データは、CSV ファイルではなく Actian Zen データベース ファイルに格納されているとします。並べ替えはどのように機能するのでしょうか?

Zen データ ファイルは、API 呼び出しを使用して、または Zen Control Center(ZenCC)アプリケーションを使用して作成することができます。ZenCC インターフェイスは簡単に使えて、テーブルとそのフィールドをすばやく定義できます。CSV と同じデータを含む Employee という名前のテーブルを作成しました。Zen データ ファイルのデータにアクセスする方法は 2 つあります。SQL コマンドでデータベース ドライバーを使用するか、または Btrieve 2 NoSQL API を使用して、データを読み取ることができます。どちらの方法の SDK もさまざまなプラットフォームおよびプログラミング言語で使用でき、異なる開発環境間で幅広い移植性オプションを提供します。どちらの方法にも利点はありますが、Btrieve 2 の方が使いやすく(SQL を知る必要はありません)より速く実行するため、ここではこれを使用します。

Zen データ ファイルのフィールドは CSV のフィールドと似ています。CSV ファイルにデータが既に存在する場合は、データベース テーブルを右クリックして[データのインポート]コマンドを選択すれば、データをインポートできます。

Btrieve 2 クラスは、データに関する細かな作業の多くを処理します。ほんの数行のコードで、データベース ファイルを開いて読み取りを開始することができます。Btrieve 2 で Zen データ ファイルを使用する利点はほかにもあります。Btrieve は、解析が必要になる可能性のある文字列を読み取るのではなく、一致する型のデータ構造に直接読み取ることができます。

struct Date {
         unsigned char Day;
         unsigned char Month;
         short int Year;
};
struct Employee { 
         int ID;
         char FirstName[33];
         char LastName[33];
         Date DOB;
};

一部の C/C++ コンパイラには、構造体内のすべてのフィールドを 4 バイト境界に整列するオプションがあります。これらの構造を必要に応じてコンパイルする場合には、このオプションをオフにする必要があります。オプションをオフにしていないと、データを読み取ったときに間違ったフィールドに入力される可能性があります。

5 件のレコードを読み取って、それらの値を出力しましょう。

const char* FILE_NAME = "C:\\zendb\\Employees\\EMPLOYEE.MKD";
BtrieveClient btrieveClient;
Btrieve::StatusCode status;
BtrieveFile btrieveFile;
Employee record;
int recordCount = 0;

status = btrieveClient.FileOpen(&btrieveFile, FILE_NAME, NULL, Btrieve::OPEN_MODE_NORMAL);
if (status != Btrieve::STATUS_CODE_NO_ERROR)
    return -1;
int bytesRead = btrieveFile.RecordRetrieveFirst(Btrieve::INDEX_NONE, (char*)&record, sizeof(record));
while (status == Btrieve::STATUS_CODE_NO_ERROR && recordCount < 5)
{
    cout << record.FirstName << " " << record.LastName << endl;
    btrieveFile.RecordRetrieveNext((char*)&record, sizeof(record));
    status = btrieveFile.GetLastStatusCode();
    ++recordCount;
}
btrieveClient.FileClose(&btrieveFile);
btrieveClient.Reset();

値の読み取りはさらに簡単で、値を解析して個々のフィールドに入力するための追加コードは必要ありませんでした。しかし、ただ 5 件のレコードを読み取りたいわけではありません。フィールドの値に応じて、上位 5 件のレコードを読み取りたいのです。

インデックスを使用して Zen のデータを並べ替える

Zen データ ファイルでは、インデックスを使用することで、レコードが読み取られる順序を選択することができます。インデックスは、レコード内の一連のキー フィールドに基づいてレコードを取得する迅速かつ効率的な方法を提供します。Btrieve 2 を使用すると、いくつかの関数呼び出しによって新しいインデックスを作成できます。

インデックスを作成するには、レコードの並べ替えで考慮するフィールドを特定します。これは単一のフィールドでも複数のフィールドでもかまいません。これらのフィールドは併せてインデックス ID に割り当てられます。

前の例で、データベースからレコードを読み取ったときに使用されたインデックスは Btrieve::INDEX_NONE でした。この値を使用した場合は、特定のインデックスが使用されないため、レコードは不明確な順序で戻されます。新しく定義したインデックスの番号を使用した場合は、インデックスによって表される順序でレコードが取得されます。

従業員名で並べ替えるためのインデックスを作成しましょう。このインデックスでは、姓を 1 番目の並べ替え値、名を 2 番目の並べ替え値とします。同じ姓が 2 人いる場合は、名によって並べ替え順を決定します。

//Declarations for the variables that will be used
BtrieveIndexAttributes nameIndex;
BtrieveKeySegment lastNameKey, firstNameKey;
BtrieveFileInformation bfi;

//Setting the index number for our index
Btrieve::Index targetIndex = Btrieve::INDEX_100;
nameIndex.SetIndex(targetIndex);

const int LastName_Offset = 37;
lastNameKey.SetField(LastName_Offset, 34, Btrieve::DataType::DATA_TYPE_CHAR);
const int FirstName_Offset = 4; // offsetof(Employee, FirstName);
firstNameKey.SetField(FirstName_Offset, 34, Btrieve::DataType::DATA_TYPE_CHAR);

//Add the last name field to our collection of keys for
//     the index and then the first name
lastNameIndex.SetIndex(targetIndex);

status = btrieveFile.IndexDrop(targetIndex);
status = btrieveFile.IndexCreate(&nameIndex);

インデックスは、作成されるときにファイル内の既存のすべてのレコードから関連するデータが入力され、後続のすべての挿入/更新/削除操作で自動的に保持されます。データを並べ替えたいたびに、インデックスを再作成する必要はありません。インデックスが作成された後、前の例の中で、インデックスを使用するために変更する必要のある行が 1 行あります。その行は、以前は次のように書かれていました。

int bytesRead = btrieveFile.RecordRetrieveFirst(Btrieve::INDEX_NONE, (char*)&record, sizeof(record));

更新されたコード行は次のとおりです。

int bytesRead = btrieveFile.RecordRetrieveFirst(targetIndex, (char*)&record, sizeof(record));

逆順で結果を読み取りたい場合は、RecordRetrieveFirst/RecordRetrieveNext を使用する代わりに RecordRetrieveLast/RecordRetrievePrevious で読み取りを開始します。

または、インデックスの作成に使用されたキーの並べ替え順を、デフォルトの昇順ではなく降順を使用するように設定できます。

lastNameKey.SetDescendingSortOrder(true);
firstNameKey.SetDescendingSortOrder(true);

フィールドの並べ替え順の設定には、同じインデックス内のキーごとで昇順と降順を混合して使用するオプションがあります。

複数のインデックスを定義することにより、使用されるインデックスを変更することで、レコードを取得する順番を選択可能になります。姓順、名順、および誕生日順に並べ替えるためのインデックスが定義されていれば、いくつかのパラメーターに基づいてインデックスを簡単に選択できます。たとえば、プログラムでコマンド ライン文字列を受け入れるようにし、指定可能な値を firstlastdob とした場合、次のようなコードでインデックスを選択することができます。

Btrieve::Index sortIndex = Btrieve::INDEX_NONE;
if (argc > 1)
{
     if (!strncmp("first", argv[1], 5))
          sortIndex = Btrieve::INDEX_2;
     else if (!strncmp("last", argv[1], 4))
          sortIndex = Btrieve::INDEX_3;
     else if (!strncmp("dob", argv[1], 3))
          sortIndex = Btrieve::INDEX_4;
}

これで、プログラムは実行時に並べ替え順を変更できるようになりました。

並べ替え順の選択に Btrieve 2 API を使用すると、自分で並べ替えアルゴリズムを実装するよりもはるかに少ない労力で済みます。非常に少ないコードで、メモリにデータの完全なセットを読み込まずに、並べ替え条件に基づいてデータの上位の要素を読み取ることができるようになりました。

Btrieve 2 および Zen データベースの使用方法の詳細については、オンライン ドキュメントの「開発者リファレンス」>「データ アクセス方法」にある Btrieve 2 コンテンツを参照してください。Actian Zen の SDK は SDK Library for Zen / PSQL ページにあります。