Skip to content

Commit 4f61b1e

Browse files
authored
profiled execution plan (#152)
* initial work on profiled execution plan * address review
1 parent b27bc08 commit 4f61b1e

File tree

3 files changed

+102
-9
lines changed

3 files changed

+102
-9
lines changed

redisgraph/execution_plan.py

+30-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
1+
import re
2+
3+
4+
class ProfileStats:
5+
"""
6+
ProfileStats, runtime execution statistics of operation.
7+
"""
8+
9+
def __init__(self, records_produced, execution_time):
10+
self.records_produced = records_produced
11+
self.execution_time = execution_time
12+
13+
114
class Operation:
215
"""
316
Operation, single operation within execution plan.
417
"""
518

6-
def __init__(self, name, args=None):
19+
def __init__(self, name, args=None, profile_stats=None):
720
"""
821
Create a new operation.
922
1023
Args:
1124
name: string that represents the name of the operation
1225
args: operation arguments
26+
profile_stats: profile statistics
1327
"""
1428
self.name = name
1529
self.args = args
30+
self.profile_stats = profile_stats
1631
self.children = []
1732

1833
def append_child(self, child):
@@ -32,7 +47,7 @@ def __eq__(self, o: object) -> bool:
3247
return (self.name == o.name and self.args == o.args)
3348

3449
def __str__(self) -> str:
35-
args_str = "" if self.args is None else f" | {self.args}"
50+
args_str = "" if self.args is None else " | " + self.args
3651
return f"{self.name}{args_str}"
3752

3853

@@ -131,21 +146,30 @@ def _operation_tree(self):
131146
stack = []
132147
current = None
133148

149+
def _create_operation(args):
150+
profile_stats = None
151+
name = args[0].strip()
152+
args.pop(0)
153+
if len(args) > 0 and "Records produced" in args[-1]:
154+
records_produced = int(re.search("Records produced: (\\d+)", args[-1]).group(1))
155+
execution_time = float(re.search("Execution time: (\\d+.\\d+) ms", args[-1]).group(1))
156+
profile_stats = ProfileStats(records_produced, execution_time)
157+
args.pop(-1)
158+
return Operation(name, None if len(args) == 0 else args[0].strip(), profile_stats)
159+
134160
# iterate plan operations
135161
while i < len(self.plan):
136162
current_op = self.plan[i]
137163
op_level = current_op.count(" ")
138164
if op_level == level:
139165
# if the operation level equal to the current level
140166
# set the current operation and move next
141-
args = current_op.split("|")
142-
current = Operation(args[0].strip(), None if len(args) == 1 else args[1].strip())
167+
current = _create_operation(current_op.split("|"))
143168
i += 1
144169
elif op_level == level + 1:
145170
# if the operation is child of the current operation
146171
# add it as child and set as current operation
147-
args = current_op.split("|")
148-
child = Operation(args[0].strip(), None if len(args) == 1 else args[1].strip())
172+
child = _create_operation(current_op.split("|"))
149173
current.append_child(child)
150174
stack.append(current)
151175
current = child

redisgraph/graph.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def query(self, q, params=None, timeout=None, read_only=False):
219219
def execution_plan(self, query, params=None):
220220
"""
221221
Get the execution plan for given query,
222-
GRAPH.EXPLAIN returns an array of operations.
222+
GRAPH.EXPLAIN returns ExecutionPlan object.
223223
224224
Args:
225225
query: the query that will be executed
@@ -231,6 +231,21 @@ def execution_plan(self, query, params=None):
231231
plan = self.redis_con.execute_command("GRAPH.EXPLAIN", self.name, query)
232232
return ExecutionPlan(plan)
233233

234+
def profile(self, query, params=None):
235+
"""
236+
Get the profield execution plan for given query,
237+
GRAPH.PROFILE returns ExecutionPlan object.
238+
239+
Args:
240+
query: the query that will be executed
241+
params: query parameters
242+
"""
243+
if params is not None:
244+
query = self._build_params_header(params) + query
245+
246+
plan = self.redis_con.execute_command("GRAPH.PROFILE", self.name, query)
247+
return ExecutionPlan(plan)
248+
234249
def delete(self):
235250
"""
236251
Deletes graph.

tests/functional/test_all.py

+56-2
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ def test_cached_execution(self):
246246

247247
def test_execution_plan(self):
248248
redis_graph = Graph('execution_plan', self.r)
249+
# graph creation / population
249250
create_query = """CREATE
250251
(:Rider {name:'Valentino Rossi'})-[:rides]->(:Team {name:'Yamaha'}),
251252
(:Rider {name:'Dani Pedrosa'})-[:rides]->(:Team {name:'Honda'}),
@@ -254,11 +255,11 @@ def test_execution_plan(self):
254255

255256
result = redis_graph.execution_plan("""MATCH (r:Rider)-[:rides]->(t:Team)
256257
WHERE t.name = $name
257-
RETURN r.name, t.name, $params
258+
RETURN r.name, t.name
258259
UNION
259260
MATCH (r:Rider)-[:rides]->(t:Team)
260261
WHERE t.name = $name
261-
RETURN r.name, t.name, $params""", {'name': 'Yehuda'})
262+
RETURN r.name, t.name""", {'name': 'Yamaha'})
262263
expected = '''\
263264
Results
264265
Distinct
@@ -290,6 +291,59 @@ def test_execution_plan(self):
290291

291292
redis_graph.delete()
292293

294+
def test_profile(self):
295+
redis_graph = Graph('profile', self.r)
296+
# graph creation / population
297+
create_query = """UNWIND range(1, 30) as x CREATE (:Person {id: x})"""
298+
redis_graph.query(create_query)
299+
300+
plan = redis_graph.profile("""MATCH (p:Person)
301+
WHERE p.id > 15
302+
RETURN p""")
303+
304+
results = plan.structured_plan
305+
self.assertEqual(results.name, "Results")
306+
self.assertEqual(results.profile_stats.records_produced, 15)
307+
self.assertGreater(results.profile_stats.execution_time, 0)
308+
309+
project = results.children[0]
310+
self.assertEqual(project.name, "Project")
311+
self.assertEqual(project.profile_stats.records_produced, 15)
312+
self.assertGreater(project.profile_stats.execution_time, 0)
313+
314+
filter = project.children[0]
315+
self.assertEqual(filter.name, "Filter")
316+
self.assertEqual(filter.profile_stats.records_produced, 15)
317+
self.assertGreater(filter.profile_stats.execution_time, 0)
318+
319+
node_by_label_scan = filter.children[0]
320+
self.assertEqual(node_by_label_scan.name, "Node By Label Scan")
321+
self.assertEqual(node_by_label_scan.profile_stats.records_produced, 30)
322+
self.assertGreater(node_by_label_scan.profile_stats.execution_time, 0)
323+
324+
redis_graph.query("CREATE INDEX FOR (p:Person) ON (p.id)")
325+
326+
plan = redis_graph.profile("""MATCH (p:Person)
327+
WHERE p.id > 15
328+
RETURN p""")
329+
330+
results = plan.structured_plan
331+
self.assertEqual(results.name, "Results")
332+
self.assertEqual(results.profile_stats.records_produced, 15)
333+
self.assertGreater(results.profile_stats.execution_time, 0)
334+
335+
project = results.children[0]
336+
self.assertEqual(project.name, "Project")
337+
self.assertEqual(project.profile_stats.records_produced, 15)
338+
self.assertGreater(project.profile_stats.execution_time, 0)
339+
340+
node_by_index_scan = project.children[0]
341+
self.assertEqual(node_by_index_scan.name, "Node By Index Scan")
342+
self.assertEqual(node_by_index_scan.profile_stats.records_produced, 15)
343+
self.assertGreater(node_by_index_scan.profile_stats.execution_time, 0)
344+
345+
redis_graph.delete()
346+
293347
def test_query_timeout(self):
294348
redis_graph = Graph('timeout', self.r)
295349
# Build a sample graph with 1000 nodes.

0 commit comments

Comments
 (0)