Relational databases
SQL

This tutorial introduces SQL using mainly MariaDB and MySQL syntax. Database-specific commands and functions may differ in PostgreSQL, SQL Server and other systems.

SQL statements can be categorized into five types:

CategoryPurpose
1. Query statements They make the search of the information possible
  • SELECT
2. Data manipulation statements Allow you to change the contents of the tables
  • INSERT
  • UPDATE
  • DELETE
3. Data definition statements Allow you to change the design of the database
  • CREATE
  • ALTER
  • DROP
  • RENAME
  • TRUNCATE
4. Transaction control statements They make the saving or cancelling of the change in the contents of rows possible
  • COMMIT
  • ROLLBACK
  • SAVEPOINT
5. Data control statements Allow you to change the user privileges
  • GRANT
  • REVOKE
CREATE DATABASE and USE

Before creating tables, you need a database to store them in. The CREATE DATABASE statement creates a new database:

CREATE DATABASE dbname;

Replace dbname with the name you want to use. Creating a database requires the appropriate privileges on the server.

In MariaDB and MySQL, select the database for the current connection with USE:

USE dbname;

After this, statements such as CREATE TABLE person (...) and SELECT * FROM person use the selected database unless you explicitly specify another database in the table name. The selection applies only to the current connection.

PostgreSQL: CREATE DATABASE dbname; also creates a database, but PostgreSQL does not support USE. Open a connection to the new database in your client, or use \connect dbname in the psql command-line client.

Comments

Use -- for a single-line comment. In MariaDB and MySQL, the second dash must be followed by whitespace or a control character. Use /* ... */ for a block comment.

-- A single-line comment
/* A comment that
   spans multiple lines */
CREATE TABLE

Tables are created with the CREATE TABLE statement. After that there will be the name of the new table and inside brackets the column names and their types.

Example
  CREATE TABLE person(
  id_person SMALLINT PRIMARY KEY,
  firstname VARCHAR(15) NOT NULL,
  lastname VARCHAR(15) NOT NULL,
  city CHAR(10),
  birthyear SMALLINT,
  salary DECIMAL(10,2));

You can add constraints and default values to individual column definitions. The following examples each show one column definition to use inside a CREATE TABLE statement.

NOT NULL

NOT NULL prevents a column from containing NULL (a missing value). The column must have a value; an empty string is still a value and is not prevented by this constraint.

firstname VARCHAR(15) NOT NULL
CHECK

CHECK restricts the values allowed in a column. For example, the following definition rejects birth years of 1900 or earlier:

birthyear SMALLINT CHECK (birthyear > 1900)

This CHECK condition still allows NULL. Add NOT NULL if a birth year is also required.

DEFAULT value

DEFAULT supplies a value when the column is omitted from an INSERT statement or the keyword DEFAULT is used explicitly. For example:

city CHAR(10) DEFAULT 'Helsinki'

If no city is supplied, the database uses 'Helsinki'. An explicitly supplied NULL is not replaced by this default.

SQL Data Types

A data type defines the kind of value a column can store. The examples below use MariaDB/MySQL syntax; supported types and their limits differ between database systems.

Data typePurposeExample column definition
CHAR(n)Fixed-length text, such as a two-character country code.country_code CHAR(2)
VARCHAR(n)Variable-length text up to the specified number of characters.lastname VARCHAR(50)
SMALLINTWhole numbers in a smaller range than INT.birthyear SMALLINT
INTWhole numbers.workhour INT
DECIMAL(p,s)Exact decimal values: p total digits, including s digits after the decimal point.salary DECIMAL(10,2)
FLOAT, DOUBLEApproximate floating-point values, such as measurements. DOUBLE generally provides more precision.temperature DOUBLE
DATEA calendar date.start_date DATE
TIMEA time of day; MariaDB/MySQL also supports durations.start_time TIME
DATETIMEA date and time.appointment_time DATETIME

For monetary amounts such as salaries, use DECIMAL to avoid the approximation associated with floating-point types. In DECIMAL(10,2), up to eight digits appear before the decimal point and two after it.

PRIMARY KEY
It is defined by adding the text PRIMARY KEY after the name of the column

Example creating a table person in which the column id_person is defined as a primary key:
  CREATE TABLE person(
  id_person SMALLINT PRIMARY KEY
  ...
The same is achieved also as follows:
  CREATE TABLE person(
  id_person SMALLINT,
  ...
  PRIMARY KEY(id_person)
  ...

If a primary key consists of more columns, the latter one will be used
Example
  PRIMARY KEY (id_project, id_person)
FOREIGN KEY

Referential integrity means that references between tables remain valid: each non-NULL foreign key value in a child table must match an existing key value in the parent table.

A FOREIGN KEY constraint defines this relationship and tells the database to enforce referential integrity. Its ON DELETE and ON UPDATE options specify how changes to referenced parent rows are handled, for example by preventing a deletion or automatically updating the related foreign key values.

In order to create table named hour to MySQL, you can use below code

  CREATE TABLE hour (
  id_project CHAR(5) NOT NULL,
  id_person SMALLINT NOT NULL,
  workhour INT,
  PRIMARY KEY (id_project,id_person),
  CONSTRAINT project_hour
  FOREIGN KEY (id_project) REFERENCES project (id_project)
    ON DELETE RESTRICT
    ON UPDATE CASCADE) ENGINE=InnoDB;
In order to execute the code above, first create a project table with a primary or unique key on id_project and a compatible column definition. And now if you update some value of id_project int the project table(parent-table), the same changes will be made automatically to hour table(child table). And you can not delete rows from project table if there is child rows in hour table.
Indexes

An index is a data structure that can help the database find rows without scanning the whole table. Indexes can cover one column or several columns and can help filtering, joins and sorting. In InnoDB, the primary key organizes the table data, and secondary indexes contain the primary key values used to find the corresponding rows.

Indexes take disk space and add work to INSERT, UPDATE and DELETE operations. Create indexes to support your queries, and use EXPLAIN to inspect how the database plans to retrieve data.

When you create an index, you can define whether duplicate values are allowed or not. Use INDEX to allow duplicate values and UNIQUE INDEX to prevent duplicate values.

Example
  CREATE TABLE person(
  id_person SMALLINT PRIMARY KEY,
  firstname VARCHAR(15) NOT NULL,
  lastname VARCHAR(15) NOT NULL,
  city CHAR(10),
  birthyear SMALLINT,
  salary DECIMAL(10,2),
  email VARCHAR(100) NOT NULL,
  INDEX lastnameIndex (lastname),
  UNIQUE INDEX emailIndex (email));
  • lastnameIndex is not unique: several people can have the same lastname value.
  • emailIndex is unique: two people cannot have the same email value. The NOT NULL constraint also requires an email value for each person.

You can also create an index that includes several columns. Add the following index definition inside the CREATE TABLE statement:

  INDEX nameIndex (firstname, lastname)

If you don't give any name to the index, MySQL names the index according to the first index column.
Example INDEX (fi,si) -> index will be named fi.

You can also add indexes later, when the table already exists. Then you can use below code:

     CREATE INDEX lastnameIndex ON person(lastname);
  

You can check what indexes the table has with below command:

    SHOW INDEXES FROM person\G
  

You can delete indexes with below command:

    DROP INDEX lastnameIndex ON person;
  

ALTER TABLE

The SQL ALTER TABLE command is used to modify the definition (structure) of a table by modifying the definition of its columns. The ALTER command is used to perform the following functions.

  1. Add, drop, modify table columns
  2. Add and drop constraints
  3. Enable and Disable constraints
  4. Change the engine in MySQL
Examples
  ALTER TABLE person
  ADD COLUMN test CHAR(20);
  ALTER TABLE person
  MODIFY COLUMN test CHAR(40);
  ALTER TABLE person
  DROP COLUMN test;
  ALTER TABLE person
  ADD INDEX ind_name (lastname);
DROP TABLE
You can delete tables from database with the DROP TABLE command. Note that the foreign keys might disallow you to drop the parent table, if the child table still exists.
Example
  DROP TABLE hour;
  DROP TABLE hour,person;
SELECT statement

A SELECT statement returns a result set. It does not create a stored database view; that requires CREATE VIEW.

Select queries consists of The following elements :
SELECT which columns will be retrieved
FROM from what table/tables or a view/views is it searched for
WHERE which rows will be included in the result set
GROUP BY how is it grouped
HAVING what of the lines which have been created as a result of the grouping are shown
ORDER BY how is it sorted

Examples

-- this will return id_person and lastname
SELECT id_person, lastname
FROM person;
-- this will return all columns from the table
SELECT *
FROM person;
DISTINCT

You can use DISTINCT statement in the SELECT query to return only those lines as a result which have differences.

SELECT DISTINCT city
FROM person;
prints all city-names only once.
LIMIT

In MariaDB and MySQL, LIMIT restricts the number of returned rows. Use ORDER BY to define which rows come first; without it, the order is not guaranteed.

SELECT *
FROM person
ORDER BY id_person
LIMIT 2;

This returns up to two rows, ordered by person ID.

SELECT *
FROM person
ORDER BY id_person
LIMIT 3, 2;

The first number is an offset: skip three rows, then return up to two rows (positions 4 and 5 in this ordering). The equivalent, more explicit syntax is:

SELECT *
FROM person
ORDER BY id_person
LIMIT 2 OFFSET 3;
ORDER BY

ORDER BY sorts the result set. ASC means ascending order and is the default; DESC means descending order. You can specify several columns to resolve ties.

SELECT id_person, lastname, salary
FROM person
ORDER BY salary DESC, id_person ASC;

This lists the highest salaries first. People with the same salary are ordered by their unique person ID, giving a defined order even when LIMIT is used.

Functions

There are several functions which you can use in the SELECT. Here are some examples, which works in MySQL.

SELECT CURRENT_DATE;
Shows today's date. In MariaDB and MySQL, CURDATE() is another option. DATE(expression) extracts the date part of a supplied value, for example DATE('2026-10-22 14:30:00').
SELECT VERSION();
Shows the version of your MySQL-server (in MySQL).
SELECT USER();
Shows your username (in MySQL).
AS keyword

If you want to change the heading of the result set, you can use the AS keyword

Example
SELECT lastname AS Name
FROM person;
In above example lastname is the column name in the database, but in the result set the heading is changed to Name. (Note: the code works also without the keyword AS, but the code is clearer, when you use it.)
String literals
Example
SELECT lastname, 'Some text' AS explanation
FROM person;
CONCAT

You can concatenate several columns and literals together

Example
SELECT CONCAT(firstname, ' ', lastname) AS Name
FROM person;
SUBSTRING
The SUBSTRING function extracts part of a character string.

Example
SELECT SUBSTRING(lastname,2,4) FROM person;
This will print 4 characters from name starting from the second character.
WHERE

You can use the WHERE clause to choose which rows will be included in the result.

Examples
SELECT *
FROM person
WHERE city ='TURKU';
SELECT *
FROM person
WHERE NOT city='TURKU';
SELECT *
FROM person
WHERE city <>'TURKU';
SELECT *
FROM person
WHERE salary=5000;
IN

If there is several option, it is shorter to use IN query

WHERE city='Turku' OR city='Tampere' OR city='Oulu'
is the same as
WHERE city in ('Turku', 'Tampere', 'Oulu')
BETWEEN

If you need to return values between some values, you can use BETWEEN

WHERE salary >= 4000 AND salary <= 5000
is the same as
WHERE salary BETWEEN 4000 AND 5000

BETWEEN includes both endpoints: 4000 and 5000 are included.

IS NULL

Missing information can be found by using IS NULL-syntax

SELECT *
FROM project
WHERE place IS NULL;
LIKE (character string search)

Example

WHERE city LIKE 'T%'
Returns those rows where the city begins with the character T
WHERE city LIKE '_i%'
Returns rows with i as the second character in the city column.
WHERE city LIKE '%i%'
Returns rows with i anywhere in the city column.

Wildcard Characters in MySQL
_ : any character (only one character)
% : any character or string (also several characters)
Aggregate functions

The Aggregate Functions are:

  • AVG
  • MIN
  • MAX
  • COUNT
  • SUM
  • GROUP_CONCAT
Example
SELECT AVG(salary)
FROM person;
GROUP BY
Used with aggregate functions, when the SELECT has also some other column/columns than the aggregate function
SELECT city, count(id_person)
FROM person
GROUP BY city;
Example
SELECT city, GROUP_CONCAT(lastname) AS peoples
FROM person
GROUP BY city;
the following result is obtained:
+----------+----------------------+
| city     | peoples              |
+----------+----------------------+
| HELSINKI | River,Simpson        |
| TAMPERE  | Daniels              |
| TURKU    | Morrison,Smith,Jones |
+----------+----------------------+
HAVING
Example
If we want to see only the places, where is at least two projects, we can use below code:
SELECT place, count(*)
FROM project
GROUP BY place
HAVING COUNT(*) >= 2;

Difference between WHERE and HAVING: WHERE filters rows before grouping, and HAVING filters groups after grouping. HAVING can also be used with an aggregate query without GROUP BY; all selected rows then form a single group.

SELECT COUNT(*) AS person_count
FROM person
HAVING COUNT(*) > 5;

This returns one row only if the table contains more than five people.

JOIN

A SQL JOIN combines rows from two or more tables based on a join condition. A declared foreign key is not required, and the joined columns can have different names. You can combine the parent and child table with JOIN and then return columns from both tables. The following sections cover three common JOIN types supported by MariaDB and MySQL:

  • INNER JOIN
  • LEFT OUTER JOIN (or LEFT JOIN)
  • RIGHT OUTER JOIN (or RIGHT JOIN)
INNER JOIN
INNER JOIN is the most common join operation used in applications and can be regarded as the default join-type. INNER JOIN creates a new result table by combining column values of two tables (A and B) based upon the join-predicate. The query compares each row of A with each row of B to find all pairs of rows which satisfy the join-predicate. When the join-predicate is satisfied, column values for each matched pair of rows of A and B are combined into a result row
The join is made in FROM clause. For example Table1 and Table2 both have a column called id (in most cases id is the primary key in one table and foreign key in the other table). The inner joint is created between them as follows:
FROM Table1 INNER JOIN Table2 ON Table1.id=Table2.id
Example: Creating an INNER JOIN between tables hour and project.
OUTER JOIN

If you want to view all the rows from one table (usually the parent-table), even when the other table does not have all matching rows, an OUTER JOIN is used.

Types of OUTER JOIN:

  • LEFT OUTER JOIN (or LEFT JOIN): Returns all rows from the left table, and matching rows from the right table. NULL values appear for non-matching rows.
  • RIGHT OUTER JOIN (or RIGHT JOIN): Returns all rows from the right table, and matching rows from the left table.

Note: The word OUTER is often omitted, so LEFT JOIN and LEFT OUTER JOIN are equivalent.

Example: LEFT JOIN between project and hour tables

Joining several tables

You can join multiple tables in a single query by chaining JOIN clauses. This is useful when you need to combine data from three or more related tables.

Example: Joining three tables from the Project database (person, hour, and project)

UNION

You can retrieve rows from several tables using a UNION query

Example
The persons which salary is over 40 and/or which belong to the project 102

SELECT lastname as Name,'Salary is over 40'
FROM person
WHERE salary>40
UNION
SELECT lastname, 'Belongs to 102'
FROM person INNER JOIN hour ON person.id_person = hour.id_person
WHERE id_project='102';
The results are
+----------+-------------------+
| Name     | Salary is over 40 |
+----------+-------------------+
| Morrison | Salary is over 40 |
| Jones    | Salary is over 40 |
| Morrison | Belongs to 102    |
| River    | Belongs to 102    |
+----------+-------------------+
You can also make UNION queries like this:
SELECT lastname as Name,'Salary is over 40'
FROM person
WHERE salary>40
UNION 
SELECT lastname as Name,'Salary is not over 40'
FROM person
WHERE salary<=40;
The results are
+----------+-----------------------+
| Name     | Salary is over 40     |
+----------+-----------------------+
| Morrison | Salary is over 40     |
| Jones    | Salary is over 40     |
| Smith    | Salary is not over 40 |
| River    | Salary is not over 40 |
| Simpson  | Salary is not over 40 |
| Daniels  | Salary is not over 40 |
+----------+-----------------------+
You can also replace the previous UNION with IF like this
SELECT lastname as Name, 
  IF(salary>40, 'Salary is over 40', 'Salary is not over 40') AS Explanation
FROM person;


Example Printing lastname and text: 'Less than 39', 'Exactly 39', 'Over 39'.
  SELECT lastname,'Less than 39'
  FROM person
  WHERE salary<39
  UNION
  SELECT lastname,'Over 39'
  FROM person
  WHERE salary>39
  UNION SELECT lastname,'Exactly 39'
  FROM person
  WHERE salary=39
  ORDER BY 2;
And the result is
+----------+--------------+
| lastname | Less than 39 |
+----------+--------------+
| Smith    | Exactly 39   |
| River    | Exactly 39   |
| Simpson  | Less than 39 |
| Daniels  | Less than 39 |
| Morrison | Over 39      |
| Jones    | Over 39      |
+----------+--------------+
UNION ALL
Please Note! UNION query is like SELECT-statement has the DISTINCT configuration. If you want to allow double values, use UNION ALL instead of UNION.

Example
SELECT column_name(s) FROM table1
UNION ALL
SELECT column_name(s) FROM table2

IF() function

In MariaDB and MySQL, the IF() function returns one of two values: IF(condition, value_if_true, value_if_false). For a portable SQL alternative, use CASE WHEN condition THEN value_if_true ELSE value_if_false END.

Example

SELECT pname, IF(priorit<2, 'Very Important', 'Normal priority') as Category
FROM project;

Subquery

Subquery or Inner query or Nested query is a query inside a query. A subquery is usually added in the WHERE Clause of the sql statement. Most of the time, a subquery is used when you know how to search for a value using a SELECT statement, but do not know the exact value.

Example All the person are fetched who were born in the same year as Morrison

SELECT lastname
FROM person
WHERE birthyear=
( SELECT birthyear
  FROM person
  WHERE lastname='Morrison');
Example
The Query shows all, which salary is at least two times bigger than that of the one which has the smallest salary
SELECT lastname, salary
FROM person
WHERE salary >=
( SELECT min(salary)*2
  FROM person);
A scalar subquery used with = must return at most one row. The Morrison example assumes exactly one matching person; use a unique person ID to avoid ambiguity. To test membership in several returned values, use IN, as in the next example.

Example Fetch the person which have worked for at least 400 hour in some project
SELECT lastname
FROM person
WHERE id_person IN
( SELECT id_person
  FROM hour
  WHERE workhour>=400);
ANY and ALL subqueries

If we use word ANY like this

WHERE salary > ANY (SELECT salary FROM person WHERE city = 'Turku')
ANY means at least one: the condition is true if the salary is greater than at least one value returned by the subquery.

and if we use word ALL, like this

WHERE salary > ALL (SELECT salary FROM person WHERE city = 'Turku')
In this case, the condition is fulfilled when the salary is greater than all of the return values of the subquery.

Example, we have this data

+----------+--------+
| city     | salary |
+----------+--------+
| HELSINKI |     39 |
| HELSINKI |     36 |
| TAMPERE  |     34 |
| TURKU    |     44 |
| TURKU    |     39 |
| TURKU    |     44 |
+----------+--------+
Then with this query
SELECT city, salary FROM person WHERE salary <
	ANY(SELECT salary FROM person WHERE city='Turku');
we will get this data (all rows where salary < 44)
+----------+--------+
| city     | salary |
+----------+--------+
| TURKU    |     39 |
| HELSINKI |     39 |
| HELSINKI |     36 |
| TAMPERE  |     34 |
+----------+--------+
And with this query
SELECT city, salary FROM person WHERE salary <
	ALL(SELECT salary FROM person WHERE city='Turku');
we will get this data (all rows where salary < 39)
+----------+--------+
| city     | salary |
+----------+--------+
| HELSINKI |     36 |
| TAMPERE  |     34 |
+----------+--------+

Subquery example: finding projects by location

Let's create a practical example using subqueries to find projects located in the same city where Jim Morrison lives.

Subquery example: finding co-workers

A more complex example: Find the names of people who have worked on the same projects as Jim Morrison.

DELETE rows

Delete rows, which meets on a condition

DELETE
FROM project
WHERE id_project=2145;
UPDATE data
Example
Salary of all the persons which lives in Turku is raised 20%

  UPDATE person
  SET salary=salary*1.2
  WHERE city='TURKU';
INSERT data
Table test has columns:
  • id SMALLINT
  • name VARCHAR(50)
  • height SMALLINT
You can insert data to table test like this:
  INSERT INTO test
  VALUES(101,'Matt',180);
or like this:
  INSERT INTO test
  VALUES(102,'Liisa',NULL);
In the previous examples the information must be given in that order as the columns are in the table and to every column a value must be fed(at least NULL.)
If you also give the column list about those values that you are going to insert, you can use the following syntax
  INSERT INTO test (name,id)
  VALUES('Liisa',103);
Add data from another table

You can also read data from one table and insert that data to a new table which does not already exist. CREATE TABLE ... SELECT derives columns from the query result; it does not clone the full table definition. Indexes and constraints are not automatically copied, and column types or attributes may differ. Define the required structure explicitly when those details matter.

Example
Below code will read data from project-table and create a new table named helsinki with that data.

  CREATE TABLE helsinki
  SELECT *
  FROM project
  WHERE place='Helsinki';
If you already have table named helsinki and you want to insert data from project, you can use below code
  INSERT INTO helsinki
  SELECT *
  FROM project
  WHERE place ='Helsinki';
Stored procedures

Stored Procedures are SQL-scripts which will be stored in the database. Every RDBMS has they own syntax for procedures.

The following examples work in MariaDB and MySQL.

DELIMITER is a client command that changes the marker used to identify the end of a statement. The default marker is ;. A procedure contains semicolons inside its body, so DELIMITER // temporarily changes the marker to //. This lets the client send the complete CREATE PROCEDURE statement to the server. After END //, use DELIMITER ; to restore the normal marker.

The MariaDB and MySQL command-line clients support this command. Other SQL clients may handle procedure scripts differently; DELIMITER itself is not SQL sent to the server.

Next example will create an procedure named searchPerson:
  DELIMITER //
  CREATE PROCEDURE searchPerson(IN sname CHAR(50))
  BEGIN
  SELECT * FROM person WHERE lastname=sname;
  END //
  DELIMITER ;
  
Then we can call this procedure with this command:
CALL searchPerson('Morrison');
We can check what procedures we have in a certain database:
SHOW PROCEDURE STATUS WHERE db='database' \G
We can check the procedure syntax:
SHOW CREATE PROCEDURE searchPerson \G
We can delete the procedure:
DROP PROCEDURE searchPerson;

NOTE : In above example the query was so simple that it is not reasonable to save it as a procedure.

Variables in procedures

The following examples work in MariaDB and MySQL. In addition to parameters such as sname in the previous example, procedures can use variables to store values:

  • User-defined variables start with @ and belong to the current connection (session). Their values remain available after the procedure finishes, until changed or the connection is closed.
  • Local variables are declared with DECLARE inside a BEGIN ... END block. They are available only within that block and are initialized on each call, using their DEFAULT value or NULL if no default is specified.
Example: user-defined variable

This procedure looks up a person by primary key and stores their first name in the session variable @chosenFname. Use the person table created earlier on this page.

DELIMITER //
CREATE PROCEDURE chooseFirstName(IN p_id SMALLINT)
BEGIN
  SET @chosenFname = (
    SELECT firstname FROM person WHERE id_person = p_id
  );
END //
DELIMITER ;
  

After creating the procedure, run these statements in your SQL client using the same connection. Replace 1 with an existing person's ID:

CALL chooseFirstName(1);
SELECT @chosenFname AS chosen_firstname;
  

If person 1 has the first name 'Anna', the SELECT returns Anna. The variable remains available even though the procedure has finished. It is not shared with other connections, even if they use the same username.

The primary key lookup returns at most one row. If the person does not exist, the subquery returns NULL and the SET statement replaces any previous value of @chosenFname with NULL.

Example: local variable

This version uses a local variable without @. DECLARE statements must appear at the beginning of the block, before executable statements.

DELIMITER //
CREATE PROCEDURE showFirstName(IN p_id SMALLINT)
BEGIN
  DECLARE chosenFname VARCHAR(50) DEFAULT NULL;
  SET chosenFname = (
    SELECT firstname FROM person WHERE id_person = p_id
  );
  SELECT chosenFname AS chosen_firstname;
END //
DELIMITER ;
  

Call the procedure from your SQL client:

CALL showFirstName(1);

The SELECT inside the procedure returns the name as a result set. The local variable itself is no longer accessible after the procedure finishes. It is separate from the session variable @chosenFname and does not change its value.

IF ... THEN ... ELSE ... END IF

In MariaDB and MySQL procedures, an IF statement chooses which statements to execute based on a condition. The basic syntax is:

IF condition THEN
  -- Statements executed when the condition is true
ELSE
  -- Statements executed otherwise
END IF;

The ELSE branch is optional. End the statement with END IF;, not just END;. This control-flow statement is different from the IF() function used in SELECT expressions.

The following procedure checks an amount and returns a message. It does not require any tables:

DELIMITER //
CREATE PROCEDURE checkAmount(IN p_amount DECIMAL(10,2))
BEGIN
  IF p_amount > 0 THEN
    SELECT 'Amount is positive' AS message;
  ELSE
    SELECT 'Enter an amount greater than zero' AS message;
  END IF;
END //
DELIMITER ;

After creating the procedure, test both branches in your SQL client:

CALL checkAmount(10.00);
-- Returns: Amount is positive

CALL checkAmount(-5.00);
-- Returns: Enter an amount greater than zero

The first call executes the THEN branch because 10.00 > 0 is true. The second executes the ELSE branch. Zero or NULL also takes the ELSE branch because the condition is not true.

Stored Procedures have several benefits:

  • Creating several applications will be easier (ex. Web-appliation, Mobile-application), because we can call the same procedures
  • User privileges: we can give privileges to call Procedures, without any other privileges
  • Security: procedures can expose controlled operations. They still require safe parameter handling; dynamically concatenated SQL can introduce SQL injection.
  • Less network traffic: one Procedure can make several things

Transaction

A transaction groups database operations into one unit: either commit the changes together or roll them back. An SQL error does not always roll back the whole transaction automatically. The application or procedure must handle errors and issue ROLLBACK when needed.

START TRANSACTION begins an explicit transaction, COMMIT makes its changes permanent, and ROLLBACK cancels uncommitted changes. These examples require transactional tables, so both tables explicitly use InnoDB. DDL commands such as CREATE TABLE can cause an implicit commit in MariaDB/MySQL and should not be mixed into this transfer transaction.

Transaction example

Create a table with below code

CREATE TABLE bank_account(
account_number VARCHAR(20) PRIMARY KEY,
balance DECIMAL(10,2) NOT NULL,
credit_limit DECIMAL(10,2) NOT NULL
) ENGINE=InnoDB;
  
And another table with below code
CREATE TABLE action (
idaction int PRIMARY KEY AUTO_INCREMENT,
account_number VARCHAR(20) NOT NULL,
action VARCHAR(45) NOT NULL,
action_time timestamp NOT NULL,
amount DECIMAL(10,2) NOT NULL
) ENGINE=InnoDB;
  
Then we will create two accounts, which both have balance 500.
  INSERT INTO bank_account VALUES('FI001',500,1000);
  INSERT INTO bank_account VALUES('FI002',500,2000);
  
Then create a procedure to transfer a positive amount between two different accounts. These procedures manage their own transactions: call them when the connection has no active transaction. The exception handler rolls back on SQL errors and RESIGNAL reports the error to the caller. A deadlock can still occur during concurrent transfers; the caller should retry the complete transaction when appropriate.
DELIMITER //
CREATE PROCEDURE debit_transfer(
	IN first_account VARCHAR(20), 
	IN second_account VARCHAR(20), 
	IN transfer_amount DECIMAL(10,2) )
  BEGIN
  DECLARE test1,test2 INT DEFAULT 0;
  START TRANSACTION;
  UPDATE bank_account SET balance=balance-transfer_amount WHERE account_number=first_account AND balance >= transfer_amount;
  SET test1=ROW_COUNT();
  UPDATE bank_account SET balance=balance+transfer_amount WHERE account_number=second_account;
  SET test2=ROW_COUNT();
    IF (test1 > 0 AND test2 >0) THEN
      INSERT INTO action(account_number,action,amount,action_time) VALUES(first_account,'withdrawal',transfer_amount,NOW());
      INSERT INTO action(account_number,action,amount,action_time) VALUES(second_account,'deposit',transfer_amount,NOW());
      COMMIT;
      SELECT 1 AS success;
    ELSE
      ROLLBACK;
      SELECT 0 AS success;
  END IF;
  END //
DELIMITER ;
  
Explanation:
  • If first account does not exists or there is not enough money, variable test1 = 0
  • If second account does not exists, variable test2 = 0
  • If both updates affect a row, the procedure writes both log entries and commits; otherwise it rolls back and returns success = 0.
  • If any SQL statement raises an exception, the handler rolls back and rethrows the error. Invalid amounts and identical accounts also raise an error.
You can test the Procedure with below commands:
  CALL debit_transfer('FI001','FI002',100);
  SELECT * FROM bank_account;
  SELECT * FROM action;
and
  CALL debit_transfer('FI001','ACC003',100);
  SELECT * FROM bank_account;
  SELECT * FROM action;
and
  CALL debit_transfer('FI001','FI002',2000);
  SELECT * FROM bank_account;
  SELECT * FROM action;  
Above transaction is suitable for bank_accounts where is not allowed that the balance is negative.
If it is allowed that the balance is negative as far as is not more than the credit_limit, we can use below transaction:
DELIMITER //
CREATE PROCEDURE credit_transfer(
	IN first_account VARCHAR(20), 
	IN second_account VARCHAR(20), 
	IN transfer_amount DECIMAL(10,2) )
  BEGIN
  DECLARE test1,test2 INT DEFAULT 0;
  START TRANSACTION;
  UPDATE bank_account SET balance=balance-transfer_amount WHERE account_number=first_account AND balance + credit_limit >= transfer_amount;
  SET test1=ROW_COUNT();
  UPDATE bank_account SET balance=balance+transfer_amount WHERE account_number=second_account;
  SET test2=ROW_COUNT();
    IF (test1 > 0 AND test2 >0) THEN
      INSERT INTO action(account_number,action,amount,action_time) VALUES(first_account,'withdrawal',transfer_amount,NOW());
      INSERT INTO action(account_number,action,amount,action_time) VALUES(second_account,'deposit',transfer_amount,NOW());
      COMMIT;
      SELECT 1 AS success;
    ELSE
      ROLLBACK;
      SELECT 0 AS success;
  END IF;
  END //
DELIMITER ;
Transaction animation

The following animation demonstrates how transactions work with the bank account example:

Key concepts demonstrated in the animation:

  • START TRANSACTION: Begins a new transaction block
  • ROW_COUNT(): Returns the number of rows affected by the previous UPDATE
  • COMMIT: Makes all changes permanent when conditions are met
  • ROLLBACK: Cancels all changes if any condition fails
  • Atomic operations: Either all changes succeed together, or none do
  • Data integrity: Balance cannot go negative without sufficient funds
Triggers

Triggers are little modules stored on database. These modules are executed automatically, when certain SQL-commands are executed. We can define the triggers start when UPDATE-, INSERT-, or DELETE-command is executed.

Example
Let's create tables Customers and Logs
  CREATE TABLE Customers(
  idCustomers INT PRIMARY KEY AUTO_INCREMENT,
  customerName VARCHAR(20)
  );

  CREATE TABLE Logs(
  idLogs INT PRIMARY KEY AUTO_INCREMENT,
  customerName VARCHAR(20),
  idCustomers INT,
  updateType VARCHAR(50) DEFAULT NULL,
  updateTime TIMESTAMP
  );
  
Then create a trigger that saves the previous customer name to the Logs table whenever a Customers row is updated. Also we will add the time and explanation "update".
  DELIMITER $$
  CREATE TRIGGER updateCustomer
      BEFORE UPDATE ON Customers
      FOR EACH ROW
  BEGIN
  INSERT INTO Logs
    SET updateType = 'update',
    idCustomers = OLD.idCustomers,
    customerName = OLD.customerName,
    updateTime = NOW();
  END$$
  DELIMITER ;
  
Also we can create trigger, which will save the name of the Customer, to Logs-table, if we delete the Customer. Also the time and explanation "delete" will be saved.
  DELIMITER $$
  CREATE TRIGGER deleteCustomer
      BEFORE DELETE ON Customers
      FOR EACH ROW
  BEGIN
  INSERT INTO Logs
    SET updateType = 'delete',
    idCustomers = OLD.idCustomers,
    customerName = OLD.customerName,
    updateTime = NOW();
  END$$
  DELIMITER ;
  
Events

MariaDB and MySQL can schedule SQL statements using events. Events require the EVENT privilege for the database and a running Event Scheduler. Check SHOW VARIABLES LIKE 'event_scheduler';. An administrator can enable it with SET GLOBAL event_scheduler = ON;; to keep it enabled after a restart, configure event_scheduler=ON in the server configuration. The following example updates a value after one minute.

Create a test database and a table with below code

create database eventtest;

use eventtest;

create table atest(
id_atest INT PRIMARY KEY AUTO_INCREMENT,
amount int);

insert into atest(amount) values(5);
And create the event with below code
CREATE EVENT myevent
    ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 MINUTE
    DO
      UPDATE atest SET amount=amount + 1;
If you check the data after in a minute, you should see that the value of amount has been updated.

You can check what kind of event your database has with below command

SHOW events;
Note! above event will be deleted after it has been executed. If you want to make an event which will be executed every 1 minute, you can use below code
DELIMITER //
CREATE EVENT permanent_event
  ON SCHEDULE EVERY 1 MINUTE
  DO
    BEGIN
      UPDATE atest SET amount=amount + 1;
    END //
DELIMITER ;
If you now check the data once in a minute, you should see that the value of amount will be updated. You can remove the event with the command
DROP EVENT permanent_event;

SQL injection

If you are going to make an application (web-application, Java-application etc), you should take care that there is no risk for SQL injection. SQL injection is a code injection technique that exploits security vulnerabilities in an application's database layer.

The first two animation steps illustrate a deliberately vulnerable legacy login that concatenates input and compares plain-text passwords. For this demonstration only, assume person also contains a password column; do not add plain-text passwords to the tutorial's person table. The final step uses a separate users table, password hashes and a parameterized query.

CREATE TABLE users (
  id_user INT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL
) ENGINE=InnoDB;

During registration, generate the hash with password_hash($password, PASSWORD_DEFAULT) and save it using a parameterized INSERT. During login, retrieve the hash by username and check it with password_verify(). The final animation assumes this table contains a registered user named Morrison.

Click "Start Demo" to see how SQL injection attacks work and how to prevent them

For more information about SQL injection and security best practices, visit:
OWASP SQL Injection Prevention Cheat Sheet



Toggle Menu