{ "metadata": { }, "nbformat": 4, "nbformat_minor": 5, "cells": [ { "id": "metadata", "cell_type": "markdown", "source": "
\n\n# Introduction to SQL\n\nby [The Carpentries](https://training.galaxyproject.org/hall-of-fame/carpentries/), [Helena Rasche](https://training.galaxyproject.org/hall-of-fame/hexylena/), [Donny Vrins](https://training.galaxyproject.org/hall-of-fame/dirowa/), [Bazante Sanders](https://training.galaxyproject.org/hall-of-fame/bazante1/), [Avans Hogeschool](https://training.galaxyproject.org/hall-of-fame/avans-atgm/)\n\nCC-BY licensed content from the [Galaxy Training Network](https://training.galaxyproject.org/)\n\n**Objectives**\n\n- How can I get data from a database?\n- How can I sort a query's results?\n- How can I remove duplicate values from a query's results?\n- How can I select subsets of data?\n- How can I calculate new values on the fly?\n- How do databases represent missing information?\n- What special handling does missing information require?\n\n**Objectives**\n\n- Explain the difference between a table, a record, and a field.\n- Explain the difference between a database and a database manager.\n- Write a query to select all values for specific fields from a single table.\n- Write queries that display results in a particular order.\n- Write queries that eliminate duplicate values from data.\n- Write queries that select records that satisfy user-specified conditions.\n- Explain the order in which the clauses in a query are executed.\n- Write queries that calculate new values for each selected record.\n- Explain how databases represent missing information.\n- Explain the three-valued logic databases use when manipulating missing information.\n- Write queries that handle missing information correctly.\n\n**Time Estimation: 3H**\n
\n", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-0", "source": "
\n
Comment
\n

This tutorial is significantly based on the Carpentries Databases and SQL lesson, which is licensed CC-BY 4.0.

\n

Abigail Cabunoc and Sheldon McKay (eds): “Software Carpentry: Using Databases and SQL.” Version 2017.08, August 2017,\ngithub.com/swcarpentry/sql-novice-survey, https://doi.org/10.5281/zenodo.838776

\n

Adaptations have been made to make this work better in a GTN/Galaxy environment.

\n
\n
\n
Agenda
\n

In this tutorial, we will cover:

\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-1", "source": [ "# This preamble sets up the sql \"magic\" for jupyter. Use %%sql in your cells to write sql!\n", "!python3 -m pip install ipython-sql sqlalchemy\n", "!wget -c http://swcarpentry.github.io/sql-novice-survey/files/survey.db\n", "import sqlalchemy\n", "engine = sqlalchemy.create_engine(\"sqlite:///survey.db\")\n", "%load_ext sql\n", "%sql sqlite:///survey.db\n", "%config SqlMagic.displaycon=False" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-2", "source": "

Selecting Data

\n

A relational database\nis a way to store and manipulate information.\nDatabases are arranged as table.\nEach table has columns (also known as fields) that describe the data,\nand rows (also known as records) which contain the data.

\n

When we are using a spreadsheet,\nwe put formulas into cells to calculate new values based on old ones.\nWhen we are using a database,\nwe send commands\n(usually called queries)\nto a database manager:\na program that manipulates the database for us.\nThe database manager does whatever lookups and calculations the query specifies,\nreturning the results in a tabular form\nthat we can then use as a starting point for further queries.

\n

Queries are written in a language called Structured Query Language (SQL),\nSQL provides hundreds of different ways to analyze and recombine data.\nWe will only look at a handful of queries,\nbut that handful accounts for most of what scientists do.

\n
\n
\n

Many database managers — Oracle,\nIBM DB2, PostgreSQL, MySQL, Microsoft Access, and SQLite — understand\nSQL but each stores data in a different way,\nso a database created with one cannot be used directly by another.\nHowever, every database manager\ncan import and export data in a variety of formats like .csv, SQL,\nso it is possible to move information from one to another.

\n
\n

Before we get into using SQL to select the data, let’s take a look at the tables of the database we will use in our examples:

\n

Person: people who took readings.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
idpersonalfamily
dyerWilliamDyer
pbFrankPabodie
lakeAndersonLake
roeValentinaRoerich
danforthFrankDanforth
\n

Site: locations where readings were taken.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
namelatlong
DR-1-49.85-128.57
DR-3-47.15-126.72
MSK-4-48.87-123.4
\n

Visited: when readings were taken at specific sites.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
idsitedated
619DR-11927-02-08
622DR-11927-02-10
734DR-31930-01-07
735DR-31930-01-12
751DR-31930-02-26
752DR-3None
837MSK-41932-01-14
844DR-11932-03-22
\n

Survey: the actual readings. The field quant is short for quantitative and indicates what is being measured. Values are rad, sal, and temp referring to ‘radiation’, ‘salinity’ and ‘temperature’, respectively.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
takenpersonquantreading
619dyerrad9.82
619dyersal0.13
622dyerrad7.8
622dyersal0.09
734pbrad8.41
734lakesal0.05
734pbtemp-21.5
735pbrad7.22
735Nonesal0.06
735Nonetemp-26.0
751pbrad4.35
751pbtemp-18.5
751lakesal0.1
752lakerad2.19
752lakesal0.09
752laketemp-16.0
752roesal41.6
837lakerad1.46
837lakesal0.21
837roesal22.5
844roerad11.25
\n

Notice that three entries — one in the Visited table,\nand two in the Survey table — don’t contain any actual\ndata, but instead have a special None entry:\nwe’ll return to these missing values.

\n

For now,\nlet’s write an SQL query that displays scientists’ names.\nWe do this using the SQL command SELECT,\ngiving it the names of the columns we want and the table we want them from.\nOur query and its output look like this:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-3", "source": [ "%%sql\n", "SELECT family, personal FROM Person;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-4", "source": "

The semicolon at the end of the query\ntells the database manager that the query is complete and ready to run.\nWe have written our commands in upper case and the names for the table and columns\nin lower case,\nbut we don’t have to:\nas the example below shows,\nSQL is case insensitive.

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-5", "source": [ "%%sql\n", "SeLeCt FaMiLy, PeRsOnAl FrOm PeRsOn;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-6", "source": "

You can use SQL’s case insensitivity to your advantage. For instance,\nsome people choose to write SQL keywords (such as SELECT and FROM)\nin capital letters and field and table names in lower\ncase. This can make it easier to locate parts of an SQL statement. For\ninstance, you can scan the statement, quickly locate the prominent\nFROM keyword and know the table name follows. Whatever casing\nconvention you choose, please be consistent: complex queries are hard\nenough to read without the extra cognitive load of random\ncapitalization. One convention is to use UPPER CASE for SQL\nstatements, to distinguish them from tables and column names. This is\nthe convention that we will use for this lesson.

\n
\n
Question: Is a personal and family name column a good design?
\n

If you were tasked with designing a database to store this same data, is storing the name data in\nthis way the best way to do it? Why or why not?

\n

Can you think of any names that would be difficult to enter in such a schema?

\n
👁 View solution\n
\n

No, it is generally not. There are a lot of falsehoods that programmers believe about names.\nThe situation is much more complex as you can read in that article, but names vary wildly and\ngenerally placing constraints on how names are entered is only likely to frustrate you or your\nusers later on when they need to enter data into that database.

\n

In general you should consider using a single text field for the name and allowing users to\nspecify them as whatever they like (if it is a system with registration), or asking what they\nwish to be recorded (if you are doing this sort of data collection).

\n

If you are doing scientific research, you might know that names are generally very poor\nidentifiers of a single human, and in that case consider recording their\nORCiD which will help you reference that individual when you are\npublishing later.

\n

This is also a good time to consider what data you really need to collect. If you are working\nin the EU under GDPR, do you really need their full legal name? Is that necessary? Do you have a\nplan for ensuring that data is correct when publishing, if any part of their name has changed\nsince?

\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-7", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-8", "source": "

While we are on the topic of SQL’s syntax, one aspect of SQL’s syntax\nthat can frustrate novices and experts alike is forgetting to finish a\ncommand with ; (semicolon). When you press enter for a command\nwithout adding the ; to the end, it can look something like this:

\n
SELECT id FROM Person\n...>\n...>\n
\n

This is SQL’s prompt, where it is waiting for additional commands or\nfor a ; to let SQL know to finish. This is easy to fix! Just type\n; and press enter!

\n

Now, going back to our query,\nit’s important to understand that\nthe rows and columns in a database table aren’t actually stored in any particular order.\nThey will always be displayed in some order,\nbut we can control that in various ways.\nFor example,\nwe could swap the columns in the output by writing our query as:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-9", "source": [ "%%sql\n", "SELECT personal, family FROM Person;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-10", "source": "

or even repeat columns:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-11", "source": [ "%%sql\n", "SELECT id, id, id FROM Person;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-12", "source": "

As a shortcut,\nwe can select all of the columns in a table using *:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-13", "source": [ "%%sql\n", "SELECT * FROM Person;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-14", "source": "
\n
Question: Selecting Site Names
\n

Write a query that selects only the name column from the Site table.

\n
👁 View solution\n
\n
SELECT name FROM Site;\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
name
DR-1
DR-3
MSK-4
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-15", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-16", "source": "
\n
Question: Query Style
\n

Many people format queries as:

\n
SELECT personal, family FROM person;\n
\n

or as:

\n
select Personal, Family from PERSON;\n
\n

What style do you find easiest to read, and why?

\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-17", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-18", "source": "

Sorting and Removing Duplicates

\n

In beginning our examination of the Antarctic data, we want to know:

\n\n

To determine which measurements were taken at each site,\nwe can examine the Survey table.\nData is often redundant,\nso queries often return redundant information.\nFor example,\nif we select the quantities that have been measured\nfrom the Survey table,\nwe get this:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-19", "source": [ "%%sql\n", "SELECT quant FROM Survey;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-20", "source": "

This result makes it difficult to see all of the different types of\nquant in the Survey table. We can eliminate the redundant output to\nmake the result more readable by adding the DISTINCT keyword to our\nquery:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-21", "source": [ "%%sql\n", "SELECT DISTINCT quant FROM Survey;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-22", "source": "

If we want to determine which visit (stored in the taken column)\nhave which quant measurement,\nwe can use the DISTINCT keyword on multiple columns.\nIf we select more than one column,\ndistinct sets of values are returned\n(in this case pairs, because we are selecting two columns):

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-23", "source": [ "%%sql\n", "SELECT DISTINCT taken, quant FROM Survey;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-24", "source": "

Notice in both cases that duplicates are removed\neven if the rows they come from didn’t appear to be adjacent in the database table.

\n

Our next task is to identify the scientists on the expedition by looking at the Person table.\nAs we mentioned earlier,\ndatabase records are not stored in any particular order.\nThis means that query results aren’t necessarily sorted,\nand even if they are,\nwe often want to sort them in a different way,\ne.g., by their identifier instead of by their personal name.\nWe can do this in SQL by adding an ORDER BY clause to our query:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-25", "source": [ "%%sql\n", "SELECT * FROM Person ORDER BY id;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-26", "source": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
idpersonalfamily
danfortFrankDanforth
dyerWilliamDyer
lakeAndersonLake
pbFrankPabodie
roeValentinaRoerich
\n

By default, when we use ORDER BY,\nresults are sorted in ascending order of the column we specify\n(i.e.,\nfrom least to greatest).

\n

We can sort in the opposite order using DESC (for “descending”):

\n
\n
\n

While it may look that the records are consistent every time we ask for them in this lesson, that is because no one has changed or modified any of the data so far. Remember to use ORDER BY if you want the rows returned to have any sort of consistent or predictable order.

\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-27", "source": [ "%%sql\n", "SELECT * FROM person ORDER BY id DESC;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-28", "source": "

(And if we want to make it clear that we’re sorting in ascending order,\nwe can use ASC instead of DESC.)

\n

In order to look at which scientist measured quantities during each visit,\nwe can look again at the Survey table.\nWe can also sort on several fields at once.\nFor example,\nthis query sorts results first in ascending order by taken,\nand then in descending order by person\nwithin each group of equal taken values:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-29", "source": [ "%%sql\n", "SELECT taken, person, quant FROM Survey ORDER BY taken ASC, person DESC;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-30", "source": "

This query gives us a good idea of which scientist was involved in which visit,\nand what measurements they performed during the visit.

\n

Looking at the table, it seems like some scientists specialized in\ncertain kinds of measurements. We can examine which scientists\nperformed which measurements by selecting the appropriate columns and\nremoving duplicates.

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-31", "source": [ "%%sql\n", "SELECT DISTINCT quant, person FROM Survey ORDER BY quant ASC;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-32", "source": "
\n
Question: Finding Distinct Dates
\n

Write a query that selects distinct dates from the Visited table.

\n
👁 View solution\n
\n
SELECT DISTINCT dated FROM Visited;\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
dated
1927-02-08
1927-02-10
1930-01-07
1930-01-12
1930-02-26
 
1932-01-14
1932-03-22
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-33", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-34", "source": "
\n
Question: Displaying Full Names
\n

Write a query that displays the full names of the scientists in the Person table,\nordered by family name.

\n
👁 View solution\n
\n
SELECT personal, family FROM Person ORDER BY family ASC;\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
personalfamily
FrankDanforth
WilliamDyer
AndersonLake
FrankPabodie
ValentinaRoerich
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-35", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-36", "source": "
\n
\n

If you are someone with a name which falls at the end of the alphabet, you’ve likely been\npenalised for this your entire life. Alphabetically sorting names should always be looked at\ncritically and through a lens to whether you are fairly reflecting everyone’s contributions,\nrather than just the default sort order.

\n

There are many options, either by some metric of contribution that everyone could agree on, or\nbetter, consider random sorting, like the GTN uses with our Hall of Fame\npage where we intentionally order randomly to tell contributors that no one persons\ncontributions matter more than anothers.

\n
\n

The evidence provided in a variety of studies leaves no doubt that an\nalphabetical author ordering norm disadvantages researchers with\nlast names toward the end of the alphabet. There is furthermore con-\nvincing evidence that researchers are aware of this and that they\nreact strategically to such alphabetical discrimination, for example\nwith their choices of who to collaborate with. See Weber 2018 for more.

\n
\n
\n
\n
\n

When you are sorting things in SQL, you need to be aware of something called collation which can\naffect your results if you have values that are not the letters A-Z. Collating is the process of\nsorting values, and this affects many human languages when storing data in a database.

\n

Here is a Dutch example. In the old days their alphabet contained a ÿ which was later replaced\nwith ij, a digraph of two characters squished together. This is commonly rendered as ij\nhowever, two separate characters, due to the internet and widespread use of keyboards featuring\nmainly ascii characters. However, it is still the 25th letter of their alphabet.

\n
sqlite> create table nl(value text);\nsqlite> insert into nl values ('appel'), ('beer'), ('index'), ('ijs'), ('jammer'), ('winkel'), ('zon');\nsqlite> select * from nl order by value;\nappel\nbeer\nindex\nijs\njammer\nwinkel\nzon\n
\n

Find a dutch friend and ask them if this is the correct order for this list. Unfortunately it\nisn’t. Even though it is ij as two separate characters, it should be sorted as if it was ij or\nÿ, before z. Like so: appel, beer, index, jammer, winkel, ijs, zon

\n

While there is not much you can do about it now (you’re just beginning!) it is something you\nshould be aware of. When you later need to know about this, you will find the term ‘collation’\nuseful, and you’ll find the procedure is different for every database engine.

\n
\n

Filtering

\n

One of the most powerful features of a database is\nthe ability to filter data,\ni.e.,\nto select only those records that match certain criteria.\nFor example,\nsuppose we want to see when a particular site was visited.\nWe can select these records from the Visited table\nby using a WHERE clause in our query:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-37", "source": [ "%%sql\n", "SELECT * FROM Visited WHERE site = 'DR-1';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-38", "source": "

The database manager executes this query in two stages.\nFirst,\nit checks at each row in the Visited table\nto see which ones satisfy the WHERE.\nIt then uses the column names following the SELECT keyword\nto determine which columns to display.

\n

This processing order means that\nwe can filter records using WHERE\nbased on values in columns that aren’t then displayed:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-39", "source": [ "%%sql\n", "SELECT id FROM Visited WHERE site = 'DR-1';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-40", "source": "

![SQL Filtering in Action]`(../../images/carpentries-sql/sql-filter.svg)

\n

We can use many other Boolean operators to filter our data.\nFor example,\nwe can ask for all information from the DR-1 site collected before 1930:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-41", "source": [ "%%sql\n", "SELECT * FROM Visited WHERE site = 'DR-1' AND dated < '1930-01-01';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-42", "source": "
\n
\n

Most database managers have a special data type for dates.\nIn fact, many have two:\none for dates,\nsuch as “May 31, 1971”,\nand one for durations,\nsuch as “31 days”.\nSQLite doesn’t:\ninstead,\nit stores dates as either text\n(in the ISO-8601 standard format “YYYY-MM-DD HH:MM:SS.SSSS”),\nreal numbers\n(Julian days, the number of days since November 24, 4714 BCE),\nor integers\n(Unix time, the number of seconds since midnight, January 1, 1970).\nIf this sounds complicated,\nit is,\nbut not nearly as complicated as figuring out\nhistorical dates in Sweden.

\n
\n
\n
\n

Storing the year as the last two digits causes problems in databases, and is part of what caused\nY2K. Be sure to use the databases’ built in\nformat for storing dates, if it is available as that will generally avoid any major issues.

\n

Similarly there is a “Year 2038 problem”,\nas the timestamps mentioned above that count seconds since Jan 1, 1970 were running out of space\non 32-bit machines. Many systems have since migrated to work around this with 64-bit timestamps.

\n
\n

If we want to find out what measurements were taken by either Lake or Roerich,\nwe can combine the tests on their names using OR:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-43", "source": [ "%%sql\n", "SELECT * FROM Survey WHERE person = 'lake' OR person = 'roe';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-44", "source": "

Alternatively,\nwe can use IN to see if a value is in a specific set:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-45", "source": [ "%%sql\n", "SELECT * FROM Survey WHERE person IN ('lake', 'roe');" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-46", "source": "

We can combine AND with OR,\nbut we need to be careful about which operator is executed first.\nIf we don’t use parentheses,\nwe get this:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-47", "source": [ "%%sql\n", "SELECT * FROM Survey WHERE quant = 'sal' AND person = 'lake' OR person = 'roe';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-48", "source": "

which is salinity measurements by Lake,\nand any measurement by Roerich.\nWe probably want this instead:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-49", "source": [ "%%sql\n", "SELECT * FROM Survey WHERE quant = 'sal' AND (person = 'lake' OR person = 'roe');" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-50", "source": "

We can also filter by partial matches. For example, if we want to\nknow something just about the site names beginning with “DR” we can\nuse the LIKE keyword. The percent symbol acts as a\nwildcard, matching any characters in that\nplace. It can be used at the beginning, middle, or end of the string\nSee this page on wildcards for more information:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-51", "source": [ "%%sql\n", "SELECT * FROM Visited WHERE site LIKE 'DR%';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-52", "source": "

Finally,\nwe can use DISTINCT with WHERE\nto give a second level of filtering:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-53", "source": [ "%%sql\n", "SELECT DISTINCT person, quant FROM Survey WHERE person = 'lake' OR person = 'roe';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-54", "source": "

But remember:\nDISTINCT is applied to the values displayed in the chosen columns,\nnot to the entire rows as they are being processed.

\n
\n
\n

What we have just done is how most people “grow” their SQL queries.\nWe started with something simple that did part of what we wanted,\nthen added more clauses one by one,\ntesting their effects as we went.\nThis is a good strategy — in fact,\nfor complex queries it’s often the only strategy — but\nit depends on quick turnaround,\nand on us recognizing the right answer when we get it.

\n

The best way to achieve quick turnaround is often\nto put a subset of data in a temporary database\nand run our queries against that,\nor to fill a small database with synthesized records.\nFor example,\ninstead of trying our queries against an actual database of 20 million Australians,\nwe could run it against a sample of ten thousand,\nor write a small program to generate ten thousand random (but plausible) records\nand use that.

\n
\n
\n
Question: Fix This Query
\n

Suppose we want to select all sites that lie within 48 degrees of the equator.\nOur first query is:

\n
SELECT * FROM Site WHERE (lat > -48) OR (lat < 48);\n
\n

Explain why this is wrong,\nand rewrite the query so that it is correct.

\n
👁 View solution\n
\n

Because we used OR, a site on the South Pole for example will still meet\nthe second criteria and thus be included. Instead, we want to restrict this\nto sites that meet both criteria:

\n
SELECT * FROM Site WHERE (lat > -48) AND (lat < 48);\n
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-55", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-56", "source": "
\n
Question: Finding Outliers
\n

Normalized salinity readings are supposed to be between 0.0 and 1.0.\nWrite a query that selects all records from Survey\nwith salinity values outside this range.

\n
👁 View solution\n
\n
SELECT * FROM Survey WHERE quant = 'sal' AND ((reading > 1.0) OR (reading < 0.0));\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
takenpersonquantreading
752roesal41.6
837roesal22.5
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-57", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-58", "source": "
\n
Question: Matching Patterns
\n

Which of these expressions are true?

\n
    \n
  1. 'a' LIKE 'a'
  2. \n
  3. 'a' LIKE '%a'
  4. \n
  5. 'beta' LIKE '%a'
  6. \n
  7. 'alpha' LIKE 'a%%'
  8. \n
  9. 'alpha' LIKE 'a%p%'
  10. \n
\n
👁 View solution\n
\n
    \n
  1. True because these are the same character.
  2. \n
  3. True because the wildcard can match zero or more characters.
  4. \n
  5. True because the % matches bet and the a matches the a.
  6. \n
  7. True because the first wildcard matches lpha and the second wildcard matches zero characters (or vice versa).
  8. \n
  9. True because the first wildcard matches l and the second wildcard matches ha.
  10. \n
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-59", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-60", "source": "
\n
\n

But what about if you don’t care about if it’s ALPHA or alpha in the database, and you are\nusing a language that has a notion of case (unlike e.g. Chinese, Japenese)?

\n

Then you can use the ILIKE operator for ‘case Insensitive LIKE’.\nfor example the following are true:

\n\n
\n

Calculating New Values

\n

After carefully re-reading the expedition logs,\nwe realize that the radiation measurements they report\nmay need to be corrected upward by 5%.\nRather than modifying the stored data,\nwe can do this calculation on the fly\nas part of our query:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-61", "source": [ "%%sql\n", "SELECT 1.05 * reading FROM Survey WHERE quant = 'rad';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-62", "source": "

When we run the query,\nthe expression 1.05 * reading is evaluated for each row.\nExpressions can use any of the fields,\nall of usual arithmetic operators,\nand a variety of common functions.\n(Exactly which ones depends on which database manager is being used.)\nFor example,\nwe can convert temperature readings from Fahrenheit to Celsius\nand round to two decimal places:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-63", "source": [ "%%sql\n", "SELECT taken, round(5 * (reading - 32) / 9, 2) FROM Survey WHERE quant = 'temp';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-64", "source": "

As you can see from this example, though, the string describing our\nnew field (generated from the equation) can become quite unwieldy. SQL\nallows us to rename our fields, any field for that matter, whether it\nwas calculated or one of the existing fields in our database, for\nsuccinctness and clarity. For example, we could write the previous\nquery as:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-65", "source": [ "%%sql\n", "SELECT taken, round(5 * (reading - 32) / 9, 2) as Celsius FROM Survey WHERE quant = 'temp';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-66", "source": "

We can also combine values from different fields,\nfor example by using the string concatenation operator ||:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-67", "source": [ "%%sql\n", "SELECT personal || ' ' || family FROM Person;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-68", "source": "

But of course that can also be solved by simply having a single name field which avoids other\nissues.

\n
\n
Question: Fixing Salinity Readings
\n

After further reading,\nwe realize that Valentina Roerich\nwas reporting salinity as percentages.\nWrite a query that returns all of her salinity measurements\nfrom the Survey table\nwith the values divided by 100.

\n
👁 View solution\n
\n
SELECT taken, reading / 100 FROM Survey WHERE person = 'roe' AND quant = 'sal';\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
takenreading / 100
7520.416
8370.225
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-69", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-70", "source": "
\n
Question: Unions
\n

The UNION operator combines the results of two queries:

\n
SELECT * FROM Person WHERE id = 'dyer' UNION SELECT * FROM Person WHERE id = 'roe';\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
idpersonalfamily
dyerWilliamDyer
roeValentinaRoerich
\n

The UNION ALL command is equivalent to the UNION operator,\nexcept that UNION ALL will select all values.\nThe difference is that UNION ALL will not eliminate duplicate rows.\nInstead, UNION ALL pulls all rows from the query\nspecifics and combines them into a table.\nThe UNION command does a SELECT DISTINCT on the results set.\nIf all the records to be returned are unique from your union,\nuse UNION ALL instead, it gives faster results since it skips the DISTINCT step.\nFor this section, we shall use UNION.

\n

Use UNION to create a consolidated list of salinity measurements\nin which Valentina Roerich’s, and only Valentina’s,\nhave been corrected as described in the previous challenge.\nThe output should be something like:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
takenreading
6190.13
6220.09
7340.05
7510.1
7520.09
7520.416
8370.21
8370.225
\n
👁 View solution\n
\n
SELECT taken, reading FROM Survey WHERE person != 'roe' AND quant = 'sal' UNION SELECT taken, reading / 100 FROM Survey WHERE person = 'roe' AND quant = 'sal' ORDER BY taken ASC;\n
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-71", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-72", "source": "
\n
Question: Selecting Major Site Identifiers
\n

The site identifiers in the Visited table have two parts\nseparated by a ‘-‘:

\n
SELECT DISTINCT site FROM Visited;\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
site
DR-1
DR-3
MSK-4
\n

Some major site identifiers (i.e. the letter codes) are two letters long and some are three.\nThe “in string” function instr(X, Y)\nreturns the 1-based index of the first occurrence of string Y in string X,\nor 0 if Y does not exist in X.\nThe substring function substr(X, I, [L])\nreturns the substring of X starting at index I, with an optional length L.\nUse these two functions to produce a list of unique major site identifiers.\n(For this data,\nthe list should contain only “DR” and “MSK”).

\n
👁 View solution\n
\n
SELECT DISTINCT substr(site, 1, instr(site, '-') - 1) AS MajorSite FROM Visited;\n
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-73", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-74", "source": "

Missing Data

\n

Real-world data is never complete — there are always holes.\nDatabases represent these holes using a special value called null.\nnull is not zero, False, or the empty string;\nit is a one-of-a-kind value that means “nothing here”.\nDealing with null requires a few special tricks\nand some careful thinking.

\n

By default, the Python SQL interface does not display NULL values in its output, instead it shows None.

\n

To start,\nlet’s have a look at the Visited table.\nThere are eight records,\nbut #752 doesn’t have a date — or rather,\nits date is null:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-75", "source": [ "%%sql\n", "SELECT * FROM Visited;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-76", "source": "

Null doesn’t behave like other values.\nIf we select the records that come before 1930:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-77", "source": [ "%%sql\n", "SELECT * FROM Visited WHERE dated < '1930-01-01';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-78", "source": "

we get two results,\nand if we select the ones that come during or after 1930:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-79", "source": [ "%%sql\n", "SELECT * FROM Visited WHERE dated >= '1930-01-01';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-80", "source": "

we get five,\nbut record #752 isn’t in either set of results.\nThe reason is that\nnull<'1930-01-01'\nis neither true nor false:\nnull means, “We don’t know,”\nand if we don’t know the value on the left side of a comparison,\nwe don’t know whether the comparison is true or false.\nSince databases represent “don’t know” as null,\nthe value of null<'1930-01-01'\nis actually null.\nnull>='1930-01-01' is also null\nbecause we can’t answer to that question either.\nAnd since the only records kept by a WHERE\nare those for which the test is true,\nrecord #752 isn’t included in either set of results.

\n

Comparisons aren’t the only operations that behave this way with nulls.\n1+null is null,\n5*null is null,\nlog(null) is null,\nand so on.\nIn particular,\ncomparing things to null with = and != produces null:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-81", "source": [ "%%sql\n", "SELECT * FROM Visited WHERE dated = NULL;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-82", "source": "

produces no output, and neither does:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-83", "source": [ "%%sql\n", "SELECT * FROM Visited WHERE dated != NULL;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-84", "source": "

To check whether a value is null or not,\nwe must use a special test IS NULL:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-85", "source": [ "%%sql\n", "SELECT * FROM Visited WHERE dated IS NULL;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-86", "source": "

or its inverse IS NOT NULL:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-87", "source": [ "%%sql\n", "SELECT * FROM Visited WHERE dated IS NOT NULL;" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-88", "source": "

Null values can cause headaches wherever they appear.\nFor example,\nsuppose we want to find all the salinity measurements\nthat weren’t taken by Lake.\nIt’s natural to write the query like this:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-89", "source": [ "%%sql\n", "SELECT * FROM Survey WHERE quant = 'sal' AND person != 'lake';" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-90", "source": "

but this query filters omits the records\nwhere we don’t know who took the measurement.\nOnce again,\nthe reason is that when person is null,\nthe != comparison produces null,\nso the record isn’t kept in our results.\nIf we want to keep these records\nwe need to add an explicit check:

\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-91", "source": [ "%%sql\n", "SELECT * FROM Survey WHERE quant = 'sal' AND (person != 'lake' OR person IS NULL);" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-92", "source": "

We still have to decide whether this is the right thing to do or not.\nIf we want to be absolutely sure that\nwe aren’t including any measurements by Lake in our results,\nwe need to exclude all the records for which we don’t know who did the work.

\n

In contrast to arithmetic or Boolean operators, aggregation functions\nthat combine multiple values, such as min, max or avg, ignore\nnull values. In the majority of cases, this is a desirable output:\nfor example, unknown values are thus not affecting our data when we\nare averaging it. Aggregation functions will be addressed in more\ndetail in the next section.

\n
\n
Question: Sorting by Known Date
\n

Write a query that sorts the records in Visited by date,\nomitting entries for which the date is not known\n(i.e., is null).

\n
👁 View solution\n
\n
SELECT * FROM Visited WHERE dated IS NOT NULL ORDER BY dated ASC;\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
idsitedated
619DR-11927-02-08
622DR-11927-02-10
734DR-31930-01-07
735DR-31930-01-12
751DR-31930-02-26
837MSK-41932-01-14
844DR-11932-03-22
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-93", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-94", "source": "
\n
Question: NULL in a Set
\n

What do you expect the following query to produce?

\n
SELECT * FROM Visited WHERE dated IN ('1927-02-08', NULL);\n
\n

What does it actually produce?

\n
👁 View solution\n
\n

You might expect the above query to return rows where dated is either ‘1927-02-08’ or NULL.\nInstead it only returns rows where dated is ‘1927-02-08’, the same as you would get from this\nsimpler query:

\n
SELECT * FROM Visited WHERE dated IN ('1927-02-08');\n
\n

The reason is that the IN operator works with a set of values, but NULL is by definition\nnot a value and is therefore simply ignored.

\n

If we wanted to actually include NULL, we would have to rewrite the query to use the IS NULL condition:

\n
SELECT * FROM Visited WHERE dated = '1927-02-08' OR dated IS NULL;\n
\n
\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-95", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-96", "source": "
\n
Question: Pros and Cons of Sentinels
\n

Some database designers prefer to use\na sentinel value\nto mark missing data rather than null.\nFor example,\nthey will use the date “0000-00-00” to mark a missing date,\nor -1.0 to mark a missing salinity or radiation reading\n(since actual readings cannot be negative).\nWhat does this simplify?\nWhat burdens or risks does it introduce?

\n
\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "id": "cell-97", "source": [ "%%sql\n", "-- Try solutions here!" ], "cell_type": "code", "execution_count": null, "outputs": [ ], "metadata": { "attributes": { "classes": [ "sql" ], "id": "" } } }, { "id": "cell-98", "source": "\n", "cell_type": "markdown", "metadata": { "editable": false, "collapsed": false } }, { "cell_type": "markdown", "id": "final-ending-cell", "metadata": { "editable": false, "collapsed": false }, "source": [ "# Key Points\n\n", "- A relational database stores information in tables, each of which has a fixed set of columns and a variable number of records.\n", "- A database manager is a program that manipulates information stored in a database.\n", "- We write queries in a specialized language called SQL to extract information from databases.\n", "- Use SELECT... FROM... to get values from a database table.\n", "- SQL is case-insensitive (but data is case-sensitive).\n", "- The records in a database table are not intrinsically ordered: if we want to display them in some order, we must specify that explicitly with ORDER BY.\n", "- The values in a database are not guaranteed to be unique: if we want to eliminate duplicates, we must specify that explicitly as well using DISTINCT.\n", "- Use WHERE to specify conditions that records must meet in order to be included in a query's results.\n", "- Use AND, OR, and NOT to combine tests.\n", "- Filtering is done on whole records, so conditions can use fields that are not actually displayed.\n", "- Write queries incrementally.\n", "- Queries can do the usual arithmetic operations on values.\n", "- Use UNION to combine the results of two or more queries.\n", "- Databases use a special value called NULL to represent missing information.\n", "- Almost all operations on NULL produce NULL.\n", "- Queries can test for NULLs using IS NULL and IS NOT NULL.\n", "\n# Congratulations on successfully completing this tutorial!\n\n", "Please [fill out the feedback on the GTN website](https://training.galaxyproject.org/training-material/topics/data-science/tutorials/sql-basic/tutorial.html#feedback) and check there for further resources!\n" ] } ] }