Week 11 - Procedures

This week we'll look at procedures in MySQL and how to store them.

  1. What is a stored procedure?
  2. The syntax for creating a stored procedure
  3. How to use procedures with CALL
  4. Parameters for procedures: IN, OUT, & INOUT variables
    1. Input Parameters
    2. Output Parameters
    3. Session variables
    4. INOUT Parameters
  5. Exercises

What is a stored procedure?

Stored procedures have a lot of similarities with functions and triggers. The main difference is they get called on their own - not as a part of select statement, or an automated process.

The syntax for creating a stored procedure

DELIMITER //
CREATE PROCEDURE {procedureName} ({parameters}) 
BEGIN
  -- Do stuff
END; //
DELIMITER ;

Looks pretty familiar right?

Just like in our functions and triggers, we set a custom delimiter, use the CREATE command, name the procedure, & define our parameters.

Following that, we use the BEGIN and END statements to create a space where we define the things we want our database to do, and then end the CREATE statement with our custom delimiter, and finish by resetting the delimiter to a semicolon.

So what's new here?

The stuff we do in the "do stuff" part is different from a function - rather than transforming data for the purpose of returning a value, we'll be executing commands, like we did with triggers.

Unlike triggers, however, it won't happen automatically - it will happen when we invoke the procedure with our new command - CALL.

The big thing I want you to take away here is this: if you find yourself saying, "Oh man, am I ever getting bored writing out these commands - you know, inserts, updates, deletes, or even select statements - over and over again; I wish there was a way I could simplify this!", oh boy are you in luck!

How to use procedures with CALL

Oh man, calling procedures is so easy!

CALL {procedureName}({arguments});

Remember that arguments are the values we use for the parameters, a.k.a. the input variables.

Let's create a procedure with no parameters to begin with, and then use the procedure with the CALL command.

DELIMITER //
CREATE PROCEDURE boyThatsEasy ()
BEGIN
  SELECT 'Boy, that WAS easy' AS 'Was that easy?';
END;
//
DELIMITER ;

Once you run that code as a script (remember - script, not statement), then, whenever you want, you can run this command:

CALL boyThatsEasy;

Remember - these things are stored in our database, meaning we can use them whenever we want! Their intended use is to reduce the amount of code we have to write in the long run.

When a procedure (or function) has no parameters, you can use it without parentheses after the name. Or have an empty set of parentheses, like this: CALL boyThatsEasy(); - that's okay, too!

Parameters for our procedures: IN, OUT, & INOUT variables

Input parameters

Just like functions, stored procedures really shine once you start using input parameters.

DELIMITER //
CREATE PROCEDURE madlibs(
  noun  VARCHAR(255),
  verb  VARCHAR(255)
)
BEGIN
  SELECT CONCAT('I could sure ', verb, ' a ', 
    noun, ' right about now.') 
    AS 'What do you feel like?';
END;
//
DELIMITER ;
-- Now we can play madlibs anytime!
CALL madlibs('poutine', 'eat');

Let's take a moment to recognize that, while using procedures just to run SELECT statements is... fine, we're really not doing anything here that couldn't be done with a VIEW.

What's cool about stored procedures is that we can use them for DML.

Let's look at something a little more advanced. The following procedure uses DML in a stored procedure, and selects a value from a table into a variable.

DROP PROCEDURE IF EXISTS addNewBookAndAuthor;
DELIMITER //
-- Let's prefix our parameters, so that 
-- they're easier to recognize. This is 
-- going to get a bit dense. 
CREATE PROCEDURE addNewBookAndAuthor(
  param_title     VARCHAR(255),
  param_first_name  VARCHAR(255),
  param_last_name   VARCHAR(255)
)
BEGIN
-- Declare two local variables.
  DECLARE var_book_id   INT;
  DECLARE var_author_id INT;
-- Get the value of the highest primary keys 
-- from the authors and books tables, add one, 
-- and then assign them as the values for the 
-- local variables. 
SELECT MAX(author_id) + 1 INTO var_author_id 
  FROM authors;
SELECT MAX(book_id) + 1 INTO var_book_id
  FROM books;
-- Insert into the books table the values of 
-- the maximum book id plus one, and the title 
-- passed in as a parameter. 
  INSERT INTO books (book_id, title)
    VALUES (var_book_id, param_title);
-- Do the same thing for the authors table. 
  INSERT INTO authors (
      author_id,
      first_name,
      last_name 
    ) VALUES (
      var_author_id, 
      param_first_name, 
      param_last_name
    );
-- Connect these entries in 
-- the bridging table. 
  INSERT INTO authorship (book_id, author_id) 
      VALUES (var_book_id, var_author_id);
END;
//
DELIMITER ;
-- Now we can add a new book and author into 
-- the books, authors and authorship tables with 
-- one line of code, and without having to look 
-- up the ids in order to create our entry in 
-- the bridging table. 
CALL addNewBookAndAuthor(
  'My Best Friend''s Exorcism', 
  'Grady', 
  'Hendrix'); 

Is the procedure we just defined a perfect fix? Not especially, as you'd want to use an IF statement to see if the book and author really were both new, and throw an error if they weren't.

Instead, what I want you to see is how powerful stored procedures can be for automating and simplifying your DML, while still maintaining the well-defined relationships and entity separations required by good, normalized database architecture.

Output parameters

Stored procedures don't return a value in the same way that functions return a value - you can't use the RETURNS or RETURN commands in a stored procedure, and you can't call a stored procedure in a SELECT statement.

However, that doesn't mean that our stored procedures are a black hole, where none of the values inside the procedure can ever escape. Obviously, values can be added or altered in tables via the DML statements, but what if we wanted to take a value from inside a stored procedure, and then make it available for use in other statements?

Not pictured: stored procedures.

In order to talk about how to pass values from a stored procedure out into the wild, we need to talk about a new kind of variable:

Session variables

So far, when we've talked about variables, whether they're parameters or local variables, they've only been in the context of a function. That is the limit of their "scope".

What is variable scope?

The "scope" of a variable means the context in which a variable is accessible.

If we define a function like this:

DELIMITER //
CREATE FUNCTION what_is_anything_plus_two(
  some_user_input INT
)
RETURNS INT
BEGIN
  RETURN some_user_input + 2;
END//

...we can't access the variable some_user_input by name outside of that function.

-- This won't work:
SELECT some_user_input

The scope of the variable some_user_input is limited to that function. The same goes for stored procedures, and triggers.

However, there is a type of variable called a "user-defined variable", or, as it is sometimes more helpfully known, a "session variable".

These variables are accessible by any functions, procedures, SELECT statements, DML, etc. that are in the same session.

What is a session?

In MySQL, a session is a successful connection to the database (and anything that happens over that connection).

For example, in dBeaver, if you open an SQL editor, write some code, run it, write some more code, and then run that, that's all considered to be part of the same session.

If you open a new SQL editor, that's considered a new session, because dBeaver is creating a new connection. If you wait too long between code execution, you may get disconnected. Anytime you disconnect and then reconnect is consider a new session.

Syntax for session variables

User-defined variables must begin with the @ symbol, otherwise MySQL will think they're system variables (and you don't want to mess with system variables if you don't have to).

SET @v1 = 1;
SET @v2 = 4;
SET @v3 = 10;
-- Selects 50
SELECT (@v1 + @v2) * @v3 AS 'Result';

Session variables do not get declared with the DECLARE command. Try using DECLARE outside of a BEGIN/END block and you'll throw an error.

Session variables instead entirely rely on the SET command, like so:

SET @my_variable = 'It''s mine.';
SET @my_variable = 'No, it''s MINE';
-- Selects 'No, it's MINE';
SELECT @my_variable;

Available data types for session variables

Note that, according to the docs Opens in a new window, we can only store a limited number of data types in our user-defined variables: integers, decimals, floats, strings, and NULL values.

However, MySQL is pretty smart, so you can do something like this:

SET @v1 = NOW();
-- Selects '2021-01-19 08:26:56'
SELECT DATE_ADD(@v1, INTERVAL 2 MONTH) AS 'Result';

In this instance, the value of the NOW() function is converted into a string by the variable, but the DATE_ADD() function is able to recognize that string as being in a valid date format, so it can re-interpret it as a date.

I just wanted to give you a heads-up on this, as you may find yourself getting unexpected results from session variables due to your data types getting messed with, or needing to CAST() the results to achieve the desired data type.

Selecting into multiple variables

You can select multiple values into multiple variables in a single statement. Take a look at this:

SELECT first_name, last_name 
  INTO @fname, @lname 
  FROM authors
  WHERE author_id = 4;
-- Selects Denis  
SELECT @fname;
-- Selects Johnson
SELECT @lname;

Note that I'd get an error if my query returned more than one row.

Hopefully you find session variables handy! But let me get back to why I was showing you how to use session variables in the first place: OUT parameters.

Stored procedures can set the value of a session variable. That's really handy! There are a few steps involved...

1. First, you've got to define one of the parameters in your procedure as an output:

DELIMITER //
CREATE PROCEDURE imAnEcho (
-- Input parameter, defined with 'IN'
  IN thingToEcho  VARCHAR(255),
-- Output parameter, defined with 'OUT'
  OUT theEcho   VARCHAR(255)
)
BEGIN
  -- Do stuff
END//
DELIMITER ;

2. Then you've got to SELECT {a value} INTO {the output parameter}, like so:

DELIMITER //
CREATE PROCEDURE imAnEcho (
  IN thingToEcho  VARCHAR(255),
  OUT theEcho   VARCHAR(255)
)
BEGIN
  SELECT thingToEcho INTO theEcho;
END//
DELIMITER ;

3. Then you've got to call the procedure, passing in a session variable as the output parameter:

CALL imAnEcho('repeat after me', @sesh_var_echo);

At this point, your session variable has been set, and you can use it as you please:

-- Selects 'repeat after me' 
SELECT @sesh_var_echo;
-- Selects 
-- rpt   |first_name |
-- ------|-----------|
-- repeat|Christopher|
-- repeat|David      |
--
SELECT SUBSTR(@sesh_var_echo,1,6) 
  AS 'rpt', first_name 
FROM authors LIMIT 2;

INOUT parameters

Now I know what you're thinking - what if I've got a value for a session variable already set, and I want to use my procedure to update the existing value of the session variable??

Good news, friend! There is a third, hybrid type of parameter in addition to IN and OUT parameters - the INOUT parameter.

An INOUT parameter allows you to pass in a session variable, transform it, and then output it.

-- Set the session variable
SET @howMuchNow = 24;
-- Selects 24
SELECT @howMuchNow;
-- The procedure that will add an amount to the 
-- session variable.
DELIMITER //
CREATE PROCEDURE runningTotal (
  IN    amtToUpdate   INT,
  INOUT theTotal    INT
)
BEGIN
  SELECT amtToUpdate + theTotal INTO theTotal;
END//
DELIMITER ;
-- Calling the procedure
CALL runningTotal(12, @howMuchNow);
-- Selects 36
SELECT @howMuchNow;
-- Calling the procedure again
CALL runningTotal(14, @howMuchNow);
-- Selects 50
SELECT @howMuchNow;

Now this procedure doesn't really do anything you couldn't get done with a function, just simple addition. But remember why we use procedures - they can perform complex DML statements in addition to setting the value of a session variable.

DELIMITER //
CREATE PROCEDURE updateAuthors (
  IN    param_first_name  VARCHAR(255),
  IN    param_last_name   VARCHAR(255),
  OUT   param_insertedAuthor  VARCHAR(511),
  OUT   param_theTotal    INT
)
BEGIN
  DECLARE fullname  VARCHAR(511);
  SET fullname = CONCAT(
    param_first_name, 
    ' ', 
    param_last_name
  );
  INSERT INTO authors (first_name, last_name)
    VALUES (param_first_name, param_last_name);
  SELECT fullname INTO param_insertedAuthor;
  SELECT COUNT(*) INTO param_theTotal FROM authors;
END//
DELIMITER ;
-- Call the procedure
CALL updateAuthors(
  'Nico',
  'Walker',
  @lastInserted, 
  @authorCount
);
-- Returns 8, if that's how many authors there are
SELECT @authorCount;
-- Returns 'Nico Walker'
SELECT @lastInserted;

Exercises

Alright, this week, we're going to create functions and stored procedures in our own databases!

Functions

  1. Take an integer as an input parameter, multiply it by itself, and then multiply that by the value of π (note: you can easily get the value of π with the built-in function PI()). This function can calculate the area of a circle based on the radius!
  2. Take two strings as your input parameters. Concatenate them, starting with the longer of the two strings. (Remember - there is a built-in function for checking the length of strings.)
  3. Set a due date - books are due three weeks from the withdrawal date, unless that date is a Tuesday. The library has shortened hours on Tuesdays, so books withdrawn on a Tuesday are due in three weeks plus one day. Have your function return the due date based on the withdrawal date.
  4. Let's cleanse some data! Take a string as an input parameter. If the string begins with 'ca' (regardless of the casing), and the string is not Cabo Verde, Cambodia, or Cameroon, return 'CAN'. Otherwise, return the value of the input parameter.
  5. Take three integers as your input parameters. Return the sum of those numbers. If arguments passed in for the first or second parameters are null, set the value of the null argument to zero. If the value of the third argument is null, throw an error.

Procedures

  1. Create a procedure that accepts four input parameters: two strings, a date, and a number. Have your procedure perform a select statement that selects values from your input parameters in the following format:
    "{date}'s episode of Sesame Street has been brought to you by the letters {first letter of string1, uppercased} and {first letter of string2, uppercased}, and the number {number}."
  2. Create a procedure that, when called, will insert values into three of your database's tables: two tables with a many-to-many relationship, and the bridging table that manages that relationship. See the example I used of books & authors on how to do this while maintaining data integrity.
  3. Create a procedure that, when called, will set a session variable to the current time. Additionally, write a SELECT statement using that session variable that will subtract the session variable from the current time.