Take the 2-minute tour ×

I'm creating a caching system to take data from an SQLite database table using a sorted/filtered query and display it. The tables I'm pulling from can be potentially very large and, of course, I need to minimize impact on memory by only retaining a maximum number of rows in memory at any given time. This is easily done by using LIMIT and OFFSET to load only the records I need and update the cache as needed. Implementing this is trivial. The problem I'm having is determining where the insertion index is for a new record inserted into a particular query so I can update my UI appropriately. Is there an easy way to do this? So far the ideas I've had are:

  1. Dump the entire cache, re-count the Query results (there's no guarantee the new row will be included), refresh the cache and refresh the entire UI. I hope it's obvious why that's not really desirable.
  2. Use my own algorithm to determine whether the new row is included in the current query, if it is included in the current cached results and at what index it should be inserted into if it's within the current cached scope. The biggest downfall of this approach is it's complexity and the risk that my own sorting/filtering algorithm won't match SQLite's.

Of course, what I want is to be able to ask SQLite: Given 'Query A' what is the index of 'Row B', without loading the entire query results. However, so far I haven't been able to find a way to do this.

I don't think it matters but this is all occurring on an iOS device, using the objective-c programming language.

More Info

The Query and subsequent cache is based off of user input. Essentially the user can re-sort and filter (or search) to alter the results they're seeing. My reticence in simply recreating the cache on insertions (and edits, actually) is to provide a 'smoother' UI experience.

I should point out that I'm leaning toward option "2" at the moment. I played around with creating my own caching/indexing system by loading all the records in a table and performing the sort/filter in memory using my own algorithms. So much of the code needed to determine whether and/or where a particular record is in the cache is already there, so I'm slightly predisposed to use it. The danger lies in having a cache that doesn't match the underlying query. If I include a record in the cache that the query wouldn't return, I'll be in trouble and probably crash.

share|improve this question
As a side note: using OFFSET can become inefficient for large values because SQLite has to step through all these records to count them. Such a query could execute faster if you replace the offset with a condition on an indexed field that makes the query start at your desired window; e.g., add WHERE Name > ?LastPreviousValue. –  CL. Sep 7 '12 at 18:34
Interesting, I'll play around with that but I appreciate the idea. It'll force me to track my cache a little more closely but I think I can make that work. –  Aaron Hayman Sep 7 '12 at 18:46

3 Answers 3

You don't need record numbers.

Save the values of the ordered field in the first and last records of the LIMITed query result. Then you can use these to check whether the new record falls into this range.

In other words, assuming that you order by the Name field, and that the original query was this:

SELECT Name, ...
  FROM mytab
  WHERE some_conditions

then try to get at the new record with a similar query:

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

Similarly, to find out before (or after) which record the new record was inserted, start directly after the inserted record and use a limit of one, like this:

  FROM mytab
  WHERE some_conditions
    AND Name > MyInsertedName
    AND Name BETWEEN CachedMin AND CachedMax

This doesn't give you a number; you still have to check where the returned Name is in your cache.

share|improve this answer
This would tell me "if" a record were in the current cache, but wouldn't give me any information about where that record was in the cache. –  Aaron Hayman Sep 7 '12 at 18:49
You solution only works if 'Name' enforces unique values. Which I can't assume...because I don't enforce unique values. –  Aaron Hayman Sep 14 '12 at 18:35

Typically you'd expect a cache to be invalidated if there were underlying data changes. I think dropping it and starting over will be your simplest, maintainable solution. I would recommend it unless you have a very good reason.

You could write another query that just returned the row count (example below) to see if your cache should be invalidated. That would save recreating the cache when it did not change.

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

The information you'd need from sqlite to know when your cache was invalidated would require some rather intimate knowledge of how the query and/or index was working. I would say that is fairly high coupling.

Otherwise, you'd want to know where it was inserted with regards to the sorting. You would probably key each page on the sorted field. Delete anything greater than the insert/delete field. Any time you change the sorting you'd drop everything.

Something like the below would be a start if you were using C++. I realize you aren't doing C++, but hopefully it is evident as to what I'm trying to do.

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

You'd have to do some wrangling to get different datatypes of key to work nicely, but its possible.

Theoretically you could just leave the pages out of it and only use the objects themselves. The database would no longer "own" the data though. If you only fill pages from the database, then you'll have less data consistency worries.

This may be a bit off topic, you aren't re-implementing views are you? It doesn't cache per se, but it isn't clear if that is a requirement of your project.

share|improve this answer
Yeah, dumping the cache is by far the easiest solution but I'd like to avoid it. I'm in full control of the insertion and I'd prefer to use that to avoid a round trip to the database. I don't see how I'd be re-implementing SQL views. I've always viewed them useful for much more 'static' situations (commonly used Queries) than for sorting/filtering data triggered from a UI. Am I wrong? –  Aaron Hayman Sep 7 '12 at 18:43
@AaronHayman Yes, you would expect them to be used in static situations. It isn't clear what you are sorting and filtering on from the question. If it's based off user input, then it probably isn't appropriate. –  Tom Kerr Sep 7 '12 at 18:53
Good point, I'll update the question. But yes, it is based off of user input. I'm ok with dumping the cache when they re-sort/filter, but on insertions/edits I'd rather alter the cache to match what would it would be if I did refresh it. I know I'm asking for a lot, but it'll make the UI experience much better. –  Aaron Hayman Sep 7 '12 at 19:00
@AaronHayman Added an example on how I would approach it, though it is in the wrong language. Hopefully that is clear enough. –  Tom Kerr Sep 7 '12 at 19:17
up vote 0 down vote accepted

The solution I came up with is not exactly simple, but it's currently working well. I realized that the index of a record in a Query Statement is also the Count of all it's previous records. What I needed to do was 'convert' all the ORDER statements in the query to a series of WHERE statements that would return only the preceding records and take a count of those records. It's trickier than it sounds (or maybe not...it sounds tricky). The biggest issue I had was making sure the query was, in fact, sorted in a way I could predict. This meant I needed to have an order column in the Order Parameters that was based off of a column with unique values. So, whenever a user sorts on a column, I append to the statement another order parameter on a unique column (I used a "Modified Date Stamp") to break ties.

Creating the WHERE portion of the statement requires more than just tacking on a bunch of ANDs. It's easier to demonstrate. Say you have 3 Order columns: "LastName" ASC, "FirstName" DESC, and "Modified Stamp" ASC (the tie breaker). The WHERE statement would have to look something like this ('?' = record value):

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

Each set of WHERE parameters grouped together by parenthesis are tie breakers. If, in fact, the record values of "LastName" are equal, we must then look at "FirstName", and finally "Modified Stamp". Obviously, this statement can get really long if you're sorting by a bunch of order parameters.

There's still one problem with the above solution. Mathematical operations on NULL values always return false, and yet when you sort SQLite sorts NULL values first. Therefore, in order to deal with NULL values appropriately you've gotta add another layer of complication. First, all mathematical equality operations, =, must be replace by IS. Second, all < operations must be nested with an OR IS NULL to include NULL values appropriately on the < operator. This turns the above operation into:

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

I then take a count of the RowID using the above WHERE parameter.

It turned out easy enough for me to do mostly because I had already constructed a set of objects to represent various aspects of my SQL Statement which could be assembled to generate the statement. I can't even imagine trying to manipulate a SQL statement like this any other way.

So far, I've tested using this on several iOS devices with up to 10,000 records in a table and I've had no noticeable performance issues. Of course, it's designed for single record edits/insertions so I don't really need it to be super fast/efficient.

share|improve this answer

Your Answer


