首页 > 解决方案 > 从 SQLite 中选择大量数据时应用程序崩溃

问题描述

我正在开发一个应用程序,该应用程序将数据本地保存在 SQLite DB 中,然后单击按钮同步到服务器。我面临的问题是,当我尝试从超过 1000 行的表中选择数据时,我的应用程序崩溃了。这就是我选择数据的方式:

Cursor crsOutletData = mDatabase.rawQuery("SELECT * FROM table_name WHERE some_column_1='complete' AND some_column_2 IS NULL", null);

注意 some_column_1 和 some_column_2 不是主键。

谢谢。

标签: androidsqliteandroid-sqlite

解决方案


尽管游标存在限制,但可以处理数百万行。

限制是如果一行包含的数据多于 CursorWindow(1M(早期版本)或 2M)中可以容纳的数据。通常,这只发生在图像或视频等非常大的项目上。

具有 3,000,000 行的示例

这是一个处理插入和提取 300 万行的示例应用程序(虽然非常耗时)。

1.DBDone.java

用于建立非 UI 线程何时完成的接口

  • (不运行主 UI 线程可能会由于 Application Not Rresponding (ANR) 而导致崩溃)
    • 1000 行不太可能导致 ANR

:-

public interface DBDone {
    void dbDone();
}

2.DBHelper.java

具有一些基本方法的数据库助手,以允许添加和检索数据(也不允许选择 WAL 或日记模式,后者用作默认模式,直到 Android 9)。

public class DBHelper extends SQLiteOpenHelper {

    public static final String DBNAME = "mydb";
    public static final int DBVERSION = 1;
    public static final String TBL_TABLENAME = "table_name";
    public static final String COL_SOMECOLUMN1 = "some_column_1";
    public static final String COL_SOMECOLUMN2 = "some_column_2";

    public static final String crt_tablename_sql = "CREATE TABLE IF NOT EXISTS " + TBL_TABLENAME + "(" +
            COL_SOMECOLUMN1 + " TEXT, " +
            COL_SOMECOLUMN2 + " TEXT" +
            ")";

    private static boolean mWALMode = false;
    SQLiteDatabase mDB;

    public DBHelper(Context context, boolean wal_mode) {
        super(context, DBNAME, null, DBVERSION);
        mWALMode = wal_mode;
        mDB = this.getWritableDatabase();
    }

    @Override
    public void onConfigure(SQLiteDatabase db) {
        super.onConfigure(db);
        if (mWALMode) {
            db.enableWriteAheadLogging();
        } else {
            db.disableWriteAheadLogging();
        }
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(crt_tablename_sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int i, int i1) {

    }

    public long insert(String c1_value, String c2_value) {

        String nullcolumnhack = null;
        ContentValues cv = new ContentValues();
        if ((c1_value == null && c2_value == null)) {
            nullcolumnhack = COL_SOMECOLUMN1;
        }
        if (c1_value != null) {
            cv.put(COL_SOMECOLUMN1,c1_value);
        }
        if (c2_value != null) {
            cv.put(COL_SOMECOLUMN2,c2_value);
        }
        return mDB.insert(TBL_TABLENAME,nullcolumnhack,cv);
    }

    public long insertJustColumn1(String c1_value) {
        return this.insert(c1_value, null);
    }

    public long insertJustColumn2(String c2_value) {
        return this.insert(null,c2_value);
    }

    public Cursor getAllFromTableName() {
        return this.getAll(TBL_TABLENAME);
    }

    public Cursor getAll(String table) {
        return mDB.query(table,null,null,null,null,null,null);
    }
}

3. MainActivity.java

  1. MainActivity 实例化 DatabaseHelper(mDBHlpr 注意到这将创建数据库和基础表,因为构造函数通过调用getWritableDatabase来强制创建)。

  2. 然后它调用 dbDone 方法,当 mStage 设置为 0 时,将在新线程中清空表。

  3. 当表被清空时,dbDone 被调用,mStage 将为 1,因此数据被添加(如果不存在,它不应该因为表已被清空)。

    • 注意添加 3,000,000 行可能需要一段时间。
  4. 插入数据后,将调用 dbDone 并且由于 mStage 现在为 2,在提取光标后将记录一些信息将记录所有行。遍历游标中的所有行,并计算两列都为空的行的计数。

    • 提取的行数 (3,000,000) 将写入日志。
    • 将计算两列都为空的行数(只是为了对游标做一些事情)。随着随机插入空值,该数字会有所不同。

:-

public class MainActivity extends AppCompatActivity implements DBDone {

    DBHelper mDBHlpr;
    int mStage = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDBHlpr = new DBHelper(this,false);
        dbDone();
    }

    // Add some data but not in the UI Thread
    private void addData() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                addSomeData(3000000);
                dbDone(); // All done so notify Main Thread
            }
        }
        ).start();
    }

    public void dbDone() {
        switch (mStage) {
            case 0:
                emptyTable();
                break;
            case 1:
                addData();
                break;
            case 2:
                logSomeInfo();
                break;
        }
        mStage++;
    }

    /**
     * Add some rows (if none exist) with random data
     * @param rows_to_add   number of rows to add
     */
    private void addSomeData(int rows_to_add) {
        Log.d("ADDSOMEDATA","The addSomeData method hass been invoked (will run in a non UI thread)");
        SQLiteDatabase db = mDBHlpr.getWritableDatabase();
        if(DatabaseUtils.queryNumEntries(db,DBHelper.TBL_TABLENAME) > 0) return;
        // Random data that can be added to the first column
        String[] potential_data1 = new String[]{null,"complete","started","stage1","stage2","stage3","stage4","stage5"};
        // Random data that can be added to the second column
        String[] potential_data2 = new String[]{null,"something else","another","different","unusual","normal"};
        Random r = new Random();
        db.beginTransaction();
        for (int i=0; i < rows_to_add; i++) {
            mDBHlpr.insert(
                    potential_data1[(r.nextInt(potential_data1.length))],
                    potential_data2[(r.nextInt(potential_data2.length))]
            );
        }
        db.setTransactionSuccessful();
        db.endTransaction();
    }


    /**
     * Log some basic info from the Cursor always traversinf the entire cursor
     */
    private void logSomeInfo() {
        Log.d("LOGSOMEINFO","The logSomeInfo method has been invoked.");
        Cursor csr = mDBHlpr.getAllFromTableName();
        StringBuilder sb = new StringBuilder("Rows in Cursor = " + String.valueOf(csr.getCount()));
        int both_null_column_count = 0;
        while (csr.moveToNext()) {
            if (csr.getString(csr.getColumnIndex(DBHelper.COL_SOMECOLUMN1)) == null && csr.getString(csr.getColumnIndex(DBHelper.COL_SOMECOLUMN2)) == null) {
                both_null_column_count++;
            }
        }
        sb.append("\n\t Number of rows where both columns are null is ").append(String.valueOf(both_null_column_count));
        Log.d("LOGSOMEINFO",sb.toString());
    }

    /**
     * Empty the table
     */
    private void emptyTable() {
        Log.d("EMPTYTABLE","The emptyTable method has been invoked (will run in a non UI thread)");
        new Thread(new Runnable() {
            @Override
            public void run() {
                mDBHlpr.getWritableDatabase().delete(DBHelper.TBL_TABLENAME,null,null);
                dbDone();
            }
        }).start();
    }
}

日志很可能包含 CursorWindow 完整消息,但这些消息已被处理(因为违规行将包含在下一个 CursorWindow 中)例如:-

2018-12-30 10:19:10.862 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 404 bytes, free space 204 bytes, window size 2097152 bytes
2018-12-30 10:19:11.856 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 24 bytes, free space 11 bytes, window size 2097152 bytes
2018-12-30 10:19:12.377 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 7 bytes, free space 4 bytes, window size 2097152 bytes
2018-12-30 10:19:12.902 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 8 bytes, free space 1 bytes, window size 2097152 bytes
2018-12-30 10:19:13.433 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 24 bytes, free space 1 bytes, window size 2097152 bytes
2018-12-30 10:19:13.971 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 24 bytes, free space 21 bytes, window size 2097152 bytes
2018-12-30 10:19:14.505 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 24 bytes, free space 18 bytes, window size 2097152 bytes
2018-12-30 10:19:15.045 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 404 bytes, free space 187 bytes, window size 2097152 bytes
2018-12-30 10:19:15.598 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 7 bytes, free space 0 bytes, window size 2097152 bytes
...........

时间:-

日志包括:-

2018-12-30 10:17:04.610 2799-2799/? D/EMPTYTABLE: The emptyTable method has been invoked (will run in a non UI thread)
2018-12-30 10:17:04.615 2799-2817/? D/ADDSOMEDATA: The addSomeData method hass been invoked (will run in a non UI thread)
2018-12-30 10:19:10.506 2799-2817/so53958115.so53958115 D/LOGSOMEINFO: The logSomeInfo method has been invoked.
2018-12-30 10:20:17.803 2799-2817/so53958115.so53958115 D/LOGSOMEINFO: Rows in Cursor = 3000000
         Number of rows where both columns are null is 62604

所以花了: - 5 ms 清空(已经为空的)表。- 添加 3,000,000 行需要 2 分 6 秒。- 提取和遍历光标需要 1 分 7.5 秒(并不是说您通常会提取这么多行)

但最重要的是,Cursor 已经处理了 3,000,000 行。您还可以看到 CursorWindow 在本例中为 2M(2097152 字节)。

结论

问题不太可能是 1000 行对于光标来说太大了,尽管可能是某些行,如果存储图像/视频/长文本,最大超过光标可以处理的大小。

该问题更有可能是由于其他原因,只能通过日志中的堆栈跟踪来确定。

因此,如果没有堆栈跟踪或更全面的信息,就不可能提供特定的答案。


推荐阅读