Skip to content

Commit 019b99f

Browse files
authored
Add 'QueryJob.query_plan' property. (#3799)
1 parent 72e0976 commit 019b99f

File tree

2 files changed

+406
-0
lines changed

2 files changed

+406
-0
lines changed

bigquery/google/cloud/bigquery/job.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,11 @@ def ended(self):
266266
if millis is not None:
267267
return _datetime_from_microseconds(millis * 1000.0)
268268

269+
def _job_statistics(self):
270+
"""Helper for job-type specific statistics-based properties."""
271+
statistics = self._properties.get('statistics', {})
272+
return statistics.get(self._JOB_TYPE, {})
273+
269274
@property
270275
def error_result(self):
271276
"""Error information about the job as a whole.
@@ -1280,6 +1285,20 @@ def from_api_repr(cls, resource, client):
12801285
job._set_properties(resource)
12811286
return job
12821287

1288+
@property
1289+
def query_plan(self):
1290+
"""Return query plan from job statistics, if present.
1291+
1292+
See:
1293+
https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs#statistics.query.queryPlan
1294+
1295+
:rtype: list of :class:`QueryPlanEntry`
1296+
:returns: mappings describing the query plan, or an empty list
1297+
if the query has not yet completed.
1298+
"""
1299+
plan_entries = self._job_statistics().get('queryPlan', ())
1300+
return [QueryPlanEntry.from_api_repr(entry) for entry in plan_entries]
1301+
12831302
def query_results(self):
12841303
"""Construct a QueryResults instance, bound to this job.
12851304
@@ -1306,3 +1325,149 @@ def result(self, timeout=None):
13061325
super(QueryJob, self).result(timeout=timeout)
13071326
# Return a QueryResults instance instead of returning the job.
13081327
return self.query_results()
1328+
1329+
1330+
class QueryPlanEntryStep(object):
1331+
"""Map a single step in a query plan entry.
1332+
1333+
:type kind: str
1334+
:param kind: step type
1335+
1336+
:type substeps:
1337+
:param substeps: names of substeps
1338+
"""
1339+
def __init__(self, kind, substeps):
1340+
self.kind = kind
1341+
self.substeps = list(substeps)
1342+
1343+
@classmethod
1344+
def from_api_repr(cls, resource):
1345+
"""Factory: construct instance from the JSON repr.
1346+
1347+
:type resource: dict
1348+
:param resource: JSON representation of the entry
1349+
1350+
:rtype: :class:`QueryPlanEntryStep`
1351+
:return: new instance built from the resource
1352+
"""
1353+
return cls(
1354+
kind=resource.get('kind'),
1355+
substeps=resource.get('substeps', ()),
1356+
)
1357+
1358+
def __eq__(self, other):
1359+
if not isinstance(other, self.__class__):
1360+
return NotImplemented
1361+
return self.kind == other.kind and self.substeps == other.substeps
1362+
1363+
1364+
class QueryPlanEntry(object):
1365+
"""Map a single entry in a query plan.
1366+
1367+
:type name: str
1368+
:param name: name of the entry
1369+
1370+
:type entry_id: int
1371+
:param entry_id: ID of the entry
1372+
1373+
:type wait_ratio_avg: float
1374+
:param wait_ratio_avg: average wait ratio
1375+
1376+
:type wait_ratio_max: float
1377+
:param wait_ratio_avg: maximum wait ratio
1378+
1379+
:type read_ratio_avg: float
1380+
:param read_ratio_avg: average read ratio
1381+
1382+
:type read_ratio_max: float
1383+
:param read_ratio_avg: maximum read ratio
1384+
1385+
:type copute_ratio_avg: float
1386+
:param copute_ratio_avg: average copute ratio
1387+
1388+
:type copute_ratio_max: float
1389+
:param copute_ratio_avg: maximum copute ratio
1390+
1391+
:type write_ratio_avg: float
1392+
:param write_ratio_avg: average write ratio
1393+
1394+
:type write_ratio_max: float
1395+
:param write_ratio_avg: maximum write ratio
1396+
1397+
:type records_read: int
1398+
:param records_read: number of records read
1399+
1400+
:type records_written: int
1401+
:param records_written: number of records written
1402+
1403+
:type status: str
1404+
:param status: entry status
1405+
1406+
:type steps: List(QueryPlanEntryStep)
1407+
:param steps: steps in the entry
1408+
"""
1409+
def __init__(self,
1410+
name,
1411+
entry_id,
1412+
wait_ratio_avg,
1413+
wait_ratio_max,
1414+
read_ratio_avg,
1415+
read_ratio_max,
1416+
compute_ratio_avg,
1417+
compute_ratio_max,
1418+
write_ratio_avg,
1419+
write_ratio_max,
1420+
records_read,
1421+
records_written,
1422+
status,
1423+
steps):
1424+
self.name = name
1425+
self.entry_id = entry_id
1426+
self.wait_ratio_avg = wait_ratio_avg
1427+
self.wait_ratio_max = wait_ratio_max
1428+
self.read_ratio_avg = read_ratio_avg
1429+
self.read_ratio_max = read_ratio_max
1430+
self.compute_ratio_avg = compute_ratio_avg
1431+
self.compute_ratio_max = compute_ratio_max
1432+
self.write_ratio_avg = write_ratio_avg
1433+
self.write_ratio_max = write_ratio_max
1434+
self.records_read = records_read
1435+
self.records_written = records_written
1436+
self.status = status
1437+
self.steps = steps
1438+
1439+
@classmethod
1440+
def from_api_repr(cls, resource):
1441+
"""Factory: construct instance from the JSON repr.
1442+
1443+
:type resource: dict
1444+
:param resource: JSON representation of the entry
1445+
1446+
:rtype: :class:`QueryPlanEntry`
1447+
:return: new instance built from the resource
1448+
"""
1449+
records_read = resource.get('recordsRead')
1450+
if records_read is not None:
1451+
records_read = int(records_read)
1452+
1453+
records_written = resource.get('recordsWritten')
1454+
if records_written is not None:
1455+
records_written = int(records_written)
1456+
1457+
return cls(
1458+
name=resource.get('name'),
1459+
entry_id=resource.get('id'),
1460+
wait_ratio_avg=resource.get('waitRatioAvg'),
1461+
wait_ratio_max=resource.get('waitRatioMax'),
1462+
read_ratio_avg=resource.get('readRatioAvg'),
1463+
read_ratio_max=resource.get('readRatioMax'),
1464+
compute_ratio_avg=resource.get('computeRatioAvg'),
1465+
compute_ratio_max=resource.get('computeRatioMax'),
1466+
write_ratio_avg=resource.get('writeRatioAvg'),
1467+
write_ratio_max=resource.get('writeRatioMax'),
1468+
records_read=records_read,
1469+
records_written=records_written,
1470+
status=resource.get('status'),
1471+
steps=[QueryPlanEntryStep.from_api_repr(step)
1472+
for step in resource.get('steps', ())],
1473+
)

0 commit comments

Comments
 (0)