Node.js × Sequelzie × MySQL使用時にJavaScript heap out of memoryエラーが発生した際の解決方法

## 概要
SequelzieのfindOneメソッド使用時に2つのテーブルを外部結合させた場合に以下のエラーが出た。
```
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
```
メモリ不足のエラーである。
今回は上記エラーの解決方法を記載する。

### エラー全文

```
<--- Last few GCs --->

[2875:0x32446e0]    30101 ms: Scavenge 1392.1 (1423.1) -> 1391.4 (1423.6) MB, 2.8 / 0.0 ms  (average mu = 0.180, current mu = 0.127) allocation failure
[2875:0x32446e0]    30106 ms: Scavenge 1392.3 (1423.6) -> 1391.6 (1424.1) MB, 2.6 / 0.0 ms  (average mu = 0.180, current mu = 0.127) allocation failure
[2875:0x32446e0]    30110 ms: Scavenge 1392.4 (1424.1) -> 1391.8 (1425.1) MB, 2.6 / 0.0 ms  (average mu = 0.180, current mu = 0.127) allocation failure


<--- JS stacktrace --->

==== JS stack trace =========================================

    0: ExitFrame [pc: 0x263b1adcfc5d]
    1: StubFrame [pc: 0x263b1ad8a111]
    2: ConstructFrame [pc: 0x263b1ad89cc4]
Security context: 0x26fc7171d9f1 <JSObject>
    3: /* anonymous */ [0x206b09796f21] [/app/node_modules/mysql/lib/protocol/Protocol.js:~242] [pc=0x263b1b05cba0](this=0x1032f9c492e9 <Protocol map = 0x38efb1f55c71>)
    4: arguments adaptor frame: 1->0
    5: _parsePacket [0x33355a07b0e1] [/app/node_m...

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

Writing Node.js report to file: report.20220830.032123.2875.0.001.json
Node.js report completed
 1: 0x95bd00 node::Abort() [/root/.nvm/versions/node/v11.15.0/bin/node]
 2: 0x95cc46 node::OnFatalError(char const*, char const*) [/root/.nvm/versions/node/v11.15.0/bin/node]
 3: 0xb3dbde v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/root/.nvm/versions/node/v11.15.0/bin/node]
 4: 0xb3de14 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/root/.nvm/versions/node/v11.15.0/bin/node]
 5: 0xf3ce52  [/root/.nvm/versions/node/v11.15.0/bin/node]
 6: 0xf3cf58 v8::internal::Heap::CheckIneffectiveMarkCompact(unsigned long, double) [/root/.nvm/versions/node/v11.15.0/bin/node]
 7: 0xf49678 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/root/.nvm/versions/node/v11.15.0/bin/node]
 8: 0xf4a18b v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/root/.nvm/versions/node/v11.15.0/bin/node]
 9: 0xf4cec1 v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationSpace, v8::internal::AllocationAlignment) [/root/.nvm/versions/node/v11.15.0/bin/node]
10: 0xf170f4 v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationSpace) [/root/.nvm/versions/node/v11.15.0/bin/node]
11: 0x11cd3fe v8::internal::Runtime_AllocateInNewSpace(int, v8::internal::Object**, v8::internal::Isolate*) [/root/.nvm/versions/node/v11.15.0/bin/node]
12: 0x263b1adcfc5d
```

## 環境
■ Docker image
[node:11](https://hub.docker.com/_/node)
[mysql:5.7](https://hub.docker.com/_/mysql)
■ Sequelizeバージョン
3.30.4
■ mysql Nodeパッケージ バージョン
2.13.0

## テーブル定義

- parents（親テーブル）
    - id（主キー）
- children1（子テーブル）
    - id
    - parent_id（外部キー）
- children2（子テーブル）
    - id
    - parent_id（外部キー）

```ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin```

今回取得しようとしている`parents`テーブルに紐づいている子テーブルのレコード数は以下である。
children1：211件
children2：2848件

## エラーが起きたコード
`parents`テーブルの`id`カラムを指定してレコードを1件のみ取得したい。
また、子テーブルはレコードのカラム等は不要でテーブル結合数をそれぞれ取得したい。
下記では、テーブル結合させた値をJavaScriptの`length`を使用して数を取得しようとしている。
### Sequelize
```js
Parents.findOne({
    where: {id: 1},
    include: [
        { model: sequelize.models.Children1 },
        { model: sequelize.models.Children2 }
    ]
})
```

## 原因
MySQLから取得するレコード数が多すぎることが原因であった。
count関数でレコード数を取得すると`600,928`件のレコードを取得していた。

## 解決方法
解決方法は3通りあると考えられる。
## ①メモリ領域を現状より多く確保する。
`max-old-space-size`を任意の値で指定することで現状より多くのメモリを確保することでも解決可能である。
しかしこの方法だと、今回のSQLは解決できるかもしれないが他のSQLでエラーになった場合に再度メモリ容量について考察しなければならず、根本的な解決にはならないので今回は別の解決方法を探すことにした。

## ②SELECT句でcount関数を使用して、LIMIT句を使用する
以下のような形でCOUNT関数を使用して外部結合しているテーブル数を取得して、最終的に取得するレコード数をLIMIT句を使用して絞る。

### Sequelize
```js
Parents.findOne({
    where: { id: 1 },
    include: [
        { model: sequelize.models.Children1 },
        { model: sequelize.models.Children2 }
    ],
    attributes: [
        // 必要なカラム,
        [sequelize.literal("COUNT(DISTINCT `Children1`.`id`)"), "1_count"],
        [sequelize.literal("COUNT(DISTINCT `Children2`.`id`)"), "2_count"]
    ],
    LIMIT: 1
})
```

上記でも解決可能であった。
この解決方法は、取得したい親テーブルのレコードが複数件ある場合に有効である。（その場合はLIMIT句の値を変更する必要がある）
しかし、今回の取得したい親テーブルのレコードは1件だけであるので、再度別の解決方法を探すことにした。

## ③クエリーを分ける
クエリーを分けることでレコード数を抑えられて、メモリエラーにならずにレコードを取得することができた。


```js
sequelize.Promise.all([
    Parents.findOne({
        where: { id: 1 },
    }),
    sequelize.models.Children1.count({
        where: { parent_id: 1 },
    }),
    sequelize.models.Children2.count({
        where: { parent_id: 1 },
    })
])
```

## まとめ
①の解決方法は一時的には有効だが、基本的には根本的原因を探ることが好ましいように思う。
②の解決方法は、取得したい親のレコード数が複数ある場合には有効である。
③の解決方法は、今回の様に親のレコードは1件のみ取得したい場合に有効であった。