oguzata
oguzata

Reputation: 53

Insert in SQLite faster using single Android Room transaction rather than multi transactions

By default, each method annotated with @Insert, @Update, or @Delete in DAO class will be executed within its own transaction and additional transactions are queued and executed on a first come, first serve order. If it is desired to run multi methods using a single transaction runInTransaction() method can be used. Correct me if I'm wrong about this.

I expected using single transaction rather than multi transactions performs better while inserting data in DB. But nothing has changed in terms of performance even after I call 2 different DAO methods in the single transaction.

I used two different methods to measure performance. First of them insert the data without using runInTransaction()

    private void insert() {
        Log.d(DEBUG_TAG, "Is it running in transaction?: " + resourceRoomDatabase.inTransaction());
        startTime = Instant.now();
        disposable.add(dao.insertLanguageResourceList(languageResourceList)
                .subscribeOn(Schedulers.io())
                .subscribe(() -> {
                            endTime = Instant.now();
                            Log.d(DEBUG_TAG, "insertLanguageResourceList completed. Duration is " + (Duration.between(startTime, endTime).toMillis()) + " milliseconds!");
                            startTime = endTime;
                        },
                        (error) -> Log.d(DEBUG_TAG, "insertLanguageResourceList error: " + error)));

        disposable.add(dao.insertProducts(productList)
                .subscribeOn(Schedulers.io())
                .subscribe(() -> {
                            endTime = Instant.now();
                            Log.d(DEBUG_TAG, "insertProducts completed. Duration is " + (Duration.between(startTime, endTime).toMillis()) + " milliseconds!");
                            startTime = endTime;
                        },
                        (error) -> Log.d(DEBUG_TAG, "insertProducts error: " + error)));
    }

Here is the logcat after I call this method

14:49:55.860  D  Is it running in transaction?: false
14:50:00.480  D  insertLanguageResourceList completed. Duration is 4619 milliseconds!
14:50:05.299  D  insertProducts completed. Duration is 4820 milliseconds!

And other way I tried is running this method inside runInTransaction()

   private void insertWithSingleTransaction() {
        new Thread(() -> resourceRoomDatabase.runInTransaction(this::insert)).start();
    }

Here is the logcat after I call insertWithSingleTransaction()

14:54:07.495  D  Is it running in transaction?: true
14:54:12.483  D  insertProducts completed. Duration is 4988 milliseconds!
14:54:16.573  D  insertLanguageResourceList completed. Duration is 4090 milliseconds!

My DAO class

@Dao
public abstract class LanguageResourceDao {
    @Insert
    public abstract Completable insertLanguageResourceList(List<LanguageResource> languageResourceList);

    @Insert
    public abstract Completable insertProducts(List<Product> productList);
}

In conclusion without using runInTransaction() it takes 9349 milliseconds and with runInTransaction() it takes 9078 milliseconds to insert data in DB.

If these two different ways acts the same in performance what's the point of executing two different methods in the same transaction? Is it wrong to be expecting single transaction performs better than the default behavior? How can I increase the speed of inserting data in DB?

Upvotes: 0

Views: 227

Answers (1)

MikeT
MikeT

Reputation: 57043

The transaction is purely the Database aspect. There are other aspects that you are not considering in the timings; the wrapper around the database actions.

You can basically ascribe a transaction to equate to disk activity which would be writing the resultant data to the disk (the -WAL file), perhaps including checkpointing (writing some of the -WAL file data to the actual database file).

  • if not using the default WAL mode, i.e. Journal Mode, then the disk activity is to write to both the actual database file and the -journal file.

Data is also stored in pages, if one test requests more or fewer new pages than the other for the same amount of activity, perhaps due to previous test then this may also be an unfair test.

  • e.g. say a test requires 1.5 pages and one test has an empty page then it will require the creation of just one new page. Whilst a test that starts with a page that is 3/4 full will require the creation of 2 pages.

Another factor could be IO caching which may be advantageous to one of the two tests.

You really need to run more comprehensive tests that provide a fairer/more consistent result.

However as expected, the runInTransaction does show a circa 3% improvement.

Demo

Here's a demo that shows a marked distinction (twice as fast when using a single transaction).

  • D/DBTIMING_OVERALL: Total for in 1 transaction = 13376. Total for in many transactions = 27681.

Rather than running in threads it is run purely on the main thread (.allowMainThreadQueries).

It is based on a very simple single table:-

@Entity
class Basic {
   @PrimaryKey
   Long id=null;
   String name;
}

No use of @Dao annotated methods rather a SupportSQLiteDatabase is used in a method of the Database class as per:-

long insertMany(int count, boolean inTransaction, String tagSuffix) {
    SupportSQLiteDatabase db = this.getOpenHelper().getWritableDatabase();
    if (inTransaction) db.beginTransaction();
    long start = System.currentTimeMillis();
    for (int i=0; i < count; i++) {
        db.execSQL("INSERT INTO basic (name) VALUES('xxxx:" + start + "');");
    }
    if (inTransaction()) {
        db.setTransactionSuccessful();
        db.endTransaction();
    }
    Log.d("DBTIMING_" + tagSuffix ,"To insert " + count + " rows, took " + (System.currentTimeMillis() - start) + " milliseconds. Done in 1 transaction " + inTransaction);
    return System.currentTimeMillis() - start;
}

Then some activity code:-

public class MainActivity extends AppCompatActivity {

    TheDatabase db;
    AllDAOs dao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        long in1=0;
        long inmany=0;

        db = TheDatabase.getInstance(this);
        dao = db.geAllDAOs();

        in1 = in1 + db.insertMany(20000,true,"R1");
        inmany = inmany + db.insertMany(20000,false,"R2");
        inmany = inmany + db.insertMany(20000,false,"R3");
        in1 = in1 + db.insertMany(20000,true,"R4");

        inmany = inmany + db.insertMany(20000,false,"R5");
        in1 = in1 +db.insertMany(20000,true,"R6");
        in1 = in1 +db.insertMany(20000,true,"R7");
        inmany = inmany + db.insertMany(20000,false ,"R8");

        in1 = in1 + db.insertMany(20000,true,"R9");
        inmany = inmany + db.insertMany(20000,false,"R10");
        inmany = inmany + db.insertMany(20000,false,"R11");
        in1 = in1 + db.insertMany(20000,true,"R12");

        inmany = inmany + db.insertMany(20000,false,"R13");
        in1 = in1 +db.insertMany(20000,true,"R14");
        in1 = in1 +db.insertMany(20000,true,"R15");
        inmany = inmany + db.insertMany(20000,false ,"R16");

        Log.d("DBTIMING_OVERALL","Total for in 1 transaction = " + in1 + ". Total for in many transactions = " + inmany + ".");
    }
}
  • Noting that 2 pairs of runs are used, the first pair reversing the sequence of if done in a single transaction. Also a fair number of inserts.

The full results written to the log being:-

2023-06-02 11:39:18.445 D/DBTIMING_R1: To insert 20000 rows, took 1897 milliseconds. Done in 1 transaction true
2023-06-02 11:39:21.934 D/DBTIMING_R2: To insert 20000 rows, took 3489 milliseconds. Done in 1 transaction false
2023-06-02 11:39:25.385 D/DBTIMING_R3: To insert 20000 rows, took 3451 milliseconds. Done in 1 transaction false
2023-06-02 11:39:27.018 D/DBTIMING_R4: To insert 20000 rows, took 1632 milliseconds. Done in 1 transaction true
2023-06-02 11:39:30.452 D/DBTIMING_R5: To insert 20000 rows, took 3434 milliseconds. Done in 1 transaction false
2023-06-02 11:39:32.108 D/DBTIMING_R6: To insert 20000 rows, took 1656 milliseconds. Done in 1 transaction true
2023-06-02 11:39:33.744 D/DBTIMING_R7: To insert 20000 rows, took 1636 milliseconds. Done in 1 transaction true
2023-06-02 11:39:37.167 D/DBTIMING_R8: To insert 20000 rows, took 3423 milliseconds. Done in 1 transaction false
2023-06-02 11:39:38.814 D/DBTIMING_R9: To insert 20000 rows, took 1646 milliseconds. Done in 1 transaction true
2023-06-02 11:39:42.246 D/DBTIMING_R10: To insert 20000 rows, took 3432 milliseconds. Done in 1 transaction false
2023-06-02 11:39:45.737 D/DBTIMING_R11: To insert 20000 rows, took 3491 milliseconds. Done in 1 transaction false
2023-06-02 11:39:47.358 D/DBTIMING_R12: To insert 20000 rows, took 1621 milliseconds. Done in 1 transaction true
2023-06-02 11:39:50.844 D/DBTIMING_R13: To insert 20000 rows, took 3486 milliseconds. Done in 1 transaction false
2023-06-02 11:39:52.490 D/DBTIMING_R14: To insert 20000 rows, took 1645 milliseconds. Done in 1 transaction true
2023-06-02 11:39:54.132 D/DBTIMING_R15: To insert 20000 rows, took 1642 milliseconds. Done in 1 transaction true
2023-06-02 11:39:57.606 D/DBTIMING_R16: To insert 20000 rows, took 3474 milliseconds. Done in 1 transaction false
2023-06-02 11:39:57.606 D/DBTIMING_OVERALL: Total for in 1 transaction = 13376. Total for in many transactions = 27681.
  • As can be seen throughout, the results consistently show a benefit of doing the work in a single transaction.
  • There is also a marked reduction in wrapping code.

Upvotes: 1

Related Questions