As a software developer I bet you spend the majority of time creating functions in different kinds of programming languages like: Typecript, Python, Java, PHP, C# and so on. 💻 That’s terrific! Nevertheless, you can also build functions using PL/pgSQL (Procedural Language/PostgreSQL) in order to encapsulate SQL statements in one place and execute calculations and iterations with data, directly in the DB (data base). Since the syntax is a little different to what developers are used to, this article might be useful and save you some time.
PostgreSQL 🐘 is one popular relational data base management system (RDBMS), which is open-source and has a big community giving support to it. Read More
In this occasion, I will be sharing a brief practical example involving various useful Must-Knows that will help you quick start your development and data management with PostgreSQL functions.
TOPICS COVERED
- Variables Declaration and Types
- FOR loops
- Conditionals
- Logs
- Helper Functions
- Generic Error Handling
QUICK NOTE: in case you are short on time, you can jump directly to the Analysis Section to review the PL/pgSQL function examples and start creating your own functions⏩
Table of Contents
📍FIRST - Logs
📍SECOND - Helper functions
📍THIRD - The Main Function
Introduction
Let’s say a movie theater manager 🎥 decides to start a discount promotion by giving coupons representing future discounts to existing customers. These coupons most be created for each customer that has watched movies with specific genres. Depending on the genre of the movie watched, the coupon will accumulate more, less or none points. Also, if the movie duration is less than 2 hours (120 min), the coupon will not apply regardless the movie genre.
So having this in mind 💡, here is the test DB:
/**
* For simplificy purposes, I'm not specifying FOREIGN keys and the creation
* of more tables Ex. Writer, MovieWriters, Actor, MovieCast, etc.
* NOTE: replace gen_random_uuid() with uuid_generate_v4() in case you're
* using a previous PostgreSQL version. I'm using PostgreSQL 17.0.
*/
CREATE TABLE "Movie"(
id UUID PRIMARY KEY DEFAULT(gen_random_uuid()),
"title" VARCHAR(255),
"duration" NUMERIC,
"mainDirectorId" UUID,
"writers" TEXT,
"cast" TEXT
);
CREATE TABLE "Genre" (
id UUID PRIMARY KEY DEFAULT(gen_random_uuid()),
"name" VARCHAR(255),
"pointsApply" BOOLEAN,
"points" NUMERIC
);
CREATE TABLE "MovieGenre" (
id UUID PRIMARY KEY DEFAULT(gen_random_uuid()),
"movieId" UUID,
"genreId" UUID
);
CREATE TABLE "Director" (
id UUID PRIMARY KEY DEFAULT(gen_random_uuid()),
"name" VARCHAR(255),
"otherWorks" TEXT
);
CREATE TABLE "Customer" (
id UUID PRIMARY KEY DEFAULT(gen_random_uuid()),
"name" VARCHAR(255),
"phoneNumber" VARCHAR(255)
);
CREATE TABLE "CustomerWatchedMovie" (
id UUID PRIMARY KEY DEFAULT(gen_random_uuid()),
"customerId" UUID,
"movieId" UUID
);
CREATE TABLE "Coupon" (
id UUID PRIMARY KEY DEFAULT(gen_random_uuid()),
"movieId" UUID,
"customerId" UUID,
"points" NUMERIC
);
and this is the sample data for our DB:
INSERT INTO "Genre" (name, "pointsApply", "points") VALUES ('Action', TRUE, 15),('Comedy', TRUE, 3),('Drama', TRUE, 4),('Horror', FALSE, 0),('Sci-Fi', TRUE, 10),('Romance', FALSE, 0),('Fantasy', TRUE, 4),('Thriller', FALSE, 0),('Documentary', FALSE, 0),('Adventure', TRUE, 10);
INSERT INTO "Director" (name, "otherWorks") VALUES('Christopher Nolan', 'Inception, The Dark Knight, Interstellar'),('Quentin Tarantino', 'Pulp Fiction, Kill Bill, Django Unchained'),('Steven Spielberg', 'Jurassic Park, E.T., Schindler''s List'),('Martin Scorsese', 'Goodfellas, Taxi Driver, The Irishman'),('James Cameron', 'Titanic, Avatar, Terminator'),('Greta Gerwig', 'Lady Bird, Little Women'),('Jordan Peele', 'Get Out, Us, Nope'),('Ridley Scott', 'Gladiator, Alien, The Martian'),('Wes Anderson', 'The Grand Budapest Hotel, Moonrise Kingdom'),('Denis Villeneuve', 'Blade Runner 2049, Dune, Arrival'),('George Lucas', 'Star Wars, Indiana Jones');
INSERT INTO "Movie" (title, "duration", "mainDirectorId", "writers", "cast") VALUES('Inception', 148, (SELECT id FROM "Director" WHERE name = 'Christopher Nolan'), 'Christopher Nolan', 'Leonardo DiCaprio, Joseph Gordon-Levitt, Ellen Page'),('Pulp Fiction', 149, (SELECT id FROM "Director" WHERE name = 'Quentin Tarantino'), 'Quentin Tarantino', 'John Travolta, Uma Thurman, Samuel L. Jackson'),('Jurassic Park', 127, (SELECT id FROM "Director" WHERE name = 'Steven Spielberg'), 'Michael Crichton', 'Sam Neill, Laura Dern, Jeff Goldblum'),('Goodfellas', 146, (SELECT id FROM "Director" WHERE name = 'Martin Scorsese'), 'Nicholas Pileggi', 'Robert De Niro, Ray Liotta, Joe Pesci'),('Avatar', 162, (SELECT id FROM "Director" WHERE name = 'James Cameron'), 'James Cameron', 'Sam Worthington, Zoe Saldana, Sigourney Weaver'),('Lady Bird', 95, (SELECT id FROM "Director" WHERE name = 'Greta Gerwig'), 'Greta Gerwig', 'Saoirse Ronan, Laurie Metcalf, Tracy Letts'),('Get Out', 104, (SELECT id FROM "Director" WHERE name = 'Jordan Peele'), 'Jordan Peele', 'Daniel Kaluuya, Allison Williams, Bradley Whitford'),('Gladiator', 155, (SELECT id FROM "Director" WHERE name = 'Ridley Scott'), 'David Franzoni', 'Russell Crowe, Joaquin Phoenix, Connie Nielsen'),('The Grand Budapest Hotel', 100, (SELECT id FROM "Director" WHERE name = 'Wes Anderson'), 'Wes Anderson', 'Ralph Fiennes, Tony Revolori, Saoirse Ronan'),('Dune', 155, (SELECT id FROM "Director" WHERE name = 'Denis Villeneuve'), 'Denis Villeneuve', 'Timothée Chalamet, Rebecca Ferguson, Oscar Isaac'),('Star Wars', 121, (SELECT id FROM "Director" WHERE name = 'George Lucas'), 'George Lucas', 'Mark Hamill, Harrison Ford, Carrie Fisher');
INSERT INTO "MovieGenre" ("movieId", "genreId") VALUES((SELECT id FROM "Movie" WHERE title = 'Inception'), (SELECT id FROM "Genre" WHERE name = 'Sci-Fi')),((SELECT id FROM "Movie" WHERE title = 'Pulp Fiction'), (SELECT id FROM "Genre" WHERE name = 'Thriller')),((SELECT id FROM "Movie" WHERE title = 'Jurassic Park'), (SELECT id FROM "Genre" WHERE name = 'Adventure')),((SELECT id FROM "Movie" WHERE title = 'Jurassic Park'), (SELECT id FROM "Genre" WHERE name = 'Drama')),((SELECT id FROM "Movie" WHERE title = 'Goodfellas'), (SELECT id FROM "Genre" WHERE name = 'Drama')),((SELECT id FROM "Movie" WHERE title = 'Avatar'), (SELECT id FROM "Genre" WHERE name = 'Fantasy')),((SELECT id FROM "Movie" WHERE title = 'Lady Bird'), (SELECT id FROM "Genre" WHERE name = 'Drama')),((SELECT id FROM "Movie" WHERE title = 'Get Out'), (SELECT id FROM "Genre" WHERE name = 'Horror')),((SELECT id FROM "Movie" WHERE title = 'Gladiator'), (SELECT id FROM "Genre" WHERE name = 'Action')),((SELECT id FROM "Movie" WHERE title = 'The Grand Budapest Hotel'), (SELECT id FROM "Genre" WHERE name = 'Comedy')),((SELECT id FROM "Movie" WHERE title = 'Dune'), (SELECT id FROM "Genre" WHERE name = 'Sci-Fi')),((SELECT id FROM "Movie" WHERE title = 'Star Wars'), (SELECT id FROM "Genre" WHERE name = 'Sci-Fi')),((SELECT id FROM "Movie" WHERE title = 'Star Wars'), (SELECT id FROM "Genre" WHERE name = 'Adventure'));
INSERT INTO "Customer" (name, "phoneNumber") VALUES('Alice Johnson', '465 101 010'),('Bob Smith', '111 222 333'),('Charlie Brown', '798 444 222'),('Daisy Miller', '432 222 789'),('Edward Green', '899 654 333');
INSERT INTO "CustomerWatchedMovie" ("customerId", "movieId") VALUES ((SELECT id FROM "Customer" WHERE name = 'Alice Johnson'), (SELECT id FROM "Movie" WHERE title = 'Inception')), ((SELECT id FROM "Customer" WHERE name = 'Alice Johnson'), (SELECT id FROM "Movie" WHERE title = 'Lady Bird')), ((SELECT id FROM "Customer" WHERE name = 'Alice Johnson'), (SELECT id FROM "Movie" WHERE title = 'Star Wars')), ((SELECT id FROM "Customer" WHERE name = 'Bob Smith'), (SELECT id FROM "Movie" WHERE title = 'Pulp Fiction')), ((SELECT id FROM "Customer" WHERE name = 'Bob Smith'), (SELECT id FROM "Movie" WHERE title = 'Get Out')), ((SELECT id FROM "Customer" WHERE name = 'Charlie Brown'), (SELECT id FROM "Movie" WHERE title = 'Jurassic Park')), ((SELECT id FROM "Customer" WHERE name = 'Charlie Brown'), (SELECT id FROM "Movie" WHERE title = 'Gladiator')), ((SELECT id FROM "Customer" WHERE name = 'Daisy Miller'), (SELECT id FROM "Movie" WHERE title = 'Goodfellas')), ((SELECT id FROM "Customer" WHERE name = 'Daisy Miller'), (SELECT id FROM "Movie" WHERE title = 'The Grand Budapest Hotel')), ((SELECT id FROM "Customer" WHERE name = 'Edward Green'), (SELECT id FROM "Movie" WHERE title = 'Avatar')), ((SELECT id FROM "Customer" WHERE name = 'Edward Green'), (SELECT id FROM "Movie" WHERE title = 'Dune'));
The INSERTs
will basically do the following:
- Insert 10 genres, movies and directors
- Insert 5 customers
- Link genres and movies respectively
- Add at least 2–3 watched movies per customer
Analysis
Ok, awesome! 💯 Now that we have the data ready, let's review our main goal:
Create coupons for each customer that has watched movies with specific genres. Points will accumulate according to the movie genre and no coupon will be added if the duration is less than 2 hours.
This is the syntax for a PL/pgSQL function:
-- $$: opens and closes the body of the function
CREATE OR REPLACE FUNCTION "functionOne"(param1 VARCHAR, param2 INTEGER)
RETURNS VARCHAR
LANGUAGE plpgsql
AS
$$
DECLARE
-- variables declaration here
BEGIN
-- function logic here
RETURN 'Done.';
END;
$$;
FIRST - Logs
Depending on the amount of data and complexity of calculations, functions can take several minutes to complete. For debugging purposes, it's recommended to add info console logs which will let you know the progress of the function overall. However, for a post data analysis, it's important to consider a more robust logs approach, so that when the function finishes you have a way to verify the results:
-- Console log in PL/pgSQL
RAISE NOTICE 'Movie: % Customer: %', movieName, customerName;
-- Will store the logs of the backfill process
CREATE TABLE "CouponBackfillHistory"(
id UUID PRIMARY KEY DEFAULT(gen_random_uuid()),
"movieId" UUID,
"movieGenreId" UUID,
"customerId" UUID,
"accumulatedPoints" NUMERIC,
"couponCreatedSuccessfully" BOOLEAN, -- indicator
"notes" TEXT -- info regarding coupon insertion
);
SECOND - Helper functions
Since we need to iterate in Movies, Customers, Genres and add Coupons depending on what it's found, we need to reuse code:
-- Helper function to insert into CouponBackfillHistory table
CREATE OR REPLACE FUNCTION "insertCouponBackfillHistory"(
movieId UUID,
movieGenreId UUID,
customerId UUID,
accumulatedPoints NUMERIC,
couponCreatedSuccessfully BOOLEAN,
notes TEXT
)
RETURNS VOID -- will not return any value
LANGUAGE plpgsql
AS
$$
BEGIN
INSERT INTO "CouponBackfillHistory"
("movieId", "movieGenreId", "customerId", "accumulatedPoints", "couponCreatedSuccessfully", "notes")
VALUES
(movieId, movieGenreId, customerId, accumulatedPoints, couponCreatedSuccessfully, notes);
END;
$$;
-- Helper function to handle generic errors
-- Ref: https://www.postgresql.org/docs/current/plpgsql-errors-and-messages.html
CREATE OR REPLACE FUNCTION "handleGenericError"(pgSqlState TEXT, pgSqlErrMsg TEXT, pgFunctionName TEXT)
RETURNS VOID -- will not return any value
LANGUAGE plpgsql
AS
$$
BEGIN
RAISE EXCEPTION USING
ERRCODE = pgSqlState,
MESSAGE = pgSqlErrMsg,
HINT = pgSqlErrMsg || ' in function ' || pgFunctionName;
END;
$$;
THIRD - The Main Function
CREATE OR REPLACE FUNCTION "backfillCoupons"(customerName VARCHAR(255))
RETURNS VARCHAR
LANGUAGE plpgsql
AS
$$
DECLARE
customers RECORD; -- Iterator variables
movies RECORD;
genreName VARCHAR(255); -- Other variables
genrePoints INTEGER;
genrePointsApply BOOLEAN;
movieDuration INTEGER;
pointsCnt INTEGER := 0; -- Counters
recordsCreated INTEGER := 0;
BEGIN
RAISE NOTICE '----------------- START ------------------';
RAISE NOTICE 'Customer: %', customerName;
FOR customers IN -- Customer records loop
SELECT c.id, c.name, cwm."movieId" watched_movie_id
FROM "Customer" c
JOIN "CustomerWatchedMovie" cwm ON cwm."customerId" = c.id
WHERE c.name = customerName
LOOP
pointsCnt := 0;
FOR movies IN -- Movie records loop
SELECT DISTINCT m.id, m.title, m.duration, mg."genreId" genre_id,
m.writers, m.cast, d.name director_name
FROM "Movie" m
JOIN "Director" d on d.id = m."mainDirectorId"
JOIN "MovieGenre" mg on mg."movieId" = m.id
WHERE m.id = customers.watched_movie_id
LOOP
RAISE NOTICE 'Movie: % Id: %', movies.title, movies.id;
BEGIN
SELECT g.name, g."pointsApply", g.points
INTO genreName, genrePointsApply, genrePoints
FROM "Genre" g
WHERE g.id = movies.genre_id;
IF genrePointsApply = FALSE THEN
RAISE NOTICE 'Points do not apply for Genre';
PERFORM "insertCouponBackfillHistory"(movies.id,movies.genre_id, customers.id, 0, FALSE, 'Points do not apply for Genre');
CONTINUE;
ELSIF movies.duration < 120 THEN
RAISE NOTICE 'Movie duration is not equal or greater than 2 hours';
PERFORM "insertCouponBackfillHistory"(movies.id, movies.genre_id, customers.id, genrePoints, FALSE, 'Movie duration is not equal or greater than 2 hours');
CONTINUE;
END IF;
pointsCnt := pointsCnt + genrePoints;
INSERT INTO "Coupon" ("movieId", "customerId", "points")
VALUES (movies.id,customers.id,genrePoints);
PERFORM "insertCouponBackfillHistory"(movies.id, movies.genre_id, customers.id, genrePoints, TRUE, 'Coupon created succesfully');
recordsCreated := recordsCreated +1;
EXCEPTION --Handle unexpected errors
WHEN OTHERS THEN
PERFORM "handleGenericError"(SQLSTATE, SQLERRM, 'backfillCoupons');
END;
END LOOP; -- End Movie records loop
RAISE NOTICE 'Total Points Accumulated: %', pointsCnt;
END LOOP; --End Customer records loop
RAISE NOTICE 'Total number of coupons created: %', recordsCreated;
RAISE NOTICE 'Check CouponsBackfillHistory for details';
RETURN 'Success.';
END;
$$;
The function is simple, but looks wild at first sight 🍃🐯 haha let's break it down. It basically performs the following actions:
- Iterate in one Customer
- Iterate in each Movie watched by the Customer
- Verify the Genre points and the Movie duration
- Add a Coupon or jump to the next iteration depending on the conditions
Side note: the
SELECT.. INTO..
query could have been omitted and included in a JOIN clause in the previous query, but was added for DEMO purposes.
Execute the function:
-- for all customers
SELECT "backfillCoupons"(name) FROM "Customer";
-- for one customer
SELECT "backfillCoupons"(name) FROM "Customer" WHERE c.name = 'Daisy Miller';
Logs for Daisy Miller:
----------------- START ------------------
Customer: Daisy Miller
Movie: Goodfellas Id: 487e3227-f6f0-4faf-8011-13dfa7abcc77
Total Points Accumulated: 4
Movie: The Grand Budapest Hotel Id: 3bd9fd29-f593-4b1f-8904-59456918e41a
Movie duration is not equal or greater than 2 hours
Total Points Accumulated: 0
Total number of coupons created: 1
Check CouponsBackfillHistory for details
Now let's execute a query to see the records:
-- Query 1
SELECT m.title "movieTitle", g."name" genre, cbh."accumulatedPoints", cbh."couponCreatedSuccessfully", cbh.notes
FROM "CouponBackfillHistory" cbh
JOIN "Movie" m ON m.id = cbh."movieId"
JOIN "Genre" g ON g.id = cbh."movieGenreId";
The coupon created:
-- Query 2
SELECT c.id, m.title "movieTitle", g."name" genre, cu.name, c.points
FROM "Coupon" c
JOIN "Movie" m ON m.id = c."movieId"
JOIN "MovieGenre" mg ON mg."movieId" = m.id
JOIN "Genre" g ON g.id = mg."genreId"
JOIN "Customer" cu ON cu.id = c."customerId";
Important things to keep in mind:
- Use
""
to usecamelCase
instead ofsnake_case
in PostgreSQL. - Init all needed variables in the
DECLARE
section -
FOR LOOPs
andSELECT.. INTO..
statements depend on previous declared variables to work - Organize the code inside your function with
BEGIN.. END;
statements - Use
SELECT DISTINCT..
to avoid duplicate rows - No changes will be committed to the DB unless the function completes
-
EXCEPTIONS
will stop the execution of the function - Use
CONTINUE;
statement to jump to the next iteration in a loop
Conclusions
- Getting familiarized with PL/pgSQL functions, is a nice ability that will help you automate SQL statements, iterate in complex data and execute operations depending on certain conditions.
- Always consider a robust logs approach when creating any kind of function involving data changes.
- Take into consideration the time you have and decide whether to use PL/pgSQL or implement a PostgreSQL connector with your preferred programming language for a better debugging experience.
PostgreSQL has lots of docs and examples throughout the internet. Checkout these references, I consider very helpful, for further reading:
My favorite DB IDE: DBeaver Community
Happy Coding! 😃💻
Top comments (0)