Graph exploration basics

Reusable Kusto Query Language (KQL) query patterns for exploring and analyzing graphs with graph semantics.

Graph exploration basics

This page provides reusable Kusto Query Language (KQL) patterns for quickly exploring graph datasets and answering common questions about structure, nodes, edges, and properties.

Common analysis queries

These reusable query patterns work across all graph models and help you understand the structure and characteristics of any graph dataset. The example below use sample graphs available on our help cluster in the Samples database. For detailed information about these graphs, see Graph sample datasets and examples. Use these queries to explore new graphs, perform basic analysis, or as starting points for more complex graph investigations.

Graph overview and statistics

Understanding the basic characteristics of your graph is essential for analysis planning and performance optimization. These queries provide fundamental metrics about graph size and structure.

Count total nodes and edges:

Use these queries to understand the scale of your graph dataset. Node and edge counts help determine appropriate query strategies and identify potential performance considerations. These examples use the Simple graph, which is ideal for learning basic graph operations.

// Get node count
graph('Simple')
| graph-match (node)
    project node
| count
Count
11
// Get edge count
graph('Simple')
| graph-match (source)-[edge]->(target)
    project edge
| count
Count
20

Get graph summary statistics:

This combined query efficiently provides both metrics in a single result, useful for initial graph assessment and reporting. This example demonstrates the technique using the Simple graph.

let nodes = view() { graph('Simple') | graph-match (node) project node | count }; 
let edges = view() { graph('Simple') | graph-match (source)-[edge]->(target) project edge | count };
union withsource=['Graph element'] nodes, edges
Graph elementCount
nodes11
edges20

Alternative using graph-to-table:

For basic counting, the graph-to-table operator can be more efficient as it directly exports graph elements without pattern matching overhead. This example shows the alternative approach using the same Simple graph.

let nodes = view() { graph('Simple') | graph-to-table nodes | count };
let edges = view() { graph('Simple') | graph-to-table edges | count };
union nodes, edges
Count
11
20

Node analysis

Node analysis helps you understand the entities in your graph, their types, and distribution. These patterns are essential for data quality assessment and schema understanding.

Discover all node types (labels):

This query reveals the different entity types in your graph and their frequencies. Use it to understand your data model, identify the most common entity types, and spot potential data quality issues. This example uses the Simple graph, which contains Person, Company, and City entities.

graph('Simple')
| graph-match (node) 
    project labels = labels(node)
| mv-expand label = labels to typeof(string)
| summarize count() by label
| order by count_ desc
labelcount_
Person5
Company3
City3

Find nodes with multiple labels:

Identifies nodes that belong to multiple categories simultaneously. This is useful for understanding overlapping classifications and complex entity relationships in your data model. This example uses the BloodHound_Entra graph, which contains Microsoft Entra objects with multiple label classifications.

graph('BloodHound_Entra')
| graph-match (node) 
    project node_id = node.id, labels = labels(node), label_count = array_length(labels(node))
| where label_count > 1
| take 3
node_idlabelslabel_count
2[
“AZBase”,
“AZServicePrincipal”
]
2
4[
“AZBase”,
“AZUser”
]
2
5[
“AZBase”,
“AZUser”
]
2

Sample nodes by type:

Retrieves representative examples of specific node types to understand their structure and properties. Essential for data exploration and query development. This example uses the BloodHound_Entra graph to explore AZUser node properties in Microsoft Entra environments.

graph('BloodHound_Entra')
| graph-match (node) 
    where labels(node) has "AZUser"
    project node_id = node.id, properties = node.properties
| sample 2
node_idproperties
5{
“lastseen”: “2025-08-11T09:21:19.002Z[UTC]”,
“lastcollected”: “2025-08-11T09:21:07.472380514Z[UTC]”,
“enabled”: true,
“displayname”: “Jack Miller”,
“name”: “JMILLER@PHANTOMCORP.ONMICROSOFT.COM”,
“tenantid”: “6c12b0b0-b2cc-4a73-8252-0b94bfca2145”,
“objectid”: “9a20c327-8cc7-4425-9480-11fb734db194”,
“onpremid”: “”,
“usertype”: “Member”,
“title”: “”,
“userprincipalname”: “jmiller@phantomcorp.onmicrosoft.com”,
“system_tags”: “admin_tier_0”,
“pwdlastset”: “2021-06-16T17:51:03Z[UTC]”,
“onpremsyncenabled”: false,
“whencreated”: “2021-06-16T17:29:16Z[UTC]”,
“email”: “"
}
10{
“lastseen”: “2025-08-11T09:21:07.472380514Z[UTC]”,
“onpremid”: “”,
“usertype”: “Member”,
“title”: “”,
“lastcollected”: “2025-08-11T09:21:07.472380514Z[UTC]”,
“enabled”: true,
“userprincipalname”: “cjackson@phantomcorp.onmicrosoft.com”,
“system_tags”: “admin_tier_0”,
“displayname”: “Chris Jackson”,
“pwdlastset”: “2022-07-19T15:18:49Z[UTC]”,
“onpremsyncenabled”: false,
“name”: “CJACKSON@PHANTOMCORP.ONMICROSOFT.COM”,
“tenantid”: “6c12b0b0-b2cc-4a73-8252-0b94bfca2145”,
“whencreated”: “2022-07-19T15:01:55Z[UTC]”,
“email”: “cjackson@phantomcorp.onmicrosoft.com”,
“objectid”: “bfb6a9c2-f3c8-4b9c-9d09-2924d38895f7”
}

Edge analysis

Understanding relationships in your graph is crucial for identifying patterns, data quality issues, and potential analysis directions.

Discover all edge types (works with different graph schemas):

This query identifies all relationship types in your graph, helping you understand the connections available for analysis. Different graphs use different property names for edge types, so multiple variations are provided. This example uses the BloodHound_Entra graph to show permission relationships in Microsoft Entra environments.

graph('BloodHound_Entra')
| graph-match (source)-[edge]->(target)
    project edge_labels = labels(edge)
| mv-expand label = edge_labels to typeof(string)
| summarize count() by label
| top 5 by count_ desc
labelcount_
AZMGAddOwner403412
AZMGAddSecret345324
AZAddSecret24666
AZContains12924
AZRunsAs6269

Find most connected nodes (highest degree):

Node degree analysis reveals the most influential or central entities in your graph. High-degree nodes often represent key players, bottlenecks, or important infrastructure components. This example uses the LDBC_SNB_Interactive graph, a social network dataset ideal for analyzing connection patterns and influence.

// Find nodes with highest total degree (in + out)
graph('LDBC_SNB_Interactive')
| graph-match (node)
    project node_id = node.id, 
            in_degree = node_degree_in(node),
            out_degree = node_degree_out(node),
            total_degree = node_degree_in(node) + node_degree_out(node)
| order by total_degree desc
| take 5
node_idin_degreeout_degreetotal_degree
041076141077
135169135170
5012080112081
4911554111555
58757117572

Find nodes with highest in-degree (most incoming connections):

High in-degree nodes are often targets of influence, popular destinations, or central resources. In social networks, these might be influential people; in infrastructure graphs, these could be critical services. This example uses the LDBC_Financial graph to identify accounts receiving the most transactions.

graph('LDBC_Financial')
| graph-match (node)
    project node_id = node.node_id, 
            node_labels = labels(node),
            in_degree = node_degree_in(node)
| order by in_degree desc
| take 3
node_idnode_labelsin_degree
Account::99079191802151398[
“ACCOUNT”
]
314
Account::4868391197187506662[
“ACCOUNT”
]
279
Account::4896538694858573544[
“ACCOUNT”
]
184

Find nodes with highest out-degree (most outgoing connections):

High out-degree nodes are often sources of influence, distributors, or connector hubs. These entities typically initiate many relationships or distribute resources to others. This example uses the LDBC_Financial graph to identify accounts making the most transactions.

graph('LDBC_Financial')
| graph-match (node)
    project node_id = node.node_id, 
            node_labels = labels(node),
            out_degree = node_degree_out(node)
| order by out_degree desc
| take 3
node_idnode_labelsout_degree
Account::236720455413661980[
“ACCOUNT”
]
384
Account::56576470318842045[
“ACCOUNT”
]
106
Account::4890627720347648300[
“ACCOUNT”
]
81

Relationship pattern analysis

These queries help identify structural patterns and complex relationships that might indicate important behaviors or anomalies in your data.

Discover triangular relationships (nodes connected in a triangle):

Triangular patterns often indicate tight collaboration, mutual dependencies, or closed-loop processes. In social networks, these represent groups of friends; in business processes, they might indicate approval chains or redundancy patterns. This example uses the BloodHound_AD graph to identify circular privilege relationships in Active Directory environments.

graph('BloodHound_AD')
| graph-match (a)-->(b)-->(c)-->(a)
    where a.id != b.id and b.id != c.id and c.id != a.id
    project node1 = a.name, node2 = b.name, node3 = c.name
| take 3
node1node2node3
GHOST.CORPUSERS@GHOST.CORPDOMAIN CONTROLLERS@GHOST.CORP
WRAITH.CORPUSERS@WRAITH.CORPDOMAIN CONTROLLERS@WRAITH.CORP
DU001@PHANTOM.CORPADMINISTRATORS@PHANTOM.CORPDOMAIN ADMINS@PHANTOM.CORP

Property analysis

Understanding the properties available on your nodes helps you build more sophisticated queries and identify data quality issues.

Explore node properties:

This query reveals what information is stored with your nodes, helping you understand the available attributes for filtering and analysis. This example uses the BloodHound_Entra graph to explore the schema of AZUser nodes and understand what properties are available for Microsoft Entra user objects.

graph('BloodHound_Entra')
| graph-match (node)
    where labels(node) has "AZUser"  // Replace with actual label
    project properties = node.properties
| mv-apply properties on (
        mv-expand kind=array properties
        | where isnotempty(properties[1])
        | extend bag =bag_pack(tostring(properties[0]), properties[1])
        | summarize properties = make_bag(bag)
    )
| summarize buildschema(properties)
schema_properties
{
“onpremsyncenabled”: “bool”,
“system_tags”: “string”,
“lastcollected”: “string”,
“pwdlastset”: “string”,
“usertype”: “string”,
“userprincipalname”: “string”,
“email”: “string”,
“tenantid”: “guid”,
“name”: “string”,
“lastseen”: “string”,
“displayname”: “string”,
“enabled”: “bool”,
“title”: “string”,
“onpremid”: “string”,
“objectid”: “guid”,
“whencreated”: “string”
}

Find all properties of all nodes by label:

This advanced schema discovery query identifies all property names that exist across nodes of each label type. Unlike the previous query that shows the schema structure, this query aggregates property names across all nodes of the same type, helping you understand which properties are consistently available and which might be optional or rare. This example uses the LDBC_SNB_Interactive graph to explore the complete property landscape of different entity types in the social network dataset.

graph('LDBC_SNB_Interactive')
| graph-match (node)
    project properties = node, labels = labels(node)
| mv-apply properties on (
        mv-expand kind=array properties
        | where isnotempty(properties[1])
        | summarize properties = make_set(properties[0])
    )
| mv-expand label = labels to typeof(string)
| summarize properties =make_set(properties) by label
| take 3
labelproperties
TAGCLASS[
“id”,
“node_id”,
“lbl”,
“name”,
“url”
]
TAG[
“id”,
“node_id”,
“lbl”,
“name”,
“url”
]
FORUM[
“id”,
“creationDate”,
“node_id”,
“lbl”,
“title”
]

Find all properties of all edges by label:

This query performs schema discovery for edge (relationship) properties, showing what information is stored with each type of relationship in your graph. Understanding edge properties is crucial for analyzing relationship metadata such as timestamps, weights, confidence scores, or other attributes that provide context about connections. This example uses the BloodHound_AD graph to explore the properties available on different types of Active Directory privilege relationships.

graph('BloodHound_AD')
| graph-match ()-[e]-()
    project properties = e, labels = labels(e)
| mv-apply properties on (
        mv-expand kind=array properties
        | where isnotempty(properties[1])
        | summarize properties = make_set(properties[0])
    )
| mv-expand label = labels to typeof(string)
| summarize properties =make_set(properties) by label
| take 3
labelproperties
GetChangesAll[
“id”,
“lbl”,
“src”,
“dst”,
“properties”,
“lastseen”
]
OwnsRaw[
“id”,
“lbl”,
“src”,
“dst”,
“properties”,
“lastseen”
]
AddKeyCredentialLink[
“id”,
“lbl”,
“src”,
“dst”,
“properties”,
“lastseen”
]

Find nodes with specific property values:

Use this pattern to locate entities with particular characteristics or to validate data quality by checking for expected property values. This example uses the BloodHound_Entra graph to find nodes with specific name properties in Microsoft Entra environments.

graph('BloodHound_Entra')
| graph-match (node)
    where isnotempty(node.properties.name)
    project node_id = node.id, property_value = node.properties.name
| take 3
node_idproperty_value
1JJACOB@PHANTOMCORP.ONMICROSOFT.COM
10CJACKSON@PHANTOMCORP.ONMICROSOFT.COM
12RHALL@PHANTOMCORP.ONMICROSOFT.COM

Topology of the graph

Understanding the overall topology of your graph reveals the types of connections that exist between different entity types. This analysis helps you understand the data model, identify the most common relationship patterns, and discover potential paths for traversal queries. The topology query shows which node labels connect to which other node labels through specific edge types, providing a comprehensive view of your graph’s structure.

//Topology of the graph - What's connected to what?
graph('LDBC_Financial')
| graph-match (src)-[e]->(dst)
    project SourceLabels = labels(src), EdgeLabels = labels(e), DestinationLabels = labels(dst)
| mv-expand EdgeLabel = EdgeLabels to typeof(string)
| mv-expand SourceLabel = SourceLabels to typeof(string)
| mv-expand DestinationLabel = DestinationLabels to typeof(string)
| summarize Count = count() by SourceLabel, EdgeLabel, DestinationLabel
SourceLabelEdgeLabelDestinationLabelCount
COMPANYGUARANTEECOMPANY202
COMPANYAPPLYLOAN449
PERSONAPPLYLOAN927
ACCOUNTREPAYLOAN2747
LOANDEPOSITACCOUNT2758
ACCOUNTTRANSFERACCOUNT8132
ACCOUNTWITHDRAWACCOUNT9182
PERSONGUARANTEEPERSON377
COMPANYOWNACCOUNT671
COMPANYINVESTCOMPANY679
PERSONOWNACCOUNT1384
MEDIUMSIGN_INACCOUNT2489
PERSONINVESTCOMPANY1304