4

我正在创建一个缓存系统,以使用排序/过滤查询从 SQLite 数据库表中获取数据并显示它。我从中提取的表可能非常大,当然,我需要通过在任何给定时间仅保留内存中的最大行数来最大限度地减少对内存的影响。这很容易通过使用LIMITOFFSET仅加载我需要的记录并根据需要更新缓存来完成。实现这一点是微不足道的。我遇到的问题是确定插入到特定查询中的新记录的插入索引在哪里,以便我可以适当地更新我的 UI。是否有捷径可寻?到目前为止,我的想法是:

  1. 转储整个缓存,重新计算查询结果(不保证会包含新行),刷新缓存并刷新整个 UI。我希望很明显为什么这不是真正可取的。
  2. 使用我自己的算法来确定新行是否包含在当前查询中,是否包含在当前缓存结果中,以及如果它在当前缓存范围内,则应将其插入到哪个索引中。这种方法最大的缺点是它的复杂性和我自己的排序/过滤算法与 SQLite 不匹配的风险。

当然,我想要的是能够询问 SQLite:Given 'Query A' 'Row B' 的索引是什么,而不需要加载整个查询结果。但是,到目前为止,我还没有找到一种方法来做到这一点。

我认为这并不重要,但这一切都发生在 iOS 设备上,使用的是 Objective-C 编程语言。

更多信息

查询和后续缓存基于用户输入。本质上,用户可以重新排序和过滤(或搜索)以改变他们看到的结果。我对简单地重新创建插入(实际上是编辑)缓存的沉默是为了提供“更流畅”的 UI 体验。

我应该指出,我目前倾向于选项“2”。我通过将所有记录加载到表中并使用我自己的算法在内存中执行排序/过滤来创建自己的缓存/索引系统。确定特定记录是否和/或在缓存中的位置所需的大量代码已经存在,所以我有点倾向于使用它。危险在于缓存与底层查询不匹配。如果我在缓存中包含一条查询不会返回的记录,我就会遇到麻烦并且可能会崩溃。

4

3 回答 3

1

通常,如果存在基础数据更改,您会期望缓存失效。我认为放弃它并重新开始将是您最简单、可维护的解决方案。我会推荐它,除非你有一个很好的理由。

您可以编写另一个刚刚返回行数的查询(下面的示例),以查看您的缓存是否应该失效。当缓存没有改变时,这将节省重新创建缓存。

SELECT name,address FROM people WHERE area_code=970;
SELECT COUNT(rowid) FROM people WHERE area_code=970;

您需要从 sqlite 了解缓存何时失效的信息需要对查询和/或索引的工作方式有一些相当深入的了解。我会说这是相当高的耦合。

否则,您会想知道它在排序方面被插入的位置。您可能会在排序字段上键入每个页面。删除大于插入/删除字段的任何内容。任何时候你改变排序,你都会放弃一切。

如果您使用 C++,下面的内容将是一个开始。我意识到你不是在做 C++,但希望我正在尝试做的事情是显而易见的。

struct Person {
  std::string name;
  std::string addr;
};

struct Page {
  std::string key;
  std::vector<Person> persons;
  struct Less {
    bool operator()(const Page &lhs, const Page &rhs) const {
      return lhs.key.compare(rhs.key) < 0;
    }
  };
};

typedef std::set<Page, Page::Less> pages_t;
pages_t pages;

void insert(const Person &person) {
  if (sql_insert(person)) {
    pages_t::iterator drop_cache_start = pages.lower_bound(person);
    //... drop this page and everything after it
  }
}

您必须进行一些争论才能使不同的数据类型key正常工作,但这是可能的。

从理论上讲,您可以将页面排除在外,只使用对象本身。但是,数据库将不再“拥有”数据。如果您只从数据库中填充页面,那么您对数据一致性的担忧就会减少。

这可能有点跑题了,您不是在重新实现视图吗?它本身不缓存,但不清楚这是否是您的项目的要求。

于 2012-09-07T18:18:10.307 回答
1

您不需要记录编号。

将有序字段的值保存在 LIMITed 查询结果的第一条和最后一条记录中。然后您可以使用这些来检查新记录是否在此范围内。

换句话说,假设您按Name字段排序,并且原始查询是这样的:

SELECT Name, ...
  FROM mytab
  WHERE some_conditions
  ORDER BY Name
  LIMIT x OFFSET y

然后尝试使用类似的查询获取新记录:

SELECT 1
  FROM mytab
  WHERE some_conditions
    AND PrimaryKey = LastInsertedValue
    AND Name BETWEEN CachedMin AND CachedMax

类似地,要找出插入新记录之前(或之后)哪个记录,直接在插入的记录之后开始并使用一个限制,如下所示:

SELECT Name
  FROM mytab
  WHERE some_conditions
    AND Name > MyInsertedName
    AND Name BETWEEN CachedMin AND CachedMax
  ORDER BY Name
  LIMIT 1

这不会给你一个数字。您仍然需要检查返回的名称在缓存中的位置。

于 2012-09-07T18:24:56.783 回答
1

我想出的解决方案并不简单,但目前运行良好。我意识到查询语句中记录的索引也是Count它之前所有记录的索引。我需要做的是将ORDER查询中的所有语句“转换”为一系列WHERE语句,这些语句将只返回前面的记录并计算这些记录。它比听起来更棘手(或者可能不是......听起来很棘手)。我遇到的最大问题是确保查询实际上是以我可以预测的方式排序的。这意味着我需要在 Order Parameters 中有一个 order 列,该列基于具有唯一值的列。因此,每当用户对一列进行排序时,我都会在语句中附加一个唯一列上的另一个订单参数(我使用了“修改日期戳”

创建WHERE语句的一部分需要的不仅仅是添加一堆ANDs。更容易演示。假设您有 3 个订单列:“姓氏”ASC、“名字”DESC 和“修改后的邮票”ASC(决胜局)。该WHERE语句必须看起来像这样('?' = 记录值):

WHERE
    "LastName" < ? OR
    ("LastName" = ? AND "FirstName" > ?) OR
    ("LastName" = ? AND "FirstName" = ? AND "Modified Stamp" < ?)

通过括号组合在一起的每组WHERE参数都是决胜局。如果事实上“LastName”的记录值相等,那么我们必须查看“FirstName”,最后查看“Modified Stamp”。显然,如果您按一堆订单参数排序,此语句可能会变得很长。

上面的解决方案还有一个问题。对值的数学运算NULL总是返回 false,但是当你对 SQLite 进行排序时,首先对NULL值进行排序。因此,为了适当地处理NULL值,您必须添加另一层复杂性。首先,所有数学等式运算 ,=, 必须替换为IS。其次,所有<操作都必须嵌套在操作符上以适当OR IS NULL地包含NULL<。这将上述操作变为:

WHERE
    ("LastName" < ? OR "LastName" IS NULL) OR
    ("LastName" IS ? AND "FirstName" > ?) OR
    ("LastName" IS ? AND "FirstName" IS ? AND ("Modified Stamp" < ? OR "Modified Stamp" IS NULL))

然后我使用上述WHERE参数计算 RowID。

事实证明,这对我来说很容易,主要是因为我已经构建了一组对象来表示我的 SQL 语句的各个方面,这些方面可以组合起来生成语句。我什至无法想象试图以任何其他方式操纵这样的 SQL 语句。

到目前为止,我已经在几台 iOS 设备上使用它进行了测试,表中有多达 10,000 条记录,并且没有明显的性能问题。当然,它是为单条记录编辑/插入而设计的,所以我真的不需要它超级快速/高效。

于 2012-09-14T19:10:10.013 回答