Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

Data persistence forms the foundation of virtually every successful Android application. While SQLite has long been the go-to solution for local data storage, writing raw SQL queries and managing database operations manually often leads to boilerplate code, runtime errors, and maintenance headaches. Room Database revolutionizes Android data persistence by providing a modern, type-safe abstraction layer over SQLite that dramatically improves developer productivity and code reliability.
Room Database represents Google’s modern approach to local data persistence in Android applications. Built as part of Android Architecture Components, Room provides compile-time verification of SQL queries, eliminates boilerplate code, and seamlessly integrates with other Jetpack libraries to create robust, maintainable data layers.
The architecture consists of three primary components that work together to create a comprehensive database solution: Entity classes that define your data structure, Data Access Objects (DAOs) that define database operations, and the Database class that serves as the main access point to your app’s persisted data.

Traditional SQLite implementation in Android required extensive boilerplate code, manual cursor management, and provided no compile-time verification of SQL queries. Developers frequently encountered runtime crashes due to typos in SQL statements, schema mismatches, and complex lifecycle management issues.
Room addresses these fundamental problems by providing:
Compile-time SQL Verification: Room validates SQL queries at compile time, catching syntax errors and table mismatches before your app reaches users.
Automatic Object Mapping: Complex cursor operations are handled automatically, with Room generating efficient mapping code between database rows and Kotlin objects.
Integration with Architecture Components: Seamless integration with LiveData, Flow, and other reactive programming constructs enables powerful data observation patterns.
Migration Support: Built-in tools for handling database schema changes ensure smooth app updates without data loss.
Understanding Room’s architecture requires deep knowledge of its three fundamental components and how they interact to create a cohesive data persistence solution.
Entity classes represent tables in your Room database. These classes use annotations to define table structure, relationships, and constraints while maintaining clean, readable Kotlin code.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "first_name")
val firstName: String,
@ColumnInfo(name = "last_name")
val lastName: String,
val email: String,
@ColumnInfo(name = "created_at")
val createdAt: Long = System.currentTimeMillis(),
@ColumnInfo(name = "is_active", defaultValue = "1")
val isActive: Boolean = true
)
Entity annotations provide precise control over database structure:
@Entity: Marks the class as a database table, with optional parameters for table name, indices, and foreign key constraints.
@PrimaryKey: Designates the primary key field, with autoGenerate option for automatic ID assignment.
@ColumnInfo: Customizes column properties including name, type affinity, and default values.
@Ignore: Excludes fields from database persistence, useful for computed properties or temporary state.
Modern applications require complex data relationships that Room handles elegantly through relationship annotations:
@Entity(tableName = "posts")
data class Post(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val title: String,
val content: String,
val authorId: Long,
val createdAt: Long = System.currentTimeMillis()
)
@Entity(tableName = "comments")
data class Comment(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val postId: Long,
val authorId: Long,
val content: String,
val createdAt: Long = System.currentTimeMillis()
)
data class PostWithComments(
@Embedded val post: Post,
@Relation(
parentColumn = "id",
entityColumn = "postId"
)
val comments: List<Comment>
)
data class UserWithPosts(
@Embedded val user: User,
@Relation(
parentColumn = "id",
entityColumn = "authorId"
)
val posts: List<Post>
)
These relationship patterns enable powerful data modeling while maintaining database normalization and query efficiency.
DAOs define the interface for database operations, combining the flexibility of custom SQL with the safety of compile-time verification. Room generates implementation code automatically, handling cursor management, type conversion, and error handling.
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC")
fun getAllActiveUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Long): User?
@Query("SELECT * FROM users WHERE email = :email LIMIT 1")
suspend fun getUserByEmail(email: String): User?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User): Long
@Insert
suspend fun insertUsers(users: List<User>): List<Long>
@Update
suspend fun updateUser(user: User): Int
@Delete
suspend fun deleteUser(user: User): Int
@Query("DELETE FROM users WHERE id = :userId")
suspend fun deleteUserById(userId: Long): Int
@Query("UPDATE users SET is_active = 0 WHERE id = :userId")
suspend fun deactivateUser(userId: Long): Int
@Transaction
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserWithPosts(userId: Long): UserWithPosts?
@Query("""
SELECT u.* FROM users u
INNER JOIN posts p ON u.id = p.authorId
WHERE p.created_at > :since
GROUP BY u.id
ORDER BY COUNT(p.id) DESC
""")
suspend fun getMostActiveAuthors(since: Long): List<User>
}
DAO methods leverage different annotation types for various database operations:
@Query: Enables custom SQL queries with parameter binding and return type validation.
@Insert: Generates insertion code with configurable conflict resolution strategies.
@Update: Creates update operations based on primary key matching.
@Delete: Implements deletion operations using entity primary keys.
@Transaction: Ensures multiple database operations execute atomically.
The database class serves as the main access point to your app’s persisted data, defining database configuration, version management, and DAO access:
@Database(
entities = [User::class, Post::class, Comment::class],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
abstract fun commentDao(): CommentDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.addCallback(DatabaseCallback())
.build()
INSTANCE = instance
instance
}
}
}
private class DatabaseCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// Pre-populate database if needed
}
}
}
Effective Room implementation requires understanding modern Android development patterns and best practices that ensure scalable, maintainable code.
The repository pattern provides a clean abstraction between your data layer and business logic, enabling easier testing and more flexible data source management:
class UserRepository(
private val userDao: UserDao,
private val apiService: ApiService
) {
fun getAllUsers(): Flow<List<User>> = userDao.getAllActiveUsers()
suspend fun getUserById(userId: Long): Result<User> {
return try {
val user = userDao.getUserById(userId)
if (user != null) {
Result.success(user)
} else {
// Try to fetch from network
val networkUser = apiService.getUser(userId)
userDao.insertUser(networkUser)
Result.success(networkUser)
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun refreshUsers(): Result<Unit> {
return try {
val users = apiService.getAllUsers()
userDao.insertUsers(users)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun createUser(user: User): Result<Long> {
return try {
val localId = userDao.insertUser(user)
// Sync with backend
val remoteUser = apiService.createUser(user)
userDao.updateUser(remoteUser.copy(id = localId))
Result.success(localId)
} catch (e: Exception) {
Result.failure(e)
}
}
}
Modern Room implementation benefits greatly from dependency injection, particularly using Hilt for Android:
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return AppDatabase.getDatabase(context)
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
@Provides
fun providePostDao(database: AppDatabase): PostDao {
return database.postDao()
}
@Provides
fun provideCommentDao(database: AppDatabase): CommentDao {
return database.commentDao()
}
}
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
@Provides
@Singleton
fun provideUserRepository(
userDao: UserDao,
apiService: ApiService
): UserRepository {
return UserRepository(userDao, apiService)
}
}
Room requires type converters to handle complex data types that don’t map directly to SQLite types:
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
@TypeConverter
fun fromStringList(value: List<String>): String {
return Gson().toJson(value)
}
@TypeConverter
fun toStringList(value: String): List<String> {
return Gson().fromJson(value, object : TypeToken<List<String>>() {}.type)
}
@TypeConverter
fun fromUserRole(role: UserRole): String {
return role.name
}
@TypeConverter
fun toUserRole(role: String): UserRole {
return UserRole.valueOf(role)
}
}
Proper migration handling ensures your app can update smoothly without losing user data, a critical aspect of production Android applications.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
// Create new table
database.execSQL("""
CREATE TABLE user_preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user_id INTEGER NOT NULL,
preference_key TEXT NOT NULL,
preference_value TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)
""")
// Create index
database.execSQL("""
CREATE INDEX index_user_preferences_user_id
ON user_preferences(user_id)
""")
}
}
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
// Complex migration with data transformation
database.execSQL("""
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
full_name TEXT NOT NULL,
email TEXT NOT NULL,
phone_number TEXT,
created_at INTEGER NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1
)
""")
database.execSQL("""
INSERT INTO users_new (id, full_name, email, phone_number, created_at, is_active)
SELECT id, first_name || ' ' || last_name, email, phone_number, created_at, is_active
FROM users
""")
database.execSQL("DROP TABLE users")
database.execSQL("ALTER TABLE users_new RENAME TO users")
}
}
Enable schema export for testing and documentation:
@Database(
entities = [User::class, Post::class, Comment::class],
version = 4,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
// Database implementation
}
Create migration tests to ensure data integrity:
@RunWith(AndroidJUnit4::class)
class MigrationTest {
private val TEST_DB = "migration-test"
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
)
@Test
fun migrate1To2() {
var db = helper.createDatabase(TEST_DB, 1).apply {
execSQL("INSERT INTO users (first_name, last_name, email) VALUES ('John', 'Doe', 'john@example.com')")
close()
}
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)
val cursor = db.query("SELECT * FROM users")
cursor.moveToFirst()
// Verify migration results
assertEquals("john@example.com", cursor.getString(cursor.getColumnIndex("email")))
assertTrue(cursor.getColumnIndex("phone_number") >= 0)
}
}
Room provides excellent performance out of the box, but understanding optimization techniques ensures your app scales effectively with data growth.
Indexing: Create appropriate indices for frequently queried columns:
@Entity(
tableName = "users",
indices = [
Index(value = ["email"], unique = true),
Index(value = ["created_at"]),
Index(value = ["first_name", "last_name"])
]
)
data class User(
// Entity definition
)
Pagination: Implement pagination for large datasets using Paging 3:
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY created_at DESC")
fun getAllUsersPaged(): PagingSource<Int, User>
@Query("SELECT * FROM users WHERE first_name LIKE :query OR last_name LIKE :query ORDER BY created_at DESC")
fun searchUsersPaged(query: String): PagingSource<Int, User>
}
class UserRepository(private val userDao: UserDao) {
fun getAllUsers(): Flow<PagingData<User>> {
return Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false
),
pagingSourceFactory = { userDao.getAllUsersPaged() }
).flow
}
}
Efficient Queries: Write efficient SQL queries with proper JOIN strategies:
@Query("""
SELECT u.id, u.first_name, u.last_name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.author_id
WHERE u.is_active = 1
GROUP BY u.id
HAVING COUNT(p.id) > :minPosts
ORDER BY post_count DESC
LIMIT :limit
""")
suspend fun getTopAuthors(minPosts: Int, limit: Int): List<UserWithPostCount>
Flow vs LiveData: Choose appropriate reactive types based on your architecture:
@Dao
interface UserDao {
// Use Flow for Compose and coroutine-based architectures
@Query("SELECT * FROM users WHERE is_active = 1")
fun getActiveUsersFlow(): Flow<List<User>>
// Use LiveData for traditional View-based architectures
@Query("SELECT * FROM users WHERE is_active = 1")
fun getActiveUsersLiveData(): LiveData<List<User>>
}
Database Threading: Room handles threading automatically with suspend functions and Flow, but understanding the underlying mechanics helps with optimization:
// Room automatically handles threading
class UserRepository(private val userDao: UserDao) {
// Flow operations are automatically dispatched to background threads
fun getAllUsers(): Flow<List<User>> = userDao.getAllActiveUsers()
// Suspend functions run on background threads
suspend fun insertUser(user: User): Long = userDao.insertUser(user)
// For blocking operations (not recommended), specify dispatcher
suspend fun performBulkOperation() = withContext(Dispatchers.IO) {
// Bulk database operations
}
}
Comprehensive testing ensures your data layer behaves correctly across different scenarios and device configurations.
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java
).build()
userDao = database.userDao()
}
@After
fun closeDb() {
database.close()
}
@Test
fun insertAndGetUser() = runTest {
val user = User(
firstName = "John",
lastName = "Doe",
email = "john@example.com"
)
val userId = userDao.insertUser(user)
val retrievedUser = userDao.getUserById(userId)
assertNotNull(retrievedUser)
assertEquals(user.email, retrievedUser?.email)
assertEquals(user.firstName, retrievedUser?.firstName)
}
@Test
fun getUsersFlow_emitsUpdates() = runTest {
val users = listOf(
User(firstName = "John", lastName = "Doe", email = "john@example.com"),
User(firstName = "Jane", lastName = "Smith", email = "jane@example.com")
)
userDao.insertUsers(users)
userDao.getAllActiveUsers().test {
val emittedUsers = awaitItem()
assertEquals(2, emittedUsers.size)
assertTrue(emittedUsers.any { it.email == "john@example.com" })
}
}
}
@RunWith(AndroidJUnit4::class)
class UserRepositoryTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
private lateinit var database: AppDatabase
private lateinit var repository: UserRepository
private lateinit var mockApiService: ApiService
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries()
.build()
mockApiService = mockk()
repository = UserRepository(database.userDao(), mockApiService)
}
@Test
fun getUserById_returnsLocalUser_whenExists() = runTest {
val user = User(id = 1, firstName = "John", lastName = "Doe", email = "john@example.com")
database.userDao().insertUser(user)
val result = repository.getUserById(1)
assertTrue(result.isSuccess)
assertEquals("john@example.com", result.getOrNull()?.email)
}
@Test
fun getUserById_fetchesFromNetwork_whenNotLocal() = runTest {
val networkUser = User(id = 1, firstName = "Jane", lastName = "Smith", email = "jane@example.com")
coEvery { mockApiService.getUser(1) } returns networkUser
val result = repository.getUserById(1)
assertTrue(result.isSuccess)
assertEquals("jane@example.com", result.getOrNull()?.email)
// Verify user was cached locally
val localUser = database.userDao().getUserById(1)
assertEquals("jane@example.com", localUser?.email)
}
}
Room integrates seamlessly with modern Android architecture patterns, particularly when combined with Jetpack Compose and modern state management approaches.
@Composable
fun UserListScreen(
userRepository: UserRepository = hiltViewModel()
) {
val users by userRepository.getAllUsers().collectAsState(initial = emptyList())
LazyColumn {
items(users) { user ->
UserItem(
user = user,
onUserClick = { /* Handle click */ }
)
}
}
}
@Composable
fun UserItem(
user: User,
onUserClick: (User) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { onUserClick(user) }
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "${user.firstName} ${user.lastName}",
style = MaterialTheme.typography.headlineSmall
)
Text(
text = user.email,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
val users: Flow<List<User>> = userRepository.getAllUsers()
fun createUser(firstName: String, lastName: String, email: String) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
val user = User(
firstName = firstName,
lastName = lastName,
email = email
)
val result = userRepository.createUser(user)
if (result.isSuccess) {
_uiState.update { it.copy(isLoading = false, message = "User created successfully") }
} else {
_uiState.update { it.copy(isLoading = false, error = "Failed to create user") }
}
} catch (e: Exception) {
_uiState.update { it.copy(isLoading = false, error = e.message) }
}
}
}
}
data class UserUiState(
val isLoading: Boolean = false,
val error: String? = null,
val message: String? = null
)
Room provides several advanced features that enable sophisticated data management scenarios in production applications.
@Entity(tableName = "articles")
@Fts4
data class Article(
@PrimaryKey @ColumnInfo(name = "rowid") val id: Long,
val title: String,
val content: String,
val author: String
)
@Dao
interface ArticleDao {
@Query("SELECT * FROM articles WHERE articles MATCH :query")
suspend fun searchArticles(query: String): List<Article>
@Query("""
SELECT * FROM articles
WHERE articles MATCH :query
ORDER BY bm25(articles)
LIMIT :limit
""")
suspend fun searchArticlesRanked(query: String, limit: Int): List<Article>
}
@DatabaseView(
viewName = "user_post_summary",
value = """
SELECT
u.id as user_id,
u.first_name,
u.last_name,
u.email,
COUNT(p.id) as post_count,
MAX(p.created_at) as latest_post_date
FROM users u
LEFT JOIN posts p ON u.id = p.author_id
GROUP BY u.id
"""
)
data class UserPostSummary(
val userId: Long,
val firstName: String,
val lastName: String,
val email: String,
val postCount: Int,
val latestPostDate: Long?
)
@Database(
entities = [User::class, Post::class],
views = [UserPostSummary::class],
version = 1
)
abstract class AppDatabase : RoomDatabase() {
// Database implementation
}
For large applications, consider organizing Room components across multiple modules:
// :data:database module
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val firstName: String,
val lastName: String,
val email: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<UserEntity>>
@Insert
suspend fun insertUser(user: UserEntity): Long
}
// :data:repository module
class UserRepository @Inject constructor(
private val userDao: UserDao,
private val userMapper: UserMapper
) {
fun getAllUsers(): Flow<List<User>> = userDao.getAllUsers()
.map { entities -> entities.map { userMapper.entityToDomain(it) } }
suspend fun createUser(user: User): Result<Long> {
return try {
val entity = userMapper.domainToEntity(user)
val id = userDao.insertUser(entity)
Result.success(id)
} catch (e: Exception) {
Result.failure(e)
}
}
}
// Domain model in :domain module
data class User(
val id: Long,
val fullName: String,
val email: String,
val isActive: Boolean
)
Room Database represents the gold standard for Android data persistence, providing a robust, type-safe, and performant solution that scales with your application needs. By embracing Room’s modern architecture patterns, developers can create maintainable data layers that integrate seamlessly with contemporary Android development practices.
The key to successful Room implementation lies in understanding its core components, following established architectural patterns, and leveraging its advanced features appropriately. Whether building simple CRUD applications or complex data-intensive systems, Room provides the tools and flexibility needed to create exceptional user experiences while maintaining code quality and developer productivity.
As Android development continues evolving, Room remains at the forefront of modern data persistence solutions, continuously improving through regular updates and community contributions. Mastering Room Database architecture is a crucial component of building scalable, maintainable Android applications. For developers looking to implement Room within a comprehensive modern Android architecture, this data persistence layer integrates seamlessly with other contemporary development practices covered in our complete guide to modern Android app development. Understanding how Room fits into the broader ecosystem of Jetpack Compose, MVVM architecture, dependency injection, and reactive programming patterns enables developers to create robust applications that handle data persistence with confidence and efficiency.
Room Database is Android’s modern SQLite abstraction library that provides compile-time verification of SQL queries, automatic object mapping, and seamless integration with Architecture Components. It eliminates boilerplate code while offering type-safe database operations, making it the recommended solution for local data persistence in Android applications.
Room differs from raw SQLite by providing compile-time SQL validation, automatic cursor management, and type-safe queries. While SQLite requires manual cursor handling and offers no compile-time error checking, Room generates efficient mapping code automatically and catches SQL errors during compilation, significantly reducing runtime crashes and development time.
The three main components of Room are Entity classes that define database tables using annotations, DAO (Data Access Object) interfaces that define database operations, and the Database class that serves as the main access point. These components work together to create a complete, type-safe database solution with minimal boilerplate code.
Room Database offers excellent performance through compile-time optimizations, efficient query generation, and automatic cursor management. It supports database indexing, query optimization, and pagination for large datasets. Room’s performance matches or exceeds hand-written SQLite code while providing better maintainability and fewer runtime errors.
Yes, Room works seamlessly with Jetpack Compose through Flow and StateFlow integration. Room’s reactive data streams automatically trigger UI recomposition when database changes occur. Combined with ViewModels and dependency injection, Room provides a modern, reactive data layer that perfectly complements Compose’s declarative UI approach.
Room handles database migrations through Migration objects that define schema changes between versions. You create Migration instances specifying start and end versions with SQL commands for schema updates. Room validates migrations at compile-time and provides testing tools to ensure data integrity during app updates.
Flow is Room’s modern reactive stream that works better with Coroutines and Compose, offering more operators and better performance. LiveData is lifecycle-aware and automatically manages UI updates but is primarily designed for View-based architectures. Flow is recommended for new projects, especially those using Jetpack Compose.
Room testing involves using in-memory databases for unit tests, Migration testing helpers for schema changes, and Flow testing utilities for reactive streams. Room provides comprehensive testing support including DAO testing, repository testing, and integration testing with other Architecture Components, ensuring reliable data operations.
Room type converters handle complex data types that don’t map directly to SQLite types. They use @TypeConverter annotations to define conversion methods between custom types and SQLite-supported types. Type converters enable storage of dates, enums, lists, and custom objects while maintaining type safety and query efficiency.