Room 数据库 android 真的不支持 lag() 和 over() 函数吗?
这不是真正的 Room,限制是与设备捆绑的 SQLite,根据 Android API,它应该处于最低级别,并且始终落后于可用的 SQLite。是已安装的 SQLite(我相信)决定了哪些功能可用。那就是我相信 Room 只是将 SQL 传递给 SQLite,它(安装的 SQLite)确定它是否是有效/无效的 SQL。
我能以更有效的方式做到这一点吗?
索引
batteryDetails 表的 batteryId 列是否被索引?如果没有,那么添加这样的索引可以减少时间。
- 基于使用 1000000 行的测试(如下所示),没有索引的时间约为。 45秒,索引约。 13 秒。
触发器
但是,您可能希望考虑使用触发器,以消除通过子查询确定状态变化的需要(如果我正确解释了您的 SQL)。
假设 batteryDetails 表的 batteryId 是对电池的引用,那么您可以在电池表中添加一列来保存最后的电池状态。当插入一个batteryDetails行时,它可以通过触发器获取状态并将其保存在batteryDetails列(例如previousStatus)中,然后触发器可以通过同一个触发器将插入的状态更新为电池的最后状态。
测试这个,然后等效查询,对于 1000000 行表是 1/10 秒。
测试 SQL(使用 Navicat,但可以复制到任何等效的 SQLite 工具):-
DROP TABLE IF EXISTS battery;
DROP TABLE IF EXISTS batteryDetails;
DROP TABLE IF EXISTS eventTypes;
CREATE TABLE IF NOT EXISTS batteryDetails (batteryId INTEGER, timestamp INTEGER, status INTEGER);
CREATE TABLE IF NOT EXISTS eventTypes (eventType INTEGER PRIMARY KEY, name TEXT);
/* Some add event types */
INSERT INTO eventTypes VALUES (1,'flat (less than 10%)'),(2,'low (10%-50%)'),(3,'medium (50%-75%)'),(4,'full (75%-100%)');
/* Load some testing data */
WITH
RECURSIVE i(counter) AS (SELECT 1 UNION ALL SELECT counter + 1 FROM i LIMIT 1000000)
INSERT INTO batteryDetails SELECT abs(random() % 100)+ 1,strftime('%s','now') - counter,(abs(random() % 4)) + 1 FROM i
;
/* The original Query (with a little extra output)
rather calculated value have been used for start and end
the battery ID is one with the 100 batteries
*/
SELECT DISTINCT
rowid,
strftime('%s','now','-2 day') AS starting,
strftime('%s','now','-1 day') AS ending,
strftime('%Y-%m-%d','now','-2 day') AS startingYYMMDD,
strftime('%Y-%m-%d','now','-1 day') AS endingYYMMDD,
strftime('%Y-%m-%d',datetime(DISTINCT timeStamp, 'unixepoch')) AS eventAtDate,
e.name
FROM (
SELECT
rowid,
b1.*,(
SELECT b2.status
FROM batteryDetails b2
WHERE b2.batteryId = b1.batteryId
AND b2.timeStamp < b1.timeStamp
ORDER BY b2.timeStamp DESC LIMIT 1
) AS oldStatus
FROM batteryDetails b1
) batteryDetails
LEFT JOIN eventTypes e ON e.eventType = batteryDetails.status
WHERE oldStatus <> status
AND batteryId= 10
AND (timeStamp between strftime('%s','now','-2 day') AND strftime('%s','now','-1 day'))
GROUP BY timeStamp
ORDER BY timeStamp DESC
;
/* Adding Index if not already indexed */
CREATE INDEX idx_batteryDetails_batteryId ON batteryDetails (batteryId);
/* use the original query again but with the indexed batteryId column */
SELECT DISTINCT
rowid,
strftime('%s','now','-2 day') AS starting,
strftime('%s','now','-1 day') AS ending,
strftime('%Y-%m-%d','now','-2 day') AS startingYYMMDD,
strftime('%Y-%m-%d','now','-1 day') AS endingYYMMDD,
strftime('%Y-%m-%d',datetime(DISTINCT timeStamp, 'unixepoch')) AS eventAtDate,
e.name
FROM (
SELECT
rowid,
b1.*,(
SELECT b2.status
FROM batteryDetails b2
WHERE b2.batteryId = b1.batteryId
AND b2.timeStamp < b1.timeStamp
ORDER BY b2.timeStamp DESC LIMIT 1
) AS oldStatus
FROM batteryDetails b1
) batteryDetails
LEFT JOIN eventTypes e ON e.eventType = batteryDetails.status
WHERE oldStatus <> status
AND batteryId= 10
AND (timeStamp between strftime('%s','now','-2 day') AND strftime('%s','now','-1 day'))
GROUP BY timeStamp
ORDER BY timeStamp DESC
;
/* Now going try trigger */
DROP TABLE IF EXISTS Battery;
DROP TABLE IF EXISTS BatteryDetails;
DROP TRIGGER IF EXISTS setPreviousStatus;
/* Create the battery table with the lastStatus column (THE CRUX of the technique) */
CREATE TABLE IF NOT EXISTS Battery (batteryId INTEGER PRIMARY KEY, lastStatus DEFAULT 1);
/* Add the batteries to the battery table (id's 1 to 100) defaulting to last status of flat*/
WITH RECURSIVE b(id) AS (SELECT 1 UNION ALL SELECT id+1 FROM b LIMIT 100)
INSERT INTO battery SELECT id,1 FROM b
;
/* Create the modified batteryDetails table i.e. adding the previousStatus column */
CREATE TABLE IF NOT EXISTS batteryDetails (batteryId INTEGER, timestamp INTEGER, status INTEGER, previousStatus INTEGER);
/* Create the TRIGGER to
1. apply the last status of the battery to the newly inserted batteryDetails row
2. update the battery to the new status
NOTE ROOM doesn't cater for TRIGGERS via annotation you would need to add it (add a callback and do in onCreate)
*/
CREATE TRIGGER IF NOT EXISTS setPreviousStatus AFTER INSERT ON batteryDetails
BEGIN
UPDATE batteryDetails SET previousStatus = (SELECT laststatus FROM battery WHERE batteryId = new.batteryId) WHERE rowid = new.rowid;
UPDATE battery SET lastStatus = new.status WHERE batteryId = new.batteryId;
END
;
/*
Add some data again BUT now with previousStatus column (set to null, it wil/ be updated by the trigger)
*/
WITH
RECURSIVE i(counter) AS (SELECT 1 UNION ALL SELECT counter + 1 FROM i LIMIT 1000000)
INSERT INTO batteryDetails SELECT abs(random() % 100)+ 1,strftime('%s','now') - counter,(abs(random() % 4)) + 1,null FROM i
;
/* I believe the equivalent query that utilises the previousStatus column */
SELECT DISTINCT
strftime('%Y-%m-%d',datetime(DISTINCT timeStamp, 'unixepoch')) AS eventAtDate,
e.name
FROM batteryDetails
LEFT JOIN eventTypes e ON e.eventType = batteryDetails.status
WHERE status <> previousStatus
AND batteryId= 10
AND (timeStamp between strftime('%s','now','-2 day') AND strftime('%s','now','-1 day'))
GROUP BY timeStamp
ORDER BY timeStamp DESC
;
/* For myself cleanup the testing environment (trigger and index should be deleted by SQLite)*/
DROP TABLE IF EXISTS battery;
DROP TABLE IF EXISTS batteryDetails;
DROP TABLE IF EXISTS eventTypes;
示例运行(又名消息日志以及时间):-
DROP TABLE IF EXISTS battery
> OK
> Time: 0.282s
DROP TABLE IF EXISTS batteryDetails
> OK
> Time: 1.309s
DROP TABLE IF EXISTS eventTypes
> OK
> Time: 0.299s
CREATE TABLE IF NOT EXISTS batteryDetails (batteryId INTEGER, timestamp INTEGER, status INTEGER)
> OK
> Time: 0.084s
CREATE TABLE IF NOT EXISTS eventTypes (eventType INTEGER PRIMARY KEY, name TEXT)
> OK
> Time: 0.096s
/* Some add event types */
INSERT INTO eventTypes VALUES (1,'flat (less than 10%)'),(2,'low (10%-50%)'),(3,'medium (50%-75%)'),(4,'full (75%-100%)')
> Affected rows: 4
> Time: 0.149s
/* Load some testing data */
WITH
RECURSIVE i(counter) AS (SELECT 1 UNION ALL SELECT counter + 1 FROM i LIMIT 1000000)
INSERT INTO batteryDetails SELECT abs(random() % 100)+ 1,strftime('%s','now') - counter,(abs(random() % 4)) + 1 FROM i
> Affected rows: 1000000
> Time: 0.847s
/* The original Query (with a little extra output)
rather calculated value have been used for start and end
the battery ID is one with the 100 batteries
*/
SELECT DISTINCT
rowid,
strftime('%s','now','-2 day') AS starting,
strftime('%s','now','-1 day') AS ending,
strftime('%Y-%m-%d','now','-2 day') AS startingYYMMDD,
strftime('%Y-%m-%d','now','-1 day') AS endingYYMMDD,
strftime('%Y-%m-%d',datetime(DISTINCT timeStamp, 'unixepoch')) AS eventAtDate,
e.name
FROM (
SELECT
rowid,
b1.*,(
SELECT b2.status
FROM batteryDetails b2
WHERE b2.batteryId = b1.batteryId
AND b2.timeStamp < b1.timeStamp
ORDER BY b2.timeStamp DESC LIMIT 1
) AS oldStatus
FROM batteryDetails b1
) batteryDetails
LEFT JOIN eventTypes e ON e.eventType = batteryDetails.status
WHERE oldStatus <> status
AND batteryId= 10
AND (timeStamp between strftime('%s','now','-2 day') AND strftime('%s','now','-1 day'))
GROUP BY timeStamp
ORDER BY timeStamp DESC
> OK
> Time: 47.66s
/* Adding Index if not already indexed */
CREATE INDEX idx_batteryDetails_batteryId ON batteryDetails (batteryId)
> OK
> Time: 1.01s
/* use the original query again but with the indexed batteryId column */
SELECT DISTINCT
rowid,
strftime('%s','now','-2 day') AS starting,
strftime('%s','now','-1 day') AS ending,
strftime('%Y-%m-%d','now','-2 day') AS startingYYMMDD,
strftime('%Y-%m-%d','now','-1 day') AS endingYYMMDD,
strftime('%Y-%m-%d',datetime(DISTINCT timeStamp, 'unixepoch')) AS eventAtDate,
e.name
FROM (
SELECT
rowid,
b1.*,(
SELECT b2.status
FROM batteryDetails b2
WHERE b2.batteryId = b1.batteryId
AND b2.timeStamp < b1.timeStamp
ORDER BY b2.timeStamp DESC LIMIT 1
) AS oldStatus
FROM batteryDetails b1
) batteryDetails
LEFT JOIN eventTypes e ON e.eventType = batteryDetails.status
WHERE oldStatus <> status
AND batteryId= 10
AND (timeStamp between strftime('%s','now','-2 day') AND strftime('%s','now','-1 day'))
GROUP BY timeStamp
ORDER BY timeStamp DESC
> OK
> Time: 13.525s
/* Now going try trigger */
DROP TABLE IF EXISTS Battery
> OK
> Time: 0s
DROP TABLE IF EXISTS BatteryDetails
> OK
> Time: 1.925s
DROP TRIGGER IF EXISTS setPreviousStatus
> OK
> Time: 0s
/* Create the battery table with the lastStatus column (THE CRUX of the technique) */
CREATE TABLE IF NOT EXISTS Battery (batteryId INTEGER PRIMARY KEY, lastStatus DEFAULT 1)
> OK
> Time: 0.243s
/* Add the batteries to the battery table (id's 1 to 100) defaulting to last status of flat*/
WITH RECURSIVE b(id) AS (SELECT 1 UNION ALL SELECT id+1 FROM b LIMIT 100)
INSERT INTO battery SELECT id,1 FROM b
> Affected rows: 100
> Time: 0.083s
/* Create the modified batteryDetails table i.e. adding the previousStatus column */
CREATE TABLE IF NOT EXISTS batteryDetails (batteryId INTEGER, timestamp INTEGER, status INTEGER, previousStatus INTEGER)
> OK
> Time: 0.216s
/* Create the TRIGGER to
1. apply the last status of the battery to the newly inserted batteryDetails row
2. update the battery to the new status
NOTE ROOM doesn't cater for TRIGGERS via annotation you would need to add it (add a callback and do in onCreate)
*/
CREATE TRIGGER IF NOT EXISTS setPreviousStatus AFTER INSERT ON batteryDetails
BEGIN
UPDATE batteryDetails SET previousStatus = (SELECT laststatus FROM battery WHERE batteryId = new.batteryId) WHERE rowid = new.rowid;
UPDATE battery SET lastStatus = new.status WHERE batteryId = new.batteryId;
END
> Affected rows: 100
> Time: 0.086s
/*
Add some data again BUT now with previousStatus column (set to null, it wil/ be updated by the trigger)
*/
WITH
RECURSIVE i(counter) AS (SELECT 1 UNION ALL SELECT counter + 1 FROM i LIMIT 1000000)
INSERT INTO batteryDetails SELECT abs(random() % 100)+ 1,strftime('%s','now') - counter,(abs(random() % 4)) + 1,null FROM i
> Affected rows: 1000000
> Time: 1.907s
/* I believe the equivalent query that utilises the previousStatus column */
SELECT DISTINCT
strftime('%Y-%m-%d',datetime(DISTINCT timeStamp, 'unixepoch')) AS eventAtDate,
e.name
FROM batteryDetails
LEFT JOIN eventTypes e ON e.eventType = batteryDetails.status
WHERE status <> previousStatus
AND batteryId= 10
AND (timeStamp between strftime('%s','now','-2 day') AND strftime('%s','now','-1 day'))
GROUP BY timeStamp
ORDER BY timeStamp DESC
> OK
> Time: 0.092s
/* For myself cleanup the testing environment (trigger and index should be deleted by SQLite)*/
DROP TABLE IF EXISTS battery
> OK
> Time: 0.091s
DROP TABLE IF EXISTS batteryDetails
> OK
> Time: 1.442s
DROP TABLE IF EXISTS eventTypes
> OK
> Time: 0.101s
-
注意上面的数据显然是人为的(很多是随机生成的),可能反映了真实数据,并且已经做出了一些假设。
在房间中应用上述内容(使用触发器)
实体(电池、事件类型和电池详细信息):-
电池:-
@Entity(tableName = TABLENAME)
data class Battery(
@PrimaryKey
@ColumnInfo(name = ID_COLUMN)
var id: Long? = null,
var lastStatus: Int = 1
) {
companion object {
const val TABLENAME = "battery"
const val ID_COLUMN = "batteryId"
const val LASTSTATUS_COLUMN = "lastStatus"
}
}
- 注意,我强烈建议表组件名称使用常量(例如,特别是用于生成 TRIGGER 以消除拼写错误)
事件类型:-
@Entity(tableName = TABLENAME)
data class EventType(
@PrimaryKey
@ColumnInfo(name = EVENTTYPE_COLUMN)
var eventType: Long? = null,
@ColumnInfo(name = EVENTTYPE_NAME_COLUMN)
var name: String
) {
companion object {
const val TABLENAME = "eventTypes"
const val EVENTTYPE_COLUMN = "eventType"
const val EVENTTYPE_NAME_COLUMN = "name"
}
}
BatteryDetail(虽然很啰嗦):-
@Entity(
tableName = TABLENAME,
indices = [
Index(value = [TIMESTAMP_COLUMN]),
Index(value = [BatteryDetail.BATTERY_ID_COLUMN])
],
foreignKeys = [
ForeignKey(
entity = Battery::class,
parentColumns = [Battery.ID_COLUMN],
childColumns = [BatteryDetail.BATTERY_ID_COLUMN],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class BatteryDetail(
@PrimaryKey @ColumnInfo(name = ID_COLUMN)
var id: Long? = null,
@ColumnInfo(name = BATTERY_ID_COLUMN)
var batteryId: Long,
@ColumnInfo(name = TIMESTAMP_COLUMN)
var timestamp: Long = System.currentTimeMillis() / 1000,
@ColumnInfo(name = STATUS_COLUMN)
var status: Int,
@ColumnInfo(name = PREVSIOUS_STATUS_COLUMN)
var previousStatus: Int = -1
) {
companion object {
const val TABLENAME = "batteryDetails"
const val ID_COLUMN: String = "id"
const val BATTERY_ID_COLUMN = "batteryId"
const val TIMESTAMP_COLUMN = "timestamp"
const val STATUS_COLUMN = "status"
const val PREVSIOUS_STATUS_COLUMN = "previousStatus"
/*
CREATE TRIGGER IF NOT EXISTS setPreviousStatus AFTER INSERT ON batteryDetails
BEGIN
UPDATE batteryDetails SET previousStatus = (SELECT laststatus FROM battery WHERE batteryId = new.batteryId) WHERE rowid = new.rowid;
UPDATE battery SET lastStatus = new.status WHERE batteryId = new.batteryId;
END;
*/
const val TRIGGER_NAME = "trigger_" + BatteryDetail.TABLENAME + "_afterinsert"
const val TRIGGER_CREATE_SQL =
"CREATE TRIGGER IF NOT EXISTS " +
TRIGGER_NAME +
" AFTER INSERT ON " + BatteryDetail.TABLENAME +
" BEGIN " +
/* 1st apply the last battery status to the previous status column */
" UPDATE " + BatteryDetail.TABLENAME + " SET " + BatteryDetail.PREVSIOUS_STATUS_COLUMN +
" = (" +
"SELECT " + Battery.LASTSTATUS_COLUMN + " FROM " + Battery.TABLENAME +
" WHERE " + Battery.ID_COLUMN + " = new." + BatteryDetail.BATTERY_ID_COLUMN +
") WHERE rowid = new.rowid;" +
/* 2nd Update the battery status with the new status */
" UPDATE " + Battery.TABLENAME + " SET " + Battery.LASTSTATUS_COLUMN + " = new." + BatteryDetail.STATUS_COLUMN +
" WHERE " + Battery.ID_COLUMN + " = new." + BatteryDetail.BATTERY_ID_COLUMN + ";" +
"END;"
const val TRIGGER_DROP_SQL = " DROP TRIGGER IF EXISTS $TRIGGER_NAME;"
}
}
相关查询结果的 POJO EventStatusChange :-
data class EventStatusChange(
var eventAtDate: String,
var name: String
)
@Dao 类 AllDao :-
@Dao
abstract class AllDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(battery: Battery): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(eventType: EventType): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(batteryDetail: BatteryDetail): Long
@Query(
"SELECT DISTINCT " +
"strftime('%Y-%m-%d',datetime(DISTINCT timestamp,'unixepoch')) AS eventAtDate, " +
"e.name FROM batteryDetails " +
"LEFT JOIN eventTypes e ON e.eventType = batteryDetails.status " +
"WHERE status <> previousStatus " +
"AND batteryId=:batteryId " +
"AND timestamp BETWEEN :start AND :end " +
"GROUP BY timestamp " +
"ORDER BY timestamp DESC;"
)
abstract fun getEventChanges(batteryId: Long, start: Long, end: Long): List<EventStatusChange>
}
@Database 类 TheDatabase - 创建触发器:-
@Database(entities = [Battery::class,EventType::class,BatteryDetail::class], version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
@Volatile
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase::class.java,
"battery.db"
)
.allowMainThreadQueries()
.addCallback(cb())
.build()
}
return instance as TheDatabase
}
class cb: Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
Log.d("DB_ONCREATE","Dropping and create trigger ${BatteryDetail.TRIGGER_NAME}")
db.execSQL(BatteryDetail.TRIGGER_DROP_SQL)
db.execSQL(BatteryDetail.TRIGGER_CREATE_SQL)
}
}
}
}
- 为了简洁/方便,请注意
.allowMainThreadQueries。
把它完全放在一个 Activity MainActivity 中:-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val r = Random
db = TheDatabase.getInstance(this)
db.openHelper.writableDatabase
dao = db.getAllDao()
val batterycount: Int = 10
val eventTypeCount: Int = 4
val intervalInSeconds = 10
val day = 24 * 60 * 60
val baseStartTime = (System.currentTimeMillis() / 1000) - (day * 2)
for(i in 1..batterycount) {
dao.insert(Battery(id = i.toLong()))
}
for (i in 1..eventTypeCount) {
dao.insert(EventType(name = "Type${i}", eventType = i.toLong()))
}
for (i in 1..100) {
for(ii in 1..batterycount) {
var e_type: Int = r.nextInt(eventTypeCount - 1) + 1
dao.insert(BatteryDetail(timestamp = baseStartTime + (i * intervalInSeconds ),batteryId = ii.toLong(),status = e_type))
}
}
for(esc: EventStatusChange in dao.getEventChanges(5,1632291539, 1632291549)) {
Log.d("DBINFO","Date = ${esc.eventAtDate} Name = ${esc.name}")
}
}
}
- 加载数据,batteryDetail 使用随机状态。
- 然后使用上述查询进行提取(对于电池 5 和一些时间戳(我通过查看实际数据来获取值作弊))。输出到日志
:-
2021-09-24 16:23:36.090 D/DBINFO: Date = 2021-09-22 Name = Type2
2021-09-24 16:23:36.090 D/DBINFO: Date = 2021-09-22 Name = Type3
- 并不是说这是正确的,数据是随机的,所以不容易检查。但是,它确实证明了代码有效(即使不是按要求)。我建议您使用您知道所需结果的数据。
通过 Android Studio 的 App Inspection 数据库中的数据:-
事件类型
电池
BatteryDetails(50 行的一半):-
其他一些注意事项
使用诸如 Navicat(SQLite 和其他数据库浏览器)之类的工具可以让您的生活更轻松。为了进行优化,请考虑在查询之前使用 EXPLAIN QUERY PLAN 和 EXPLAIN(查询不会运行,而是生成可能有助于优化的输出)。
您可能会发现以下使用链接(和嵌入的链接)https://sqlite.org/eqp.html