Сбой приложения при выборе большого объема данных из SQLite - PullRequest
0 голосов
/ 28 декабря 2018

Я работаю над приложением, которое сохраняет данные локально в БД SQLite, а затем синхронизирует их с сервером одним нажатием кнопки.Проблема, с которой я сталкиваюсь, заключается в том, что мое приложение падает, когда я пытаюсь выбрать данные из таблицы, которая содержит более 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 не являются первичными ключами.

Спасибо.

1 Ответ

0 голосов
/ 30 декабря 2018

Несмотря на наличие ограничений для курсора, могут обрабатываться миллионы строк.

Ограничение заключается в том, что строка содержит больше данных, чем может храниться в CursorWindow (1M (более ранние версии) или 2M).Обычно это происходит только с очень большими объектами, такими как изображения или видео.

Пример с 3 000 000 строк

Вот пример приложения, которое вставляет и извлекает 3 миллиона строк, которые обрабатываются (очень много времени).

1.DBDone.java

Интерфейс для установления завершения потоков, не относящихся к пользовательскому интерфейсу

  • (в противном случае не запуск из основного потока пользовательского интерфейса может привести к сбою из-за отсутствия приложения (ANR))
    • 1000 строк вряд ли приведет к ANR

: -

public interface DBDone {
    void dbDone();
}

2.DBHelper.java

Помощник по базам данных с некоторыми основными методами, позволяющими добавлять и извлекать данные (также не позволяет выбирать режим WAL или Journal, последний используется так, что до 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 мс, чтобы очистить (уже пустую) таблицу.- 2 минуты и 6 секунд, чтобы добавить 3 000 000 строк.- 1 минута и 7,5 секунды для извлечения и перемещения курсора (не то, что вы обычно извлекаете столько строк)

Но самое главное, что курсор справился с 3 000 000 строк.Вы также можете видеть, что CursorWindow в этом случае составляет 2M (2097152 байта).

Заключение

Маловероятно, что 1000 строк слишком велики для курсора, хотя это может бытьчто некоторые строки при хранении изображений / видео / длинных текстов максимально превышают размер, который может обрабатывать курсор.

Эта проблема, скорее всего, связана с другими причинами, которые могут быть установлены только через стек.след в журнале.

Таким образом, невозможно предоставить конкретный ответ без отслеживания стека или более полной информации.

...