Skip to content

Commit 040a39e

Browse files
tseavertswast
authored andcommitted
Add 'QueryJob.query_plan' property. (#3799)
1 parent 63e0ebe commit 040a39e

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.
@@ -1281,6 +1286,20 @@ def from_api_repr(cls, resource, client):
12811286
job._set_properties(resource)
12821287
return job
12831288

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

0 commit comments

Comments
 (0)