rendertree

command
v0.1.11-alpha.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 17, 2026 License: Apache-2.0 Imports: 1 Imported by: 0

README

rendertree

rendertree renders YAML or JSON representations of Cloud Spanner query plans as ASCII tables.

Input and modes

Supported input formats:

It can render both PLAN and PROFILE inputs.

Basic usage

# from file
$ cat queryplan.yaml | rendertree --mode=PLAN
+----+-----------------------------------------+
| ID | Operator                                |
+----+-----------------------------------------+
| *0 | Distributed Union                       |
|  1 | +- Local Distributed Union              |
|  2 |    +- Serialize Result                  |
| *3 |       +- FilterScan                     |
|  4 |          +- Table Scan (Table: Singers) |
+----+-----------------------------------------+

Predicates(identified by ID):
 0: Split Range: ($SingerId = 1)
 3: Seek Condition: ($SingerId = 1)

# with gcloud spanner databases execute-sql
$ gcloud spanner databases execute-sql ${DATABASE_ID} --sql="SELECT * FROM Singers" --format=json --query-mode=PROFILE |
    rendertree --mode=PROFILE
+----+-------------------------------------------------------+------+-------+---------+
| ID | Operator                                              | Rows | Exec. | Latency |
+----+-------------------------------------------------------+------+-------+---------+
|  0 | Distributed Union                                     | 1000 |     1 | 6.29 ms |
|  1 | +- Local Distributed Union                            | 1000 |     1 | 6.21 ms |
|  2 |    +- Serialize Result                                | 1000 |     1 | 6.16 ms |
|  3 |       +- Table Scan (Full scan: true, Table: Singers) | 1000 |     1 | 5.78 ms |
+----+-------------------------------------------------------+------+-------+---------+

Note: --mode=PLAN and --mode=PROFILE can be omitted because the default --mode=AUTO can detect whether the input has execution statistics or not.

Scalar appendices

rendertree prints predicate-like scalar parameters by default. The --print flag accepts intent-based presets:

  • basic prints predicate-like scalar links. This is the default when --print is omitted.
  • enhanced prints predicates, ordering details, and aggregate details.
  • full prints all scalar links, including unnamed links, as a raw debug dump.
  • none suppresses appendix output. An explicit empty value, --print="", also suppresses appendix output.

The --print flag can also select one or more low-level appendix sections:

  • predicates prints predicate-like scalar links such as Condition, Residual Condition, Seek Condition, Search Predicate, and Split Range.
  • ordering prints ordering details from Sort, Sort Limit, Minor Sort, and Minor Sort Limit operators.
  • aggregate prints Key and Agg details from Aggregate operators.
  • typed prints all typed scalar links as a raw debug dump.
  • full prints all scalar links, including unnamed links, as a raw debug dump.

Use a comma-separated list for focused sections, for example --print=predicates,ordering. Preset names are standalone choices and cannot be mixed into section lists. typed and full are intentionally noisy debug dumps and cannot be combined with other sections.

Scalar variable display

Semantic appendix sections hide scalar assignment variable names by default. Use --show-vars when the assignment name itself is useful, for example to inspect what $v1 is assigned to. Use --resolve-vars to replace direct scalar variable aliases with their assigned expression in semantic appendix sections. --resolve-vars-recursive is experimental and recursively traces aliases; it is useful for investigation, but can produce noisier expanded expressions.

Example

For example, the following query has a WHERE predicate, aggregation, and ordering:

SELECT SongGenre, COUNT(*) AS SongCount
FROM Songs
WHERE SongName LIKE 'A%'
GROUP BY SongGenre
ORDER BY SongCount DESC, SongGenre
LIMIT 5

Rendering the captured PLAN JSON with --print=predicates,ordering,aggregate shows only the selected scalar details:

$ rendertree --mode=PLAN --print=predicates,ordering,aggregate < plan.json
+-----+--------------------------------------------------------------------------------------+
| ID  | Operator                                                                             |
+-----+--------------------------------------------------------------------------------------+
|   0 | Serialize Result <Row>                                                               |
|   1 | +- Global Sort Limit <Row>                                                           |
|   2 |    +- Global Hash Aggregate <Row>                                                    |
|  *3 |       +- Distributed Union on SongsBySongName <Row>                                  |
|   4 |          +- Local Hash Aggregate <Row>                                               |
|  *5 |             +- Distributed Cross Apply <Row>                                         |
|   6 |                +- [Input] Create Batch <Batch>                                       |
|   7 |                |  +- RowToDataBlock                                                  |
|   8 |                |     +- Local Distributed Union <Row>                                |
|   9 |                |        +- Filter Scan <Row> (seekable_key_size: 1)                  |
| *10 |                |           +- Index Scan on SongsBySongName <Row> (scan_method: Row) |
|  22 |                +- [Map] Local Hash Aggregate <Row>                                   |
|  23 |                   +- Cross Apply <Row>                                               |
|  24 |                      +- [Input] KeyRangeAccumulator <Row>                            |
|  25 |                      |  +- DataBlockToRow                                            |
|  26 |                      |     +- Batch Scan on $v5 <Batch> (scan_method: Batch)         |
|  33 |                      +- [Map] Local Distributed Union <Row>                          |
|  34 |                         +- Filter Scan <Row> (seekable_key_size: 0)                  |
| *35 |                            +- Table Scan on Songs <Row> (scan_method: Row)           |
+-----+--------------------------------------------------------------------------------------+

Predicates(identified by ID):
  3: Split Range: STARTS_WITH($SongName, 'A')
  5: Split Range: (($Songs_key_SingerId'3 = $Songs_key_SingerId'2) AND ($Songs_key_AlbumId'3 = $Songs_key_AlbumId'2) AND ($Songs_key_TrackId'3 = $Songs_key_TrackId'2))
 10: Seek Condition: STARTS_WITH($SongName, 'A')
 35: Seek Condition: (($Songs_key_SingerId'3 = $batched_Songs_key_SingerId'3) AND ($Songs_key_AlbumId'3 = $batched_Songs_key_AlbumId'3) AND ($Songs_key_TrackId'3 = $batched_Songs_key_TrackId'3))

Ordering(identified by ID):
  1: Key: $SongCount DESC, $group_SongGenre'2

Aggregates(identified by ID):
  2: Key: $group_SongGenre'
     Agg: COUNT_FINAL($v1)
  4: Key: $group_SongGenre
     Agg: COUNT_FINAL($v3)
 22: Key: $SongGenre
     Agg: COUNT()

Custom stats columns

Rendered stats columns are customizable using --custom-file or repeatable --custom-column flags. --custom-file, --custom-column, and deprecated --custom are mutually exclusive.

$ cat custom.yaml
- name: ID
  template: '{{.FormatID}}'
  alignment: RIGHT
- name: Operator
  template: '{{.Text}}'
  alignment: LEFT
- name: Rows
  template: '{{.ExecutionStats.Rows.Total}}'
  alignment: RIGHT
  inline: NEVER
- name: Scanned
  template: '{{.ExecutionStats.ScannedRows.Total}}'
  alignment: RIGHT
  inline: CAN
- name: remote_calls
  template: '{{.ExecutionStats.RemoteCalls.Total}}'
  alignment: RIGHT
  inline: ALWAYS
Inline stats

inline field in the custom configuration and the --inline-stats command-line flag together control how execution statistics are rendered. Inline stats are particularly useful for displaying sparse statistics (those that only appear on a few operators) without adding many empty columns to the main table, thus improving readability.

The following table shows how the inline field setting for a specific statistic interacts with the --inline-stats flag to determine its display location:

inline/--inline-stats true false
unspecified (except ID and Operator) inline in table
NEVER in table in table
CAN inline in table
ALWAYS inline inline

In summary, the --inline-stats flag enables inline display for statistics marked as CAN, or for those whose inline property is unspecified (this applies to columns other than ID and Operator). ALWAYS forces inline display regardless of the --inline-stats flag, and NEVER always keeps the statistic in a separate table column.

Default Behavior for ID and Operator:

If the inline property for ID or Operator columns is not specified (either by relying on the tool's built-in defaults or by defining them in a custom.yaml file without an inline field), they will always be displayed in the table (effectively behaving as if inline: NEVER was set).

$ rendertree --custom-file custom.example.yaml < profile.yaml
+-----+--------------------------------------------------------------------------+------+---------+
| ID  | Operator                                                                 | Rows | scanned |
+-----+--------------------------------------------------------------------------+------+---------+
|  *0 | Distributed Union on SongsBySongName <Row> (remote_calls=0)              | 3069 |         |
|  *1 | +- Distributed Cross Apply <Row> (remote_calls=0)                        | 3069 |         |
|   2 |    +- [Input] Create Batch <Row>                                         |      |         |
|   3 |    |  +- Local Distributed Union <Row> (remote_calls=0)                  | 3069 |         |
|   4 |    |     +- Compute Struct <Row>                                         | 3069 |         |
|  *5 |    |        +- Filter Scan <Row> (seekable_key_size: 1)                  | 3069 |         |
|  *6 |    |           +- Index Scan on SongsBySongName <Row> (scan_method: Row) | 3069 |   14212 |
|  24 |    +- [Map] Serialize Result <Row>                                       | 3069 |         |
|  25 |       +- Cross Apply <Row>                                               | 3069 |         |
|  26 |          +- [Input] KeyRangeAccumulator <Row>                            |      |         |
|  27 |          |  +- Batch Scan on $v2 <Row> (scan_method: Row)                |      |         |
|  32 |          +- [Map] Local Distributed Union <Row> (remote_calls=0)         | 3069 |         |
|  33 |             +- Filter Scan <Row> (seekable_key_size: 0)                  |      |         |
| *34 |                +- Table Scan on Songs <Row> (scan_method: Row)           | 3069 |    3069 |
+-----+--------------------------------------------------------------------------+------+---------+

Predicates(identified by ID):
  0: Split Range: (STARTS_WITH($SongName, 'Th') AND ($SongName LIKE 'Th%e'))
  1: Split Range: (($SingerId' = $SingerId) AND ($AlbumId' = $AlbumId) AND ($TrackId' = $TrackId))
  5: Residual Condition: ($SongName LIKE 'Th%e')
  6: Seek Condition: STARTS_WITH($SongName, 'Th')
 34: Seek Condition: (($SingerId' = $batched_SingerId) AND ($AlbumId' = $batched_AlbumId) AND ($TrackId' = $batched_TrackId))
$ rendertree --inline-stats --custom-file custom.example.yaml < profile.yaml
+-----+-----------------------------------------------------------------------------------------+------+
| ID  | Operator                                                                                | Rows |
+-----+-----------------------------------------------------------------------------------------+------+
|  *0 | Distributed Union on SongsBySongName <Row> (remote_calls=0)                             | 3069 |
|  *1 | +- Distributed Cross Apply <Row> (remote_calls=0)                                       | 3069 |
|   2 |    +- [Input] Create Batch <Row>                                                        |      |
|   3 |    |  +- Local Distributed Union <Row> (remote_calls=0)                                 | 3069 |
|   4 |    |     +- Compute Struct <Row>                                                        | 3069 |
|  *5 |    |        +- Filter Scan <Row> (seekable_key_size: 1)                                 | 3069 |
|  *6 |    |           +- Index Scan on SongsBySongName <Row> (scan_method: Row, scanned=14212) | 3069 |
|  24 |    +- [Map] Serialize Result <Row>                                                      | 3069 |
|  25 |       +- Cross Apply <Row>                                                              | 3069 |
|  26 |          +- [Input] KeyRangeAccumulator <Row>                                           |      |
|  27 |          |  +- Batch Scan on $v2 <Row> (scan_method: Row)                               |      |
|  32 |          +- [Map] Local Distributed Union <Row> (remote_calls=0)                        | 3069 |
|  33 |             +- Filter Scan <Row> (seekable_key_size: 0)                                 |      |
| *34 |                +- Table Scan on Songs <Row> (scan_method: Row, scanned=3069)            | 3069 |
+-----+-----------------------------------------------------------------------------------------+------+

Predicates(identified by ID):
  0: Split Range: (STARTS_WITH($SongName, 'Th') AND ($SongName LIKE 'Th%e'))
  1: Split Range: (($SingerId' = $SingerId) AND ($AlbumId' = $AlbumId) AND ($TrackId' = $TrackId))
  5: Residual Condition: ($SongName LIKE 'Th%e')
  6: Seek Condition: STARTS_WITH($SongName, 'Th')
 34: Seek Condition: (($SingerId' = $batched_SingerId) AND ($AlbumId' = $batched_AlbumId) AND ($TrackId' = $batched_TrackId))
Repeatable custom columns

You can also use repeatable --custom-column flags. Each flag value is a single column definition in JSON or flow-style YAML, using the same schema as --custom-file.

$ cat distributed_cross_apply_profile.yaml | \
    rendertree \
      --custom-column '{"name":"ID","template":"{{.FormatID}}","alignment":"RIGHT"}' \
      --custom-column '{"name":"Operator","template":"{{.Text}}"}' \
      --custom-column '{"name":"CPU Time","template":"{{.ExecutionStats.CpuTime | secsToS}}"}' \
      --custom-column '{"name":"remote_calls","template":"{{.ExecutionStats.RemoteCalls.Total}}","inline":"ALWAYS"}'
+-----+-------------------------------------------------------------------------------------------+----------+
| ID  | Operator                                                                                  | CPU Time |
+-----+-------------------------------------------------------------------------------------------+----------+
|   0 | Distributed Union on AlbumsByAlbumTitle <Row> (remote_calls=0)                            | 0.59 ms  |
|  *1 | +- Distributed Cross Apply <Row> (remote_calls=0)                                         | 0.57 ms  |
|   2 |    +- [Input] Create Batch <Row>                                                          |          |
|   3 |    |  +- Local Distributed Union <Row> (remote_calls=0)                                   | 0.28 ms  |
|   4 |    |     +- Compute Struct <Row>                                                          | 0.27 ms  |
|   5 |    |        +- Index Scan on AlbumsByAlbumTitle <Row> (Full scan, scan_method: Automatic) | 0.26 ms  |
|  11 |    +- [Map] Serialize Result <Row>                                                        | 0.22 ms  |
|  12 |       +- Cross Apply <Row>                                                                | 0.2 ms   |
|  13 |          +- [Input] Batch Scan on $v2 <Row> (scan_method: Row)                            | 0.01 ms  |
|  16 |          +- [Map] Local Distributed Union <Row> (remote_calls=0)                          | 0.19 ms  |
| *17 |             +- Filter Scan <Row> (seekable_key_size: 0)                                   |          |
|  18 |                +- Index Scan on SongsBySongGenre <Row> (Full scan, scan_method: Row)      | 0.18 ms  |
+-----+-------------------------------------------------------------------------------------------+----------+

Predicates(identified by ID):
  1: Split Range: ($AlbumId = $AlbumId_1)
 17: Residual Condition: ($AlbumId = $batched_AlbumId_1)
Deprecated custom syntax

The older --custom=<name>:<template>[:<align>[:<inline_type>]] form is still accepted for compatibility, but it is deprecated because the delimiter-based mini-language cannot represent all valid template strings robustly.

Narrow width output

rendertree supports compact formatting and wrapping for limited-width environments.

  • --compact enables the compact format:
    • Each level of depth in the Query Plan tree adds only one character to its indentation.
    • Whitespaces are not inserted for operator and metadata display unless it causes ambiguity.
  • --wrap-width specifies the number of characters at which to wrap the content of the Operator column.
    • The tree won't be broken even when operator lines are wrapped.
  • --hanging-indent enables hanging indent for wrapped lines.
    • Wrapped continuation lines align after node-local prefixes such as [Input] and [Map] .
    • Without this flag, wrapped lines keep the original tree-aligned indentation.
$ rendertree --compact --wrap-width=60 < testdata/distributed_cross_apply.yaml 
+-----+--------------------------------------------------------------+
| ID  | Operator                                                     |
+-----+--------------------------------------------------------------+
|   0 | Distributed Union on AlbumsByAlbumTitle<Row>                 |
|  *1 | +Distributed Cross Apply<Row>                                |
|   2 |  +[Input]Create Batch<Row>                                   |
|   3 |  |+Local Distributed Union<Row>                              |
|   4 |  | +Compute Struct<Row>                                      |
|   5 |  |  +Index Scan on AlbumsByAlbumTitle<Row>(Full scan,scan_me |
|     |  |   thod:Automatic)                                         |
|  11 |  +[Map]Serialize Result<Row>                                 |
|  12 |   +Cross Apply<Row>                                          |
|  13 |    +[Input]Batch Scan on $v2<Row>(scan_method:Row)           |
|  16 |    +[Map]Local Distributed Union<Row>                        |
| *17 |     +Filter Scan<Row>(seekable_key_size:0)                   |
|  18 |      +Index Scan on SongsBySongGenre<Row>(Full scan,scan_met |
|     |       hod:Row)                                               |
+-----+--------------------------------------------------------------+

Predicates(identified by ID):
  1: Split Range: ($AlbumId = $AlbumId_1)
 17: Residual Condition: ($AlbumId = $batched_AlbumId_1)
$ rendertree --wrap-width=50 --hanging-indent < testdata/distributed_cross_apply.yaml
...
|  13 |          +- [Input] Batch Scan on $v2 <Row> (scan_ |
|     |          |          method: Row)                   |
...

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL