indexedDB の特性まとめ

# まとめ
- indexedDBのトランザクション分離レベルは一番高いシリアライザブル
- 各種操作にかかるおおよその時間比
  - シンプルなオブジェクトのread＝1
  - シンプルなオブジェクトのwrite=1
  - readonlyトランザクションの獲得=3
  - readwriteトランザクションの獲得=40

※もちろん、実行環境（Windows10 chromeで実施）によって、傾向が変わってくる可能性はある

# 背景
最近[クロム拡張作成](https://qiita.com/largetownsky/items/ee9db46b372e5adb20f0)で、滅茶苦茶indexedDBを使い倒しているが、細かい仕様を把握しきれていない事が気になってきた。特にトランザクション周りは、RDBなら[トランザクション分離レベル](https://qiita.com/song_ss/items/38e514b05e9dabae3bdb)として明確に規定されているが、indexedDBでは、明確な仕様が見つからなかったので、動作を見てみた。

# 検証環境
以下、全て適当なWebページ上のデベロッパーツールConsole上で検証した。コピペで簡単に再現が取れる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747214/0fc9746b-300e-8344-1bef-65bd87b7bc7c.png)

また、indexedDBの状態は、同じデベロッパーツールのApplication上で確認できる。検証が終わったら、ここから削除しておくとゴミが残らない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747214/b68c5b14-e052-744b-d84a-f3c1e0c1ef3e.png)

# 検証コード

## DB構築
```
const db = indexedDB.open("test")
db.onupgradeneeded = (event) => {
    event.target.result.createObjectStore("test", {keyPath: "i"})
}
db.onsuccess = (event) => {
    console.log("succeed")
}
```

## 書き込み時間計測
手元の環境だと、１０万件の書き込みに５秒かかった
```
const db = indexedDB.open("test")
const loop = 100000
db.onsuccess = (event) => {
    console.log(new Date())
    const tx = event.target.result.transaction("test", "readwrite")
    const store = tx.objectStore("test")
    for (let i = 0; i < loop; ++i) {
        store.put({i})
    }
    tx.oncomplete = () => {
        console.log(new Date())
    }
}
```

## 読み込み時間計測
手元の環境だと、１００万件の読み込みに５６秒かかった
```
const db = indexedDB.open("test")
const loop = 1000000
db.onsuccess = (event) => {
    console.log(new Date())
    const tx = event.target.result.transaction("test")
    const store = tx.objectStore("test")
    for (let i = 0; i < loop; ++i) {
        store.get(i)
    }
    tx.oncomplete = () => {
        console.log(new Date())
    }
}
```

## トランザクションを分けて書き込み時間計測
手元の環境だと、１万件の書き込みに１９秒かかった
```
const db = indexedDB.open("test")
const loop = 10000
db.onsuccess = (event) => {
    console.log(new Date())
    for (let i = 0; i < loop; ++i) {
        const tx = event.target.result.transaction("test", "readwrite")
        const store = tx.objectStore("test")
        store.put({i})
        if (i === loop - 1) {
            tx.oncomplete = () => {
                console.log(new Date())
            }
        }
    }
}
```

## トランザクションを分けて読み込み時間計測
手元の環境だと、１０万件の読み込みに１４秒かかった
```
const db = indexedDB.open("test")
const loop = 100000
db.onsuccess = (event) => {
    console.log(new Date())
    for (let i = 0; i < loop; ++i) {
        const tx = event.target.result.transaction("test")
        const store = tx.objectStore("test")
        store.get(i)
        if (i === loop - 1) {
            tx.oncomplete = () => {
                console.log(new Date())
            }
        }
    }
}
```

## 複数の書き込みトランザクションの処理観察
```
const db = indexedDB.open("test")
const loop = 10
const max = 100000
db.onsuccess = (event) => {
    console.log("start " + new Date())
    for (let i = 0; i < loop; ++i) {
        const tx = event.target.result.transaction("test", "readwrite")
        const store = tx.objectStore("test")
        const batchSize = Math.floor(Math.random() * max) + 1
        for (let j = 0; j < batchSize; ++j) {
            store.put({i: j});
        }
        tx.oncomplete = () => {
            console.log("order " + i + " : size " + batchSize + " : end " + new Date())
        }
    }
}
```
の結果が以下の通り、直前のトランザクション終了から、概ね書き込み数に比例する時間が掛かっており、順番に処理されている事を示唆している。
```
start Thu Jul 28 2022 22:05:27 GMT+0900 (日本標準時)
order 0 : size 47472 : end Thu Jul 28 2022 22:05:33 GMT+0900 (日本標準時)
order 1 : size 81169 : end Thu Jul 28 2022 22:05:39 GMT+0900 (日本標準時)
order 2 : size 35398 : end Thu Jul 28 2022 22:05:42 GMT+0900 (日本標準時)
order 3 : size 5768 : end Thu Jul 28 2022 22:05:42 GMT+0900 (日本標準時)
order 4 : size 47452 : end Thu Jul 28 2022 22:05:45 GMT+0900 (日本標準時)
order 5 : size 56800 : end Thu Jul 28 2022 22:05:49 GMT+0900 (日本標準時)
order 6 : size 74466 : end Thu Jul 28 2022 22:05:54 GMT+0900 (日本標準時)
order 7 : size 10861 : end Thu Jul 28 2022 22:05:55 GMT+0900 (日本標準時)
order 8 : size 37561 : end Thu Jul 28 2022 22:05:58 GMT+0900 (日本標準時)
order 9 : size 52309 : end Thu Jul 28 2022 22:06:01 GMT+0900 (日本標準時)
```

## 複数の読み込みトランザクションの処理観察
```
const db = indexedDB.open("test")
const loop = 10
const max = 100000
db.onsuccess = (event) => {
    console.log("start " + new Date())
    for (let i = 0; i < loop; ++i) {
        const tx = event.target.result.transaction("test")
        const store = tx.objectStore("test")
        const batchSize = Math.floor(Math.random() * max) + 1
        for (let j = 0; j < batchSize; ++j) {
            store.get(j);
        }
        tx.oncomplete = () => {
            console.log("order " + i + " : size " + batchSize + " : end " + new Date())
        }
    }
}
```
の結果が以下の通り、なぜか、全て終わってから一斉に発注順に結果を返してくる。readonlyトランザクションは同時実行できるとは言え、早く終わったのは早く返してくれても良いのだが。
```
start Thu Jul 28 2022 22:07:48 GMT+0900 (日本標準時)
order 0 : size 25061 : end Thu Jul 28 2022 22:08:03 GMT+0900 (日本標準時)
order 1 : size 47133 : end Thu Jul 28 2022 22:08:03 GMT+0900 (日本標準時)
order 2 : size 83405 : end Thu Jul 28 2022 22:08:03 GMT+0900 (日本標準時)
order 3 : size 96431 : end Thu Jul 28 2022 22:08:03 GMT+0900 (日本標準時)
order 4 : size 72978 : end Thu Jul 28 2022 22:08:03 GMT+0900 (日本標準時)
order 5 : size 82764 : end Thu Jul 28 2022 22:08:03 GMT+0900 (日本標準時)
order 6 : size 95637 : end Thu Jul 28 2022 22:08:03 GMT+0900 (日本標準時)
order 7 : size 81326 : end Thu Jul 28 2022 22:08:03 GMT+0900 (日本標準時)
order 8 : size 11256 : end Thu Jul 28 2022 22:08:03 GMT+0900 (日本標準時)
order 9 : size 94405 : end Thu Jul 28 2022 22:08:03 GMT+0900 (日本標準時)
```

## ダーティリードのチェック
```
const db = indexedDB.open("test")
const loop = 5
const max = 10
const writeSize = 100000

function watchStoreSize(resDb) {
    const tx = resDb.transaction("test")
    const store = tx.objectStore("test")
    const counts = new Set()
    for (let i = 0; i < max; ++i) {
        const req = store.count()
        req.onsuccess = () => {
            counts.add(req.result)
        }
    }
    tx.oncomplete = () => {
        console.log("counts " + [...counts])
    }
}

db.onsuccess = (event) => {
    const resDb = event.target.result
    // レコードを空にする
    let writeTx = resDb.transaction("test", "readwrite")
    let writeStore = writeTx.objectStore("test")
    writeStore.clear()
    // レコード数を監視
    watchStoreSize(resDb)
    // レコードを追加
    writeTx = resDb.transaction("test", "readwrite")
    writeStore = writeTx.objectStore("test")
    for (let i = 0; i < writeSize; ++i) {
        writeStore.put({i})
    }
    // レコード数を監視
    watchStoreSize(resDb)
}
```
の結果が以下の通り、ダーティリードが発生すると、オブジェクト追加途中の状態が見えるため、0と100000の中間の数が計測される。何回実施してもこの結果という事は、未コミットの中途半端な状態が、他のトランザクションから観測される事はない様だ。
```
counts 0
counts 100000
```

## ファジーリードのチェック
```
const db = indexedDB.open("test")
const loop = 1
const size = 10000

function watchChangeSize(resDb) {
    const tx = resDb.transaction("test")
    const store = tx.objectStore("test")
    let changeCount = 0
    // 衝突しやすいように書き込みと逆順で監視
    for (let i = size - 1; i >= 0; --i) {
        const req = store.get(i)
        req.onsuccess = () => {
            if (req.result.j) {
                ++changeCount
            }
        }
    }
    tx.oncomplete = () => {
        console.log("change " + changeCount)
    }
}

db.onsuccess = (event) => {
    const resDb = event.target.result
    // レコードをセット
    let writeTx = resDb.transaction("test", "readwrite")
    let writeStore = writeTx.objectStore("test")
    for (let i = 0; i < size; ++i) {
        writeStore.put({i})
    }
    // レコードの変更を監視
    watchChangeSize(resDb)
    // レコードを変更
    for (let i = 0; i < size; ++i) {
        writeTx = resDb.transaction("test", "readwrite")
        writeStore = writeTx.objectStore("test")
        writeStore.put({i, j: "changed"})
        writeTx.commit()
    }
    // レコードの変更を監視
    watchChangeSize(resDb)
}
```
ファジーリードが発生すると、読み取りトランザクション内の複数回の読み取りが、他の書き込みトランザクションで変更されたコミット結果に影響されて変化してしまい、0と10000の中間の変更後オブジェクト数が観測される。何回実行しても以下の結果という事は、ファジーリードも発生しない様だ。
```
change 0
change 10000
```

## ファントムリードのチェック
```
const db = indexedDB.open("test")
const loop = 5
const max = 10
const writeSize = 10000

function watchStoreSize(resDb) {
    const tx = resDb.transaction("test")
    const store = tx.objectStore("test")
    const counts = new Set()
    for (let i = 0; i < max; ++i) {
        const req = store.count()
        req.onsuccess = () => {
            counts.add(req.result)
        }
    }
    tx.oncomplete = () => {
        console.log("counts " + [...counts])
    }
}

db.onsuccess = (event) => {
    const resDb = event.target.result
    // レコードを空にする
    let writeTx = resDb.transaction("test", "readwrite")
    let writeStore = writeTx.objectStore("test")
    writeStore.clear()
    // レコード数を監視
    watchStoreSize(resDb)
    // レコードを追加
    for (let i = 0; i < writeSize; ++i) {
        writeTx = resDb.transaction("test", "readwrite")
        writeStore = writeTx.objectStore("test")
        writeStore.put({i})
        writeTx.commit()
    }
    // レコード数を監視
    watchStoreSize(resDb)
}
```
やっていることは、ダーティリードのチェックとほぼ同じだが、１件書き込みごとに別トランザクションに分けている点が違う。
ファントムリードが発生すると、読み取りトランザクション内の複数回の読み取りが、他の書き込みトランザクションのレコード追加に影響されて変化し、0と10000の中間のオブジェクト数が観測される。何回実行しても以下の結果という事は、ファントムリードも発生しない様だ。
```
counts 0
counts 10000
```