# 今週何知った? week:27 ## 各自発表 > [name=ken3ypa] ## range_optimizer_max_mem_size について ### 背景 - Aurora V2(MySQL5.7互換)をDBとして利用するアプリにて、あるSELECTクエリにてIN句の数が一定以上になったら、EXPLAIN時の type が eq_ref から index に切り替わる現象に遭遇した - range_optimizer_max_mem_size の設定が関係してそうなので調べてみた ### 概要 |項目|値| | --- | --- | |コマンド行形式|--range-optimizer-max-mem-size=#| |システム変数|range_optimizer_max_mem_size| |スコープ|グローバル、セッション| |動的|はい| |SET_VAR ヒントの適用|いいえ| |型|Integer| |デフォルト値|8,388,608byte| |最小値|0| |最大値|18,446,744,073,709,551,615byte| > 範囲オプティマイザのメモリー消費の制限。 値 0 は 「制限なし」を表します。 オプティマイザによって考慮される実行計画で範囲アクセス方法が使用されているが、オプティマイザはこの方法に必要なメモリー量が制限を超えると見積もった場合、計画を破棄し、他の計画を考慮します。 詳細は、範囲最適化のためのメモリー使用の制限を参照してください。 範囲オプティマイザで使用可能なメモリーを制御できる設定。デフォルトだと `8,388,608byte` が割り当てられており、0を設定することで上限なしになる。また、指定した制限を超過した場合、オプティマイザが最適化候補の検索を諦めてフルテーブルスキャンになる https://dev.mysql.com/doc/refman/8.0/ja/range-optimization.html ### 必要なメモリ数の見積り方 > 範囲式の処理に必要なメモリー量を見積もるには、次のガイドラインを使用します: > 範囲アクセス方法の候補キーが 1 つある次のような単純なクエリーの場合、OR と組み合された各述語では約 230 バイトが使用されます: ``` SELECT COUNT(*) FROM t WHERE a=1 OR a=2 OR a=3 OR .. . a=N; ``` N = 10 の場合は、2,300bytes > 同様に、次のようなクエリーでは、AND と組み合された各述語で約 125 バイトが使用されます: ``` SELECT COUNT(*) FROM t WHERE a=1 AND b=1 AND c=1 ... N; ``` N = 10 の場合、1,250bytes - IN() 述語を含むクエリーの場合: ``` SELECT COUNT(*) FROM t WHERE a IN (1,2, ..., M) AND b IN (1,2, ..., N); ``` > IN() リストの各リテラル値は、OR と組み合された述語としてカウントされます。 2 つの IN() リストがある場合、OR と組み合せた述語の数は、各リストのリテラル値の数になります。 したがって、前述の例で OR と組み合されている述語の数は、M× N です。 M = 10, N = 10 の場合、24,000bytes デフォルトの 8,388,608bytes設定の場合、大体37,000件ぐらいでメモリ超過でフルスキャンになる ## チラシの裏 - range_optimizer_max_mem_size の導入は MySQL5.7から - Aurora MySQL Ver.1(5.6互換) -> Aurora MySQL Ver.2(5.7互換)にアップデートした際発生しているケースが各社TechBlogであがっている ### freee社のケース https://developers.freee.co.jp/entry/large-in-clouse-length-cause-full-scan > 今回話題にするクエリは以下のようなシンプルなものです。 SELECT * FROM hoge WHERE id IN (...) > MySQLのパラメーター次第ですが、デフォルトの設定だとこのIN句の中の値の数が数万になると適切なインデックスが用意されていてもフルスキャンが発生する事がありました。 ### ANDPAD社のケース https://tech.andpad.co.jp/entry/2022/06/02/100000#:~:text=%E5%BE%8C%E3%81%AE%E5%95%8F%E9%A1%8C-,%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9%E5%BD%B1%E9%9F%BF%E3%81%97%E3%81%9F%E9%83%A8%E5%88%86,-IN%E3%81%AE%E4%BB%B6%E6%95%B0 > INの件数が多い処理は大きく影響を受けたようです。range_optimizer_max_mem_sizeの変更がありINの指定が4万件程度からフルスキャンになる事はあるのですが、それだけでなくUPDATEの際のINは5千件くらいでも影響を受けました。Active Recordではpreloadを使ってINが大量発生しがちなので、テストの際に気をつけた方が良いです。弊社では2処理ほど緊急で1000件ずつに処理を分割してもらいました。 ### Railsの文脈にて - Rails5系までは Oracle での IN句が1,000件までしか指定できないという制約に対応するため [in_clause_length](https://api.rubyonrails.org/v5.1/classes/ActiveRecord/ConnectionAdapters/DatabaseLimits.html#method-i-in_clause_length) の個数で slice してクエリを投げてくれる仕組みがあった模様 https://blog.kamipo.net/entry/2017/12/23/230514 > MySQL以外のことはよくわからないんですが、どうやらOracleにはINの中の個数が1000個までしか入れられないという制限があるようで、これに対応するためにActiveRecordには in_clause_length の個数ずつにsliceしてクエリを投げる仕組みが元々存在します > 【追記】 > Rails 6.0.0以降sliceしたINをひとつのクエリで投げるのでここで期待した効果は得られなくなりました 今までは、例えばIN句の中身が10,000件だった場合、IN句の中身を1,000個に設定したSELECT分を10件にわけて投げていたものの、これからは IN(1000件分の何か) OR IN(1000件分の何か)というシグニチャで1つのクエリで投げるようになってしまったため、利用されるメモリの量は変わらないため退避策として使えなくなった…ということ? --- > [name=makicamel] ## スクリプト・モジュールとは - スクリプト - ふつうの JavaScript ファイル - モジュール - `import` または `export` をひとつ以上含む JavaScript ファイル - import された最初の 1 回目のみ評価される ### strict mode モジュールは常に strict mode として扱われる strict mode とは [厳格モード - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Strict_mode) ## モジュールの歴史 - Node.js 以前 - すべてがグローバルだった - Node.js 以降 - 2011 年にリリースされた [`Browserify`](https://browserify.org) が CommonJS モジュールをクライアントサイドの JavaScript でも利用可能にした - 現在では JavaScript 自体の仕様に `ES モジュール` が組み込まれ、Node.js でも利用可能になっている - ref [O'Reilly Japan - ハンズオンNode.js](https://www.oreilly.co.jp/books/9784873119236/) ### `CommonJS` > [CommonJS](https://www.commonjs.org/) というのは主にサーバーサイド JavaScript の仕様を統一するための仕様で、その中の一部に Modules があります。Node.js のモジュールシステムはこれを元にしており、`require` や `exports` を使ってモジュールの依存関係を解決します。 [賢く使うBrowserify | 第1回 Browserifyとは | CodeGrid](https://www.codegrid.net/articles/2015-browserify-1/) - 読み込み - `require()` で JavaScript ファイルを読み込む ```js const package1 = require('package1') ``` - 書き出し - `module.exports` で出力する。最後のひとつだけが適用される ```js // increment.js module.exports = (i) => i + 1 // index.js const increment = require('./increment') console.log(increment(3)) // => 4 ``` - `exports` で出力する。名前を変更したくない場合に利用できる。分割代入も行える ```js // util.js exports.increment = (i) => i + 1 // index.js const util = require('./util') console.log(util.increment(3)) // => 4 // separate.js const { increment } = require('./util') // separate2.js const { increment: inc } = require('./util') ``` ### `ES Module` > ES modulesは、ES2015仕様において策定された、JavaScriptファイルから別のJavaScriptファイルを読み込む仕組みです。 > > Node.jsでは、ほかのJSファイルの読み込みはCommonJSの仕様に沿った方法ですでに実現していましたが、ES modulesは標準としてNode.jsとブラウザ両方に対応したモジュールシステムの仕様と位置づけられます。 [ES modules基礎知識 | 第1回 仕様の概要とその周辺課題 | CodeGrid](https://www.codegrid.net/articles/2017-es-modules-1/) - 読み込み - `import` - require()と同じく他の.js, .tsファイルを読み込む機能 - require()はファイル内のどこにでも書くことができるがimportは必ずファイルの一番上に書く必要がある - `import()` - モジュールの読み込みを非同期で行う ```js import('./util').then(({ increment }) => { console.log(increment(3)) ) ``` - 書き出し - `export default` - `module.exports` に対応するもの。ひとつのファイルにはひとつの `export default` で複数書くと動作しない - `export` - `exports` に相当するもの ## CommonJS か、ES Module か - Node.js では `CommonJS` が長く使われている - Node.js 13.2.0 で `ES Module` も正式にサポートされた - Node.js は `CommonJS` で動作することが前提なので `ES Module` を使いたい場合は準備(後述)が必要 - TypeScript では - 一般的に `ES Module` 方式に則った記法で書く - コンパイル時の設定で `CommonJS`、`ES Module` の両方に対応した形式で出力されるため - サーバ用なら `CommonJS` 、ブラウザ用なら `ES Module` が無難な選択肢 ### `ES Module` を使うための準備 #### `.mjs` - ES Module として動作させたい JavaScript のファイルを `.mjs` に変更する - import で使うファイルの拡張子は省略できない #### `"type": "module"` - package.json に `"type": "module"` を追加するとパッケージ全体が ES Module をサポートする - `"type": "module"` と書くと拡張子を `mjs` にせず `js` のままで `ES Module` を使えるようになる - 省略すると `"type": "commonjs"` とみなされる - パッケージが `"type": "module"` に対応していない場合動作しないので注意 #### `.cjs` - `CommonJS` で書かれた JavaScript をよみたくなった場合は `CommonJS` で書かれているファイルを `.cjs` に変更する必要がある ## 参考 [import、export、require | TypeScript入門『サバイバルTypeScript』](https://typescriptbook.jp/reference/import-export-require)