Kusto クエリのパフォーマンスを高めるために

# 目次
[はじめに](#はじめに)
[処理効率を考える](#処理効率を考える)
[分かりやすさを意識する](#分かりやすさを意識する)
[参考](#参考)
[おわりに](#おわりに)



# はじめに
Kusto クエリを書く上で、パフォーマンスを考慮することは非常に重要です。

処理効率の悪いクエリを放置すると、場合によってタイムアウトが発生し、後続処理や他の機能に影響を与えてしまう可能性があります。また、クエリ自体の分かりやすさを意識することも、可読性、保守性といったパフォーマンスを高めるために必要な観点です。

本記事は、LogAnalytics (Sentinel) 上で Kusto を使用することを想定し、クエリパフォーマンス向上のために気を付けるポイントをまとめました。


# 処理効率を考える
まずは、クエリ自体の処理パフォーマンスについて見ていきましょう。

### 演算子の推奨事例
同じような効果をもたらす演算子であっても、処理効率や処理を高速にするために推奨される使い方があります。以下はその一例です。

##### has / contains
文字列検索の場合、contains よりも has を使用します。
has は3文字以上の用語インデックスを検索し、contains は文字列全体をスキャンすることで検索する、という点に違いがあります。
スキャンは用語インデックスの検索よりも低速なため、3文字以上の文字列検索には、has の使用が推奨されます。

:::note info
has 演算子
https://learn.microsoft.com/ja-jp/azure/data-explorer/kusto/query/has-operator

contains 演算子
https://learn.microsoft.com/ja-jp/azure/data-explorer/kusto/query/contains-operator
:::

##### == / =~
完全一致の文字列検索には、=~ ではなく == を使用します。
処理効率も上げるためには、== のように大文字と小文字が区別される演算子を使用することが望ましいです。

##### in / in~
複数の要素からの文字列検索には、in~ ではなく in を使用します。
== と同じく、大文字と小文字を区別して処理が行われる演算子が推奨されます。

##### ワイルドカード(*) によるテキスト検索
すべての列に対してフルテキスト検索が行われるため、ワイルドカード(*)の使用は推奨されていません。


### 処理されるデータ量を減らす
クエリの処理速度は、処理する必要があるデータ量に左右されます。処理されるデータが少ないほど消費されるリソースも少なくて済み、高速化が期待できるでしょう。

#### where 演算子
テーブル参照直後にフィルター処理することによって、データを絞ることができます。
より多くフィルターすることのできる where 条件をクエリの上部に配置することで、その後の処理速度の向上が見込めます。

#### project 演算子
project 演算子によって処理に必要な項目のみに絞ります。常にテーブルの全項目を出力させる必要はありません。

#### join 演算子
join 演算子は、where や project でデータ量を絞った上で使用すると、より効率的に処理を実行できます。また、結合するテーブルのうち、よりデータ量が少ないテーブルを左側に配置することで、読み込むデータの全体量を減らすことができます。



### 冗長な書き方を避ける
1つのクエリの中に、同じ処理を重複して書くことを避けるため、変数や演算子を使用する方法です。

#### let ステートメント
共通の処理を変数で定義することで、1つのクエリ内で複数回使用することが可能です。

[Base] [Exclude]2つの変数を定義し、複数回使用している例です。
```:let ステートメント
let Base = AlertInfo
| where Timegenerated >ago(1d) and AlertName == "XXX"
;
let Exclude = Base
| where HostName has "abc"
| distinct UserName
;
Base
| where UserName !in (Exclude)
```


#### as 演算子
入力式の途中に as 演算子による名前付けを追加することで、それまでの式の値を定義することができます。let ステートメントとは異なり、クエリを分割することなく式の値を複数回参照可能です。

[Base] という名前で式の値を定義している例です。
```:as 演算子
AlertInfo
| where Timegenerated >ago(1d) and AlertName == "XXX"
| as Base
| where HostName has "abc"
| distinct UserName
| join kind=rightanti Base on UserName
```



# 分かりやすさを意識する
プログラムのコードを書く時と同様、Kusto クエリにも「自分以外の人が内容を理解できるか？」という視点が欠かせません。
解読に時間を要するようなクエリでは、後々の開発コストが上がる上、意図しない不具合を抱えるリスクにつながります。クエリの読みやすさ（可読性）、修正・拡張のしやすさ（保守性）の有無が、開発チームのパフォーマンスに影響を与えます。


### 適切なコメントを入れる
クエリに説明を付加するために欠かせないコメントですが、長すぎる冗長なコメントは返ってクエリの可読性を下げてしまいます。
処理の大まかな流れや区切り、主要なロジックの業務的意味合いを端的に記載するに留めましょう。また、コードレビュー時のコメントは、不要なものとして削除すべきです。

以下は、メインの処理とチューニング処理を分割し、チューニング内容のコメントを付加した例です。
```:コメント例1
SigninLogs
| where TimeGenerated > ago(1h)
| where RiskLevelDuringSignIn !in ("none","low")
| order by TimeGenerated desc
| project TimeGenerated,IPAddress,AppDisplayName,UserPrincipalName,ResultType,RiskLevelDuringSignIn
//----- チューニング -----//
| where ResultType in (SuccessCodes) 
| where IPAddress !has("111.222.333.444", "555.666.777.888") //既知のアクセスを除外
| where AppDisplayName != "XXX.com" //既知のアプリを除外
```

以下は、削除すべきレビューコメントが残ってしまっている例です。
```:コメント例2
SigninLogs
| where TimeGenerated > ago(12h) //TODO:あとで1hに戻すこと
| where RiskLevelDuringSignIn !in ("none","low")
| where isempty(UserPrincipalName) //この条件は不要:レビューxx/xx
| order by TimeGenerated desc
| project TimeGenerated,IPAddress,AppDisplayName,UserPrincipalName,ResultType,RiskLevelDuringSignIn
//----- チューニング -----//
// 以下の必要な条件が含まれていない。要再テスト:レビューxx/xx
//| where ResultType in (SuccessCodes)
```


### 適切な変数名を設定する
「A」「B」のような、何を意味しているか判別できない変数名は避けるべきです。その変数がどんな意図で作成されたのか分かる、意味のある変数名を設定しましょう。


```:変数名の例
// NG
let A = ProductTable
| where RegistryDate >ago(30d)
;
let B = DeviceTable
| where Type == "Mobile"
;

// OK
let BaseProduct = ProductTable
| where RegistryDate >ago(30d)
;
let MobileDevice = DeviceTable
| where Type == "Mobile"
;
```


### 共通処理の関数化
LogAnalytics には処理を関数化する機能があります。クエリ内での変数定義と似ていますが、クエリ外部に関数として定義することで、異なるクエリ間で共通した処理を実行することが可能になります。

Sentinel の Log 画面にて、SuccessCode という関数を呼び出す処理の例です。

`1. サインインに成功した際のコード値をまとめた関数 [SuccessCodes] を、ワークスペース関数として保存する。`
`2. クエリから関数を呼び出す。`
![Sentinel_Log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3551604/26b03bf8-63be-f1a7-cc19-616d50d966e8.png)

関数を活用することで、クエリ内の条件分岐や入れ子の処理が減り、よりシンプルなロジックを展開することができます。仕様変更や機能追加時の修正も容易になり、保守性も向上するでしょう。




# 参考
KQL クイックリファレンス
https://learn.microsoft.com/ja-jp/azure/data-explorer/kusto/query/kql-quick-reference

クエリのベストプラクティス
https://learn.microsoft.com/ja-jp/azure/data-explorer/kusto/query/best-practices


# おわりに
パフォーマンス向上のために重要なポイントをまとめてみました。
ただ条件を並べて書くのではなく、これらのポイントを参考に「パフォーマンスの高い」Kustoクエリを作成していきましょう。