Make a new plan, Stan

I’ve produced some more software.

This one creates diagrams of query execution plans for MySQL.

^ There's tooltips on the things that have tooltips.

Explain that to me like I know what SQL is

You can get a query execution plan table from vanilla MySQL by entering EXPLAIN before the query, which looks a bit like this:

Input:

EXPLAIN select A.*, B.sales 
from (
    select sto.store_id, cit.city, cou.country
    from store sto
    left join address adr on sto.address_id = adr.address_id
    join city cit on adr.city_id = cit.city_id
    join country cou on cit.country_id = cou.country_id
) A
join (
    select cus.store_id, sum(pay.amount) sales
    from customer cus
    join payment pay on pay.customer_id = cus.customer_id
    group by cus.store_id
) B
on A.store_id = B.store_id
order by A.store_id;

Output:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY sto index PRIMARY,idx_fk_address_id PRIMARY 1 2 100.00
1 PRIMARY adr eq_ref PRIMARY,idx_fk_city_id PRIMARY 2 sakila.sto.address_id 1 100.00 Using where
1 PRIMARY cit eq_ref PRIMARY,idx_fk_country_id PRIMARY 2 sakila.adr.city_id 1 100.00
1 PRIMARY cou eq_ref PRIMARY PRIMARY 2 sakila.cit.country_id 1 100.00
1 PRIMARY ref 1 sakila.sto.store_id 160 100.00
3 DERIVED cus index PRIMARY,idx_fk_store_id idx_fk_store_id 1 599 100.00 Using index
3 DERIVED pay ref idx_fk_customer_id idx_fk_customer_id 2 sakila.cus.customer_id 26 100.00

And if you want a bit more detail, you can put EXPLAIN FORMAT=JSON in there, which gives it a bit more structure, and adds a heap of other useful information.

Output:

{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "120.39"
    },
    "ordering_operation": {
      "using_filesort": false,
      "nested_loop": [
        {
          "table": {
            "table_name": "sto",
            "access_type": "index",
            "possible_keys": [
              "PRIMARY",
              "idx_fk_address_id"
            ],
            "key": "PRIMARY",
            "used_key_parts": [
              "store_id"
            ],
            "key_length": "1",
            "rows_examined_per_scan": 2,
            "rows_produced_per_join": 2,
            "filtered": "100.00",
            "cost_info": {
              "read_cost": "1.00",
              "eval_cost": "0.20",
              "prefix_cost": "1.20",
              "data_read_per_join": "32"
            },
            "used_columns": [
              "store_id",
              "address_id"
            ]
          }
        },
        {
          "table": {
            "table_name": "adr",
            "access_type": "eq_ref",
            "possible_keys": [
              "PRIMARY",
              "idx_fk_city_id"
            ],
            "key": "PRIMARY",
            "used_key_parts": [
              "address_id"
            ],
            "key_length": "2",
            "ref": [
              "sakila.sto.address_id"
            ],
            "rows_examined_per_scan": 1,
            "rows_produced_per_join": 2,
            "filtered": "100.00",
            "cost_info": {
              "read_cost": "2.00",
              "eval_cost": "0.20",
              "prefix_cost": "3.40",
              "data_read_per_join": "1K"
            },
            "used_columns": [
              "address_id",
              "city_id"
            ],
            "attached_condition": "(`sakila`.`adr`.`city_id` is not null)"
          }
        },
        {
          "table": {
            "table_name": "cit",
            "access_type": "eq_ref",
            "possible_keys": [
              "PRIMARY",
              "idx_fk_country_id"
            ],
            "key": "PRIMARY",
            "used_key_parts": [
              "city_id"
            ],
            "key_length": "2",
            "ref": [
              "sakila.adr.city_id"
            ],
            "rows_examined_per_scan": 1,
            "rows_produced_per_join": 2,
            "filtered": "100.00",
            "cost_info": {
              "read_cost": "2.00",
              "eval_cost": "0.20",
              "prefix_cost": "5.60",
              "data_read_per_join": "432"
            },
            "used_columns": [
              "city_id",
              "city",
              "country_id"
            ]
          }
        },
        {
          "table": {
            "table_name": "cou",
            "access_type": "eq_ref",
            "possible_keys": [
              "PRIMARY"
            ],
            "key": "PRIMARY",
            "used_key_parts": [
              "country_id"
            ],
            "key_length": "2",
            "ref": [
              "sakila.cit.country_id"
            ],
            "rows_examined_per_scan": 1,
            "rows_produced_per_join": 2,
            "filtered": "100.00",
            "cost_info": {
              "read_cost": "2.00",
              "eval_cost": "0.20",
              "prefix_cost": "7.80",
              "data_read_per_join": "416"
            },
            "used_columns": [
              "country_id",
              "country"
            ]
          }
        },
        {
          "table": {
            "table_name": "B",
            "access_type": "ref",
            "possible_keys": [
              ""
            ],
            "key": "",
            "used_key_parts": [
              "store_id"
            ],
            "key_length": "1",
            "ref": [
              "sakila.sto.store_id"
            ],
            "rows_examined_per_scan": 160,
            "rows_produced_per_join": 321,
            "filtered": "100.00",
            "cost_info": {
              "read_cost": "80.42",
              "eval_cost": "32.17",
              "prefix_cost": "120.39",
              "data_read_per_join": "7K"
            },
            "used_columns": [
              "store_id",
              "sales"
            ],
            "materialized_from_subquery": {
              "using_temporary_table": true,
              "dependent": false,
              "cacheable": true,
              "query_block": {
                "select_id": 3,
                "cost_info": {
                  "query_cost": "17759.50"
                },
                "grouping_operation": {
                  "using_filesort": false,
                  "nested_loop": [
                    {
                      "table": {
                        "table_name": "cus",
                        "access_type": "index",
                        "possible_keys": [
                          "PRIMARY",
                          "idx_fk_store_id"
                        ],
                        "key": "idx_fk_store_id",
                        "used_key_parts": [
                          "store_id"
                        ],
                        "key_length": "1",
                        "rows_examined_per_scan": 599,
                        "rows_produced_per_join": 599,
                        "filtered": "100.00",
                        "using_index": true,
                        "cost_info": {
                          "read_cost": "5.00",
                          "eval_cost": "59.90",
                          "prefix_cost": "64.90",
                          "data_read_per_join": "341K"
                        },
                        "used_columns": [
                          "customer_id",
                          "store_id"
                        ]
                      }
                    },
                    {
                      "table": {
                        "table_name": "pay",
                        "access_type": "ref",
                        "possible_keys": [
                          "idx_fk_customer_id"
                        ],
                        "key": "idx_fk_customer_id",
                        "used_key_parts": [
                          "customer_id"
                        ],
                        "key_length": "2",
                        "ref": [
                          "sakila.cus.customer_id"
                        ],
                        "rows_examined_per_scan": 26,
                        "rows_produced_per_join": 16085,
                        "filtered": "100.00",
                        "cost_info": {
                          "read_cost": "16086.00",
                          "eval_cost": "1608.60",
                          "prefix_cost": "17759.50",
                          "data_read_per_join": "377K"
                        },
                        "used_columns": [
                          "payment_id",
                          "customer_id",
                          "amount"
                        ]
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      ]
    }
  }
}

Interpretting what that actually means is pretty difficult, so the makers of MySQL created a thingamajig inside MySQL Workbench to visualise that explanation, which creates diagrams like this:

sakila-7g

Which is a bit easier to handle, as it shows you when your indexes are being used ( the green boxes ) and when they're not ( the red boxes ).

Well what's the problem then ?

This is all well and good, but unfortunately:

  • MySQL Workbench is a desktop app, which makes it a little bit difficult to create these diagrams on demand inside your web application when it hits a particularly slow query.
  • MySQL Workbench is GPL licensed, so if you felt like reusing that code in another application that isn't a desktop app, you'll have to disclose the source code to that application, due to Eric Stallman getting a hissy fit with AT&T back in the 80s. A lot of people don't want to do that.
  • It doesn't handle quite a few common SQL constructs

So here's a thing I've dubbed the SqlExplainerator, which does the same sort of thing, but:

  • it's a Java library, which you might find easier to invoke programmatically
  • there's a CLI as well, which you might find easier to invoke programmatically from languages that aren't Java
  • it generates SVG or HTML, some of which can be styled with CSS
  • it's BSD licensed
  • it handles a few more SQL constructs
  • if you want to intentionally cripple it to look more like mysql workbench, there's also a 'workbench-compatible' mode which will prevent it from handling anything workbench doesn't handle.
    • and because there's a pretty good chance that at some stage in the future MySQL workbench will start supporting those constructs, I've separated the bit that does my own little extrapolation of JSON into the visual realm into a separate layout algorithm. That way I can 'fix' the code to mimic workbench again if and when they start supporting those constructs later on.
  • it has three flavours of tooltips. Four if you include not having tooltips at all.

So what sorts of things does this thing handle which workbench doesn't

  • HAVING clauses
  • INSERT INTO queries
  • CTEs that are used more than once
  • window functions

Four flavours of tooltips ?

Oh yes. You've got

  • SVG title elements, when you need to embed the SVG somewhere that doesn't allow javascript
  • SVG + javascript, if you're able to include javascript inside the SVG, which then uses foreignObjects to format things a bit nicer
  • SVG + attributes, which is similar to SVG + javascript but without the javascript. You can BYO your own javascript to draw the tooltips instead.
  • no tooltips whatsoever.

Give me a rundown of the sort of things it can do:

The CLI gives you a rundown of the sorts of things it can do:

C:\util\java> java -jar sql-explainerator-0.0.1-with-dependencies.jar --help
usage: SqlExplaineratorCli [options]
 -h,--help                  This usage text
 -i,--infile <infile>       input file, or '-' for stdin; default = stdin
 -o,--outfile <outfile>     output file, or '-' for stdout; default = stdout
 -l,--layout <layout>       layout format (workbench or explainerator); default = explainerator
 -f,--format <format>       output format (svg or html); default = svg
 -t,--tooltip <tooltip>     tooltip type (none, title, attribute, javascript); default = title
 -j,--jdbc <jdbc>           JDBC connection string
 -u,--username <username>   JDBC username
 -p,--password <password>   JDBC password
 -d,--driver <driver>       JDBC driver class name; default = org.mariadb.jdbc.Driver
 -q,--sql <sql>             SQL to explain
 -c,--css <css>             alternate css file
 -s,--script <script>       alternate javascript file

This command will convert a MySQL JSON execution plan into an SVG diagram.
There are two layout methods: 'workbench' which will try to mimic the diagrams generated from MySQL
Workbench, or 'explainerator', which adds support for inserts, 'having' clauses, and window functions.

The execution plan can be supplied via stdin or --infile (Example 1), or can be retrieved from a
MySQL server (Example 2).

Example 1: To generate the query plan JSON, execute an 'EXPLAIN FORMAT=JSON' statement:

  mysql --user=root --password=abc123 --silent --raw --skip-column-names \
    --execute "EXPLAIN FORMAT=JSON SELECT 1 FROM DUAL" sakila > plan.json

then to generate the SVG diagram, supply this JSON as input to SqlExplaineratorCli:

  SqlExplaineratorCli --infile plan.json --outfile plan.svg
or
  cat plan.json | SqlPlainToImageCli > plan.svg


Example 2: To generate the diagram from an SQL statement, you will need to also supply a JDBC
connection string and any credentials required to connect, e.g.:

  SqlExplaineratorCli --jdbc jdbc:mysql://localhost/sakila --username root --password abc123 \
    --sql "SELECT 1 fROM DUAL" --outfile plan.svg

There's an example of the type of diagram it produces at the top of this post.

Is there anything it can't do ?

Probably. There's quite a few things that MySQL can generate in its plan JSON that it still doesn't process, but I haven't encountered those yet in any real-world queries.

Where it's at

It's up on github, and if you're using maven it's got the artifactId:groupId co-ordinates of com.randomnoun.db:sql-explainerator

Here's some links to it.

The github link has a few more examples, including comparisons between this thing and Workbench.

sql-explainerator
git@github.com:randomnoun/sql-explainerator.git

sql-explainerator
com.randomnoun.db:sql-explainerator

One Comment

Add a Comment

Your email address will not be published. Required fields are marked *