【问题标题】:Improving Android SQLite query performance for relational tables提高关系表的 Android SQLite 查询性能
【发布时间】:2016-10-24 11:27:09
【问题描述】:

场景:

我在我的 Android 应用中使用我认为相当大的 SQLite 数据库(大约 20 MB),它由大约 50 个表组成。

这些表中的大多数都是通过外键链接的,而且很多时候,我需要一次从两个或多个表中检索信息。举例说明:

表 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  |     ...      |     ...      |     ...

有时,以这种方式链接在一起的表格不止两个。几乎所有时间,列数都比上面显示的多,通常有 1000 行左右。

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


我的方法:

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

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

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

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;
}

当我将两个表中的值打印到 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 中的每一行 - 我认为它效率低下。


一个想法:

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

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

// 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 数据库时的性能。

所以新表看起来像这样:

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

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

这意味着虽然表中会有更多列,但我将避免为第一个表中的每一行搜索多个表,并且数据也将更容易访问(用于过滤和类似的事情) .大部分“加载”将在开始时完成,并希望通过交易方法加快速度。


概述:

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

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

【问题讨论】:

  • 对于那些投票结束和/或否决这个问题的人,我不认为这太宽泛了。我正在寻求实现上述目标的最佳方法,这可以通过修改第一种方法、使用第二种方法或其他方法来实现。我知道可能没有一个明确的答案,因为人们可能对此有不同的答案,但我还应该问什么是实现某事的最佳方法?我已在我的问题中添加了详细信息,但我不同意“这种格式的好答案会太长”。
  • 我认为有些用户可能认为这“过于宽泛”,因为您仅使用“table1”和“table2”来解释您的情况。如果我们不知道每张桌子有什么,我们无法建议这是否是一个明智的想法。你可以给我们举一些例子。您可以使用虚拟数据作为示例,但实体应该是相同或相似的类别。是否应该合并两个表是一个棘手的情况。您不想一次又一次地添加列等等。
  • @Sometowngeek 我明白你的意思,但我也提到这些只是作为一个例子,我的实际数据包含大约 50 个表,列更多,通常大约 1000 行。我认为数据到底是什么并不重要,但它有多少,它是如何组织的,以及我正在用它做什么,因为我的问题不涉及数据,而是我如何处理它。但是,如果有帮助,请告诉我,我会更新我的答案。希望我没有听起来太防御或任何东西;)
  • 在我的脑海中,我认为最快的方法是通过 SQL 查询将数据转储到 Excel 电子表格等输出文件中,然后创建一个新表。之后,将输出文件数据导入到新表中。不过,请确保在删除旧表之前验证数据是否正确。如果两个表通过 FK 链接在一起,这很容易做到。
  • 显然,我的评论已经在@danail-alexiev 第一点。你应该听从他的建议。

标签: android database performance android-sqlite database-performance


【解决方案1】:

您应该尝试的几件事:

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

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

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

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

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

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

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

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

【讨论】:

  • 感谢您的回答。我以前没有听说过这些概念中的大部分,因为我主要使用 Android 并没有太多使用数据库。我明白第一点。我之前没有听说过索引,在some research 之后,我不确定有什么区别。我发现了a SO answer 的区别,但我仍然不确定。我现在对reading about them 之后的观点有了更好的理解。最后,我将如何在 Android 中实现这一点?
  • @FarbodSalamat-Zadeh 查看我的编辑以及一些附加信息。评论有点太长了。
  • 感谢您的更新,Danail - 我还有一些问题。为了澄清,我应该在我的关系表上使用索引(以便加入更快)吗?我将如何在 Android 中查询视图 - 通常,我会使用 cursor.query(...) 将表名作为其参数之一传递(表名会在这里)?另外,仔细检查一下,这是否意味着我应该继续使用android-sqlite-asset-helper 库,以便我可以将.db 文件复制到我的应用程序中,从而避免从应用程序中的CSV 文件创建数据库?
  • 是的,表上的索引(外键列以加快连接速度)。查询视图与查询表相同 - 您只需将视图名称作为表名传递。它以相同的方式工作。您甚至可以指定 where 子句和所有内容。我没有使用您所指的库。如果您可以修改表创建脚本以包含索引和视图,那么一切都应该没问题。我认为这意味着要继续使用该库,您应该在将您作为资产插入之前修改 .db 文件。
  • 再次感谢,Danail - 我现在就试试这个方法。通常情况下,我会在尝试了这个想法之后奖励赏金,但你的建议显然很详细,而且由于奖励赏金的时间不多,我现在就奖励它。如果我在实施时有任何问题,我会告诉你的。
【解决方案2】:

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

【讨论】:

  • 看起来不错,但我不想花很多时间在我的应用中实现它,然后发现性能几乎没有提升。
  • 是的,我能理解。我有其他解决方案,但我不知道(再次^^)您是否可以实施。想象一下,您必须加入 100 行,如果您将 sql 拆分为 10 个子查询(加入前 10 个、下一个 10、下一个 10 等),您可能会启动 10 个异步请求。一个请求将快 10 倍。最后,合并 10 个结果数组。
  • 感谢您提供替代解决方案。我已经决定使用上面 Danail 的解决方案,但是异步向数据库添加行听起来也不错。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-03
  • 1970-01-01
  • 2013-02-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多