Using VALUES / ROW To Create Derived Table From Static Values In MySQL 8.0.19
Most of the time, when writing SQL statements, I'm working with data that is wholly contained within the database itself. But, when running reports or helping the Support team debug an issue, I sometimes need to write a SQL statement that provides temporary table data as part of the SQL statement itself. This has always been possible in MySQL (for as long as I can remember). However, in the recent releases of MySQL 8, the VALUES
/ ROW
data manipulation language (DML) statements have greatly reduced the verbosity of such statements.
Consider this list of user ID and Email values:
1, 'sarah.hong@example.com' | |
2, 'jon.connor@example.com' | |
3, 'rick.martin@example.com' | |
4, 'emily.hill@example.com' | |
5, 'aaron.masters@example.com' | |
6, 'todd.mccleod@example.com' | |
7, 'molly.oswald@example.com' | |
8, 'bolton.taylor@example.com' | |
9, 'ellen.green@example.com' | |
10, 'hanah.oconnor@example.com' | |
11, 'william.pops@example.com' | |
12, 'dan.smith@example.com' | |
13, 'jenny.reed@example.com' | |
14, 'harry.fitzgerald@example.com' |
Pre-MySQL 8, if I wanted to author a SQL statement that provides these values as a derived table, I'd have to perform a SELECT..AS..UNION
statement for each row of data:
SELECT | |
derived.id, | |
derived.email, | |
COALESCE( m.id, 0 ) AS memberID | |
FROM | |
( | |
SELECT 1 AS id, 'sarah.hong@example.com' AS email UNION | |
SELECT 2 AS id, 'jon.connor@example.com' AS email UNION | |
SELECT 3 AS id, 'rick.martin@example.com' AS email UNION | |
SELECT 4 AS id, 'emily.hill@example.com' AS email UNION | |
SELECT 5 AS id, 'aaron.masters@example.com' AS email UNION | |
SELECT 6 AS id, 'todd.mccleod@example.com' AS email UNION | |
SELECT 7 AS id, 'molly.oswald@example.com' AS email UNION | |
SELECT 8 AS id, 'bolton.taylor@example.com' AS email UNION | |
SELECT 9 AS id, 'ellen.green@example.com' AS email UNION | |
SELECT 10 AS id, 'hanah.oconnor@example.com' AS email UNION | |
SELECT 11 AS id, 'william.pops@example.com' AS email UNION | |
SELECT 12 AS id, 'dan.smith@example.com' AS email UNION | |
SELECT 13 AS id, 'jenny.reed@example.com' AS email UNION | |
SELECT 14 AS id, 'harry.fitzgerald@example.com' AS email | |
) AS derived | |
LEFT OUTER JOIN | |
member m | |
ON | |
m.email = derived.email | |
; |
In this approach, each input row of user data is being used to define a table-less SELECT
statement. Each one of these results is then UNION
ed together in order to populate the derived table. And, once I have the derived table, I can JOIN
to it like any other SQL table.
This approach has worked for as long as I can remember. But, it's hella verbose. Much of this SQL statement is just repetitive noise used to define the derived table structure.
As of MySQL 8, we can reduce some of this noise by moving the column names into the AS derived
specification:
SELECT | |
derived.id, | |
derived.email, | |
COALESCE( m.id, 0 ) AS memberID | |
FROM | |
( | |
SELECT 1, 'sarah.hong@example.com' UNION | |
SELECT 2, 'jon.connor@example.com' UNION | |
SELECT 3, 'rick.martin@example.com' UNION | |
SELECT 4, 'emily.hill@example.com' UNION | |
SELECT 5, 'aaron.masters@example.com' UNION | |
SELECT 6, 'todd.mccleod@example.com' UNION | |
SELECT 7, 'molly.oswald@example.com' UNION | |
SELECT 8, 'bolton.taylor@example.com' UNION | |
SELECT 9, 'ellen.green@example.com' UNION | |
SELECT 10, 'hanah.oconnor@example.com' UNION | |
SELECT 11, 'william.pops@example.com' UNION | |
SELECT 12, 'dan.smith@example.com' UNION | |
SELECT 13, 'jenny.reed@example.com' UNION | |
SELECT 14, 'harry.fitzgerald@example.com' | |
) AS derived ( id, email ) | |
LEFT OUTER JOIN | |
member m | |
ON | |
m.email = derived.email | |
; |
This time, instead of include AS id
and AS email
in every single sub-select, we're factoring all of the column names out and into the derived table specification:
AS derived ( id, email )
This is a marked improvement over the MySQL 5.7 implementation. But, as of MySQL 8.0.19, we can do even better. MySQL 8.0.19 introduces the VALUES
and ROW
combination designed to do exactly what we're trying to do:
SELECT | |
derived.id, | |
derived.email, | |
COALESCE( m.id, 0 ) AS memberID | |
FROM | |
( | |
VALUES | |
ROW ( 1, 'sarah.hong@example.com' ), | |
ROW ( 2, 'jon.connor@example.com' ), | |
ROW ( 3, 'rick.martin@example.com' ), | |
ROW ( 4, 'emily.hill@example.com' ), | |
ROW ( 5, 'aaron.masters@example.com' ), | |
ROW ( 6, 'todd.mccleod@example.com' ), | |
ROW ( 7, 'molly.oswald@example.com' ), | |
ROW ( 8, 'bolton.taylor@example.com' ), | |
ROW ( 9, 'ellen.green@example.com' ), | |
ROW ( 10, 'hanah.oconnor@example.com' ), | |
ROW ( 11, 'william.pops@example.com' ), | |
ROW ( 12, 'dan.smith@example.com' ), | |
ROW ( 13, 'jenny.reed@example.com' ), | |
ROW ( 14, 'harry.fitzgerald@example.com' ) | |
) AS derived ( id, email ) | |
LEFT OUTER JOIN | |
member m | |
ON | |
m.email = derived.email | |
; |
This time, we're using the VALUES
/ ROW
combination to collate the static data into a derived table. And, we're using the AS derived
statement to define the column names. This is about as bare-bones as we can get.
For improved readability, we can use WITH
to move this derived table into a common table expression (CTE):
WITH derived ( id, email ) AS ( | |
VALUES | |
ROW ( 1, 'sarah.hong@example.com' ), | |
ROW ( 2, 'jon.connor@example.com' ), | |
ROW ( 3, 'rick.martin@example.com' ), | |
ROW ( 4, 'emily.hill@example.com' ), | |
ROW ( 5, 'aaron.masters@example.com' ), | |
ROW ( 6, 'todd.mccleod@example.com' ), | |
ROW ( 7, 'molly.oswald@example.com' ), | |
ROW ( 8, 'bolton.taylor@example.com' ), | |
ROW ( 9, 'ellen.green@example.com' ), | |
ROW ( 10, 'hanah.oconnor@example.com' ), | |
ROW ( 11, 'william.pops@example.com' ), | |
ROW ( 12, 'dan.smith@example.com' ), | |
ROW ( 13, 'jenny.reed@example.com' ), | |
ROW ( 14, 'harry.fitzgerald@example.com' ) | |
) | |
SELECT | |
derived.id, | |
derived.email, | |
COALESCE( m.id, 0 ) AS memberID | |
FROM | |
derived | |
LEFT OUTER JOIN | |
member m | |
ON | |
m.email = derived.email | |
; |
How beautiful is that?! MySQL 8 really makes it easy to take static data and provide it to the database as a derived table. I'm not sure if you can get more concise than that. Which is important because MySQL has a max packet size that it can accept. Granted, it's pretty huge (defaults to 64mb). But, the more noise that we can remove from these types of SQL statements, the more data we can provide.
Want to use code from this post? Check out the license.
Reader Comments
How is that more concise than:
select email, id from member where email in ({list of email addresses})?
@Scott,
Ah, great question. The main different is in what comes back when there is no matching record in the database. Given a list of emails, using the
VALUES
approach will yield the same list back even if theLEFT OUTER JOIN
fails. However, by usingIN()
, you will only get back the list of emails that exist in the database.It really just depends on what you're trying to do. In my case, I am often trying to "visually diff" the state of the database against a list that I have. Which is why I always want my list back; and then see which parts of it aren't reflected in the database itself.
Post A Comment — ❤️ I'd Love To Hear From You! ❤️