blob: d7d80f4db7f56ddab79aa7f21fda3e5e16298b85 [file] [view]
---
title: "Running Beam SQL in notebooks"
date: 2022-04-28 00:00:01 -0800
categories:
- blog
aliases:
- /blog/2022/04/28/beam-sql-with-notebooks.html
authors:
- ningk
---
<!--
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
## Intro
[Beam SQL](/documentation/dsls/sql/overview/) allows a
Beam user to query PCollections with SQL statements.
[Interactive Beam](https://github.com/apache/beam/tree/master/sdks/python/apache_beam/runners/interactive#interactive-beam)
provides an integration between Apache Beam and
[Jupyter Notebooks](https://docs.jupyter.org/en/latest/) (formerly known as
IPython Notebooks) to make pipeline prototyping and data exploration much faster
and easier.
You can set up your own notebook user interface (for example,
[JupyterLab](https://jupyterlab.readthedocs.io/en/stable/getting_started/installation.html)
or classic [Jupyter Notebooks](https://docs.jupyter.org/en/latest/install.html))
on your own device following their documentations. Alternatively, you can
choose a hosted solution that does everything for you. You are free to select
whichever notebook user interface you prefer. For simplicity, this
post does not go through the notebook environment setup and uses
[Apache Beam Notebooks](https://cloud.google.com/dataflow/docs/guides/interactive-pipeline-development)
that provides a cloud-hosted
[JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) environment and lets
a Beam user iteratively develop pipelines, inspect pipeline graphs, and parse
individual PCollections in a read-eval-print-loop (REPL) workflow.
In this post, you will see how to use `beam_sql`, a notebook
[magic](https://ipython.readthedocs.io/en/stable/interactive/magics.html), to
execute Beam SQL in notebooks and inspect the results.
By the end of the post, it also demonstrates how to use the `beam_sql` magic
with a production environment, such as running it as a one-shot job on
Dataflow. It's optional. To follow those steps, you should have a project in
Google Cloud Platform with
[necessary APIs enabled](https://cloud.google.com/dataflow/docs/guides/interactive-pipeline-development#before_you_begin)
, and you should have enough permissions to create a Google Cloud Storage bucket
(or to use an existing one), query a public Google Cloud BigQuery dataset, and
run Dataflow jobs.
If you choose to use the cloud hosted notebook solution, once you have your
Google Cloud project ready, you will need to create an Apache Beam Notebooks
instance and open the JupyterLab web interface. Please follow the instructions
given at:
https://cloud.google.com/dataflow/docs/guides/interactive-pipeline-development#launching_an_notebooks_instance
## Getting familiar with the environment
### Landing page
After starting your own notebook user interface: for example, if using Apche
Beam Notebooks, after clicking the `OPEN JUPYTERLAB` link, you will land on
the default launcher page of the notebook environment.
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image1.png"
alt="Beam SQL in Notebooks: landing page">
On the left side, there is a file explorer to view examples, tutorials and
assets on the notebook instance. To easily navigate the files, you may
double-click the `00-Start_Here.md` (#1 in the screenshot) file to view detailed
information about the files.
On the right side, it displays the default launcher page of JupyterLab. To
create and open a completely new notebook file and code with a selected version
of Apache Beam, click one of (#2) the items with Apache Beam >=2.34.0 (because
`beam_sql` was introduced in 2.34.0) installed.
### Create/open a notebook
For example, if you clicked the image button with Apache Beam 2.36.0, you would
see an `Untitled.ipynb` file created and opened.
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image2.png"
alt="Beam SQL in Notebooks: create/open a notebook ">
In the file explorer, your new notebook file has been created as
`Untitled.ipynb`.
On the right side, in the opened notebook, there are 4 buttons on top that you
may interact most frequently with:
- #1: insert an empty code block after the selected / highlighted code block
- #2: execute the code in the block that is selected / highlighted
- #3: interrupt code execution if your code execution is stuck
- #4: “Restart the kernel”: clear all states from code executions and start
from fresh
There is a button on the top-right (#5) for you to choose a different Apache
Beam version if needed, so it’s not set in stone.
You can always double-click a file from the file explorer to open it without
creating a new one.
## Beam SQL
### `beam_sql` magic
`beam_sql` is an IPython
[custom magic](https://ipython.readthedocs.io/en/stable/config/custommagics.html).
If you're not familiar with magics, here are some
[built-in examples](https://ipython.readthedocs.io/en/stable/interactive/magics.html).
It's a convenient way to validate your queries locally against known/test data
sources when prototyping a Beam pipeline with SQL, before productionizing it on
remote cluster/services.
The Apache Beam Notebooks environment has preloaded the `beam_sql` magic and
basic `apache-beam` modules so you can directly use them without additional
imports. You can also explicitly load the magic via
`%load_ext apache_beam.runners.interactive.sql.beam_sql_magics` and
`apache-beam` modules if you set up your own notebook elsewhere.
You can type:
```
%beam_sql -h
```
and then execute the code to learn how to use the magic:
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image3.png"
alt="Beam SQL in Notebooks: beam_sql magic help message ">
The selected/highlighted block is called a notebook cell. It mainly has 3
components:
- #1: The execution count. `[1]` indicates this block is the first executed
code. It increases by 1 for each piece of code you execute even if you
re-execute the same piece of code. `[ ]` indicates this block is not
executed.
- #2: The cell input: the code gets executed.
- #3: The cell output: the output of the code execution. Here it contains the
help documentation of the `beam_sql` magic.
### Create a PCollection
There are 3 scenarios for Beam SQL when creating a PCollection:
1. Use Beam SQL to create a PCollection from constant values
```
%%beam_sql -o pcoll
SELECT CAST(1 AS INT) AS id, CAST('foo' AS VARCHAR) AS str, CAST(3.14 AS DOUBLE) AS flt
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image4.png"
alt="Beam SQL in Notebooks: beam_sql creates a PCollection from raw values.">
The `beam_sql` magic creates and outputs a PCollection named `pcoll` with
element_type like `BeamSchema_...(id: int32, str: str, flt: float64)`.
**Note** that you have **not** explicitly created a Beam pipeline. You get a
PCollection because the `beam_sql` magic always implicitly creates a pipeline to
execute your SQL query. To hold the elements with each field's type info, Beam
automatically creates a
[schema](/documentation/programming-guide/#what-is-a-schema)
as the `element_type` for the created PCollection. You will learn more about
schema-aware PCollections later.
2. Use Beam SQL to query a PCollection
You can chain another SQL using the output from a previous SQL (or any
schema-aware PCollection produced by any normal Beam PTransforms) as the input
to produce a new PCollection.
**Note**: if you name the output PCollection, make sure that it’s unique in your
notebook to avoid overwriting a different PCollection.
```
%%beam_sql -o id_pcoll
SELECT id FROM pcoll
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image5.png"
alt="Beam SQL in Notebooks: beam_sql creates a PCollection from another.">
3. Use Beam SQL to join multiple PCollections
You can query multiple PCollections from a single query.
```
%%beam_sql -o str_with_same_id
SELECT id, str FROM pcoll JOIN id_pcoll USING (id)
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image6.png"
alt="Beam SQL in Notebooks: beam_sql creates a PCollection from multiple PCollections.">
Now you have learned how to use the `beam_sql` magic to create PCollections and
inspect their results.
**Tip**: if you accidentally delete some of the notebook cell outputs, you can
always check the content of a PCollection by invoking `ib.show(pcoll_name)` or
`ib.collect(pcoll_name)` where `ib` stands for “Interactive Beam”
([learn more](https://cloud.google.com/dataflow/docs/guides/interactive-pipeline-development#reading_and_visualizing_the_data)).
### Schema-aware PCollections
The `beam_sql` magic provides the flexibility to seamlessly mix SQL and non-SQL
Beam statements to build pipelines and even run them on Dataflow. However, each
PCollection queried by Beam SQL needs to have a
[schema](/documentation/programming-guide/#what-is-a-schema).
For the `beam_sql` magic, it’s recommended to use `typing.NamedTuple` when a
schema is desired. You can go through the below example to learn more details
about schema-aware PCollections.
#### Setup
In the setup of this example, you will:
- Install PyPI package `names` using the built-in `%pip` magic: you will use
the module to generate some random English names as the raw data input.
- Define a schema with `NamedTuple` that has 2 attributes: `id` - an unique
numeric identifier of a person; `name` - a string name of a person.
- Define a pipeline with an `InteractiveRunner` to utilize notebook related
features of Apache Beam.
```python
%pip install names
import names
from typing import NamedTuple
class Person(NamedTuple):
id: int
name: str
p = beam.Pipeline(InteractiveRunner())
```
There is no visible output for the code execution.
#### Create schema-aware PCollections without using SQL
```python
persons = (p
| beam.Create([Person(id=x, name=names.get_full_name()) for x in range(10)]))
ib.show(persons)
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image7.png"
alt="Beam SQL in Notebooks: create a schema-aware PCollection without SQL.">
```python
persons_2 = (p
| beam.Create([Person(id=x, name=names.get_full_name()) for x in range(5, 15)]))
ib.show(persons_2)
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image8.png"
alt="Beam SQL in Notebooks: create another schema-aware PCollection without SQL.">
Now you have 2 PCollections both with the same schema defined by the `Person`
class:
- `persons` contains 10 records for 10 persons with ids ranging from 0 to 9,
- `persons_2` contains another 10 records for 10 persons with ids ranging from
5 to 14.
#### Encode and Decode of schema-aware PCollections
For this example, you still need one more piece of data from the first `pcoll`
that you have created with instructions in this post.
You can use the original `pcoll`. Optionally, if you want to exercise using
coders explicitly with schema-aware PCollections, you can add a Text I/O into
the mix: write the content of `pcoll` into a text file retaining its schema
information, then read the file back into a new schema-aware PCollection called
`pcoll_in_file`, and use the new PCollection to join `persons` and `persons_2`
to find names with the common id in all three of them.
To encode `pcoll` into a file, execute:
```python
coder=beam.coders.registry.get_coder(pcoll.element_type)
pcoll | beam.io.textio.WriteToText('/tmp/pcoll', coder=coder)
pcoll.pipeline.run().wait_until_finish()
!cat /tmp/pcoll*
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image9.png"
alt="Beam SQL in Notebooks: write a schema-aware PCollection into a text file.">
The above code execution writes the PCollection `pcoll` (basically
`{id: 1, str: foo, flt: 3.14}`) into a text file using the coder assigned by
Beam. As you can see, the file content is recorded in a binary non
human-readable format, and that’s normal.
To decode the file content into a new PCollection, execute:
```python
pcoll_in_file = p | beam.io.ReadFromText(
'/tmp/pcoll*', coder=coder).with_output_types(
pcoll.element_type)
ib.show(pcoll_in_file)
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image10.png"
alt="Beam SQL in Notebooks: read a schema-aware PCollection from a text file.">
**Note** you have to use the same coder during encoding and decoding, and
furthermore you may assign the schema explicitly to the new PCollection through
`with_output_types()`.
Reading out the encoded binary content from the text file and decoding it with
the correct coder, the content of `pcoll` is recovered into `pcoll_in_file`. You
can use this technique to save and share your data through any Beam I/O (not
necessarily a text file) with collaborators who work on their own pipelines (not
just in your notebook session or pipelines).
#### Schema in `beam_sql` magic
The `beam_sql` magic automatically registers a `RowCoder` for your `NamedTuple`
schema so that you only need to focus on preparing your data for query without
worrying about coders. To see more verbose details of what the `beam_sql` magic
does behind the scenes, you can use the `-v` option.
For example, you can look for all elements with `id < 5` in `persons` with the
below query and assign the output to `persons_id_lt_5`.
```
%%beam_sql -o persons_id_lt_5 -v
SELECT * FROM persons WHERE id < 5
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image11.png"
alt="Beam SQL in Notebooks: beam_sql registers a schema for a PCollection.">
Since this is the first time running this query, you might see a warning message
about:
>Schema Person has not been registered to use a RowCoder. Automatically
registering it by running:
beam.coders.registry.register_coder(Person, beam.coders.RowCoder)
The `beam_sql` magic helps registering a `RowCoder` for each schema you define
and use whenever it finds one. You can also explicitly run the same code to do
so.
**Note** the output element type is `Person(id: int, name: str)` instead of
`BeamSchema_…` because you have selected all the fields from a single
PCollection of the known type `Person(id: int, name: str)`.
Another example, you can query for all names from `persons` and `persons_2` with
the same ids and assign the output to `persons_with_common_id`:
```
%%beam_sql -o persons_with_common_id -v
SELECT * FROM persons JOIN persons_2 USING (id)
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image12.png"
alt="Beam SQL in Notebooks: beam_sql creates a schema for a query.">
Note the output element type is now some
`BeamSchema_...(id: int64, name: str, name0: str)`. Because you have selected
columns from both PCollections, there is no known schema to hold the result.
Beam automatically creates a schema and differentiates the conflicted field
`name` by suffixing 0 to one of them.
And since `Person` is already previously registered with a `RowCoder`, there is
no more warning about registering it even with the `-v` option.
Additionally, you can do a join with `pcoll_in_file`, `persons` and `persons_2`:
```
%%beam_sql -o entry_with_common_id
SELECT pcoll_in_file.id, persons.name AS name_1, persons_2.name AS name_2
FROM pcoll_in_file JOIN persons ON pcoll_in_file.id = persons.id
JOIN persons_2 ON pcoll_in_file.id = persons_2.id
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image13.png"
alt="Beam SQL in Notebooks: rename fields in a query.">
The schema generated reflects the column renaming you have done in the SQL.
## An Example
You will go through an example to find out the US state with the most COVID
positive cases on a specific day with data provided by the
[covid tracking project](https://covidtracking.com/).
### Get the data
```python
import json
import requests
# The covidtracking project has stopped collecting new data, current data ends on 2021-03-07
json_current='https://api.covidtracking.com/v1/states/current.json'
def get_json_data(url):
with requests.Session() as session:
data = json.loads(session.get(url).text)
return data
current_data = get_json_data(json_current)
current_data[0]
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image14.png"
alt="Beam SQL in Notebooks: preview example data.">
The data is dated as 2021-03-07. It contains many details about COVID cases for
different states in the US. `current_data[0]` is just one of the data points.
You can get rid of most of the columns of the data. For example, just focus on
“date”, “state”, “positive” and “negative”, and then define a schema
`UsCovidData`:
```python
from typing import Optional
class UsCovidData(NamedTuple):
partition_date: str # Remember to str(e['date']).
state: str
positive: int
negative: Optional[int]
```
**Note**:
- `date` is a keyword in (Calcite)SQL, use a different field name such as
`partition_date`;
- `date` from the data is an `int` type, not `str`. Make sure you convert the
data using `str()` or use `date: int`.
- `negative` has missing values and the default is `None`. So instead of
`negative: int`, it should be `negative: Optional[int]`. Or you can convert
`None` into 0 when using the schema.
Then parse the json data into a PCollection with the schema:
```python
p_sql = beam.Pipeline(runner=InteractiveRunner())
covid_data = (p_sql
| 'Create PCollection from json' >> beam.Create(current_data)
| 'Parse' >> beam.Map(
lambda e: UsCovidData(
partition_date=str(e['date']),
state=e['state'],
positive=e['positive'],
negative=e['negative'])).with_output_types(UsCovidData))
ib.show(covid_data)
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image15.png"
alt="Beam SQL in Notebooks: parse example data with a schema.">
### Query
You can now find the biggest positive on the “current day” (2021-03-07).
```
%%beam_sql -o max_positive
SELECT partition_date, MAX(positive) AS positive
FROM covid_data
GROUP BY partition_date
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image16.png"
alt="Beam SQL in Notebooks: find the biggest positive from the data.">
However, this is just the positive number. You cannot observe the state that has
this maximum number nor the negative case number for the state.
To enrich your result, you have to join this data back to the original data set
you have parsed.
```
%%beam_sql -o entry_with_max_positive
SELECT covid_data.partition_date, covid_data.state, covid_data.positive, {fn IFNULL(covid_data.negative, 0)} AS negative
FROM covid_data JOIN max_positive
USING (partition_date, positive)
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image17.png"
alt="Beam SQL in Notebooks: enriched data with biggest positive.">
Now you can see all columns of the data with the maximum positive case on
2021-03-07.
**Note**: to handle missing values of the negative column in the original data,
you can use `{fn IFNULL(covid_data.negative, 0)}` to set null values to 0.
When you're ready to scale up, you can translate the SQLs into a pipeline with
`SqlTransform`s and run your pipeline on a distributed runner like Flink or
Spark. This post demonstrates it by launching a one-shot job on Dataflow from
the notebook with the help of `beam_sql` magic.
### Run on Dataflow
Now that you have a pipeline that parses US COVID data from json to find
positive/negative/state information for the state with the most positive cases
on each day, you can try applying it to all historical daily data and running it
on Dataflow.
The new data source you will use is a public dataset from USAFacts US
Coronavirus Database that contains all historical daily summary of COVID cases
in the US.
The schema of data is very similar to what the covid tracking project website
provides. The fields you will query are: `date`, `state`, `confirmed_cases`, and
`deaths`.
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image18.png"
alt="Beam SQL in Notebooks: schema of cloud data.">
A preview of the data looks like below (you may skip the inspection in BigQuery
and just take a look at the screenshot):
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image19.png"
alt="Beam SQL in Notebooks: preview of cloud data.">
The format of the data is **slightly different** from the json data you parsed
in the previous pipeline because the numbers are grouped by counties instead of
states, thus some additional aggregations need to be done in the SQLs.
If you need a fresh execution, you may click the “Restart the kernel” button on
the top menu.
Full code is as below, on-top of the original pipeline and queries:
- It changes the source from a single-day data to a more complete historical
data;
- It changes the I/O and schema to accommodate the new dataset;
- It changes the SQLs to include more aggregations to accommodate the new
format of the dataset.
**Prepare the data with schema**
```python
from typing import NamedTuple
from typing import Optional
# Public BQ dataset.
table = 'bigquery-public-data:covid19_usafacts.summary'
# Replace with your project.
project = 'YOUR-PROJECT-NAME-HERE'
# Replace with your GCS bucket.
gcs_location = 'gs://YOUR_GCS_BUCKET_HERE'
class UsCovidData(NamedTuple):
partition_date: str
state: str
confirmed_cases: Optional[int]
deaths: Optional[int]
p_on_dataflow = beam.Pipeline(runner=InteractiveRunner())
covid_data = (p_on_dataflow
| 'Read dataset' >> beam.io.ReadFromBigQuery(
project=project, table=table, gcs_location=gcs_location)
| 'Parse' >> beam.Map(
lambda e: UsCovidData(
partition_date=str(e['date']),
state=e['state'],
confirmed_cases=int(e['confirmed_cases']),
deaths=int(e['deaths']))).with_output_types(UsCovidData))
```
**Run on Dataflow**
To run SQL on Dataflow is very simple, you just need to add the option
`-r DataflowRunner`.
```
%%beam_sql -o data_by_state -r DataflowRunner
SELECT partition_date, state, SUM(confirmed_cases) as confirmed_cases, SUM(deaths) as deaths
FROM covid_data
GROUP BY partition_date, state
```
Different from previous `beam_sql` magic executions, you won’t see the result
immediately. Instead, a form like below is printed in the notebook cell output:
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image20.png"
alt="Beam SQL in Notebooks: empty run-on-dataflow form.">
The `beam_sql` magic tries its best to guess your project id and preferred cloud
region. You still have to input additional information necessary to submit a
Dataflow job, such as a GCS bucket to stage the Dataflow job and any additional
Python dependencies the job needs.
For now, ignore the form in the cell output, because you still need 2 more SQLs
to: 1) find the maximum confirmed cases on each day; 2) join the maximum case
data with the full data_by_state. The `beam_sql` magic allows you to chain SQLs,
so chain 2 more by executing:
```
%%beam_sql -o max_cases -r DataflowRunner
SELECT partition_date, MAX(confirmed_cases) as confirmed_cases
FROM data_by_state
GROUP BY partition_date
```
And
```
%%beam_sql -o data_with_max_cases -r DataflowRunner
SELECT data_by_state.partition_date, data_by_state.state, data_by_state.confirmed_cases, data_by_state.deaths
FROM data_by_state JOIN max_cases
USING (partition_date, confirmed_cases)
```
By default, when running `beam_sql` on Dataflow, the output PCollection will be
written to a text file on GCS. The “write” is automatically provided by
`beam_sql` and mainly for your inspection of the output data for this one-shot
Dataflow job. It’s lightweight and does not encode elements for further
development. To save the output and share it with others, you can add more Beam
I/Os into the mix.
For example, you can appropriately encode elements into text files using the
technique described in the above schema-aware PCollections example.
```python
from apache_beam.options.pipeline_options import GoogleCloudOptions
coder = beam.coders.registry.get_coder(data_with_max_cases.element_type)
max_data_file = gcs_location + '/encoded_max_data'
data_with_max_cases | beam.io.textio.WriteToText(max_data_file, coder=coder)
```
Furthermore, you can create a new BQ dataset in your own project to store the
processed data.
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image21.png"
alt="Beam SQL in Notebooks: create a new BQ dataset.">
You have to select the same data location as the public BigQuery data you are
reading. In this case, “us (multiple regions in United States)”.
Once you finish creating an empty dataset, you can execute below:
```python
output_table=f'{project}:covid_data.max_analysis'
bq_schema = {
'fields': [
{'name': 'partition_date', 'type': 'STRING'},
{'name': 'state', 'type': 'STRING'},
{'name': 'confirmed_cases', 'type': 'INTEGER'},
{'name': 'deaths', 'type': 'INTEGER'}]}
(data_with_max_cases
| 'To json-like' >> beam.Map(lambda x: {
'partition_date': x.partition_date,
'state': x.state,
'confirmed_cases': x.confirmed_cases,
'deaths': x.deaths})
| beam.io.WriteToBigQuery(
table=output_table,
schema=bq_schema,
method='STREAMING_INSERTS',
custom_gcs_temp_location=gcs_location))
```
Now back in the form of the last SQL cell output, you may fill in necessary
information to run the pipeline on Dataflow. An example input looks like below:
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image22.png"
alt="Beam SQL in Notebooks: fill in the run-on-Dataflow form.">
Because this pipeline doesn’t use any additional Python dependency, “Additional
Packages” is left empty. In the previous example where you have installed a
package called `names`, to run that pipeline on Dataflow, you have to put
`names` in this field.
Once you finish updating your inputs, you can click the `Show Options` button to
view what pipeline options have been configured based on your inputs. A variable
`options_[YOUR_OUTPUT_PCOLL_NAME]` is generated, and you can supply more
pipeline options to it if the form is not enough for your execution.
Once you are ready to submit the Dataflow job, click the `Run on Dataflow`
button. It tells you where the default output would be written, and after a
while, a line with:
>Click here for the details of your Dataflow job.
would be displayed. You can click on the hyperlink to go to your Dataflow job
page. (Optionally, you can ignore the form and continue development to extend
your pipeline. Once you are satisfied with the state of your pipeline, you can
come back to the form and submit the job to Dataflow.)
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image23.png"
alt="Beam SQL in Notebooks: a Dataflow job graph.">
As you can see, each transform name of the generated Dataflow job is prefixed
with a string `[number]: `. This is to distinguish re-executed codes in
notebooks because Beam requires each transform to have a distinct name. Under
the hood, the `beam_sql` magic also stages your schema information to Dataflow,
so you might see transforms named as `schema_loaded_beam_sql_…`. This is because
the `NamedTuple` defined in the notebook is likely in the `__main__` scope and
Dataflow is not aware of them at all. To minimize user intervention and avoid
pickling the whole main session (and it’s infeasible to pickle the main session
when it contains unpickle-able attributes), the `beam_sql` magic optimizes the
staging process by serializing your schemas, staging them to Dataflow, and then
deserialize/load them for job execution.
Once the job succeeds, the result of the output PCollection would be written to
places instructed by your I/O transforms. **Note**: running `beam_sql` on
Dataflow generates a one-shot job and it’s not interactive.
A simple inspection of the data from the default output location:
```
!gsutil cat 'gs://ningk-so-test/bq/staging/data_with_max_cases*'
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image24.png"
alt="Beam SQL in Notebooks: inspect the default output file.">
The text file with encoded binary data written by your `WriteToText`:
```
!gsutil cat 'gs://ningk-so-test/bq/encoded_max_data*'
```
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image25.png"
alt="Beam SQL in Notebooks: inspect the user-defined output file.">
The table `YOUR-PROJECT:covid_data.max_analysis` created by your
`WriteToBigQuery`:
<img class="center-block"
src="/images/blog/beam-sql-notebooks/image26.png"
alt="Beam SQL in Notebooks: inspect the output BQ dataset.">
### Run on other OSS runners directly with the `beam_sql` magic
On the day this blog is posted, the `beam_sql` magic only supports DirectRunner
(interactive) and DataflowRunner (one-shot). It's a simple wrapper on top of
the `SqlTransform` with interactive input widgets implemented by
[ipywidgets](https://ipywidgets.readthedocs.io/en/stable/). You can implement
your own runner support or utilities by following the
[instructions](https://lists.apache.org/thread/psrx1xhbyjcqbhxx6trf5nvh66c6pk3y).
Additionally, support for other OSS runners are WIP, for example,
[support using FlinkRunner with the `beam_sql` magic](https://issues.apache.org/jira/browse/BEAM-14373).
## Conclusions
The `beam_sql` magic and Apache Beam Notebooks combined is a convenient tool for
you to learn Beam SQL and mix Beam SQL into prototyping and productionizing (
e.g., to Dataflow) your Beam pipelines with minimum setups.
For more details about the Beam SQL syntax, check out the Beam Calcite SQL
[compatibility](/documentation/dsls/sql/calcite/overview/)
and the Apache Calcite SQL
[syntax](https://calcite.apache.org/docs/reference.html).