提高 Android 关系表的 SQLite 查询性能

Improving Android SQLite query performance for relational tables

场景:

我在我的 Android 应用程序中使用我认为相当大的 SQLite 数据库(大约 20 MB),它包含大约 50 tables。

这些 table 中的大多数是由外键 link 编辑的,很多时候,我需要一次从两个或更多 table 中检索信息.举例说明:

表 1:

Id  |  Name  |  Attribute1  |  Attribute2  |  ForeignKey

1   |  "Me"  |  SomeValue   |  AnotherVal  |     49
2   |  "A"   |     ...      |     ...      |     50
3   |  "B"   |              |              |     49

表 2:

Id  |  Attribute3  |  Attribute4  |  Attribute5

49  |   ThirdVal   |  FourthVal   |   FifthVal
50  |     ...      |     ...      |     ...

有时候,有两个以上的table这样link在一起。几乎所有时候,列数都比上面显示的多,通常有 1000 行左右。

我的目标是将数据库中的一些属性显示为 RecyclerView 中的项目,但我需要同时使用两个 table 来检索这些属性。


我的方法:

目前,我正在使用 android-sqlite-asset-helper 库将此数据库(.db 扩展名)从 assets 文件夹复制到应用程序中。当我记录这个复制发生的时间时,它在 732 毫秒内完成,这很好。

但是,当我想使用第一个 table 中的外键从两个 table 中检索数据时,花费的时间太长了。我测试的时候用了大约 11.47,我想加快速度。

我检索数据的方式是读取第一行中的每一行table,然后将其放入一个对象中:

public static ArrayList<FirstItem> retrieveFirstItemList(Context context) {
    Cursor cursor = new DbHelper(context).getReadableDatabase()
            .query(DbHelper.TABLE_NAME, null, null, null, null, null, null);
    ArrayList<FirstItem> arrayList = new ArrayList<>();
    cursor.moveToFirst();
    while (!cursor.isAfterLast()) {
        // I read all the values from each column and put them into variables
        arrayList.add(new FirstItem(id, name, attribute1, attribute2, foreignKey));
        cursor.moveToNext();
    }
    cursor.close();
    return arrayList;
}

FirstItem 对象将包含 getter 方法以及另一个用于从外键获取 SecondItem 对象的方法:

public SecondItem getSecondItem(Context context) {
    Cursor cursor = new SecondDbHelper(context).getReadableDatabase().query(
            SecondDbHelper.TABLE_NAME,
            null,
            SecondDbHelper.COL_ID + "=?",
            new String[] {String.valueOf(mForeignKey)},
            null, null, null);
    cursor.moveToFirst();
    SecondItem secondItem = new SecondItem(mForeignKey, attribute3, attribute4, attribute5);
    cursor.close();
    return secondItem;
}

当我将两个 table 的值打印到 logcat 时(我决定暂时不使用任何 UI 来测试数据库性能),我使用类似这个:

for (FirstItem firstItem : DBUtils.retrieveFirstItemList(this)) {
    Log.d("First item id", firstItem.getId());
    Log.d("Second item attr4", firstItem.getSecondItem(this).getAttribute4());
}

我怀疑此方法有问题,因为它需要在 Table2 中搜索 Table1 中的每一行 - 我认为它是效率低下。


一个想法:

我正在考虑使用另一种方法,但我不知道它是否比我当前的解决方案更好,或者它是否是 'proper' 实现我想要的方法。我的意思是我不确定是否有办法稍微修改我当前的解决方案以显着提高性能。尽管如此,我的想法是提高从数据库读取数据的速度。

当应用程序第一次加载时,来自 SQLite 数据库的各种 table 的数据将被读取,然后放入应用程序中的一个 SQLite 数据库中。当应用程序第一次 运行 并且每次更新数据库中的 table 时,都会发生此过程。我知道这会导致不同行的数据重复,但这是我看到的唯一方法,可以避免我必须搜索多个 table 来生成项目列表。

// read values from SQLite database and put them in arrays

ContentValues cv = new ContentValues();

// put values into variables

cv.put(COL_ID, id);
...
db.insert(TABLE_NAME, null, values);

由于这个过程也需要很长时间(因为有多行),我有点担心这不是最好的主意,但是我在一些 Stack Overflow 的答案中读到了事务,这会增加写入速度。换句话说,我会适当地使用 db.beginTransaction();db.setTransactionSuccessful();db.endTransaction(); 来提高将数据重写到新的 SQLite 数据库时的性能。

所以新的 table 看起来像这样:

Id  |  Name  |  Attribute1  |  Attribute2  |  Attribute3  |  Attribute4  | Attribute5

1   |  "Me"  |  SomeValue   |  AnotherVal  |   ThirdVal   |   FourthVal  |  FifthVal
2   |  "A"   |     ...      |     ...      |     ...      |     ...      |     ...
3   |  "B"   |  SomeValue   |  AnotherVal  |   ThirdVal   |   FourthVal  |  FifthVal

这意味着尽管 table 中会有更多列,但我将避免为第一个 table 中的每一行搜索多个 table,而数据也将更容易访问(用于过滤和类似的事情)。大多数 'loading' 将在开始时完成,并希望通过交易方法加快速度。


概览:

总而言之,我想加快从具有多个 table 的 SQLite 数据库中读取的速度,我必须在其中查看第一行 table 的每一行的这些 table ] 以产生所需的结果。这需要很长时间,而且效率低下,但我不确定是否有办法调整我目前的方法来大大提高读取速度。我认为我应该 'load' 应用程序第一个 运行 时的数据,方法是将来自各种 table 的数据重新组织成一个 table.

所以我想问一下,这两种方法哪种更好(主要是关于性能)?有没有办法可以调整我当前的方法,或者我做错了什么?最后,如果有比我已经提到的两种方法更好的方法来做到这一点,它是什么以及我将如何实施它?

也许试试这个:https://realm.io/products/java/我从没用过它,我对它们的性能一无所知。这可能是一种让您感兴趣的方式.. 或者不感兴趣 ;)

您应该尝试的几件事:

  • 优化加载。据我了解您当前的方法,它遇到了 N + 1 查询问题。你必须执行一个查询来获取第一批数据,然后对原始结果集的每一行进行另一个查询,这样你就可以获取相关数据。使用该方法遇到性能问题是正常的。我不认为它具有可扩展性,我建议您远离它。最简单的方法是使用连接而不是多个查询。这称为预加载。
  • 在您的 table 上引入适当的索引。如果您正在执行大量连接,您真的应该考虑加快它们的速度。索引是这里显而易见的选择。通常情况下,主键列默认被索引,但外键没有。这意味着您对每个连接的 table 执行线性搜索,这很慢。我会尝试在您的外键列(以及连接中使用的所有列)上引入索引。尝试测量连接前后的性能,看看您是否在那里取得了任何进展。
  • 考虑使用数据库视图。当您必须经常执行连接时,它们非常有用。创建视图时,您会得到一个预编译的查询,与每次 运行 连接相比可以节省相当多的时间。您可以尝试使用连接并针对视图执行查询,这将显示您将节省多少时间。这样做的缺点是将结果集映射到 Java 对象的层次结构有点困难,但至少根据我的经验,性能提升是值得的。
  • 您可以尝试使用某种延迟加载。除非明确请求,否则推迟加载相关数据。这可能很难实施,我认为这应该是您最后的选择,但它仍然是一个选择。您可能会发挥创意并利用动态代理或类似的东西来实际执行加载逻辑。

总而言之,在大多数情况下,巧妙地使用索引/视图应该可以解决问题。将其与急切/延迟加载相结合,您应该能够达到对性能满意的程度。

编辑:关于索引、视图和Android实施的信息

索引和视图不是同一问题的替代方法。它们有不同的特点和应用。

将索引应用于列时,可以加快对这些列值的搜索。您可以将其视为线性搜索与树搜索比较。这加快了连接速度,因为数据库已经知道哪些行对应于所讨论的外键值。它们对简单的 select 语句也有有益的影响,不仅是使用连接的语句,因为它们还加快了 where 子句条件的执行。不过,他们有一个问题。索引加快了查询速度,但它们减慢了插入、更新和删除操作的速度(因为索引也必须维护)。

视图只是预编译和存储的查询,您可以像普通查询一样查询其结果集 table。这样做的好处是您不需要每次都编译和验证查询。

你不应该将自己局限于这两件事中的一件。它们并不相互排斥,结合起来可以为您提供最佳结果。

就 Android 实施而言,没什么可做的。 SQLite 开箱即用地支持索引和查询。您唯一需要做的就是创建它们。最简单的方法是修改数据库创建脚本以包含 CREATE INDEXCREATE VIEW 语句。您可以将 table 的创建与索引的创建结合起来,或者您可以稍后手动添加它,如果您需要更新已经存在的模式。只需查看 SQLite 手册以了解适当的语法。