쿼리를 최적화하여 복잡한 쿼리를 실행하는 동안 결과를 더 빠르게 얻고 시간 제한을 방지합니다. 쿼리 성능 향상에 대한 지침은 다음과 같습니다.
- 일반 최적화 팁 - 이 문서의
-
연산자
join
최적화 - 이 문서의 -
연산자
summarize
최적화 - 이 문서의 - 쿼리 시나리오 - 이 문서의
- Kusto 쿼리 모범 사례 - 쿼리를 보다 효율적으로 만들기 위한 몇 가지 시나리오가 포함되어 있습니다.
- Azure Monitor에서 로그 쿼리 최적화 - 쿼리 최적화에 대한 추가 지침이 포함되어 있습니다.
- KQL 쿼리 최적화 (비디오) - 쿼리를 개선하는 가장 일반적인 방법
CPU 리소스 할당량 이해
크기에 따라 각 테넌트는 고급 헌팅 쿼리를 실행하기 위해 할당된 정해진 양의 CPU 리소스에 액세스할 수 있습니다. 다양한 사용 매개 변수에 대한 자세한 내용은 고급 헌팅 할당량 및 사용 매개 변수에 대해 읽어보세요.
쿼리를 실행한 후 실행 시간과 리소스 사용량(낮음, 중간, 높음)을 볼 수 있습니다. 높음은 쿼리를 실행하는 데 더 많은 리소스가 사용되었으며 결과를 보다 효율적으로 반환하도록 개선될 수 있음을 나타냅니다.
여러 쿼리를 정기적으로 실행하는 고객은 사용량을 추적하고 이 문서의 최적화 지침을 적용하여 할당량 또는 사용 매개 변수 초과로 인한 중단을 최소화해야 합니다.
일반 최적화 팁
새 쿼리 크기 조정 - 쿼리가 큰 결과 집합을 반환할 것으로 의심되는 경우 count 연산자를 사용하여 먼저 평가합니다. 큰 결과 집합을 방지하려면 제한 또는 해당 동의어를
take
사용합니다.필터를 조기에 적용 — 특히 substring(), replace(), trim(), toupper()또는 parse_json()와 같은 변환 및 구문 분석 함수를 사용하기 전에 데이터 집합을 줄이기 위해 시간 필터 및 기타 필터를 적용합니다. 아래 예제에서는 필터링 연산자가 레코드 수를 줄인 후 구문 분석 함수 extractjson() 이 사용됩니다.
DeviceEvents | where Timestamp > ago(1d) | where ActionType == "UsbDriveMount" | where DeviceName == "user-desktop.domain.com" | extend DriveLetter = extractjson("$.DriveLetter", AdditionalFields)
비트 포함- 단어 내에서 부분 문자열을 불필요하게 검색하지 않도록 하려면 대신
contains
연산자를has
사용합니다. 문자열 연산자 알아보기특정 열 살펴보기 - 모든 열에서 전체 텍스트 검색을 실행하지 않고 특정 열을 찾습니다. 를 사용하여
*
모든 열을 검사 마세요.속도에 대/소문자를 구분합니다. 대/소문자를 구분하는 검색은 더 구체적이고 일반적으로 성능이 더 높습니다. 및
contains_cs
와 같은has_cs
대/소문자를 구분하는 문자열 연산자의 이름은 일반적으로 로_cs
끝납니다. 대신 대/소문자를 구분하는 equals 연산자를==
=~
사용할 수도 있습니다.구문 분석, 추출하지 않음 - 가능하면 구문 분석 연산자 또는 구문 분석 함수 (예: parse_json()를 사용합니다.
matches regex
정규식을 사용하는 문자열 연산자 또는 extract() 함수를 사용하지 마세요. 더 복잡한 시나리오를 위해 정규식 사용을 예약합니다. 구문 분석 함수에 대해 자세히 알아보기식이 아닌 테이블 필터링 - 테이블 열을 필터링할 수 있는 경우 계산 열을 필터링하지 마세요.
3자 용어 없음 - 3자 이하의 용어를 사용하여 비교하거나 필터링하지 마세요. 이러한 용어는 인덱싱되지 않으며 일치하려면 더 많은 리소스가 필요합니다.
선택적으로 프로젝트 - 필요한 열만 프로젝팅하여 결과를 더 쉽게 이해할 수 있도록 합니다. 조인 또는 유사한 작업을 실행하기 전에 특정 열을 프로젝션하면 성능도 향상됩니다.
연산자 join
최적화
조인 연산자는 지정된 열의 값을 일치시켜 두 테이블의 행을 병합합니다. 이러한 팁을 적용하여 이 연산자를 사용하는 쿼리를 최적화합니다.
왼쪽에 있는 작은 테이블 - 연산자는
join
조인 문의 왼쪽에 있는 테이블의 레코드를 오른쪽의 레코드와 일치합니다. 왼쪽에 작은 테이블을 두면 일치하는 레코드가 더 적어질 수 있으므로 쿼리 속도가 빨라질 수 있습니다.아래 표에서는 계정 SID로 조
IdentityLogonEvents
인하기 전에 세 개의 특정 디바이스만 포함하도록 왼쪽 테이블을DeviceLogonEvents
줄입니다.DeviceLogonEvents | where DeviceName in ("device-1.domain.com", "device-2.domain.com", "device-3.domain.com") | where ActionType == "LogonFailed" | join (IdentityLogonEvents | where ActionType == "LogonFailed" | where Protocol == "Kerberos") on AccountSid
내부 조인 버전 사용 - 기본 조인 버전 또는 innerunique-join 중복 제거는 각 일치 항목의 행을 오른쪽 테이블에 반환하기 전에 조인 키로 왼쪽 테이블의 행을 중복 제거합니다. 왼쪽 테이블에 키 값이 같은
join
행이 여러 개 있는 경우 해당 행은 중복 제거되어 각 고유 값에 대해 임의의 단일 행을 남깁니다.이 기본 동작은 유용한 인사이트를 제공할 수 있는 왼쪽 테이블에서 중요한 정보를 제외할 수 있습니다. 예를 들어 아래 쿼리는 여러 전자 메일 메시지를 사용하여 동일한 첨부 파일을 보낸 경우에도 특정 첨부 파일이 포함된 하나의 전자 메일만 표시합니다.
EmailAttachmentInfo | where Timestamp > ago(1h) | where Subject == "Document Attachment" and FileName == "Document.pdf" | join (DeviceFileEvents | where Timestamp > ago(1h)) on SHA256
이 제한을 해결하기 위해 왼쪽 테이블의 모든 행을 오른쪽에 일치하는 값으로 표시하도록 지정하여
kind=inner
내부 조인 버전을 적용합니다.EmailAttachmentInfo | where Timestamp > ago(1h) | where Subject == "Document Attachment" and FileName == "Document.pdf" | join kind=inner (DeviceFileEvents | where Timestamp > ago(1h)) on SHA256
시간 창에서 레코드 조인 - 보안 이벤트를 조사할 때 분석가는 같은 기간에 발생하는 관련 이벤트를 찾습니다. 를 사용할
join
때 동일한 접근 방식을 적용하면 검사 레코드 수를 줄여 성능이 향상됩니다.아래 쿼리는 악성 파일을 받은 후 30분 이내에 로그온 이벤트를 확인합니다.
EmailEvents | where Timestamp > ago(7d) | where ThreatTypes has "Malware" | project EmailReceivedTime = Timestamp, Subject, SenderFromAddress, AccountName = tostring(split(RecipientEmailAddress, "@")[0]) | join ( DeviceLogonEvents | where Timestamp > ago(7d) | project LogonTime = Timestamp, AccountName, DeviceName ) on AccountName | where (LogonTime - EmailReceivedTime) between (0min .. 30min)
양쪽에 시간 필터 적용 - 특정 기간을 조사하지 않더라도 왼쪽 테이블과 오른쪽 테이블에 시간 필터를 적용하면 레코드 수를 줄여 검사 성능을 향상시킬
join
수 있습니다. 아래 쿼리는 지난 1시간 동안의 레코드만 조인하도록 두 테이블에 모두 적용됩니다Timestamp > ago(1h)
.EmailAttachmentInfo | where Timestamp > ago(1h) | where Subject == "Document Attachment" and FileName == "Document.pdf" | join kind=inner (DeviceFileEvents | where Timestamp > ago(1h)) on SHA256
성능에 힌트 사용 - 연산자에서
join
힌트를 사용하여 리소스 집약적 작업을 실행할 때 백 엔드에 부하를 분산하도록 지시합니다. 조인 힌트에 대해 자세히 알아보세요.예를 들어 순서 섞기 힌트 는 아래 쿼리의 와 같이
AccountObjectId
고유한 값이 많은 키인 카디널리티가 높은 키를 사용하여 테이블을 조인할 때 쿼리 성능을 향상시키는 데 도움이 됩니다.IdentityInfo | where JobTitle == "CONSULTANT" | join hint.shufflekey = AccountObjectId (IdentityDirectoryEvents | where Application == "Active Directory" | where ActionType == "Private data retrieval") on AccountObjectId
브로드캐스트 힌트는 왼쪽 테이블이 작고(최대 100,000개 레코드) 오른쪽 테이블이 매우 큰 경우에 유용합니다. 예를 들어 아래 쿼리는 특정 주체가 있는 몇 개의 전자 메일을 테이블에 링크
EmailUrlInfo
가 포함된 모든 메시지와 조인하려고 합니다.EmailEvents | where Subject in ("Warning: Update your credentials now", "Action required: Update your credentials now") | join hint.strategy = broadcast EmailUrlInfo on NetworkMessageId
연산자 summarize
최적화
summarize 연산자는 테이블의 내용을 집계합니다. 이러한 팁을 적용하여 이 연산자를 사용하는 쿼리를 최적화합니다.
고유 값 찾기 - 일반적으로 를 사용하여
summarize
반복할 수 있는 고유 값을 찾습니다. 반복적인 값이 없는 열을 집계하는 데 사용할 필요가 없습니다.단일 전자 메일은 여러 이벤트의 일부일 수 있지만, 개별 전자 메일의 네트워크 메시지 ID는 항상 고유한 보낸 사람 주소와 함께 제공되므로 아래 예제에서는 를 효율적으로 사용하지
summarize
않습니다.EmailEvents | where Timestamp > ago(1h) | summarize by NetworkMessageId, SenderFromAddress
연산자를
summarize
로 쉽게 바꿀project
수 있으며 리소스를 적게 소비하면서 잠재적으로 동일한 결과를 얻을 수 있습니다.EmailEvents | where Timestamp > ago(1h) | project NetworkMessageId, SenderFromAddress
다음 예제에서는 동일한 받는 사람 주소로 전자 메일을 보내는 보낸 사람 주소의 여러 고유 인스턴스가 있을 수 있으므로 를 보다 효율적으로 사용하는
summarize
것입니다. 이러한 조합은 덜 고유하며 중복이 있을 수 있습니다.EmailEvents | where Timestamp > ago(1h) | summarize by SenderFromAddress, RecipientEmailAddress
쿼리 순서 섞기 - 반복적인 값이 있는 열에서 가장 잘 사용되지만
summarize
동일한 열은 카디널리티가 높 거나 고유 값이 많을 수도 있습니다. 연산자와join
마찬가지로 와 함께summarize
순서 섞기 힌트를 적용하여 처리 부하를 분산하고 카디널리티가 높은 열에서 작동할 때 성능을 향상시킬 수도 있습니다.아래 쿼리는 를 사용하여
summarize
대규모 조직에서 수십만 개의 고유한 수신자 전자 메일 주소를 실행할 수 있습니다. 성능을 향상시키기 위해 을 통합합니다.hint.shufflekey
EmailEvents | where Timestamp > ago(1h) | summarize hint.shufflekey = RecipientEmailAddress count() by Subject, RecipientEmailAddress
쿼리 시나리오
프로세스 ID를 사용하여 고유한 프로세스 식별
PID(프로세스 ID)는 Windows에서 재활용할 수 있으며 새 프로세스를 위해 다시 사용됩니다. 즉, 특정 프로세스에 대한 고유 식별자로는 사용할 수 없습니다.
일반적으로 특정 디바이스에서 프로세스를 고유하게 식별하는 유일한 방법은 해당 프로세스 ID를 해당 프로세스 생성 시간과 디바이스 식별자(또는 DeviceName
)와 결합하는 것 DeviceId
이었습니다. instance 경우 다음 예제 쿼리는 포트 445(SMB)를 통해 10개 이상의 IP 주소에 액세스하는 프로세스를 찾아 파일 공유를 검색할 수 있습니다.
DeviceNetworkEvents
| where RemotePort == 445 and Timestamp > ago(12h) and InitiatingProcessId !in (0, 4)
| summarize RemoteIPCount=dcount(RemoteIP) by DeviceName, InitiatingProcessId, InitiatingProcessCreationTime, InitiatingProcessFileName
| where RemoteIPCount > 10
위의 쿼리는 동일한 프로세스 ID와 여러 프로세스를 혼합하지 않고 단일 프로세스를 살펴보도록 및 InitiatingProcessCreationTime
를 모두 InitiatingProcessId
요약합니다.
이 방법은 여전히 유효하며, 특히 Windows가 아닌 시스템의 경우 그렇습니다. 그러나 Windows에는 필드를 사용하는 보다 직접적인 메서드가 있습니다 ProcessUniqueId
. 이전 메서드와 아래에 설명된 메서드는 모두 고유한 프로세스 인스턴스를 생성하지만, 쿼리를 간소화하고 PID 재사용 시나리오를 처리할 필요가 없으므로 사용 가능한 경우 를 사용하는 ProcessUniqueId
것이 좋습니다.
이 쿼리는 및 InitiatingProcessUniqueId
필드를 사용하여 ProcessUniqueId
특정 부모 프로세스를 자식 프로세스에 연결하는 방법을 보여 줍니다. 각 자식 InitiatingProcessUniqueId
의 를 부모의 ProcessUniqueId
에 일치시켜 프로세스 ID가 시간이 지남에 따라 재사용되더라도 정확한 부모 instance 시작한 자식 프로세스만 격리합니다.
쿼리 예제:
// Step 1: Select a specific parent process instance (for instance, powershell.exe).
let parentProcess =
DeviceProcessEvents
| where FileName =~ "powershell.exe" // For your specific use case, consider modifying the FileName and adding more identifying properties to specify your query.
| where isnotempty(ProcessUniqueId)
| top 1 by Timestamp asc
| project DeviceId, DeviceName, ParentProcessUniqueId = ProcessUniqueId, ParentFileName = FileName;
// Step 2: Find all child processes started by this unique parent.
DeviceProcessEvents
| where isnotempty(InitiatingProcessUniqueId)
| join kind=inner (
parentProcess
) on DeviceId
| where InitiatingProcessUniqueId == ParentProcessUniqueId
| project
DeviceName,
ParentProcessUniqueId,
ParentFileName,
ChildProcessName = FileName,
ChildProcessId = ProcessId,
ChildProcessUniqueId = ProcessUniqueId,
Timestamp
마찬가지로 쿼리는 동일한 프로세스 ID와 여러 프로세스를 혼합하지 않고 단일 프로세스를 살펴보도록 및 InitiatingProcessCreationTime
를 모두 InitiatingProcessId
요약합니다.
쿼리 명령줄
여러 가지 방법으로 작업을 수행할 수 있는 명령줄을 만들 수 있습니다. 예를 들어 공격자는 경로 없이, 파일 확장명 없이, 환경 변수를 사용하거나 따옴표가 있는 이미지 파일을 참조할 수 있습니다. 공격자는 매개 변수의 순서를 변경하거나 여러 따옴표와 공백을 추가할 수도 있습니다.
명령줄을 중심으로 더 지속성 있는 쿼리를 만들려면 다음 방법을 적용합니다.
- 명령줄 자체를 필터링하는 대신 파일 이름 필드에서 일치하여 알려진 프로세스(예: net.exe 또는 psexec.exe)를 식별합니다.
- parse_command_line() 함수를 사용하여 명령줄 섹션 구문 분석
- 명령줄 인수에 대해 쿼리할 때 관련이 없는 여러 인수에서 특정 순서로 정확하게 일치하는 항목을 찾지 마세요. 대신 정규 표현식을 사용하거나 별도의 여러 포함 연산자를 사용합니다.
- 대/소문자를 구분하지 않는 일치 항목을 사용합니다. 예를 들어 ,
in~
및 대신in
==
,contains
및contains_cs
를 사용합니다=~
. - 명령줄 난독 처리 기술을 완화하려면 따옴표를 제거하고, 쉼표를 공백으로 바꾸고, 여러 개의 연속된 공백을 단일 공백으로 바꾸는 것이 좋습니다. 다른 접근 방식이 필요한 더 복잡한 난독 처리 기술이 있지만 이러한 조정은 일반적인 방법을 해결하는 데 도움이 될 수 있습니다.
다음 예제에서는 파일 net.exe 찾는 쿼리를 생성하여 방화벽 서비스 "MpsSvc"를 중지하는 다양한 방법을 보여 줍니다.
// Non-durable query - do not use
DeviceProcessEvents
| where ProcessCommandLine == "net stop MpsSvc"
| limit 10
// Better query - filters on file name, does case-insensitive matches
DeviceProcessEvents
| where Timestamp > ago(7d) and FileName in~ ("net.exe", "net1.exe") and ProcessCommandLine contains "stop" and ProcessCommandLine contains "MpsSvc"
// Best query also ignores quotes
DeviceProcessEvents
| where Timestamp > ago(7d) and FileName in~ ("net.exe", "net1.exe")
| extend CanonicalCommandLine=replace("\"", "", ProcessCommandLine)
| where CanonicalCommandLine contains "stop" and CanonicalCommandLine contains "MpsSvc"
외부 원본에서 데이터 수집
긴 목록 또는 큰 테이블을 쿼리에 통합하려면 externaldata 연산 자를 사용하여 지정된 URI에서 데이터를 수집합니다. TXT, CSV, JSON 또는 기타 형식의 파일에서 데이터를 가져올 수 있습니다. 아래 예제에서는 MalwareBazaar(abuse.ch)에서 제공하는 광범위한 맬웨어 SHA-256 해시 목록을 활용하여 전자 메일에 첨부 파일을 검사 방법을 보여 줍니다.
let abuse_sha256 = (externaldata(sha256_hash: string)
[@"https://bazaar.abuse.ch/export/txt/sha256/recent/"]
with (format="txt"))
| where sha256_hash !startswith "#"
| project sha256_hash;
abuse_sha256
| join (EmailAttachmentInfo
| where Timestamp > ago(1d)
) on $left.sha256_hash == $right.SHA256
| project Timestamp,SenderFromAddress,RecipientEmailAddress,FileName,FileType,
SHA256,ThreatTypes,DetectionMethods
문자열 구문 분석
구문 분석 또는 변환이 필요한 문자열을 효율적으로 처리하는 데 사용할 수 있는 다양한 함수가 있습니다.
String | 함수 | 사용 예제 |
---|---|---|
명령줄 | parse_command_line() | 명령 및 모든 인수를 추출합니다. |
경로 | parse_path() | 파일 또는 폴더 경로의 섹션을 추출합니다. |
버전 번호 | parse_version() | 최대 4개의 섹션과 섹션당 최대 8자로 버전 번호를 분해합니다. 구문 분석된 데이터를 사용하여 버전 나이를 비교합니다. |
IPv4 주소 | parse_ipv4() | IPv4 주소를 긴 정수로 변환합니다. IPv4 주소를 변환하지 않고 비교하려면 ipv4_compare()를 사용합니다. |
IPv6 주소 | parse_ipv6() | IPv4 또는 IPv6 주소를 정식 IPv6 표기법으로 변환합니다. IPv6 주소를 비교하려면 ipv6_compare()를 사용합니다. |
지원되는 모든 구문 분석 함수에 대해 알아보려면 Kusto 문자열 함수에 대해 읽어보세요.
참고
이 문서의 일부 테이블은 엔드포인트용 Microsoft Defender 사용할 수 없습니다. Microsoft Defender XDR 켜서 더 많은 데이터 원본을 사용하여 위협을 헌팅합니다. 엔드포인트용 Microsoft Defender 고급 헌팅 쿼리 마이그레이션의 단계에 따라 고급 헌팅 워크플로를 엔드포인트용 Microsoft Defender Microsoft Defender XDR 이동할 수 있습니다.
관련 항목
팁
더 자세히 알아보고 싶으신가요? Microsoft 기술 커뮤니티인 Microsoft Defender XDR 기술 커뮤니티에서 Microsoft Security 커뮤니티에 참여하세요.