用于使用 id 和 string 进行搜索的变量类型

Variable Type for searching both with id and string

我需要在内存中存储 3 个字段的大约 500-1000 个条目,并通过 int 和 str 值进行快速有效的搜索。搜索以大约 300-500 个请求的快速爆发形式进行。我不确定如何有效地做到这一点。

存储的数据由 3 个字段组成:

  1. ID - 整数,不是连续的。 IE。它会像 (3, 5, 12, 55),但不是 (1, 2, 3, 4, 5)。
  2. 名称 - 字符串。
  3. 标签 - 字符串。

有 3 种可能的情况:

  1. 按姓名获取 ID。
  2. 通过 ID 获取名称。
  3. 按 ID 获取标签。

目前,我使用两种不同的类型:

  1. 带密钥对“%s=%i”的 THashedStringList 按名称搜索。
  2. 按其他搜索的 ID 排序的记录数组。

我发现这非常低效,目前正在寻找新的想法。有什么提示吗?

正如 David Heffernan 所建议的,您可能需要为此使用适当的数据库。

但是,如果您想要一个更轻量级的解决方案,并且具有出色的性能,您可以使用一个对象列表来存储您的所有项目和两个分别通过 ID 和名称引用这些项目的字典。

举个例子,考虑一只青蛙:

type
  TFrog = class
    ID: Integer;
    Name: string;
    Address: string;
  end;

与您的示例一样,此 class 具有一个整数和两个字符串成员。我们假设每只青蛙都有一个唯一的 ID 和一个唯一的名字。 (但两只或更多只青蛙可能共享同一个地址。)

为了能够测试性能,我们创建了一个原始的青蛙生成函数:

function CreateRandomFrog: TFrog;
const
  FrogFirstNames: array[0..11] of string =
    ('Luke', 'Smith', 'John', 'Maggie', 'Rose', 'Bill', 'Edward', 'Harry',
     'Andrew', 'Michael', 'Molly', 'Arthur');
  FrogLastNames: array[0..7] of string =
    ('Jones', 'Stone', 'Rock', 'Hill', 'Waterfall', 'Sky', 'Flower', 'Rain');
  FrogInitials: array[0..25] of Char = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  FrogAddressesPrefixes: array[0..3] of string =
    ('Tree', 'Swamp', 'Lawn', 'Lake');
begin
  Result := TFrog.Create;
  try
    Result.ID := Random(10*N);
    Result.Name := FrogFirstNames[Random(Length(FrogFirstNames))] + #32 +
      FrogInitials[Random(Length(FrogInitials))] + '.' +
      FrogInitials[Random(Length(FrogInitials))] + '.' +
      FrogInitials[Random(Length(FrogInitials))] + '.' + #32 +
      FrogLastNames[Random(Length(FrogLastNames))];
    Result.Address := FrogAddressesPrefixes[Random(Length(FrogAddressesPrefixes))] +
      #32 + Random(Byte.MaxValue).ToString;
  except
    Result.Free;
    raise;
  end;
end;

这会创造出像

这样的青蛙
ID: 123
Name: Bill D.H.H. Rock
Address: Tree 52

我们还定义了一个常量

const
  N = 1000000;

这是我们将同时创建的青蛙数量。

现在,一些操作:定义一个 class

type
  TFrogFarm = class
    Frogs: TObjectList<TFrog>;
    FrogsByID: TDictionary<Integer, TFrog>;
    FrogsByName: TDictionary<string, TFrog>;
    constructor Create;
    destructor Destroy; override;
    procedure TrySearchFarm;
  end;

想法是 Frogs 列表拥有青蛙对象,而 FrogsByIDFrogsByName 字典只引用青蛙对象而不拥有它们。这些是使用 ID 和名称作为键的字典。

像这样实现它:

{ TFrogFarm }

constructor TFrogFarm.Create;
var
  Frog: TFrog;
begin

  // Create the list that owns the frog objects
  Frogs := TObjectList<TFrog>.Create;

  // Create the dictionaries that refer to the frog objects without owning them
  FrogsByID := TDictionary<Integer, TFrog>.Create;
  FrogsByName := TDictionary<string, TFrog>.Create;

  // Create N random frogs with unique IDs and names
  repeat
    Frog := CreateRandomFrog;
    if not FrogsByID.ContainsKey(Frog.ID) and not FrogsByName.ContainsKey(Frog.Name) then
    begin
      Frogs.Add(Frog); // transfer of ownership
      FrogsByID.Add(Frog.ID, Frog);
      FrogsByName.Add(Frog.Name, Frog);
    end
    else
      Frog.Free; // if this weren't a simple test project, we'd protect this better
  until Frogs.Count = N;

end;

destructor TFrogFarm.Destroy;
begin
  FreeAndNil(FrogsByName);
  FreeAndNil(FrogsByID);
  FreeAndNil(Frogs);
  inherited;
end;

procedure TFrogFarm.TrySearchFarm;
var
  Frog: TFrog;
  S1, S2: string;
  c1, c2, f: Int64;
begin

  QueryPerformanceFrequency(f);
  QueryPerformanceCounter(c1);

  if FrogsByID.TryGetValue(100, Frog) then
    S1 := 'There is a frog with ID 100.'#13#10'He or she lives at ' + Frog.Address + '.'
  else
    S1 := 'There is NO frog with ID 100.';

  if FrogsByName.TryGetValue('Maggie A.M.D. Flower', Frog) then
    S2 := 'There is a frog named "Maggie A.M.D. Flower".'#13#10'She lives at ' + Frog.Address + '.'
  else
    S2 := 'There is NO frog named "Maggie A.M.D. Flower".';

  QueryPerformanceCounter(c2);

  ShowMessage(S1 + sLineBreak + sLineBreak + S2 + sLineBreak + sLineBreak +
    'Execution time: ' + Round(1000000*(c2 - c1)/f).ToString + ' µs');

end;

要尝试这个,做

begin
  Randomize;
  while True do
    with TFrogFarm.Create do
      try
        TrySearchFarm;
      finally
        Free;
      end;
end;

在字典中查找元素是一个 O(1) 操作,因此即使在非常大的集合中也很快。而且,事实上,农场里有 100 万只青蛙 (N = 1000000),在我的系统上查找大约需要 2 微秒:

Screenshot of the program: A message dialog with text "There is NO frog with ID 100. There is a frog named 'Maggie A.M.D. Flower'. She lives at Swamp 211. Execution time: 2 µs".

我根据 Andreas Rejbrand 的建议整理了这个答案,作为他的对比 基于字典的答案。它不太可能表现得那么好,但在某些方面设置起来更简单。

它显示了基于 TDataSet 的方法在许多方面的局限性,主要 其中之一是需要为字符串字段设置最大字段大小。 FireDAC 支持 ftWideString 字段,但这并不意味着您应该使用它们来存储 Delphi “巨大” 字符串。

对于搜索,我使用了标准数据集 Locate 函数,但如果您 优化后,为不同的设置索引可能会更好 搜索类型和 select 正确的搜索类型 运行-时间。

我不确定您打算如何使用标签字段。如果你想拥有一个 每条记录有任意数量的标签,最好将它们放在数据集中 与 FDMemTable1 的主从关系的细节侧。留作练习 对于 reader.

procedure TForm2.FormCreate(Sender: TObject);
var
  AField : TField;
  i : Integer;
begin
  AField := TIntegerField.Create(Self);
  AField.FieldName := 'ID';
  AField.DataSet := FDMemTable1;

  AField := TStringField.Create(Self);
  AField.FieldName := 'Name';
  AField.Size := 80;
  AField.DataSet := FDMemTable1;

  AField := TStringField.Create(Self);
  AField.FieldName := 'Tags';
  AField.Size := 80;
  AField.DataSet := FDMemTable1;

  // FDMemTable1.IndexFieldNames := 'Name;ID';
  FDMemTable1.CreateDataSet;

  FDMemTable1.DisableControls;
  try
    for i := 1 to 1000 do
      FDMemTable1.InsertRecord([i, 'Frog' + IntToStr(i), Chr(Ord('A') + Random(26))]);
  finally
    FDMemTable1.EnableControls;
  end;
end;

function TForm2.FindByName(const AName : String) : Boolean;
begin
  Result := FDMemTable1.Locate('Name', AName, []);
end;

function TForm2.FindByID(const AID: Integer) : Boolean;
begin
  Result := FDMemTable1.Locate('ID', ID, []);
end;