Graph exploration basics

Applies to: ✅ Azure Data Explorer

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

Tip

Looking for design and performance guidance? See Best practices for graph semantics.

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 element Count
nodes 11
edges 20

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
label count_
Person 5
Company 3
City 3

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_id labels label_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_id properties
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.partner.onmschina.cn",
"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.partner.onmschina.cn",
"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.partner.onmschina.cn",
"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
label count_
AZMGAddOwner 403412
AZMGAddSecret 345324
AZAddSecret 24666
AZContains 12924
AZRunsAs 6269

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_id in_degree out_degree total_degree
0 41076 1 41077
1 35169 1 35170
50 12080 1 12081
49 11554 1 11555
58 7571 1 7572

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_id node_labels in_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_id node_labels out_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
node1 node2 node3
GHOST.CORP USERS@GHOST.CORP DOMAIN CONTROLLERS@GHOST.CORP
WRAITH.CORP USERS@WRAITH.CORP DOMAIN CONTROLLERS@WRAITH.CORP
DU001@PHANTOM.CORP ADMINISTRATORS@PHANTOM.CORP DOMAIN 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
label properties
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
label properties
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_id property_value
1 JJACOB@PHANTOMCORP.ONMICROSOFT.COM
10 CJACKSON@PHANTOMCORP.ONMICROSOFT.COM
12 RHALL@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
SourceLabel EdgeLabel DestinationLabel Count
COMPANY GUARANTEE COMPANY 202
COMPANY APPLY LOAN 449
PERSON APPLY LOAN 927
ACCOUNT REPAY LOAN 2747
LOAN DEPOSIT ACCOUNT 2758
ACCOUNT TRANSFER ACCOUNT 8132
ACCOUNT WITHDRAW ACCOUNT 9182
PERSON GUARANTEE PERSON 377
COMPANY OWN ACCOUNT 671
COMPANY INVEST COMPANY 679
PERSON OWN ACCOUNT 1384
MEDIUM SIGN_IN ACCOUNT 2489
PERSON INVEST COMPANY 1304