Apress Procedural Programming With PostgreSQL
Apress Procedural Programming With PostgreSQL
S
E
AG
NGU
Y LA
ER
D QU
RE
TU
RUC
ST
B
TA
Procedural Programming DA
with PostgreSQL DA
TAB
A SE
DA
T
PL/pgSQL
Design Complex Database-Centric
Applications with PL/pgSQL
—
Baji Shaik
Dinesh Kumar Chemuduru
Procedural Programming
with PostgreSQL
PL/pgSQL
Design Complex Database-Centric
Applications with PL/pgSQL
Baji Shaik
Dinesh Kumar Chemuduru
Procedural Programming with PostgreSQL PL/pgSQL: Design Complex
Database-Centric Applications with PL/pgSQL
Baji Shaik Dinesh Kumar Chemuduru
Texas, TX, USA Andhra Pradesh, India
Acknowledgments�������������������������������������������������������������������������������������������������xvii
Introduction������������������������������������������������������������������������������������������������������������xix
v
Table of Contents
Summary������������������������������������������������������������������������������������������������������������������������������������ 25
What’s Next��������������������������������������������������������������������������������������������������������������������������������� 25
Iterative Statement��������������������������������������������������������������������������������������������������������������������� 68
LOOP Statement�������������������������������������������������������������������������������������������������������������������� 69
WHILE Statement������������������������������������������������������������������������������������������������������������������������ 72
FOR Statement���������������������������������������������������������������������������������������������������������������������������� 74
Example Use Cases��������������������������������������������������������������������������������������������������������������������� 78
Example 1������������������������������������������������������������������������������������������������������������������������������ 78
Example 2������������������������������������������������������������������������������������������������������������������������������ 81
Best Practices of Using Control Statements in PL/pgSQL����������������������������������������������������� 84
Summary������������������������������������������������������������������������������������������������������������������������������������ 85
What’s Next��������������������������������������������������������������������������������������������������������������������������������� 85
vii
Table of Contents
viii
Table of Contents
ix
Table of Contents
Summary���������������������������������������������������������������������������������������������������������������������������������� 199
What’s Next������������������������������������������������������������������������������������������������������������������������������� 199
x
Table of Contents
xi
Table of Contents
Index��������������������������������������������������������������������������������������������������������������������� 311
xii
About the Authors
Baji Shaik, currently serving as a Senior Database
Consultant at AWS Professional Services, embarked on
his journey into the world of databases in 2011. Since
then, his expertise has encompassed an array of database
technologies, including Oracle, PostgreSQL, EDB Postgres,
Amazon RDS, Amazon Aurora, Amazon Redshift, and
Greenplum. Baji’s extensive background spans both depth
and breadth, showcasing his mastery in SQL/NoSQL
database technologies.
Baji stands out as a Database Migration Expert, having
successfully developed numerous database solutions that
tackle complex business challenges, particularly in migrating databases from on-
premises environments to Amazon RDS and Aurora PostgreSQL/MySQL. His prowess
also extends to performance optimization, having fine-tuned RDS/Aurora PostgreSQL/
MySQL databases to achieve remarkable performance benchmarks.
With a passion for knowledge sharing, Baji has authored several notable books on
PostgreSQL, such as PostgreSQL Configuration, Beginning PostgreSQL on the Cloud, and
PostgreSQL Development Essentials. His commitment to education and information
dissemination is further evident through his contributions to conferences, workshops,
and a multitude of insightful blogs within the AWS blog community.
xiii
About the Authors
A coding enthusiast at heart, Dinesh finds joy in crafting applications using Flutter,
Golang, and C++, platforms where his creativity knows no bounds. His proficiency
extends to the deployment phase, as he deftly navigates Kubernetes to bring his coding
creations to life. In the literary domain, Dinesh stands as a coauthor of the esteemed
PostgreSQL High Performance Cookbook, a testament to his mastery of the subject
matter. Beyond his own works, he actively engages in the appraisal of fellow authors’
PostgreSQL books, cementing his status as a valued participant in the exchange of
knowledge.
Dinesh’s impact reverberates through his open source contributions, which include
the inception and enrichment of projects such as PTOR – an ingenious RPO/RTO/SLA
calculator tailored for PostgreSQL. Another tool, “hammerpost,” sets a benchmark for
synthetic parameter evaluation in PostgreSQL, seamlessly integrated with HammerDB.
xiv
About the Technical Reviewer
Deepak Ramnandan Mahto works as a PostgreSQL
Database Engineer at Google Cloud. He has been working
with PostgreSQL since 2018, and he also worked as a
database migration consultant at AWS. He is also a keen
blogger and loves to publish articles on migration, best
practices, and on cloud with PostgreSQL. He loves to
code and build database-related utilities using PL/pgSQL
and SQL.
xv
Acknowledgments
I would like to express my gratitude to several individuals who have played a crucial
role in making this book a reality. A heartfelt thank-you to Apress Media for providing
me with this valuable opportunity. I am especially grateful to my coauthor and mentor,
Dinesh Kumar Chemuduru, for his exceptional collaboration. I want to express my
gratitude to Divya Modi and Nirmal Selvaraj for being understanding of our hectic
schedules and providing us with continuous support throughout the entire process.
Special thanks to Deepak Mahto for his thorough review of the book. Lastly, I am
profoundly thankful to my parents, Lalu Saheb Shaik and Nasar Bee, whose unwavering
support has shaped me into the person I am today.
—Baji Shaik
xvii
Introduction
The PostgreSQL engine comes with its own dedicated procedural language, similar
to procedural languages found in other commercial database engines. This language,
known as PL/pgSQL, offers a range of powerful features that developers have long
desired. For instance, PL/pgSQL includes certain object-oriented programming
capabilities like the ability to define custom operators and types, as well as custom
aggregates.
In contrast to other programming languages supported by PostgreSQL, PL/pgSQL is
intricately linked with the PostgreSQL database engine interface. This tight integration
ensures optimal performance and a seamless fit for constructing business logic on
the database side. In this book, we not only introduce the fundamentals of PL/pgSQL,
but we also dive deep into specific use cases that we’ve implemented for particular
scenarios. Our aim is to comprehensively cover the various features, functionalities, and
application scenarios of PL/pgSQL, offering assistance in crafting effective server-side
objects with ease.
Through the content of this book, you will gain an understanding of PL/pgSQL’s
design and dive deep into its transaction model, including how commit and rollback
operations function. You’ll discover strategies for optimizing PL/pgSQL functions and
procedures and explore the mechanics of inline or anonymous server-side code, along
with its limitations. Furthermore, you’ll acquire insights into debugging and profiling
PL/pgSQL code and learn techniques for conducting statistical analyses on the PL/
pgSQL code you create.
xix
CHAPTER 1
Introduction to PL/pgSQL
In this chapter, we will start with an introduction of PL/pgSQL, on what is PL/pgSQL
and what are the key features of it. We will talk about some common use cases where
PL/pgSQL is used. PL/pgSQL comes by default when you install the PostgreSQL server.
However, we will provide the steps to install PL/pgSQL. We will explain how PL/pgSQL
works with a simple flow diagram. We will show some basic examples of PL/pgSQL code
blocks which are called anonymous and named code blocks.
1
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_1
Chapter 1 Introduction to PL/pgSQL
PL/pgSQL Installation
PL/pgSQL is already included in PostgreSQL, so if you have PostgreSQL installed, you
should have PL/pgSQL as well. However, you may need to enable it if it is not already
enabled. Here are the steps to enable PL/pgSQL in PostgreSQL:
1. Install PostgreSQL psql client to connect to the database, or you
can use the pgAdmin client tool.
For Ubuntu, the following are the simple steps to install the client:
2
Chapter 1 Introduction to PL/pgSQL
3
Chapter 1 Introduction to PL/pgSQL
my_var := some_param * 10
4
Chapter 1 Introduction to PL/pgSQL
The SQL queries are actually parsed at this point, and parser hooks are used to replace
variables/parameters with PARAM nodes in the parse tree. The PL/pgSQL statement tree
is very similar to a PostgreSQL execution tree. After the parse and compile, the call handler
then executes that statement tree. On the first execution of a statement node that has an
SQL query in it, that query is prepared via the Server Programming Interface (SPI). The SPI
provides a simple and efficient way to execute SQL commands, retrieve query results, and
manipulate the database. The compiled code is then executed by the server. Based on any
variable and control structure declaration, the server creates a new execution environment
for the PL/pgSQL code. If the PL/pgSQL code is a function or stored procedure that returns
a result set, the server will send the result set back to the client. Once the execution of the
code is complete, the server will clean up any resources that were used by the PL/pgSQL
code, including variables and any temporary tables that were created.
Figure 1-1 represents the flow of execution.
5
Chapter 1 Introduction to PL/pgSQL
This diagram illustrates the high-level steps of the PL/pgSQL execution flow.
However, it's important to note that PL/pgSQL code can be quite complex and may
include multiple control structures, error handling blocks, and nested and even
recursive PL/pgSQL function calls and trigger invocations and database operations. The
actual execution flow of a specific PL/pgSQL function or stored procedure will depend
on the specific code and logic used. This call hierarchy is not limited to PL/pgSQL. All
procedural languages share the common entry point of the fmgr, so they can be mixed
and matched in trigger and function call stacks.
PL/pgSQL Blocks
PL/pgSQL is a block-structured language. The basic unit in any PL/pgSQL code is a
block. All PL/pgSQL code is composed of a single block or blocks that occur either
sequentially or nested within another block. There are two kinds of blocks:
• Anonymous or unnamed blocks (DO)
DO $$
[ <<label>> ]
[ DECLARE
-- Variable declaration here
]
BEGIN
-- Execute statements here
END [ label ];
$$;
6
Chapter 1 Introduction to PL/pgSQL
Now, let us start with a simple hello world code block, which does not have any
name associated with it:
postgres=# DO
$$
BEGIN
RAISE NOTICE 'Hello World';
END;
$$;
NOTICE: Hello World
DO
In the preceding example, the RAISE NOTICE command will help us to print the
given message on the client console. As you can see here, the block is declared without
a name, and if you want to print Hello World, then you have to repeat the same set of
instructions again.
Now, let us print the Hello World line by line rather than in a single line:
postgres=# DO
$o$
BEGIN
RAISE NOTICE $i$
Hello
World
$i$;
END;
$o$;
NOTICE:
Hello
World
DO
In the preceding example, we used different multiline specifiers. The whole block got
enclosed by $o$, and the inner Hello World got enclosed by $i$. From this example, we
can learn that in PL/pgSQL, we can have the nested multiliners, where each multiline
should follow its own enclosure.
7
Chapter 1 Introduction to PL/pgSQL
Now, let us write a nested BEGIN ... END inside a main BEGIN ... END block. Here
is an example:
postgres=# DO
$$
BEGIN
BEGIN
RAISE NOTICE 'Hello World';
END;
END;
$$;
NOTICE: Hello World
DO
In the preceding example, we print the Hello World message from the nested BEGIN
... END block. It is possible to have multiple nested statements inside a single BEGIN...
END block. We will see more of these in the coming chapters, where we discuss exception
handling.
Now, let us go a little deeper and print the Hello World message from the nested
unnamed code block:
postgres=# DO
$o$
BEGIN
DO
$i$
BEGIN
RAISE NOTICE 'Hello World';
END;
$i$;
END;
$o$;
NOTICE: Hello World
DO
As you can see in the preceding example, we are able to define an unnamed block
inside an unnamed block. By defining the nested code blocks, we can segregate a large
unnamed block into multiple stand-alone work units. We don’t need to write a nested
8
Chapter 1 Introduction to PL/pgSQL
unnamed block; in most of the cases, the nested BEGIN...END block would be sufficient.
But in general, we don’t keep large unnamed blocks in the database; rather, we store
them inside with a name (function/procedure), and we call that object name, whenever
it is required.
Here is another example where we can have a nested block inside an exception:
postgres=# DO $inline$
BEGIN
PERFORM 1/0;
RAISE NOTICE 'Hello World!';
EXCEPTION
WHEN OTHERS THEN
DO $$
BEGIN
RAISE NOTICE 'Got error';
END;
$$;
END;
$inline$;
NOTICE: Got error
DO
PL/pgSQL does not restrict the anonymous blocks as stand-alone objects; we can
also embed these inline definitions inside the function or procedure object. We haven’t
discussed about procedures and functions yet, but showing you an example where
you can declare the inline anonymous block inside a function:
9
Chapter 1 Introduction to PL/pgSQL
(1 row)
How it works is, unlike other procedural languages, PL/pgSQL gives an inline
statement handler plpgsql_inline_handler. By using this handler, PL/pgSQL executes
the unnamed or inline PL/pgSQL blocks. If there are any nested code blocks, then those
will be evaluated recursively by the plpgsql_inline_handler.
Note that it is not possible to return any value from the unnamed code blocks.
Always use anonymous code blocks to define the business logic, which involves
making a set of function or procedure calls. If you want to return any value from
anonymous blocks, then we might need to use any session-level variables, which
need to be set inside the anonymous block, and access them from the outside of
the blocks.
Named Blocks
Named blocks have a name associated with them, are stored in the database, can be
executed repeatedly, and can take in parameters.
A named block in PL/pgSQL is defined using the following syntax:
<<label>>
DECLARE
-- declare variables here
BEGIN
-- Named block's code here
END;
Here, label is the name of the block and is enclosed within << >> brackets.It is not
just cosmetic, but that nested code blocks can refer to outer variables by using that label
instead of finding the innermost match for the variable name.
10
Chapter 1 Introduction to PL/pgSQL
The DECLARE section is used to declare variables that are used within the block,
while the BEGIN and END sections contain the actual code for the block.
Once a named block has been defined, it can be called from within the same
function or procedure using the PERFORM statement:
PERFORM block_name;
This will execute the code within the named block. Named blocks can be called
multiple times within the same function or procedure, allowing for reusable and
modular code.
Here's an example of a PL/pgSQL function that uses named blocks to calculate the
factorial of a number:
RETURN result;
END;
$$ LANGUAGE plpgsql;
In this example, the named block factorial_loop is used within a FOR loop to
calculate the factorial of the input number. The DECLARE section declares a variable
result to store the final result, while the BEGIN and END sections contain the code for
the named block.
The named block is called within the FOR loop using the LOOP statement. This
allows the loop to continue until it reaches the specified number of iterations.
Once the FOR loop is complete, the final result is returned by the function.
To call the function and calculate the factorial of a number, you would execute the
following SQL statement:
SELECT factorial(5);
Summary
In this chapter, we talked about PL/pgSQL use cases, installation, and how it works with
a flow diagram. We have also shown how simple PL/pgSQL code blocks look like and
how to execute them. These examples will help you to understand and start with PL/
pgSQL code. In the next chapter, we will talk about the variables that are used inside
PL/pgSQL code blocks. We will start with how to declare those variables and dive deep
into different types of methods to use based on the use cases. These will help you to
decide which type of variables you should use when building the PL/pgSQL code for the
functions or procedures.
What’s Next
In the next chapter, we will be covering some key features of PL/pgSQL variables like the
following:
• Variable Types: Explore the different types of variables and learn
how to choose the appropriate variable type for your needs.
• Variable Scoping Mastery: Gain a better grasp of variable scoping
rules and how to manage variables effectively within different blocks.
• Variable Naming Conventions: Learn about naming conventions
that can help you write more maintainable and readable code.
• Advanced Variable Usage: Extend your knowledge by using
variables in more complex scenarios.
12
CHAPTER 2
PL/pgSQL Variables
In the previous chapter, we talked about what PL/pgSQL is and some use cases where
you need it. We also explained the steps to install and the execution flow of PL/pgSQL
with a simple diagram. We have discussed some PL/pgSQL anonymous and named
code block examples which will help in understanding the basics. In this chapter, we will
introduce variables that are used in the PL/pgSQL code. We will provide different types
of variables and use cases on when to use them. We will show the declaration and scope
of variables and different types of variables with some examples.
Declaring Variables
PL/pgSQL offers to declare variables in its declaration section of the block. Here is an
example:
postgres=# DO
$$
DECLARE
v_var1 INT;
v_var2 INT:=10;
BEGIN
RAISE NOTICE 'v_var1 %', v_var1;
RAISE NOTICE 'v_var2 %', v_var2;
13
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_2
Chapter 2 PL/pgSQL Variables
END;
$$;
NOTICE: v_var1 <NULL>
NOTICE: v_var2 10
DO
In the preceding example, we have declared two variables. The v_var1 variable is
declared but not defined with any value to it. The second variable v_var2 is declared but
has a value 10 to it. If we try to access a variable which is declared and not defined, then
expect that we would get NULL from it. In the preceding output, we can also see that the
v_var1 is set to NULL.
From the previous chapter, we learned that we can have nested blocks inside the
main PL/pgSQL block. By using that nested block feature, we have multiple declarations
and multiple variables in the single PL/pgSQL block. Here is an example:
postgres=# DO
$o$
DECLARE
v_var1 INT:=10;
BEGIN
DO
$i$
DECLARE
v_var1 INT:=100;
BEGIN
RAISE NOTICE 'v_var1 %', v_var1;
END;
$i$;
END;
$o$;
NOTICE: v_var1 10
NOTICE: v_var1 100
DO
14
Chapter 2 PL/pgSQL Variables
In the preceding example, we have nested unnamed blocks, where we have two
variable declarations. The variable v_var1 in the main block is declared with the value
10, and also v_var1 in the inside block is declared with the value 100. Also, the scope of
the variable is always local.
Variable Scope
The scope of the declared variables in PL/pgSQL is always local to its current block. That
is, once we declare the variable in a block, we can’t access them outside of that block.
Here is an example:
postgres=# DO
$$
DECLARE
BEGIN
DECLARE
v_var1 INT:=10;
BEGIN
RAISE NOTICE 'v_var1 %', v_var1;
END;
15
Chapter 2 PL/pgSQL Variables
From the preceding example, as you can see we got an error, column "v_var1"
does not exist, when we try to access the variable v_var1 from the outer BEGIN...
END block. That is, the scope of variable v_var1 is local to that inner BEGIN...END block,
and we can’t access them from the outside. If we declare the variable v_var1 in the
parent BEGIN...END block, then we can access that variable inside the nested BEGIN...
END blocks too. Because the v_var1 is declared at the parent block level, its scope is
at the whole block level. Now, you might have questions like what if we declare the
same variable v_var1 in parent and nested BEGIN...END blocks and how to access the
parent block’s v_var1 along with the local declared variable. Here is an example for this
use case:
postgres=# DO
$$
DECLARE
v_var1 INT:=1;
BEGIN
DECLARE
v_var1 INT:=10;
BEGIN
RAISE NOTICE 'v_var1 %', v_var1;
END;
END;
$$;
NOTICE: v_var1 10
DO
From the preceding example, we were only able to access the variable v_var1, which
was declared in the nested BEGIN...END block. To access the parent v_var1 variable,
then we should access that variable with the block’s label. That is, we have to give a label
name to the parent block, and then we should access the v_var1 along with its label
name. Here is an example:
postgres=# DO
$$
<<parent>>
DECLARE
v_var1 INT := 1;
16
Chapter 2 PL/pgSQL Variables
BEGIN
DECLARE
v_var1 INT := 10;
BEGIN
RAISE NOTICE 'Parent v_var1 %', parent.v_var1;
RAISE NOTICE 'Local v_var1 %', v_var1;
END;
END;
$$;
NOTICE: Parent v_var1 1
NOTICE: Local v_var1 10
DO
Constant Variables
We can declare constant variables inside PL/pgSQL, which shouldn’t get updated by
further instructions. Here is an example:
postgres=# DO
$$
DECLARE
v_c_pi CONSTANT REAL DEFAULT 3.14;
BEGIN
v_c_pi = 3.15;
END;
$$;
ERROR: variable "v_c_pi" is declared CONSTANT
LINE 6: v_c_pi = 3.15;
In the preceding example, we declared the variable v_c_pi as CONSTANT and set its
DEFAULT value as 3.14. But, in further instructions, when we tried to update its value as
3.15, we got the exception as the variable is declared as CONSTANT, which should not get
updated by any of the instructions.
17
Chapter 2 PL/pgSQL Variables
Variable Alias
In PL/pgSQL, we can also create a reference variable which points to another variable or
system variables. For example, if we want to create a reference variable or a short-length
variable name to another variable, then we can create those short-length variables using
ALIAS. Here is an example:
DO
$$
DECLARE
var_earth_sun_distance REAL DEFAULT 149.6;
v_e_s_d ALIAS FOR var_earth_sun_distance;
BEGIN
RAISE NOTICE 'Reference #1 %', v_e_s_d;
v_e_s_d = 149.5;
RAISE NOTICE 'Actual Variable %', var_earth_sun_distance;
END;
$$;
NOTICE: Reference #1 149.6
NOTICE: Actual Variable 149.5
DO
DO
$$
DECLARE
var_earth_sun_distance REAL DEFAULT 149.6;
v_e_s_d ALIAS FOR var_earth_sun_distance;
vd ALIAS FOR v_e_s_d;
BEGIN
RAISE NOTICE 'Reference #1 %', v_e_s_d;
RAISE NOTICE 'Reference #2 %', vd;
18
Chapter 2 PL/pgSQL Variables
In the preceding example, we created an ALIAS variable from another ALIAS variable.
That is, we created a new reference variable, which points to another reference variable.
Also, if we update the second reference variable, it will reflect those changes on the
actual variable via the first reference.
PL/pgSQL supports the following types of variables:
• Scalar Variables
• Array Variables
• Record Variables
• Cursor Variables
Scalar Variables
In all the previous examples, we demonstrated scalar variables. Scalar variables hold a
single value of a specific data type, such as an integer or a string. They can be declared
and initialized using the DECLARE keyword and can be assigned values using the :=
assignment operator.
19
Chapter 2 PL/pgSQL Variables
For example, the following code declares each type of scalar variable:
postgres=# DO $$
DECLARE
my_int integer := 1;
my_text text := 'Hello, world!';
my_bool boolean := true;
BEGIN
-- perform operations on the scalar variables
my_int := my_int + 10;
my_text := my_text || ' How are you?';
my_bool := not my_bool;
In the preceding example, we declare three scalar variables: my_int with a value
of 1 and a data type of integer, my_text with a value of 'Hello, world!' and a data
type of text, and my_bool with a value of true and a data type of boolean. We then
perform some operations on these variables using arithmetic, concatenation, and logical
negation. Finally, we print the values of the variables using the RAISE NOTICE statement.
Scalar variables are useful for storing temporary values or performing calculations
within a stored procedure or function. They can be used in a variety of ways, such as
tracking state, performing conditional logic, or holding input or output parameters.
20
Chapter 2 PL/pgSQL Variables
Array Variables
Array variables are variables that can hold multiple values of the same data type. They
are declared using a data type followed by the [] syntax, such as integer[] or text[].
For example, the following code declares an array variable:
postgres=# DO $$
DECLARE
my_array integer[] := '{1, 2, 3, 4, 5}';
BEGIN
-- print the entire array
RAISE NOTICE 'my_array = %', my_array;
In the preceding example, we declare an array variable my_array with a data type of
integer[] and initialize it with the values {1, 2, 3, 4, 5}. We then print the entire
array using the RAISE NOTICE statement, access and print the value of the second
element of the array, modify the value of the third element of the array, and print the
entire array again.
Array variables are useful for storing and manipulating sets of related data, such
as lists of numbers, strings, or boolean values. They can be used in a variety of ways,
such as for performing calculations on multiple values at once, storing input or output
parameters, or passing data between functions or procedures.
21
Chapter 2 PL/pgSQL Variables
Record Variables
Record variables are used to store a row of data from a table or a query result. They are
declared using the %ROWTYPE attribute and can be assigned values using the SELECT INTO
statement.
For example, the following code declares a record variable:
postgres=# DO $$
DECLARE
my_record emp%ROWTYPE;
BEGIN
-- select a row of data into the record variable
SELECT * INTO my_record FROM emp WHERE emp_id = 100;
22
Chapter 2 PL/pgSQL Variables
In the preceding example, we declare a record variable my_record that holds a row
of data from the emp table. We then select a row of data into the record variable using
a SELECT statement, print the values of the record variable using the RAISE NOTICE
statement, update the values of the record variable, and update the row of data in the
table using an UPDATE statement.
Record variables are useful for storing and manipulating rows of data from tables
or query results within a stored procedure or function. They can be used in a variety of
ways, such as for passing data between functions or procedures, performing calculations
on data, or storing input or output parameters.
Cursor Variables
Cursor variables are variables that hold a reference to a cursor, which is a named SQL
statement that can be executed repeatedly. They are declared using the CURSOR
keyword.
For example, the following code declares a record variable named “my_row” that
corresponds to the columns of a table named “my_table”:
postgres=# DO $$
DECLARE
my_cursor refcursor;
my_record emp%ROWTYPE;
BEGIN
-- open the cursor and fetch the first row of data
OPEN my_cursor FOR SELECT * FROM emp;
FETCH my_cursor INTO my_record;
23
Chapter 2 PL/pgSQL Variables
24
Chapter 2 PL/pgSQL Variables
Summary
In this chapter, we learned about the types of variables with nice examples and
explanations of how they work. We went through the declaration of variables in the
DECLARE section of the code as well as in an independent code block on the fly. The
scope of variables is different for each block of PL/pgSQL code. We explained how the
scope of variables varies with some examples. Also, we looked at constant variables and
how to use aliases for the variables.
What’s Next
In the next chapter, we will be covering some key features of PL/pgSQL data types like
the following:
• Advanced Data Type Exploration: Delve into complex data types
like composite and range types to efficiently manage intricate data
structures.
• Custom Domain Types: Create user-defined domain types with
specific constraints to ensure data integrity and validation in your
applications.
• Optimal Type Selection: Understand how to choose the right
data type based on application needs, considering performance,
maintainability, and space efficiency.
• Dynamic Application Building: Harness PL/pgSQL data types to
create adaptable applications that handle diverse data inputs and
outputs.
25
CHAPTER 3
Data Types
Choosing the right data type for the data being used is a best practice that developers
should follow. Choosing the correct data type can help us avoid many data validation
checks and prevent exceptions when incorrect data is entered. The PostgreSQL database
engine supports multiple data types for each data kind. By using these data types, we
build tables and insert proper data into them. We can use most of the PostgreSQL data
types inside PL/pgSQL blocks, besides pseudo-types like any, anyenum, and record. It is
not just limited to using existing data types; we can also construct new composite types
by using these existing data types.
Understanding data types is crucial for developing efficient and effective database
applications. We will cover data types such as integers, floating-point numbers,
booleans, and strings, as well as more advanced data types like arrays and composite
types. By the end of this chapter, you will have a good understanding of the different data
types available in PL/pgSQL and how to use them effectively in your business logic. We
will also cover how to declare variables and constants using these data types and how to
convert between different data types using type casting. This knowledge is essential for
writing robust and error-free PL/pgSQL code that can handle a variety of data inputs and
outputs.
27
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_3
Chapter 3 PL/pgSQL Data Types
Note You can find the list of supported data types here: www.postgresql.
org/docs/current/datatype.html.
postgres=# DO
$$
declare
v_int int:=10;
v_text text:='text';
v_boolean boolean:=false;
begin
raise notice 'v_int %',v_int;
raise notice 'v_text %',v_text;
raise notice 'v_boolean %',v_boolean;
end;
$$;
NOTICE: v_int 10
NOTICE: v_text text
NOTICE: v_boolean f
DO
In the preceding example, we declared three variables along with their data types
and printed the results. Let us rewrite the previous example as follows and see the
results:
postgres=# DO
$$
declare
int int:=10;
text text:='text';
28
Chapter 3 PL/pgSQL Data Types
boolean boolean:=false;
begin
raise notice 'int %',int;
raise notice 'text %',text;
raise notice 'boolean %', boolean;
end;
$$;
NOTICE: int 10
NOTICE: text text
NOTICE: boolean f
DO
As you can see in the preceding example, variable names can be declared with the
corresponding data type names. As data type names are not reserved keywords, we can
also use them as variable names. Here, we are demonstrating the possibility of declaring
variable names as data types. However, this kind of variable declaration is not typically
used in production.
In the previous example, as well as in the previous chapter, we discussed only the
base scalar data types in PostgreSQL. These data types accept only a single value into
their declared variable type. To obtain a list of available base scalar types in the current
database, we can query the PostgreSQL catalog table pg_type. The following query
returns base data types in the database limited to three only:
29
Chapter 3 PL/pgSQL Data Types
Supported Types
PostgreSQL not only supports base types but also composite types, enumerated types,
pseudo-types, and range types. To list all the supported type categories in the database,
query the pg_type catalog table with the following query:
In the preceding output we see, the return values are m, r, p, d, c, and b. All the types,
including the user-defined types, will fall under these category types. Table 3-1 describes
each code value.
30
Table 3-1. Code values
Code Description Example
31
PL/pgSQL Data Types
Chapter 3 PL/pgSQL Data Types
Base Type
Base types are the primitive building blocks for other data types in the database. For
example, take the domain type information_schema.yes_or_no which only accepts the
text which is of length 3, and the values have to be YES or NO. This domain type is used as
a replacement to bool to maintain the information_schema standards. Let us examine
the underlying data type of this yes_or_no domain type using the format_type function
as follows:
From the preceding output, we got the domain’s base type as 1043. By using the
following query, we see what is the data type which is mapped to 1043:
By using the following query, see whether this 1043 is a base type or not:
As you can see from the preceding results, the base types are the primitive building
blocks to the data types which we create. By using these base types, we can build our
own composite, domain, and range types.
32
Chapter 3 PL/pgSQL Data Types
Composite Type
In PostgreSQL, it is possible to create custom data types using the base data types.
Custom data types can be defined using not only the base types but also other custom
data types or other data types excluding pseudo-types.
Let us construct a simple composite type with the help of base types:
postgres=# DO
$$
DECLARE
v_composite all_fields_base;
BEGIN
v_composite:= (1, 't', current_date)::all_fields_base;
RAISE NOTICE '%', v_composite;
END;
$$;
NOTICE: (1,t,2023-03-18)
DO
As you can see in the preceding example, we created the variable v_composite with
data type all_fields_base and assigned multiple values (1, 't', current_date) as
a single unit. Here, we also used :: which is a data type casting operator. That is, this ::
operator converts the provided values into the defined data type.
33
Chapter 3 PL/pgSQL Data Types
It is not just limited to use only the base types as members inside the composite
type. We can also declare a member as a composite, pseudo, or range type. Consider the
following example:
Now, let’s write a simple example by using the preceding mixed_fields_type data
type and assign some value to it:
DO
$$
DECLARE
v_mixed_type mixed_fields_type;
BEGIN
v_mixed_type:= (3.14, (1, 't', current_date)::all_fields_
base)::mixed_fields_type;
RAISE NOTICE '%', v_mixed_type;
RAISE NOTICE 'Value % Type %', v_mixed_type.d, pg_typeof(v_mixed_type.d);
RAISE NOTICE 'Value % Type %', v_mixed_type.f, pg_typeof(v_mixed_type.f);
END;
$$;
NOTICE: (3.14,"(1,t,2023-03-18)")
NOTICE: Value 3.14 Type real
NOTICE: Value (1,t,2023-03-18) Type all_fields_base
DO
From the preceding example, as you can see we saved a nested composite value into
the nested composite variable. Also, by using the object reference notation, we are able
to access the required field from this composite type. By using pg_typeof, we displayed
the data type of the member variable.
34
Chapter 3 PL/pgSQL Data Types
Domain Type
Domain types are used when a particular set of constraints need to be enforced on a
base type. They are essentially a user-defined type that is based on an existing type.
We can create a domain type using the CREATE DOMAIN statement. A domain type can
be used when we need to enforce certain conditions on input values. For example,
information_schema.yes_or_no is a domain that only allows YES or NO values. Here is an
example:
35
Chapter 3 PL/pgSQL Data Types
Try inserting text that is more than eight characters and see if the validation is
enforced or not:
postgres=# INSERT INTO preferences VALUES ('this is big user id', 'this is
big password');
ERROR: value for domain tiny_text violates check constraint "tiny_text_
length_constraint"
In the preceding example, if you attempt to send more than eight characters, the domain
check constraint is violated, and an error message will be displayed as shown earlier.
Domain types can also be created on top of composite types. Consider the following
example, where we create a common type called products_type and then create a
domain type, toys_type, with a constraint. This type will only allow products in the toys
category and will throw an error if you try to insert products from another category.
postgres=# CREATE TYPE products_type as (id text, category text, sku int,
price real);
CREATE TYPE
Now, try to insert another category product and see if the domain constraint got
enforced or not:
Perfect! We can restrict other categories of data from entering the toys_products
table. We can achieve this with a plain check constraint on the table, but creating a
domain type allows us to not only use toys_type at the table level but also across
procedures and functions. This domain type ensures that the toys_type variable always
holds toy category data.
36
Chapter 3 PL/pgSQL Data Types
Pseudo-Type
PostgreSQL supports several pseudo data types, which have specific uses. We cannot
use them directly, but we can use them whenever required. In other words, these types
are only for internal use or to give instructions to PostgreSQL whenever it is required.
For example, void is a pseudo data type that specifies the return data type of a function.
It should only be used as a function return type, and not as a table field type. Here is an
example:
postgres=# DO
$$
DECLARE
t void;
BEGIN
t:='';
END;
$$;
ERROR: variable "t" has pseudo-type void
CONTEXT: compilation of PL/pgSQL function "inline_code_block" near line 3
Like void, there are few other pseudo data types as follows:
• The any type can be used to present any function/procedure
parameter arguments.
• trigger is a pseudo data type which we only use for the trigger’s
function return type.
• opaque is a pseudo data type which is used internally or by the
external plug-ins.
• record is a pseudo data type which in general is used as a function
return type.
37
Chapter 3 PL/pgSQL Data Types
Range Type
PostgreSQL supports range types, which are designed to handle range values. You can
store and manipulate a range of values in a single column.
This data type is useful in situations where you need to store a range of values rather
than a single value, such as
Time Ranges: You can use range data types to store time ranges,
such as business hours, meeting times, or shifts.
Numeric Ranges: You can use range data types to store ranges of
numeric values, such as temperature, age, or income ranges.
Text Ranges: You can use range data types to store ranges of text
values, such as character or string ranges.
For instance, if you want to store “from” and “to” values as a single unit, you can use
a range data type to represent such values. As an example, consider a table called fleet_
mileage that records the mileage of each vehicle using a different type of gasoline. Each
vehicle’s odometer reading will be different at the start, and the gasoline will run out at a
different odometer reading. We should store both of these values and then calculate the
mileage we achieved with the specific type of gasoline we used. Here is an example:
38
Chapter 3 PL/pgSQL Data Types
In the preceding query, we used range-specific functions like upper and lower.
These functions return the beginning and end of the range values.
PostgreSQL also offers the ability to create custom range types, similar to the DOMAIN
type. Now, let’s consider another use case where we have table reservations in a hotel:
39
Chapter 3 PL/pgSQL Data Types
Multirange Types
PostgreSQL supports multirange data type which represents a set of nonoverlapping
ranges of values of some other data type. For example, you could define a multirange
type that represents a set of nonoverlapping ranges of integers.
The multirange data type in PostgreSQL is useful for storing and querying sets of
nonoverlapping ranges of values. It can be used in various applications such as time
series analysis, event scheduling, and resource allocation.
Consider the same example table fleet_mileage that is used for range data types.
For multirange data types, you can use the built-in nummultirange data type:
40
Chapter 3 PL/pgSQL Data Types
In the preceding example, the reading column has been created as a multirange
data type, and you can see a couple of range values.
You can query the table to retrieve a list of vehicles sorted in descending order based
on their mileage. However, in multirange, it will pick up the higher and lower values
from any range in multirange values.
Summary
In this chapter, we learned about data types with nice examples and explanations on
how they work. We talked about the use cases where you can use a particular data
type so that it helps you while designing an application based on the requirements. In
the next chapter, we will talk about the control structures that are used in PL/pgSQL
programming. We will cover conditional operations and simple loops with use cases and
some examples.
What’s Next
In the next chapter, we will be covering some key features of PL/pgSQL strings, numbers,
and arrays like the following:
• String Handling: Understand different string data types (char(n),
varchar(n), text) and best practices for selection based on data
model design.
41
Chapter 3 PL/pgSQL Data Types
42
CHAPTER 4
Strings
These data types and operators include the concatenation operator (||), which allows
you to combine two or more strings into a single string, as well as functions for string
manipulation and pattern matching. Additionally, PostgreSQL provides the text data
type, which allows for efficient storage and manipulation of large amounts of text data.
PostgreSQL supports the following three string data types:
• char(n)
• varchar(n)
• text
Data types should be chosen based on the data model design. There are no
performance benefits to using one over the other. If an application needs to store fixed-
length characters, such as two-digit capital codes, the char(n) data type should be used.
43
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_4
Chapter 4 Dealing with Strings, Numbers, and Arrays
If an application stores dynamic-length characters that won’t exceed a certain length, the
varchar(n) data type should be used. If an application is unsure of the length of text that
it will store in the database, the text data type should be used.
The char(n) data type always allows fixed-length strings. If an application provides
a string of length < n, PostgreSQL fills the remaining length with spaces. Consider the
following example:
Now, let us find the column size “t” by using the following query:
Based on the preceding results, we can see that the column size is 127 bytes, despite
inserting only one character. This is because, when inserting a single character (1 byte),
PostgreSQL automatically pads it with an additional 126 bytes. Scaling this behavior
can result in a very large table in the database. This large table can be used for multiple
purposes, such as benchmarking disk performance or measuring data transfer between
nodes. Consider the following example, which creates a large table with a minimal
number of text rows:
44
Chapter 4 Dealing with Strings, Numbers, and Arrays
Now, fetch the number of rows from the table which we inserted; that should
be 4096:
We just inserted 4096 text records; now quickly check the table size:
We were able to quickly generate a table of size 480MB with only 4096 records. While
it’s possible to generate an even bigger table with a small record set and media data, we
created this large table using the char(n) data type and inserting a single character into
the table. This is a tip we often follow to create large tables in a short amount of time for
benchmarking purposes.
Function Format
PostgreSQL provides a set of string operators and functions that help in constructing
strings. For example, if we want to generate a dynamic SQL statement for all tables that
prints the number of rows from each table, we can use a function called “format.” The
following is an example:
postgres=# DO
45
Chapter 4 Dealing with Strings, Numbers, and Arrays
$$
DECLARE
v_stmt TEXT:='';
v_rec RECORD;
BEGIN
FOR v_rec IN (SELECT table_name FROM information_schema.tables WHERE table_
schema != 'pg_catalog' LIMIT 4) LOOP
RAISE NOTICE '%', format('SELECT COUNT(*) FROM %I', v_rec.table_name);
END LOOP;
END;
$$;
In the preceding example, we used the format() function, where the string
argument %I is replaced with the table names. Here, %I represents identifiers, such as
column names, table names, or any other object names. The “format()” method provides
several other options for constructing strings, such as generating fixed-length strings and
reusing the arguments. Here is an example, where the format() function will reuse the
given arguments in the string:
postgres=# DO
$$
DECLARE
v_stmt TEXT:='';
BEGIN
SELECT format('SELECT COUNT(%2$s) FROM %1$s WHERE %2$s=%L', 'table_name',
'column_name', 'literal_value') INTO v_stmt;
RAISE NOTICE '%', v_stmt;
END;
$$;
46
Chapter 4 Dealing with Strings, Numbers, and Arrays
(1 row)
In the preceding example, you can see that comparing NULL and an empty value
leads to unknown results. If you expect the result of the preceding SQL statement to be
false, you won’t get it – because NULL and empty strings are uncomparable. Not NULL
and empty strings are uncomparable. One NULL is also not comparable to another
NULL in PostgreSQL. Consider the following example:
(1 row)
Now, if we have PL/pgSQL code like the following where we have a null check in
place, then the code won’t work as expected:
postgres=# DO
$$
DECLARE
v_data TEXT;
47
Chapter 4 Dealing with Strings, Numbers, and Arrays
BEGIN
-- v_data:= Some_Expression_Which_Returns_Null()
v_data:= NULL;
IF v_data = NULL THEN
-- NULL control flow statements
RAISE NOTICE 'result is null';
ELSE
-- NOT NULL control flow statements
RAISE NOTICE 'result is not null';
END IF;
END;
$$;
NOTICE: result is not null
DO
In the preceding example, even though we included a NULL check in the condition,
we still executed NOT NULL control flow statements. This may come as a surprise to
developers who are familiar with different database management systems. To modify
this behavior in PostgreSQL, you can use the session-level setting transform_null_
equals. By setting this value to ON, the preceding behavior will be fixed. Try running the
same example with this setting enabled and observe the results.
postgres=# DO
$$
DECLARE
v_data TEXT;
BEGIN
48
Chapter 4 Dealing with Strings, Numbers, and Arrays
END IF;
END;
$$;
NOTICE: result is null
DO
Now, let’s try the SQL query that compares an empty string with NULL by enabling
the transform_null_equals parameter and see the results:
(1 row)
In the preceding example, IS DISTINCT FROM returns the expected value of false,
while the = operator returns NULL. It is recommended to use IS DISTINCT FROM and
transform_null_equals as needed to avoid unexpected behavior in the application code.
49
Chapter 4 Dealing with Strings, Numbers, and Arrays
Numbers
PostgreSQL provides multiple numerical data types. They are as follows:
• smallint (2 bytes)
• int (4 bytes)
• bigint (8 bytes)
The data types smallint, int, and bigint are used to store whole numbers (Z). If
the use case is to store small integer values, we should use the smallint data type, which
accepts whole numbers in the range of –32,767 to 32,767. Consider the following simple
casting example:
In the preceding example, we are able to cast 32767 to smallint, but the next
value 32768 is out of range. This is because the smallint data type only uses 2 bytes of
storage, and with 2 bytes, we can only generate numbers in the range of -32767 to 32767.
Consider the following example:
50
Chapter 4 Dealing with Strings, Numbers, and Arrays
With 2 bytes, there would be 16 bits (where 1 byte = 8 bits). With the help of 16 bits,
we can generate a maximum number up to 65536. Consider the following example:
In the preceding example, we used the right-shifting bitwise operator to shift bit 1
sixteen times to the right, which is equivalent to 2^16. By comparing the preceding two
results, we can confirm that we can generate a maximum of 65535 numbers, with an
extra bit used to store the sign of the number (+ or –). The more bytes we choose, the
more ranges we store in the database. That is, if we want to store big numbers to the
columns, then that column should have a proper data type.
PostgreSQL provides multiple numeric data types that allow for high-precision
data storage for financial and scientific calculations. There are several data types in
PostgreSQL that support real numbers, each with its own specific use case. For example,
the float/real data type can store 4 bytes of moderate precision, whereas the float/
double precision (8 bytes) data type provides greater precision by using 8 bytes of
storage. The real data type can provide a maximum of 6-digit precision, whereas double
precision can support up to 15-digit precision.
In the case of scientific and complex calculations, if we need to store more digits in
the precision, it is recommended to use the numeric data type rather than the float
or double precision data types. This is because the numeric data type calculates the
number of bytes required to store the data when the user submits the real value. At most,
the numeric type can store 16383 digits after the decimal value and 131072 before the
decimal value.
The binary representation of floating-point numbers (i.e., using 0/1 bits to represent
a floating-point value) cannot exactly represent values such as 0.1 in binary. This leads
to rounding errors. This phenomenon is not specific to PostgreSQL, but rather is related
to how computers handle floating-point numbers. For example, consider the following
51
Chapter 4 Dealing with Strings, Numbers, and Arrays
case where we print floating-point numbers from 0.1 to 1 using the generate_series
function:
The preceding results show that the value 0.3 was calculated as
0.30000000000000004, and the value 0.8 was calculated as 0.7999999999999999.
Computers typically represent floating-point numbers, including decimal fractions,
using a fixed number of binary digits. As a result of this binary representation, some
decimal fractions cannot be represented exactly. To resolve this floating-point rounding
error problem, we can use the numeric/decimal data type, which treats real numbers as
true decimal values and then performs the calculation. For example, let’s consider the
same example we executed using the numeric data type and see the results:
52
Chapter 4 Dealing with Strings, Numbers, and Arrays
0.8
0.9
1.0
(10 rows)
From the preceding output, it is clear that we obtained the desired real number
without any surprises. This is because float data is stored in binary format, where
rounding errors are expected. In contrast, numeric data is stored in decimal format,
treating digits as normal values and performing arithmetic calculations accordingly.
Arrays
Arrays are a complex data type in PL/pgSQL, but they can be incredibly useful for
working with lists of data. To declare an array variable, you can use the ARRAY data type.
Here is the syntax:
DECLARE
my_array INTEGER[];
DECLARE
my_array INTEGER[] := '{1, 2, 3}';
You can access individual elements of an array using square brackets. The following
is a simple example:
postgres=# DO
$$
DECLARE
my_array INTEGER[] := '{1, 2, 3}';
second_element INTEGER;
BEGIN
second_element := my_array[2];
RAISE NOTICE 'second element from my array is: %', second_element;
END;
$$;
NOTICE: second element from my array is: 2
DO
53
Chapter 4 Dealing with Strings, Numbers, and Arrays
You can also loop over the elements of an array using a FOR loop. The following is an
example:
postgres=# DO
$$
DECLARE
my_array INTEGER[] := '{1, 2, 3}';
total INTEGER := 0;
BEGIN
FOR i IN 1..array_length(my_array, 1) LOOP
total := total + my_array[i];
END LOOP;
RAISE NOTICE 'total of the elements in my array is: %', total;
END;
$$;
NOTICE: total of the elements in my array is: 6
DO
By using these techniques, you can effectively work with strings, numbers, and arrays
within the context of PL/pgSQL in PostgreSQL.
Strings
Let’s say you want to dynamically generate an SQL statement based on user input. You
can use string concatenation to achieve this.
Look at the following example with a simple customers table with some data and a
function to explain the use case:
54
Chapter 4 Dealing with Strings, Numbers, and Arrays
So the function returned a table of results containing the id, first_name, last_
name, and email of any customers with the first name “John” and last name “Doe” in the
customers table.
Numbers
Let’s say you want to calculate the average revenue for a particular set of products. You
can use numeric values to achieve this.
Let us look at the following example with products and sales tables and a function to
calculate the total revenue by category of the product:
55
Chapter 4 Dealing with Strings, Numbers, and Arrays
56
Chapter 4 Dealing with Strings, Numbers, and Arrays
Arrays
Let’s say you want to find the top-selling products for a particular month. You can use
arrays to achieve this.
Let us look at an example with products and sales tables with a function to find the
top-selling product for a given month:
-- Create the function which gives top selling product for a given month
57
Chapter 4 Dealing with Strings, Numbers, and Arrays
Let us run the function for months 1 and 2 and see the results as two rows for each
month (1 for January and 2 for February):
Time: 0.548 ms
postgres=# SELECT * FROM get_top_selling_products(2);
product_id | total_sales
------------+-------------
3 | 40.00
4 | 25.00
(2 rows)
Summary
In this chapter, we provided an overview of how to work with strings, numbers, and
arrays inside PL/pgSQL of PostgreSQL. It covered best practices for working with these
data types, including using the appropriate data type for your variables, always declaring
your variables before using them, and using descriptive variable names. It also included
58
Chapter 4 Dealing with Strings, Numbers, and Arrays
examples of how to declare variables, perform operations, and manipulate data within
these data types, as well as real-world code examples for using strings, numbers, and
arrays in PL/pgSQL.
What’s Next
In the next chapter, we will be covering some key features of PL/pgSQL control
statements like the following:
• Complex Logic: Learn to handle intricate program flow using nested
IF statements, CASE expressions, and looping constructs.
• Error Handling: Explore error handling within control statements,
ensuring your code remains robust and resilient.
• Dynamic Decision Making: Discover advanced control flow
techniques such as dynamic SQL generation and conditional
execution.
• Switching Between Statements: Explore various control statement
structures like LOOP, WHILE, and FOR loops for different scenarios.
• Code Readability: Deepen your understanding of writing clear and
well-structured control statements to improve code maintainability.
59
CHAPTER 5
Control Statements
In the previous chapter, we talked about strings, numbers, and arrays and how to use
them in PL/pgSQL programming. We walked through the use cases and examples to
deal with strings, numbers, and arrays. This chapter provides an in-depth guide to the
different types of control statements and their usage in the PL/pgSQL programming
language. This chapter covers a range of control statements, including IF, CASE, LOOP,
WHILE, FOR, and FOREACH statements. By understanding the differences between
these statements, programmers can use the appropriate one for their specific needs.
Additionally, the chapter provides information on how to use variables and loops in
conjunction with these control statements to create complex programs.
Control statements are essential for controlling the flow of a program and making
decisions based on certain conditions. This chapter is a comprehensive resource
that covers different types of control statements and their usage in the PL/pgSQL
programming language.
The chapter provides information on the following control statements and
their usage:
• IF Statement
• CASE Statement
• Iterative Statement
• LOOP Statement
• WHILE Statement
• FOR Statement
• FOREACH Statement
61
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_5
Chapter 5 Control Statements
IF/ELSE Statement
The IF statement is used to execute a block of code if a certain condition is true. Here is
the simple syntax of the IF statement:
postgres=# DO
$$
BEGIN
IF 1 = 1 THEN
RAISE NOTICE 'OK';
END IF;
END;
$$;
NOTICE: OK
DO
From the preceding example, we received the message “NOTICE: OK” because the
condition “1=1” mentioned in the example is true. PL/pgSQL uses SQL’s SPI interface to
perform conditional evaluation. This means that internally it forms a SELECT statement
around the given condition, executes it, and returns in the form of boolean. That is, the
“1=1” will be converted into the SQL statement “SELECT 1=1”, and the result will be
placed in the IF statement.
Now, consider the following example:
postgres=# DO
$$
BEGIN
IF 1 = 1 ORDER BY 1 THEN
RAISE NOTICE 'OK';
END IF;
END;
62
Chapter 5 Control Statements
$$;
NOTICE: OK
DO
In the preceding example, let’s examine the condition we mentioned: “1=1 ORDER
BY 1”. This condition appears different from a regular condition because we included
“ORDER BY 1” along with “1=1”. As previously mentioned, PL/pgSQL automatically
converts the given expression into SQL form and internally creates SQL such as “SELECT
1=1 ORDER BY 1”.
In the preceding example, including an ORDER BY clause in the expression is
unnecessary. It is only used to demonstrate the evaluation of expressions behind the
scenes. We can make the expression evaluation stricter by enclosing it in parentheses as
follows:
postgres=# DO
$$
BEGIN
IF (1 = 1 ORDER BY 1) THEN
RAISE NOTICE 'OK';
END IF;
END;
$$;
ERROR: syntax error at or near "ORDER"
LINE 4: IF (1 = 1 ORDER BY 1) THEN
As you can see, once we enclose the expression in parentheses, we receive the proper
syntax error. This is because the entire unit is evaluated as a single expression, rather
than as individual units. We can also specify an expression as a combination of single
expressions or individual units, as follows:
postgres=# DO
$$
BEGIN
IF (1 = 1) ORDER BY 1 THEN
RAISE NOTICE 'OK';
END IF;
END;
63
Chapter 5 Control Statements
$$;
NOTICE: OK
DO
The preceding examples simply demonstrate how expressions are evaluated. There
is no specific use case for it.
The IF statement is not limited to using only simple conditional expressions. We can
also use a SELECT statement as part of its expressions. Consider the following example:
postgres=# DO
$$
BEGIN
IF (select count(*) from test) < 1 THEN
RAISE NOTICE 'OK';
END IF;
END;
$$;
NOTICE: OK
DO
Note If conditional statements are placed inside loop statements, there will be
a rapid context switch between the PL/pgSQL and SQL engines. This can result in
increased resource utilization. If possible, avoid placing conditional expressions
inside a huge number of iterations.
64
Chapter 5 Control Statements
postgres=# DO
$$
BEGIN
IF EXISTS(SELECT * FROM test) THEN
RAISE NOTICE 'table is not empty';
END IF;
END;
$$;
NOTICE: table is not empty
DO
In the preceding example, we used the EXISTS operator to check the table rows,
which is an optimal way of checking the table row count status. We don’t need to write
the query like SELECT COUNT(*) FROM test!= 0, which scans the entire table and
returns the number of rows. The latter check actually takes more resources to confirm
whether the table is empty or not, while the former short-circuits (stops returning
records from SELECT *) the process whenever it finds any single record in the table.
Cascading IF Statements
PL/pgSQL supports writing cascading IF statements, where we can write chained
conditional statements. For example, consider the following use case:
65
Chapter 5 Control Statements
NOTICE: table is empty
DO
postgres=#
CASE Statement
One of the user-friendly features offered by PL/pgSQL is the ability to implement
complex conditional logic in a concise and readable way. The CASE statement in
66
Chapter 5 Control Statements
PL/pgSQL allows users to specify different actions based on different conditions. For
example, consider the following case:
postgres=# DO
$$
BEGIN
CASE
WHEN EXISTS(SELECT * FROM test) THEN
RAISE NOTICE 'table is not empty';
WHEN NOT EXISTS(SELECT * FROM test) THEN
RAISE NOTICE 'table is empty';
END CASE;
END;
$$;
NOTICE: table is empty
DO
The example of cascading if statements that we discussed earlier has been rewritten
using CASE statements. If you compare the two examples, the CASE statement example
is more declarative and readable than the cascaded if statement. We can simplify the
preceding example further by using the following statements. We keep the number of
rows in a table in a variable and only evaluate the conditions in the CASE statements:
postgres=# DO
$$
DECLARE
v_row_count INT:=0;
BEGIN
SELECT COUNT(*) INTO v_row_count FROM test LIMIT 1;
CASE v_row_count
WHEN 1 THEN
RAISE NOTICE 'table is not empty';
WHEN 0 THEN
RAISE NOTICE 'table is empty';
ELSE
RAISE NOTICE 'not reachable';
END CASE;
67
Chapter 5 Control Statements
END;
$$;
NOTICE: table is empty
DO
The preceding CASE statement code is more readable than nested if statements. If
you have more chained or nested if statements, we would suggest you to implement the
code using CASE which gives you more readability to the complex conditional checks. We
can also use CASE expressions in variable assignments, as follows:
postgres=# DO
$$
DECLARE
a_int INT:=0;
b_int INT:=0;
BEGIN
b_int = CASE a_int WHEN 0 THEN 1 ELSE 2 END CASE;
RAISE NOTICE 'Value is %', b_int;
END;
$$;
NOTICE: Value is 1
DO
Iterative Statement
PL/pgSQL offers several iterative statements, including the LOOP, WHILE, FOR, and
FOREACH statements. The LOOP statement is used to repeat a block of code until a
certain condition is met, while the WHILE statement is used to execute a block of code
repeatedly as long as a certain condition is true. The FOR statement is used to loop
through a set of values and execute a block of code for each value. The FOREACH is like
a FOR LOOP statement, but it is especially designed to work with arrays or composite
value types.
68
Chapter 5 Control Statements
Note It is not advisable to use too many nested iterative statements in PL/pgSQL
code. Doing so not only makes the code harder to read and debug but also leads
to performance issues. In the upcoming chapter, we will discuss the PL/Profiler
extension, which can help us drill down into performance issues.
LOOP Statement
You can define unconditional iterative statements that can only be controlled by either
an EXIT or RETURN statement using a simple LOOP statement. Consider the following
example, where we repeatedly execute a statement until the given condition is met:
postgres=# DO
$$
DECLARE
v_count INT:=0;
v_iteration_count INT:=1;
BEGIN
<<OuterLoop>>
LOOP
v_count:=v_count+1;
EXIT WHEN v_count=10;
v_iteration_count:= v_iteration_count+1;
END LOOP OuterLoop;
RAISE NOTICE 'Iteration Count %', v_iteration_count;
END;
$$;
NOTICE: Iteration Count 10
DO
In the preceding example, we iterate the v_count variable value ten times and exit
the loop when the value reaches ten. Without the exit condition, the loop would iterate
69
Chapter 5 Control Statements
indefinitely, and we would need to either close the session or kill the indefinite running
process. Consider the following example:
postgres=# DO
$$
DECLARE
v_count INT:=0;
v_iteration_count INT:=0;
BEGIN
<<OuterLoop>>
LOOP
v_count:=v_count+1;
CONTINUE WHEN v_count<10;
EXIT WHEN v_count=10;
v_iteration_count:= v_iteration_count+1;
END LOOP OuterLoop;
RAISE NOTICE 'Iteration Count %', v_iteration_count;
END;
$$;
NOTICE: Iteration Count 0
DO
In the preceding example, the iteration count result is zero. This is because the
line “v_iteration_count:=v_iteration_count+1” is not executed at all. The CONTINUE
statement mentioned earlier causes the loop to continue until the v_count value
reaches ten. That is, the CONTINUE operation skipped the execution of the loop
when the condition was met. The immediate EXIT statement ends the loop when the
v_count value reaches ten, so v_iteration_count never has a chance to run even once.
In the preceding example, we used the label name OuterLoop to refer to the iterative
statements. By using label names like this, we can control the execution flow in certain
conditions. Consider the following example:
postgres=# DO
$$
DECLARE
v_count INT:=0;
v_iteration_count INT:=1;
70
Chapter 5 Control Statements
BEGIN
<<OuterLoop>>
LOOP
<<InnerLoop>>
LOOP
v_count:=v_count+1;
EXIT OuterLoop WHEN v_count=10;
v_iteration_count:= v_iteration_count+1;
END LOOP InnerLoop;
-- Outerloop instructions
END LOOP OuterLoop;
RAISE NOTICE 'Iteration Count %', v_iteration_count;
END;
$$;
NOTICE: Iteration Count 10
DO
In the preceding example, we use the “OuterLoop” label to exit the iterative
statement whenever the condition is met. Labels allow us to jump forward whenever
a required condition is met. The usage of labels is not just limited inner/outer iterative
statements, we can also use labels to navigate between the blocks. Consider the
following example:
postgres=# DO
$$
<<MainBlock>>
BEGIN
<<OuterBlock>>
BEGIN
<<InnerBlock>>
BEGIN
RAISE NOTICE 'InnerBlock Complete';
EXIT OuterBlock;
END;
RAISE NOTICE 'OuterBlock Complete';
END;
71
Chapter 5 Control Statements
In the preceding example, we did not use any iterative statements. Instead, we
were able to navigate the blocks by using label names. As you can see, the execution
flow jumped directly from the Inner block to the Main block, by skipping the execution
of the Outer block. This type of execution should always move forward; we should
not go backward. In other words, while inside the Outer block, we cannot EXIT to the
Inner block.
WHILE Statement
The WHILE statement is an iterative statement that can execute a block of code repeatedly
as long as a certain condition is true. It is similar to the LOOP statement, which also
executes a block of code repeatedly based on an entry condition. The condition in the
WHILE statement is a boolean expression that is evaluated before each iteration of the
loop. If the condition is true, the loop continues to execute. If the condition is false, the
loop exits and execution continues with the next statement after the loop. Consider the
following example:
postgres=# DO
$$
DECLARE
v_count INT:=0;
v_iteration_count INT:=0;
BEGIN
WHILE v_count!=10 LOOP
v_count:=v_count+1;
v_iteration_count:=v_iteration_count+1;
END LOOP;
RAISE NOTICE 'Iteration Count %', v_iteration_count;
END;
72
Chapter 5 Control Statements
$$;
NOTICE: Iteration Count 10
DO
In the preceding example, we set the entry condition for iterative statements. Unlike
the plain LOOP statement, where the condition is set as part of the iterative statements,
here we set the entry point at the beginning of the SQL statement execution. As we
discussed earlier, every expression in PL/pgSQL is evaluated using SQL statements. This
means that any expression or condition mentioned in the code will be converted to an
SQL expression. Consider the following example:
postgres=# DO
$$
DECLARE
v_count INT:=0;
BEGIN
WHILE v_count!=3 ORDER BY 1 LOOP
v_count:=v_count+1;
RAISE NOTICE 'Count... %', v_count;
END LOOP;
END;
$$;
NOTICE: Count... 1
NOTICE: Count... 2
NOTICE: Count... 3
DO
73
Chapter 5 Control Statements
FOR Statement
PL/pgSQL provides several variants of the FOR loop statement, each designed for a
specific use case. Unlike WHILE loop statements, FOR loops make it easy to iterate over
SQL results. This is because FOR opens an implicit cursor for the specified SQL query,
while in a WHILE loop we must explicitly open the cursor and fetch records until the end
of the result set.
The FOR statement is more useful when traversing a sequential set of data, while the
WHILE statement is better for controlling iteration based on dynamic or static conditions.
In other words, using WHILE allows for more control over the iterative statements, while
FOR simplifies the data traversal. Consider the following example:
postgres=# DO
$$
DECLARE
v_rec RECORD;
BEGIN
FOR v_rec IN (SELECT * FROM test) LOOP
RAISE NOTICE 'Record ... %', v_rec;
END LOOP;
END;
$$;
NOTICE: Record ... (1)
NOTICE: Record ... (2)
NOTICE: Record ... (3)
DO
In the preceding example, we did not create any cursors to fetch data from the test
table. Instead, we included the SQL query as part of the FOR loop declaration and fetched
results until the end of the table. When compared with other formats, such as declaring
a cursor, opening the cursor, fetching data, and closing it, the FOR statement iterating SQL
query results is the easiest. We will discuss cursors further in the upcoming chapter. To
74
Chapter 5 Control Statements
run a parameterized SQL query based on the preceding example, a different format of the
FOR statement should be used as follows:
postgres=# DO
$$
DECLARE
v_rec RECORD;
BEGIN
FOR v_rec IN EXECUTE 'SELECT * FROM test WHERE t=$1' USING 1 LOOP
RAISE NOTICE 'Record ... %', v_rec;
END LOOP;
END;
$$;
NOTICE: Record ... (1)
DO
In this code, we have used the FOR EXECUTE USING form to execute a parameterized
SQL statement. As you can see, it is very convenient to iterate over the SQL results using
FOR statements. PL/pgSQL also offers another form of FOR statement, FOREACH, which is
used to iterate over arrays. Consider the following example:
postgres=# DO
$$
DECLARE
v_arr INT[]=ARRAY[1,2,3];
i INT:=0;
BEGIN
FOREACH i IN ARRAY v_arr
LOOP
RAISE NOTICE '%', i;
END LOOP;
END;
$$;
NOTICE: 1
NOTICE: 2
NOTICE: 3
DO
75
Chapter 5 Control Statements
postgres=# DO
$$
DECLARE
v_arr INT[]=ARRAY[[1,2,3], [4,5,6]];
i INT:=0;
BEGIN
FOREACH i IN ARRAY v_arr
LOOP
RAISE NOTICE '%', i;
END LOOP;
END;
$$;
NOTICE: 1
NOTICE: 2
NOTICE: 3
NOTICE: 4
NOTICE: 5
NOTICE: 6
DO
By default, the entire two-dimensional ARRAY is traversed, and its results are printed.
However, by using the FOREACH-specific SLICE option, we can traverse the ARRAY by each
individual array, rather than nested. Consider the following example:
postgres=# DO
$$
DECLARE
v_arr INT[]=ARRAY[[1,2,3], [4,5,6]];
i INT[];
BEGIN
FOREACH i SLICE 1 IN ARRAY v_arr
LOOP
RAISE NOTICE '%', i;
76
Chapter 5 Control Statements
END LOOP;
END;
$$;
NOTICE: {1,2,3}
NOTICE: {4,5,6}
DO
By using the SLICE option, we can define how to iterate through a given ARRAY.
As of PostgreSQL 15.2, the FOREACH array iteration mode should be protected by a
NULL array check. That is, if the iterative ARRAY is NULL, FOREACH will throw an exception.
Hence, always guard FOREACH with a NULL check condition as follows:
postgres=# DO
$$
DECLARE
v_arr INT[]=NULL;
i INT:=0;
BEGIN
IF v_arr IS NULL THEN
RAISE NOTICE 'Array is null';
RETURN;
END IF;
77
Chapter 5 Control Statements
Example 1
Consider two tables like users and transactions:
The users table contains information about each user, including their ID,
name, email, and balance. The transactions table contains information about each
transaction, including its ID, the ID of the user who made the transaction, and the
transaction amount.
Insert some data into the tables:
This code inserts two users into the users table and two transactions into the
transactions table. The first transaction deducts 10 from the balance of the first user, and
the second transaction deducts 20 from the balance of the second user.
78
Chapter 5 Control Statements
Let us create a function which controls the user transaction using a control statement
like IF:
This function takes in a user ID and a transaction amount as input parameters and
processes a payment transaction if the user has sufficient funds. It uses an IF statement
to check if the user has enough balance before deducting the transaction amount and
inserting a new row into the transactions table. If the user doesn’t have sufficient funds,
it raises an exception with an error message.
Let us run the function with 10 as the transaction amount:
79
Chapter 5 Control Statements
Time: 0.259 ms
postgres=#
postgres=# select * from transactions;
id | user_id | amount
----+---------+--------
1 | 1 | 10
2 | 2 | 20
(2 rows)
Time: 0.239 ms
postgres=#
postgres=#
postgres=# select process_payments(1,10);
process_payments
------------------
(1 row)
Time: 0.773 ms
postgres=#
postgres=#
postgres=# select * from users;
id | name | email | balance
----+------------+------------------------+---------
2 | Jane Smith | [email protected] | 50
1 | John Doe | [email protected] | 90
(2 rows)
Time: 0.335 ms
postgres=#
postgres=#
postgres=# select * from transactions;
id | user_id | amount
----+---------+--------
80
Chapter 5 Control Statements
Time: 0.249 ms
postgres=#
You can see the transaction of 10 in the transactions table, and 10 is deducted from
the users amount in the users table.
Let us try with 100 as the transaction amount:
As there are not as many funds as 100, it has thrown an error as expected.
Example 2
Let us look at another example that uses a CASE statement to categorize products based
on their price range.
Consider two tables, users and products, and insert some data:
Create a function which uses a CASE statement to update the category of each
product based on its price:
82
Chapter 5 Control Statements
(1 row)
Time: 2.095 ms
postgres=#
postgres=# select * from products;
id | name | price | category
----+-----------+-------+---------------
5 | Product A | 5 | low-priced
6 | Product B | 15 | medium-priced
7 | Product C | 30 | medium-priced
8 | Product D | 60 | high-priced
(4 rows)
Time: 0.286 ms
postgres=#
83
Chapter 5 Control Statements
As you can see, running the function updated the category of each product as
mentioned in the function logic.
84
Chapter 5 Control Statements
Summary
In this chapter, we learned the different types of control statements available in PL/
pgSQL, including IF, CASE, LOOP, WHILE, FOR, and FOREACH statements. The
programmers can choose the appropriate one for their specific needs. This chapter
provides a comprehensive guide to these control statements, covering their usage and
providing real-world examples. By following best practices, such as keeping control
statements simple, using meaningful variable names, and testing control statements
thoroughly, programmers can ensure their control statements are effective, efficient,
and optimized. Overall, this chapter is an excellent resource for anyone looking to
understand the different types of control statements available in PL/pgSQL and use
them effectively to create powerful programs.
What’s Next
In the next chapter, we will be covering some key features of PL/pgSQL arrays like the
following:
• Advanced Array Functions: Dive into more complex array
operations and explore functions for transforming, filtering, and
aggregating arrays.
• Array Performance: Learn optimization strategies for working
with arrays, including avoiding unnecessary loops and array
manipulation.
85
Chapter 5 Control Statements
86
CHAPTER 6
Handling Arrays
In the previous chapter, we talked about control statements in PL/pgSQL of
PostgreSQL. We covered some examples of IF ELSE, CASE statements and some iterative
statements like LOOP, WHILE, FOR and FOREACH. In this chapter, we will dive deep into
arrays. We will cover array index and array length, use foreach to iterate over the arrays,
and convert arrays to a set of rows. It will also include finding duplicate elements in an
array, appending elements to an array, and merging two arrays. By the end of this chapter,
you will have a better understanding of how to use arrays in the context of PL/pgSQL.
PostgreSQL provides the ability to store a series of homogeneous data within a
single object, by using single- and multidimensional arrays. ARRAY data type columns
can be created in a relation, and ARRAY custom data type members can be defined
as well as ARRAY variables and parameters in procedures/functions. By using arrays,
we can store, retrieve, and process large amounts of data at once. For example, an
ARRAY can represent a series of ecommerce items under a specific category, a series
of order line items in a specific order, or a series of unit recordings in time series
data. In other words, by using ARRAY, we group a certain set of values as a single
unit and process all those values at once. Consider the following example, where
we demonstrate the ARRAY feature by taking an example of an order line, which has
multiple items inside an order:
postgres=# DO
$$
DECLARE
V_order_line TEXT ='ord-line-1';
v_items_id TEXT[]= ARRAY['item1', 'item2', 'item3'];
BEGIN
RAISE NOTICE 'Received order line %', v_order_line;
RAISE NOTICE 'Order line items %', v_items_id;
END;
$$;
87
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_6
Chapter 6 Handling Arrays
Array Index
PostgreSQL arrays use a one-based index rather than a zero-based index. This means
that to access the first element in an array, we have to use an index of one instead of
zero. Unlike most of the other programming languages where arrays are zero-based
indexes, in PostgreSQL it’s a one-based index. Consider the following example, which
demonstrates this behavior:
postgres=# DO
$$
DECLARE
V_arr TEXT[]:=ARRAY['ONE', 'TWO', 'THREE'];
BEGIN
RAISE NOTICE '0-Based index %', v_arr[0];
RAISE NOTICE '1-Based index %', v_arr[1];
END; $$;
From the preceding output, it can be seen that PostgreSQL follows a default one-
based index. Now, let's try setting the array index zero explicitly and then try accessing
the element from zero:
postgres=# DO
$$
DECLARE
v_arr TEXT[]:=ARRAY['ONE', 'TWO', 'THREE'];
BEGIN
v_arr[0]='ZERO';
RAISE NOTICE '0 index %', v_arr[0];
88
Chapter 6 Handling Arrays
END;
$$;
As seen previously, PostgreSQL arrays do not restrict us from setting the zero-index
value, even though it uses a one-based index. Now, let's try setting a negative index value
and accessing it using a negative index number:
postgres=# DO
$$
DECLARE
v_arr TEXT[]:=ARRAY['ONE', 'TWO', 'THREE'];
BEGIN
v_arr[-1]='NEGATIVE';
RAISE NOTICE '-1 index %', v_arr[-1];
END;
$$;
Array Length
To find the length of an array, we can use the array_length function. The function
takes two parameters, the array column and the dimension of the array whose length
we want to find. If we want to find the length of the entire array, we can set the second
parameter to 1. If we want to find the length of a specific dimension, we can set the
second parameter to the dimension number. Consider the following example, which
demonstrates the use of the array_length function to find the length of an array:
postgres=# DO
$$
DECLARE
v_arr TEXT[]:=ARRAY['ONE', 'TWO', 'THREE'];
89
Chapter 6 Handling Arrays
BEGIN
v_arr[0] = 'ZERO';
RAISE NOTICE 'Length of array is %', array_length(v_arr, 1);
END;
$$;
From the preceding output, it can be seen that the array_length function returns
the length of the array. In this case, the length of the array is four.
Now, let’s try by adding a few more random indexes to the preceding array and then
calculate the array length:
postgres=# DO
$$
DECLARE
v_arr TEXT[]:=ARRAY['ONE', 'TWO', 'THREE'];
BEGIN
v_arr[0] = 'ZERO';
v_arr[10] = 'TEN';
RAISE NOTICE 'Length of array is %', array_length(v_arr, 1);
END;
$$;
In the preceding output, the length of an array is reported as 11, while the number
of elements in the array is actually 5. This behavior is due to the fact that in PostgreSQL,
the length of an array is calculated based on the upper and lower bounds of the array.
In this case, the upper bound (index) of the v_arr array is 10, the lower bound (index) is
0, and the number of elements between these indices is 11. When working with indexes
in PL/pgSQL, it's important to note that we should not update the values of ARRAY
indexes. This is because array indexes in PostgreSQL can be updated to any number, and
any gaps between the indexes will be filled with NULL values. Consider the following
example, where we got the array length as 11.
90
Chapter 6 Handling Arrays
postgres=# DO
$$
DECLARE
v_arr TEXT[]:=ARRAY['ONE', 'TWO', 'THREE'];
BEGIN
v_arr[0] = 'ZERO';
v_arr[10] = 'TEN';
RAISE NOTICE 'Length of array is %', array_length(v_arr, 1);
RAISE NOTICE 'Array values are %', v_arr;
END;
$$;
As you can see in the preceding result, there are six NULL values between indexes
4 and 10.
Iterate Array
To iterate over an array in PostgreSQL, we can use the FOREACH loop. This loop allows us
to iterate over each element of an array and perform some operation on each element.
Consider the following example, which demonstrates how to iterate over an array and
print each element:
postgres=# DO
$$
DECLARE
v_arr TEXT[]:=ARRAY['ONE', 'TWO', 'THREE'];
v_element TEXT;
BEGIN
FOREACH v_element IN ARRAY v_arr LOOP
RAISE NOTICE 'Element is %', v_element;
END LOOP;
91
Chapter 6 Handling Arrays
END;
$$;
NOTICE: Element is ONE
NOTICE: Element is TWO
NOTICE: Element is THREE
DO
From the preceding output, it can be seen that we have iterated over the v_arr array
and printed each element of the array.
In PostgreSQL, we can also use the unnest function to convert an array into a set of
rows. This function returns a set of rows, with each row containing a single element of
the array. Consider the following example:
In the preceding output, we have used the unnest function to convert the array
['ONE', 'TWO', 'THREE'] into a set of rows. Each row contains a single element of
the array. We have then selected all the elements from the set of rows using the SELECT
statement.
postgres=# DO
$$
DECLARE
v_arr TEXT[]:=ARRAY['ONE', 'TWO', 'THREE', 'TWO', 'FOUR', 'ONE'];
92
Chapter 6 Handling Arrays
v_dup TEXT[];
BEGIN
FOR idx IN 1..array_length(v_arr, 1) LOOP
IF v_arr[idx] = ANY(v_dup) THEN
RAISE NOTICE 'Found duplicate %', v_arr[idx];
ELSE v_dup:= v_dup||v_arr[idx];
END IF;
END LOOP;
END;
$$;
In the preceding example, we have not declared the idx variable in the declaration
which is optional while iterating over ARRAY in PL/pgSQL. And also, we used the
operator || which actually appends elements to an array.
postgres=# DO
$$
DECLARE
v_arr TEXT[]:=ARRAY['ONE', 'TWO', 'THREE'];
BEGIN
v_arr:= array_append(v_arr, 'FOUR');
RAISE NOTICE 'Array after appending element is %', v_arr;
END;
$$;
NOTICE: Array after appending element is {ONE,TWO,THREE,FOUR}
DO
93
Chapter 6 Handling Arrays
In the preceding output, we have appended the value 'FOUR' to the v_arr array
using the array_append function. We can also use the array operator || to append
elements to an array.
Array Merge
To merge two or more arrays in PostgreSQL, we can use the array_cat function. This
function takes two or more arrays as parameters and concatenates them into a single
array. Consider the following example:
postgres=# DO
$$
DECLARE
v_arr1 TEXT[]:=ARRAY['ONE', 'TWO', 'THREE'];
v_arr2 TEXT[]:=ARRAY['FOUR', 'FIVE', 'SIX'];
BEGIN
RAISE NOTICE 'Merged array is %', array_cat(v_arr1, v_arr2);
END;
$$;
In the preceding output, we have merged two arrays v_arr1 and v_arr2 using the
array_cat function. We have then printed the merged array.
Multidimensional Arrays
Multidimensional arrays are arrays with more than one level of nesting. They can be
created in PostgreSQL by using the [][] notation to specify the number of dimensions. For
example, a two-dimensional array can be created using TEXT[][]. To access the elements
of a multidimensional array, we need to use multiple indexes. The first index specifies the
row, and the second index specifies the column. Consider the following example:
postgres=#
DO
$$
94
Chapter 6 Handling Arrays
DECLARE
v_arr TEXT[][]:=ARRAY[['ONE', 'TWO', 'THREE'], ['FOUR', 'FIVE', 'SIX']];
BEGIN
RAISE NOTICE 'Element at row 1 and column 2 is %', v_arr[1][2];
END;
$$;
In the preceding output, we can see that we have created a two-dimensional array
with two rows and three columns. We have then accessed the element in the first row
and second column, which is 'TWO'. We can also use the array_dims function to find the
dimensions of an array. This function returns a string that specifies the dimensions of
the array. Consider the following example:
postgres=# DO
$$
DECLARE
v_arr TEXT[][]:=ARRAY[['ONE', 'TWO', 'THREE'], ['FOUR', 'FIVE', 'SIX']];
BEGIN
RAISE NOTICE 'Array dimensions are %', array_dims(v_arr);
END;
$$;
In the preceding output, we can see that the array_dims function returns [1:2]
[1:3], which specifies that the array has two rows and three columns. By using array_
ndims(), we can print the number of dimensions we have in that ARRAY. For example,
consider the following case:
postgres=# DO
$$
DECLARE
v_arr TEXT[][]:=ARRAY[['ONE', 'TWO', 'THREE'], ['FOUR', 'FIVE', 'SIX']];
95
Chapter 6 Handling Arrays
BEGIN
RAISE NOTICE 'Array dimensions are %', array_ndims(v_arr);
END;
$$;
Summary
In this chapter, we started with an introduction to arrays, followed by a discussion
of array index, length, iteration, and manipulation. The chapter also covered how to
convert an array into a set of rows using the unnest function, find duplicate elements in
an array, append elements to an array using the array_append function, and merge two
or more arrays using the array_cat function. Additionally, the chapter provided guidance
on creating and accessing elements of multidimensional arrays.
What’s Next
In the next chapter, we will be covering some key features of JSON strings like the
following:
• Advanced JSON Functions: Explore deeper into JSON manipulation
functions and techniques, including nested object extraction and
modification.
• JSON Aggregation: Learn how to aggregate and process JSON data
within PL/pgSQL for reporting and analysis purposes.
• Working with Large JSON Documents: Explore strategies for
efficiently handling and processing large JSON documents.
• JSON Performance Optimization: Dive into performance
considerations when working with JSON data, including indexing
and storage optimization.
96
CHAPTER 7
Handling JSON
In the previous chapter, we talked about arrays and how to use them in PL/pgSQL with
some examples. In this chapter, we will discuss about JSON data and how to use it in
PostgreSQL. We will cover different use cases where JSON data is useful and built-in
operators and functions which are used to load and query JSON data. We will go through
a few examples of JSON with PL/pgSQL programming.
What Is JSON?
JSON stands for JavaScript Object Notation, and it is a lightweight data interchange
format that is easy for humans to read and write and easy for machines to parse and
generate.
PostgreSQL has built-in support for JSON, which allows users to store JSON data in a
column of a table. This can be useful when working with semi-structured data or when
the schema of the data is not known in advance.
Here is an example of creating a table with a JSON column:
We can then insert data into the table using the INSERT statement:
97
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_7
Chapter 7 Handling JSON
To retrieve the data, we can use the SELECT statement and the -> operator to access
specific fields:
This will return a result set with the name and age fields from the JSON data. The ->
operator gets the object field by key. So, if you check the data type of the result, you get JSON:
If you want to use the result to type cast to any other data type like INT for further
purposes, it does not allow with the -> operator as it returns a key:
You can use the ->> operator when you have a use case like this. This operator gets
the field in text format so that you can type cast to an allowed type as required:
In addition to the -> and ->> operators, PostgreSQL provides several other operators
for working with JSON data, including #>, #>>, and @>. These operators allow users to
extract specific fields, navigate nested JSON structures, and perform comparisons.
98
Chapter 7 Handling JSON
#> and #>> operators are used to get the fields from any array of values in a JSON
string. For example, insert the following data into the test_table:
If you want to get a particular value from the array, you can use the #> or #>>
operator. The difference is #> results in a JSON type, and #>> results in TEXT so that you
can cast further.
99
Chapter 7 Handling JSON
Use Cases
There are several use cases for using JSON data in PostgreSQL:
This will return a result set with the answer1 and answer2 fields
from the JSON data.
100
Chapter 7 Handling JSON
This will return a result set with the resolution and size fields from
the JSON data.
3. Storing Configuration Data: JSON can be used to store
configuration data for applications. For example, a web
application could store configuration data for different
environments (development, staging, production) in JSON format.
The following is a simple example:
To retrieve the configuration data, we can use the ->> and #>>
operators to access specific fields:
This will return a result set with the development and production
database host fields from the JSON data.
4. Storing User Preferences: JSON can be used to store user
preferences for applications. For example, a social media
application could store each user’s preferences for news feed
content and notification settings in JSON format. The following is
a simple example:
To retrieve the user preferences, we can use the ->> and ->
operators to access specific fields:
102
Chapter 7 Handling JSON
show_images | email
-------------+-------
true | true
(1 row)
This will return a result set with the show_images and email fields
from the JSON data.
5. Storing NoSQL-like Data: JSON can be used to store NoSQL-like
data in PostgreSQL. For example, a document-oriented database
could use JSON to store data in a document format while still
taking advantage of PostgreSQL’s powerful indexing and querying
capabilities. The following is a simple example:
To retrieve the document data, we can use the ->> and #>
operators to access specific fields:
This will return a result set with the title, content, tags, and views
fields from the JSON data.
103
Chapter 7 Handling JSON
These are just a few examples of the many use cases for JSON in PostgreSQL. By
using JSON, users can store and query semi-structured data in a flexible and
efficient manner.
104
Chapter 7 Handling JSON
Now, create the PL/pgSQL function which returns the candidate details based on the
required experience:
experience INT
)
AS $$
BEGIN
RETURN QUERY
SELECT
id as profile_id,
emp_data->>'name' as name,
(emp_data->>'exp')::INT as experience
FROM profiles_json
WHERE (emp_data->>'exp')::INT > exp;
END;
$$ LANGUAGE plpgsql;
Let’s run the function to get the details with ten years of experience:
FROM profiles_json
WHERE emp_data->'languages' ? lang;
END;
$$ LANGUAGE plpgsql;
Let’s run the function to get the candidate details based on the “Spanish” language:
As you can see, it returned the candidate details with Spanish language skills.
Check for multiple keys’ presence.
Let’s create a function to check with multiple key values. Insert some data to verify this:
Create the function that returns the results based on multiple keys:
108
Chapter 7 Handling JSON
"languages": [ +
"English", +
"Japanese", +
{ +
"special": [ +
"brailey", +
"some_other_special_langugae"+
] +
} +
], +
"aws_certified": true +
}
(1 row)
109
Chapter 7 Handling JSON
Now run the query to see the index scan. Note that as data is less in the table, seq
scan is disabled to check the index scan:
You can also create the index on JSON members instead of the keys:
As you can see, the index on members has been picked up.
110
Chapter 7 Handling JSON
Building JSON
You can use json_object to build the json string using a text array:
Summary
In this chapter, we talked about JSON data in PostgreSQL and its supported built-in
operators to query the data. We covered different use cases to use JSON data with
examples. Also, we have shown simple functions to use JSON queries which simplify
retrieving JSON data in our required format. We also covered indexing JSON data to
improve performance. There are different built-in functions which are very useful when
dealing with JSON data. We had a look into a couple of functions.
111
Chapter 7 Handling JSON
What’s Next
In the next chapter, we will be covering some key features of PL/pgSQL cursors like the
following:
• Advanced Cursor Manipulation: Learn about scrollable cursors,
dynamic cursors, and techniques for optimizing cursor operations.
• Cursor Error Handling: Explore best practices for handling errors
when working with cursors, ensuring your code is robust and reliable.
• Multiple Cursors: Discover strategies for efficiently managing
multiple cursors within your PL/pgSQL code.
• Cursor Performance: Deepen your understanding of cursor
performance considerations and optimization techniques.
• Cursor Use Cases: Explore real-world scenarios where cursors are
essential for processing and navigating large result sets.
112
CHAPTER 8
Cursors
In the previous chapter, we talked about JSON data and how to use it in PostgreSQL. In
this chapter, we will discuss the basics of cursors in PL/pgSQL, their advantages, and
how they can be used to optimize database performance. The chapter begins with an
overview of cursors and their benefits, followed by a comparison of the response time
between executing an SQL query on the SQL interface and using cursors. Next, we will
delve into the different types of cursors available in PL/pgSQL, such as SCROLL, NO
SCROLL, and WITH HOLD cursors. Finally, we will introduce reference cursors, which
allow you to create a cursor, open it, and then pass or return the cursor to another
function as an input argument.
113
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_8
Chapter 8 Cursors
Execute the SQL query that fetches all records from the preceding table and prints
the total time it takes to fetch and return the response to the client:
Based on the preceding output, the SQL query executed on the table “many_rows_
table” fetched all records and returned the response within 02.854 seconds. Execute the
same SQL statement through a PL/pgSQL code block to fetch the result. Exit the code as
soon as the first record is read from the table.
postgres=# DO
$$
DECLARE
v_rec RECORD;
BEGIN
FOR v_rec IN (SELECT * FROM many_rows_table) LOOP
IF v_rec IS NOT NULL THEN
RAISE NOTICE 'Found record %', v_rec;
EXIT;
END IF;
END LOOP;
END;
$$;
NOTICE: Found record (1)
DO
Time: 1.540 ms
In the preceding example, we used a FOR LOOP to iterate over the SQL statement,
which creates an implicit cursor in PostgreSQL and iterates over the result set. We exit
the loop when we find a NOT NULL record, which determines the response time of
114
Chapter 8 Cursors
getting a single record from the table. As seen in the preceding output, we were able to
retrieve the first record in just 1.5 milliseconds using this method, unlike the traditional
SQL way where the entire result set is returned to the client, which took time around 2
seconds.
Using cursors can help reduce the response time when dealing with large datasets,
as it allows the user to fetch a small, manageable number of records at a time. This is
particularly useful when rendering data in a dashboard with pagination, where only a
certain number of records are displayed at once. Cursors also allow for processing of the
data as needed, rather than returning the entire result set at once to the client.
CURSOR Attributes
PL/pgSQL does not directly provide cursor attributes such as ISOPEN, FOUND,
NOTFOUND, and ROWCOUNT. However, you can use alternative workarounds for each
attribute.
ISOPEN Attribute
The ISOPEN attribute returns true if the cursor is currently open and false otherwise. You
can use a boolean variable in PL/pgSQL to achieve this.
Consider the following example:
DO
$$
DECLARE
v_cur CURSOR FOR SELECT * FROM many_rows_table;
row_data many_rows_table%ROWTYPE;
cursor_open BOOLEAN := FALSE;
BEGIN
-- Open the cursor if not already open
IF NOT cursor_open THEN
OPEN v_cur;
cursor_open := TRUE;
END IF;
115
Chapter 8 Cursors
RAISE NOTICE 'Status of the cursor: %' , CASE WHEN cursor_open='t' THEN
'OPEN' ELSE 'CLOSE' END;
-- Fetch rows using the cursor
FETCH v_cur INTO row_data;
postgres=# DO
$$
DECLARE
v_cur CURSOR FOR SELECT * FROM many_rows_table;
row_data many_rows_table%ROWTYPE;
cursor_open BOOLEAN := FALSE;
BEGIN
-- Open the cursor if not already open
IF NOT cursor_open THEN
OPEN v_cur;
cursor_open := TRUE;
END IF;
116
Chapter 8 Cursors
RAISE NOTICE 'Status of the cursor: %' , CASE WHEN cursor_open='t' THEN
'OPEN' ELSE 'CLOSE' END;
-- Fetch rows using the cursor
FETCH v_cur INTO row_data;
FOUND Attribute
The FOUND attribute returns true if the last operation on the cursor found a row,
and false otherwise. You can use the FOUND variable as an alternative in PL/
pgSQL. Consider the following example:
DO
$$
DECLARE
v_cur CURSOR FOR SELECT * FROM many_rows_table;
v_rec many_rows_table%ROWTYPE;
BEGIN
OPEN v_cur;
FETCH v_cur INTO v_rec;
IF FOUND THEN
117
Chapter 8 Cursors
CLOSE v_cur;
END;
$$;
In the preceding example, we declare and open a cursor named “v_cur”. We then
fetch the first record into the v_rec variable and use the FOUND attribute to determine
if a record was found. If a record was found, we print the record to the console using the
RAISE NOTICE statement. If no records were found, we print a message saying so.
Let’s run the function:
postgres=# DO
$$
DECLARE
v_cur CURSOR FOR SELECT * FROM many_rows_table;
v_rec many_rows_table%ROWTYPE;
BEGIN
OPEN v_cur;
FETCH v_cur INTO v_rec;
IF FOUND THEN
RAISE NOTICE 'Row found.';
ELSE
RAISE NOTICE 'No rows found.';
END IF;
CLOSE v_cur;
END;
$$;
NOTICE: Row found.
DO
As you can see, it shows “Row found” as there are records in the table.
118
Chapter 8 Cursors
NOTFOUND Attribute
The NOTFOUND attribute returns the opposite of the FOUND attribute, true if the last
operation on the cursor did not find a row and false otherwise. As an alternative, you can
use NOT FOUND in PL/pgSQL.
Consider the following example:
DO
$$
DECLARE
v_cur CURSOR FOR SELECT * FROM no_rows_table;
v_rec no_rows_table%ROWTYPE;
BEGIN
OPEN v_cur;
FETCH v_cur INTO v_rec;
IF NOT FOUND THEN
RAISE NOTICE 'No rows found.';
ELSE
RAISE NOTICE 'Row found.';
END IF;
CLOSE v_cur;
END;
$$;
In the preceding example, we declare and open a cursor named “v_cur”. We then
fetch the first record into the v_rec variable and use the NOT FOUND attribute to
determine if a record was not found. If no records were found, we print a message saying
so. If a record was found, we print the record to the console using the RAISE NOTICE
statement.
Let’s run the function by creating the table no_rows_table with empty rows:
119
Chapter 8 Cursors
v_rec no_rows_table%ROWTYPE;
BEGIN
OPEN v_cur;
FETCH v_cur INTO v_rec;
IF NOT FOUND THEN
RAISE NOTICE 'No rows found.';
ELSE
RAISE NOTICE 'Row found.';
END IF;
CLOSE v_cur;
END;
$$;
NOTICE: No rows found.
DO
As you can see, it shows “No rows found” as the table is empty.
ROWCOUNT Attribute
The ROWCOUNT attribute returns the number of rows processed by the last FETCH or
MOVE statement. You can use the GET DIAGNOSTICS command’s ROW_COUNT item
as an alternative for this. Consider the following example:
DO
$$
DECLARE
v_cur CURSOR FOR SELECT * FROM many_rows_table;
v_rec many_rows_table%ROWTYPE;
num_rows int;
BEGIN
OPEN v_cur;
MOVE FORWARD ALL FROM v_cur;
GET DIAGNOSTICS num_rows = ROW_COUNT;
120
Chapter 8 Cursors
CLOSE v_cur;
END;
$$;
In the preceding example, we declare and open a cursor named “v_cur”. We used
MOVE FORWARD to hold all the rows and GET DIAGNOSTICS to get the ROW_COUNT.
Let’s run the function and see if it displays the rows:
postgres=# DO
$$
DECLARE
v_cur CURSOR FOR SELECT * FROM many_rows_table;
v_rec many_rows_table%ROWTYPE;
num_rows int;
BEGIN
OPEN v_cur;
MOVE FORWARD ALL FROM v_cur;
GET DIAGNOSTICS num_rows = ROW_COUNT;
CLOSE v_cur;
END;
$$;
NOTICE: Number of rows fetched: 10000
DO
121
Chapter 8 Cursors
Monitor Cursors
Unlike other database engines where cursors have to be explicitly closed, in PL/pgSQL
cursors will be automatically closed when the transaction ends or when there is an
exception in the current execution block. The implicit transactions will also be closed
when their task is done. To monitor the current active transaction in PostgreSQL, we can
use the PostgreSQL system catalog table “pg_cursors”, which provides insight into the
details of opened cursors:
postgres=# \d pg_cursors
View "pg_catalog.pg_cursors"
Column | Type | Collation | Nullable | Default
--------------+--------------------------+-----------+----------+---------
name | text | | |
statement | text | | |
is_holdable | boolean | | |
is_binary | boolean | | |
is_scrollable | boolean | | |
creation_time | timestamp with time zone | | |
postgres=# DO
$$
DECLARE
v_rec RECORD;
v_rec2 RECORD;
BEGIN
FOR v_rec IN (SELECT * FROM many_rows_table) LOOP
SELECT name, creation_time FROM pg_cursors INTO v_rec2;
RAISE NOTICE 'Open cursor details %', v_rec2;
122
Chapter 8 Cursors
In the preceding output, you can see an unnamed cursor named “<unnamed portal
1>”. Typically, portals are named as system-generated pointers to query result sets. By
using these pointers, we can manage fetching data from the cursors.
If you want to use a user-defined or explicit cursor in a PL/pgSQL function, you must
first define it using the DECLARE statement and then open it using the OPEN statement.
Once the cursor is open, you can fetch rows from it using the FETCH statement. In the
case of implicit cursors, such as the one shown earlier, there is no need to create a cursor,
open it, and fetch data. This is handled automatically by the database engine.
SCROLL Cursor
PL/pgSQL provides the option to use scrollable cursors, which allow you to move back
and forth through the result set. This is particularly useful for applications that require
random access to the results, such as when implementing search functionality. To use
a scrollable cursor, you must declare it with the SCROLL keyword and then open it with
the OPEN statement. Once the cursor is open, you can fetch rows using the FETCH
statement and then move forward or backward through the result set using the MOVE
statement. Finally, you must close the cursor using the CLOSE statement.
Consider the following example, which demonstrates the behavior of a SCROLL
cursor. As we discussed earlier, the scope of the cursor is a transaction by default, so we
should try to move the scroll within the transaction rather than from a session:
123
Chapter 8 Cursors
To understand cursors better, let’s divide the preceding actions into two phases.
124
Chapter 8 Cursors
Phase 1
In the preceding example, we started the transaction using BEGIN WORK and created
an anonymous PL/pgSQL block that explicitly declares the cursor “v_cur” on the table
“scroll_test”. We also used an explicit cursor OPEN call on “v_cur”, which executes the
given SQL statement and points the cursor “v_cur” to the result set.
Phase 2
After executing the anonymous code block, we fetch the cursor “v_cur” using the
“FETCH NEXT” statement. This retrieves the data row by row, resulting in a result set of 1
and 2. We used the “MOVE BACKWARD” cursor control statement, which sets the cursor
pointer to its previous record. We did this twice to ensure that the cursor pointer is reset
to the beginning of the result set as a counterpart to the “FETCH NEXT” statement.
This is a simple way to create a SCROLL cursor, and by using FETCH NEXT or MOVE
BACKWARD, we can control the amount of result sets, which we retrieve from the cursor.
When building a dashboard with a paginated table, you can use FETCH NEXT calls to
retrieve the next page of data while keeping the cursor open in the transaction. If it is not
possible to keep the transaction open for pagination, then we should use “WITH HOLD”
cursors. These cursors hold the cursor data pointer even after the transaction is closed.
125
Chapter 8 Cursors
$$;
DO
postgres=*# FETCH NEXT FROM v_cur;
t
——
1
(1 row)
As shown in the preceding example, moving the cursor backward led to an error
because the cursor “v_cur” was declared as a “NO SCROLL” cursor. “NO SCROLL”
cursors are useful when holding the result set not only for reading data but also for
performing UPDATE operations. If you need to traverse a result set with a cursor and
perform update operations, it is recommended to use a “NO SCROLL” cursor.
Consider the following example:
126
Chapter 8 Cursors
DELETE 1
postgres=*# FETCH NEXT FROM v_cur;
t
—
2
(1 row)
postgres=*# END;
COMMIT
postgres=# DO
$$
BEGIN
127
Chapter 8 Cursors
Refcursors
PL/pgSQL supports reference cursors, which allow you to create a cursor and open it
and then pass or return the cursor to another function as an input argument. This means
that you can easily pass the cursor data pointer between multiple function calls, which
improves code reusability and maintainability. These cursors can be created, opened,
and passed or returned as input arguments to other functions. Consider the following
example:
128
Chapter 8 Cursors
postgres=# DO
$$
DECLARE
v_cur REFCURSOR;
BEGIN
OPEN v_cur FOR SELECT * FROM scroll_test;
PERFORM test_refcursor(v_cur);
END;
$$;
NOTICE: Record (2)
DO
129
Chapter 8 Cursors
Summary
In this chapter, we started with the basics of cursors in PL/pgSQL and how to use them to
optimize performance. We talked about the different types of cursors and their use cases
with different examples. Cursors allow for more efficient data retrieval, processing, and
manipulation, resulting in better database performance and faster response times. By
using different types of cursors, cursor attributes, and reference cursors, developers can
optimize their PL/pgSQL code and improve the overall efficiency of their applications.
What’s Next
In the next chapter, we will be covering some key features of custom operators like the
following:
• Complex Operator Design: Delve into advanced operator
overloading and customization to create powerful and intuitive
custom operators.
• Operator Performance: Explore optimization techniques to ensure
that custom operators perform efficiently in different scenarios.
• Operator Consistency: Learn about the importance of consistent
operator behavior and how to design operators that align with
PostgreSQL’s standards.
• Practical Operator Use: Discover real-world use cases where custom
operators simplify complex operations within PL/pgSQL code.
130
CHAPTER 9
Custom Operators
In the previous chapter, we talked about cursors and how to use them in PL/pgSQL with
some examples. In this chapter, we will start with what are different types of built-in
operators available in PostgreSQL. We will cover custom operators and their purpose.
We will walk through the use cases to create custom operators with simple examples. We
will also explain the advantages and disadvantages of having custom operators. By the
end of this chapter, you will have a better understanding of custom operators and where
to use them effectively.
In PostgreSQL, custom operators can be defined and used just like built-in operators.
In this chapter, we will explore how to create custom operators in PostgreSQL with PL/
pgSQL examples and discuss different use cases and pros and cons of using custom
operators.
Built-In Operators
We all know that an operator is a symbol or keyword to perform specific operations
on the data. There are several types of operators available in PostgreSQL, and the
documentation has all details available already: www.postgresql.org/docs/current/
functions.html.
You can look at the pg_catalog.pg_operator table to see available operators from the
database. For example, if you look at “||” operator availability, you can see what left and
right operands can be used with this operator:
132
Chapter 9 Custom Operators
However, if you try to use this operator with integers, you will see an error:
It says the operator does not exist, which is true as this operator with left and right
operands as int does not exist. So, this is where you can create custom operators.
Let’s look at some simple examples of these operators before we start with custom
operators. Note that it will not cover a complete list of operators but a few operators
which are commonly used.
• Arithmetic Operators: + (addition), - (subtraction), *
(multiplication), / (division), % (remainder), ^ (exponentiation)
A simple example is
• Comparison Operators: =, <> or !=, <, <=, >, >=, IS DISTINCT FROM,
IS NOT DISTINCT FROM
A simple example is
postgres=# select 1=1 as "=", 1<>1 as "<> or !=", 1<2 as "<", 1>2 as ">", 1<=1 as
"<=", 1>=1 as ">=", 1 IS DISTINCT FROM 1 as "IS DISTINCT FROM", 1 IS NOT DISTINCT
FROM 1 as "IS NOT DISTINCT FROM";
= | <> or != | < | > | <= | >= | IS DISTINCT FROM | IS NOT DISTINCT FROM
---+----------+---+---+----+----+------------------+----------------------
t | f | t | f | t | t | f | t
(1 row)
133
Chapter 9 Custom Operators
A simple example is
A simple example is
A simple example is
A simple example is
134
Chapter 9 Custom Operators
1
2
(2 rows)
Time: 0.285 ms
postgres=# select '1' as "INTERSECT" INTERSECT select '1';
INTERSECT
-----------
1
(1 row)
Time: 0.190 ms
postgres=# select '1' as "EXCEPT" EXCEPT select '2';
EXCEPT
--------
1
(1 row)
135
Chapter 9 Custom Operators
Simple Example
Let’s start with a simple example to check if a string is empty or not. If the string is empty,
we should return true; otherwise, return false. By default, we use PostgreSQL’s equality or
inequality operators to check for string emptiness, as follows:
What if we had a unary operator to perform the same action? For example, if we
wanted to check whether a string was empty or not, we could just put a “?” in front of it. For
instance, “?not empty” would return false. PostgreSQL is developer-friendly and allows us
to create custom operators to achieve this. Creating an operator has to be in two steps.
Step 1: Create an operator function.
136
Chapter 9 Custom Operators
Now, let us execute the desired behavior and see the results:
Here is another example where we add a custom operator for the string concat. The
built-in operator "+" is for arithmetic operations, which is basically to add two numbers
or dates. However, you have an application code where two strings are added using the
"+" operator. Usually, adding two strings is nothing but concatenation. You can simply
use the "||" operator or concat functions as follows:
Time: 0.131 ms
postgres=# select concat('', 'con','cat');
concat
--------
concat
(1 row)
Time: 0.483 ms
postgres=# select concat_ws('', 'con','cat');
concat_ws
-----------
concat
(1 row)
137
Chapter 9 Custom Operators
However, if you are migrating your database from different database engines which
support the “+” operator to add two strings to the PostgreSQL database, you would need
to change your application code with the “||” operator or concat function. To avoid the
code update, you can create a custom operator “+” which supports the concatenation of
two strings. The following are the steps:
1. Create a new schema to create a custom operator and grant
privileges to the PUBLIC role so that every user can use the
custom operator:
or
138
Chapter 9 Custom Operators
As you can see, without setting the search_path thrown the error as it was using the
operator in the pg_catalog schema.
139
Chapter 9 Custom Operators
This is a simple example based on requirements of how the application was built.
You can work around the application behavior without changing the application code
and efforts by creating a custom operator. There will be several use cases where you
would need custom operators which will save your time and efforts of changing the
application code or PL/pgSQL code inside the database.
We will cover a few scenarios where custom operators will be useful.
140
Chapter 9 Custom Operators
or
141
Chapter 9 Custom Operators
Benefits
One benefit of this option is that you don’t need to change the application queries
to add any built-in functions like lower and upper or add any operators like ~~* or
ILIKE. Additionally, you can set the search_path as a switch to make the data case-
insensitive if you want only part of your application to behave like case-insensitive
without making any changes to the application.
142
Chapter 9 Custom Operators
CREATE OPERATOR + (
LEFTARG = multi_number,
RIGHTARG = multi_number,
PROCEDURE = numbers_add
);
As you can see, it added multiple numbers. So, custom operators are helpful when
you have specific properties of interest in your complex numbers that you want to
compare. This could be the real or imaginary parts, specific combinations of these parts,
or any other property relevant to your application. Defining custom operators allows you
to create comparisons based on these properties, enabling you to sort or filter complex
numbers accordingly.
143
Chapter 9 Custom Operators
144
Chapter 9 Custom Operators
postgres=#
146
Chapter 9 Custom Operators
-- Table Definition
CREATE TABLE employees (
employee_name VARCHAR(100),
age INTEGER,
salary NUMERIC(10, 2)
);
-- Sample Data
INSERT INTO employees (employee_name, age, salary) VALUES
('John Smith', 35, 4000.00),
('Jane Doe', 45, 5500.00),
('Mike Johnson', 28, 3200.00),
('Sarah Thompson', 52, 6000.00),
('Robert Wilson', 31, 2500.00),
147
Chapter 9 Custom Operators
As you can see, the data has been classified based on their ages as defined in the
custom operator function.
This example shows how you can create a custom operator in PostgreSQL to simplify
data classification operations. It allows you to encapsulate the logic for classification in a
reusable operator, making your queries more expressive and easier to understand.
Advantages
• Custom operators can make our code more expressive and easier to
read, especially when working with domain-specific languages.
• Custom operators can facilitate complex operations and make code
more reusable.
• Developers can design custom operators that align with specific
business logic, potentially leading to more optimized query plans
and better performance compared to using standard operators.
• By defining custom operators for domain-specific operations, you
can enforce consistent usage and behavior across queries, reducing
the chances of errors due to inconsistent logic.
148
Chapter 9 Custom Operators
Disadvantages
• Custom operators can be less portable and may not be compatible
with other database systems.
• Custom operators can be harder to debug and maintain, especially if
the codebase becomes large and complex.
• Overuse or misuse of custom operators can lead to confusion among
team members, especially if the operators are not well documented
or if their behavior differs from standard operators.
• While custom operators can potentially optimize certain queries,
they can also complicate the query optimizer’s job. Complex custom
operators might result in less predictable query plans, affecting
overall performance.
Summary
In this chapter, we have learned that custom operators in PostgreSQL provide a
powerful tool for extending the functionality of the database and making our code more
expressive and easier to read. With PL/pgSQL examples, we created custom operators
that match the requirements and allow us to perform operations that are not possible
with the built-in operators. By carefully designing and implementing custom operators,
we can leverage the full power of PostgreSQL to meet the needs of our applications
and users.
What’s Next
In the next chapter, we will be covering some key features of custom casting like the
following:
• Complex Data Conversion: Learn to design and implement custom
type casts to seamlessly convert between different data types.
• Casting Consistency: Explore strategies for maintaining consistent
casting behavior and avoiding unexpected conversion outcomes.
149
Chapter 9 Custom Operators
150
CHAPTER 10
Custom Casting
In the previous chapter, we talked about operators in PostgreSQL and how built-
in operators work. We also covered how to create custom operators based on the
requirements. In this chapter, we will explore custom casting in PostgreSQL and how
to define our own custom casting functions. We will also discuss use cases for custom
casting and how to create them accordingly. Custom casting can improve the flexibility
and expressiveness of our database queries and data manipulation. By the end of this
chapter, you will have a better understanding of how to work with custom casting using
PL/pgSQL of PostgreSQL.
In PostgreSQL, casting allows you to convert data from one data type to another.
There will be implicit and explicit type casting available in PostgreSQL. We can learn
more about implicit and explicit casting in the next section.
Built-In Casts
To better understand the features of CAST objects, let's start with a simple example.
PostgreSQL provides a set of object identifier types such as regclass, regrole, regproc,
regoper, etc. You may have seen one of these while querying SQL queries on the catalog
tables. For instance, consider the regclass example shown as follows:
postgres=#SELECT 805563::regclass;
regclass
----------
test
(1 row)
151
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_10
Chapter 10 Custom Casting
----------
test
(1 row)
In the preceding example, the number 805563 is being converted into the string
test. The purpose of the CAST type is to convert from one data type to another. In this
example, the number 805563 is an object ID (oid) of the table named test. Let's query
the pg_class table to see the results:
(1 row)
Note that the OID value will vary for different environments.
We can leverage the same behavior with user object ids as well. Consider the
following example:
(1 row)
(1 row)
(1 row)
152
Chapter 10 Custom Casting
In PostgreSQL, there are a lot of built-in casts available which help you with most of
the requirements from your application. You can use the pg_cast catalog table to check
the available castings. Here is an example command:
The preceding example is to check the casts available for int (int4). Let’s look at what
these columns represent. This output example is used to explain each column of the
following output:
castsource: It represents the source of the cast. In the example, our source is int.
153
Chapter 10 Custom Casting
casttarget: It represents the target of the cast. In the example, it shows different
types of targets (which are mostly related to numbers) that the source int can cast to. If
any cast is not part of it, then we cannot use it. For example, the target column does not
have an xml type, but if you try to type cast int to xml, you will see an error:
castfunc: It represents the function used for type casting. There are functions
available for small, int, bigint, etc. In the preceding output where it shows the casts
available for integers as source, you can see smallint as the target, and the function is
int2. The cast functions listed in pg_cast must always take the cast source type as their
first argument type and return the cast destination type as their result type.
The purpose of int2 being used is you cannot cast int values which are out of the
smallint range. For example:
Hence, the int2 function is used. You can see the int2 function result is always smallint:
castcontext: It represents the way casting happened which is implicit or explicit or both.
The “i” represents implicit, “e” represents explicit, and “a” represents implicit and explicit.
154
Chapter 10 Custom Casting
As bigint value is not allowed in int, there is no point of having bigint conversion explicitly.
Explicit type casting: Explicit type casting occurs when the user explicitly
instructs PostgreSQL to convert a value from one data type to another data type. This can
be done using the CAST or :: operator.
For example, to convert the string '123' to an integer, the user can use the CAST
operator as follows:
155
Chapter 10 Custom Casting
Explicit type casting can also be used to convert values to custom data types or to
perform more complex type conversions, which we will be discussing more in this chapter.
In our example output of int, there is an explicit conversion, which is int to boolean.
So, inserting int into a boolean field requires an explicit conversion. Here is an example:
In the preceding error, HINT clearly says you need an explicit cast. So, you can use
the following:
postgres=#
156
Chapter 10 Custom Casting
postgres=# select 1259::int::regclass;
regclass
----------
pg_class
(1 row)
pg_cast provides metadata about the available type conversions and can be useful
for understanding and managing type casting operations in the database. It can be
useful to
• Retrieve a list of all available type casts in the database. You can
simply use
157
Chapter 10 Custom Casting
• It contains information about the source and target data types for
each cast entry. It allows you to determine the direction of the cast,
that is, whether it is a cast from one type to another or a reverse cast
from the target type to the source type. If you look at the following
example, the cast from int to money is allowed but not the reverse:
Hence, if you try to convert money to int, it will throw an error that
the cast does not exist:
158
Chapter 10 Custom Casting
• It provides information about the cast method used for each cast
entry. Cast methods can include explicit casting functions, implicit
casting rules, and more. If you take the following example, inserting
an int integer value to a bit data type column is not allowed implicitly:
You can check the type casting of these types from pg_cast catalog:
159
Chapter 10 Custom Casting
It's important to note that directly manipulating the pg_cast table is not
recommended, as it is an internal system catalog table. Modifying the entries without
proper knowledge and understanding of the database internals can lead to data
inconsistencies and unexpected behavior. Instead, use the appropriate SQL syntax, such
as the CAST keyword or the :: operator, to perform type casting operations in a safe and
controlled manner.
The pg_cast table is an important part of PostgreSQL's type casting system, allowing
for flexible and customizable data type conversions. By understanding how to use
the pg_cast table, developers can create more powerful and expressive queries and
operators in their PostgreSQL databases.
Custom Casts
PostgreSQL allows you to define custom casting rules to convert data from one type to
another. Custom casting can be useful when working with domain-specific data types,
where the standard set of casting rules may not be sufficient to express the desired
functionality.
160
Chapter 10 Custom Casting
Simple Example
Let us consider you have a requirement of converting temperature values from Celsius to
Fahrenheit. The following is your application table with some values:
These statements create a table called temperature_table with two columns, id and
temperature, and insert three rows with temperature values in Celsius.
Usually, if you have to convert the values in the table to Fahrenheit, you will write a
query something like the following:
161
Chapter 10 Custom Casting
So, you can use the Celsius*9/5 + 32 formula to convert Celsius to Fahrenheit.
However, you can create a custom cast function using PL/pgSQL that converts
Celsius to Fahrenheit using the same formula. The following are the steps to do it:
1. Create a custom data type as follows:
2. Create the cast function using PL/pgSQL. You can have the
formula defined inside the function logic:
4. Now you can use the custom cast to convert Celsius temperatures
to Fahrenheit automatically:
162
Chapter 10 Custom Casting
This is a simple example on how you can use a custom cast by creating PL/pgSQL
functions based on your requirements from the application. There are several use cases
where you can use the custom cast to fulfill your requirements. We will cover a few
scenarios in this chapter which help you to understand and build your own custom casts
based on the needs.
163
Chapter 10 Custom Casting
4. You can use a custom cast in your queries and expressions like any
other cast operation:
164
Chapter 10 Custom Casting
Then, assign the person type details to a person custom data type as follows:
(1 row)
Next, try to convert the custom data type into JSONB using the following SQL
statement:
postgres=# SELECT (1, 'Sai Prasanna Kidambi', '35', 'NTR Nagar, Nellore,
India')::person::jsonb;
ERROR: cannot cast type person to jsonb LINE 1: ...Kidambi', '35', 'NTR
Nagar, Nellore, India')::person::jsonb;
165
Chapter 10 Custom Casting
As expected, there is no implicit conversion between the custom type and JSONB,
resulting in an error. To fix this, create the required casting functions and operators as
follows.
To convert the person data type into JSONB, create the following PL/pgSQL function:
To convert JSONB data to the person type, create the following PL/pgSQL function:
--Do not try to convert the null JSONB to the custom type.
--Rather return null from here.
IF a = 'null'::JSONB THEN
RETURN NULL;
END IF;
RETURN ((a->>'id')::integer,(a->>'name')::text,(a->>
'age')::integer,(a->>'address')::text)::public.person;
166
Chapter 10 Custom Casting
END;
$q$ LANGUAGE PLPGSQL;
CREATE FUNCTION
Now, create the CAST object as follows, which converts JSONB to the person type:
Finally, try the same SQL query that failed earlier, and verify that it now works as
expected:
postgres=# SELECT (1, 'Sai Prasanna Kidambi', '35', 'NTR Nagar, Nellore,
India')::person::jsonb;
row
---------------------------------------------------------------------------
-------------------
{"id": 1, "age": 35, "name": "Sai Prasanna Kidambi", "address": "NTR Nagar,
Nellore, India"}
(1 row)
Now, try the conversion from JSONB to the person type and verify the results:
(1 row)
From the preceding exercise, we have successfully created the PostgreSQL casting
object that converts a custom data type into JSONB and vice versa.
167
Chapter 10 Custom Casting
Summary
In this chapter, we have learned custom casting in PostgreSQL which allows you to
create more expressive and concise queries when working with domain-specific data
types. By defining custom casts, you can represent your data in a way that matches its
semantics, making your queries more intuitive and easier to read. With the examples
provided in this chapter, you should be able to create your own custom casts to suit your
specific needs.
Custom casting allows you to define your own rules for converting data between
different types, providing flexibility and customization in how your data is interpreted
and manipulated in PostgreSQL.
What’s Next
In the next chapter, we will be covering some key features of dynamic SQL in PL/pgSQL
like the following:
• Advanced Dynamic Query Generation: Dive deeper into
constructing dynamic SQL statements for complex scenarios and
dynamic conditions.
• Parameterized Queries: Learn how to securely pass parameters to
dynamic SQL queries and prevent SQL injection vulnerabilities.
• Query Execution and Performance: Explore strategies to optimize
the execution and performance of dynamically generated SQL
queries.
• Dynamic SQL in Functions: Discover how to integrate dynamic SQL
within stored procedures to build flexible and powerful routines.
168
CHAPTER 11
Dynamic SQL
In the previous chapter, we talked about custom casting in PostgreSQL and how to define
our own custom casting functions. We have also discussed the use cases for custom
casting and how to create them accordingly. In this chapter, we will cover the core
concepts of dynamic SQL within PostgreSQL’s plpgsql environment. We will provide a
comprehensive overview of the syntax and its practical implementation. Additionally, we
will cover some real-world scenarios and share best practices for harnessing the power
of dynamic SQL effectively. By the end of this chapter, readers will have gained a solid
grasp of dynamic SQL’s intricacies, enabling them to confidently utilize this feature in
PostgreSQL for various use cases.
Simple Example
Dynamic SQL is especially useful when you need to create SQL statements at runtime,
based on the data being processed. For example, you might need to create a query that
selects data from a different table depending on the input data, or you might need to
construct a complex query with many conditions based on user input. Let’s go through
an example:
–– Create a test table and insert some data:
170
Chapter 11 Dynamic SQL
171
Chapter 11 Dynamic SQL
(1 row)
Time: 9.646 ms
postgres=#
postgres=#
172
Chapter 11 Dynamic SQL
postgres=# \d customer_data
Table "public.customer_data"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
age | integer | | |
name | text | | |
As you can see, after executing this SQL query, it created a table named “customer_
data” with the specified columns and data types.
Note that this is just an example, and you can customize the table name and column
definitions according to your requirements. Dynamic SQL gives you the flexibility to
adapt the table structure on the fly based on the input you provide to the function.
For example, consider a situation where you need to formulate a query with multiple
conditions, some of which are optional and depend on the data input.
Let’s create the function:
173
Chapter 11 Dynamic SQL
BEGIN
EXECUTE 'SELECT id, name, city, state FROM records WHERE TRUE' ||
CASE WHEN name IS NOT NULL THEN ' AND name = ' || quote_literal(name)
ELSE '' END ||
CASE WHEN city IS NOT NULL THEN ' AND city = ' || quote_literal(city)
ELSE '' END ||
CASE WHEN state IS NOT NULL THEN ' AND state = ' || quote_
literal(state) ELSE '' END
INTO v_id, v_name, v_city, v_state;
RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
–– The dynamic SQL magic starts with the EXECUTE statement, crafting a query.
It uses CASE statements to add conditions only if their corresponding input
isn’t null.
–– As the dynamic query is executed, the retrieved result is stored in the variables
id, name, city, and state.
–– The result is then emitted via RETURN NEXT.
Let’s assume you are searching for records where the name is “foo” and the state is
“California.” Execute the function like this:
As you can see, it returned the record from the table as expected.
174
Chapter 11 Dynamic SQL
The beauty of this function lies in its adaptability. It allows you to dynamically
generate index creation statements based on various input parameters. You can
customize index names and specify the columns to be indexed, enhancing query
performance as needed. This feature is particularly useful when you want to create
multiple indexes with varying configurations or when you need to automate index
creation processes based on specific conditions.
Let’s run the function:
(1 row)
Time: 4.745 ms
postgres=#
postgres=# \d sample_table
Table "public.sample_table"
Column | Type | Collation | Nullable |
Default
---------+---------+-----------+----------+
-------------------------------------
id | integer | | not null |
nextval('sample_table_id_seq'::regclass)
column1 | text | | |
column2 | text | | |
Indexes:
"sample_table_pkey" PRIMARY KEY, btree (id)
"sample_column1_index" btree (column1)
176
Chapter 11 Dynamic SQL
177
Chapter 11 Dynamic SQL
table_name: Specify the name of the table from which you want to
select columns.
column_names: Provide an array of column names you want to
include in the result.
For instance, if you want to select only the “first_name” and “salary” columns from
the “employee_data” table, you would execute the function like this:
This example illustrates how dynamic SQL can enhance the flexibility of querying.
The function select_columns allows you to retrieve specific column values based on
your requirements, enabling more efficient data retrieval and reducing unnecessary data
transfer.
By constructing the SELECT statement on the fly, you’re able to tailor your queries
to the exact columns you need, optimizing network bandwidth and query performance.
This feature is particularly valuable when you need to retrieve different subsets of data
based on varying conditions, without writing multiple static queries.
In essence, the select_columns function showcases how dynamic SQL empowers
you to be more precise in your data retrieval, contributing to more efficient and effective
querying in PostgreSQL.
Let’s execute the function:
For example:
For example:
179
Chapter 11 Dynamic SQL
3. Security Concerns
While constructing dynamic SQL, it’s crucial to limit access to only authorized users and
ensure that they have the appropriate permissions for executing the generated queries:
Security definer functions in PostgreSQL allow the function to execute with the
privileges of the user who defined it, rather than the privileges of the user who is executing
it. While this can be useful in certain situations, such as when creating a function that
accesses sensitive data, it can also be a security risk if not implemented correctly.
For example, let’s say we have a security definer function called get_sensitive_
data() that retrieves sensitive information from a table. The function is defined by a user
with high privileges and is intended to be called by other users who do not have access
to the sensitive data.
However, if the function is not implemented securely, a user with malicious intent
could potentially exploit it to gain access to the sensitive data. For example, if the function
uses dynamic SQL to construct a query based on input parameters, a user could potentially
inject malicious code into the input parameters to execute unauthorized queries.
To mitigate these risks, it is important to follow best practices for writing secure
functions and procedures in PostgreSQL, such as using proper input validation, limiting
access to sensitive data, and testing thoroughly to ensure that the function is working as
intended. Additionally, it is recommended to use security invoker functions instead of
security definer functions whenever possible, to limit the potential security risks.
4. Performance Optimization
Dynamic SQL may impact performance due to query planning overhead. To maintain
optimal performance, consider these strategies:
180
Chapter 11 Dynamic SQL
For example:
PREPARE dynamic_query(text) AS
SELECT * FROM employees WHERE last_name = $1;
EXECUTE dynamic_query(input_last_name);
By implementing best practices, sanitizing user inputs, and ensuring proper access
controls, developers can harness the benefits of dynamic SQL while mitigating potential
risks and maintaining efficient query execution.
Summary
In this chapter, we have provided an in-depth exploration of dynamic SQL within
PostgreSQL’s plpgsql environment. We have covered the essential concepts, syntax,
and hands-on implementation of dynamic SQL. Real-world scenarios were examined,
coupled with best practices to ensure effective utilization. Armed with this knowledge,
the readers are now equipped to confidently harness the capabilities of dynamic SQL in
PostgreSQL for a range of applications.
What’s Next
In the next chapter, we will be covering some key features of functions and procedures of
PL/pgSQL like the following:
• Complex Function Logic: Dive deep into advanced techniques for
building complex and versatile functions and procedures.
• Function Overloading: Explore creating overloaded functions to
provide multiple versions with varying parameter lists.
• Function Categories: Different types of function with use cases on
how to use them effectively.
• Modular Code Design: Discover strategies for designing modular
PL/pgSQL code using functions and procedures for reusability.
181
CHAPTER 12
Building Functions
and Procedures
In the previous chapter, we talked about Dynamic SQL and how to create and use them
in PL/pgSQL with real time use cases. In this chapter, we will discuss functions and
procedures in PostgreSQL and how to build them using the PL/pgSQL programming
language. We will cover the different types of available function categories and
differences between them using some examples. It will help to choose the function
category while developing. We will also explain the different types of procedures and
how to use them with some examples.
Functions and procedures can enhance the performance of database operations by
consolidating intricate logic into a single, reusable unit. It is crucial to take into account
several factors when designing these functions and procedures, such as their scope of
invocation, transaction handling, and parameter handling. By doing so, we can ensure
that our functions and procedures are efficient, dependable, and easy to maintain.
PostgreSQL functions are categorized based on their scope of invocation and their result
set, which determines whether the function is executed within or outside a transaction,
among other things. While implementing the business logic in PL/pgSQL functions or
procedures, we should consider the factors mentioned earlier.
Functions
PL/pgSQL functions are an essential part of PostgreSQL's procedural programming
capabilities, allowing developers to encapsulate custom logic and business rules directly
within the database. By combining SQL statements with procedural constructs like loops
and conditionals, PL/pgSQL functions enable advanced data manipulation, validation,
and calculation directly within the database server. These functions can be used for
183
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_12
Chapter 12 Building Functions and Procedures
tasks such as data transformation, enforcing complex constraints, and creating reusable
code modules. PL/pgSQL functions can be divided into categories based on their
invocation mode.
Defining Functions
To define a function in PostgreSQL, you use the CREATE FUNCTION statement. This
statement specifies the name of the function, the input parameters, and the instructions
that make up the function.
Here is an example of a simple function that adds two numbers together:
This function takes two input parameters (num1 and num2) and returns the sum
of those two numbers. The function is defined using the plpgsql language, which is a
procedural language that supports a wide range of programming constructs.
Calling Functions
Once you have defined a function, you can call it from any program or script that has
access to the database. To call a function in PostgreSQL, you use the SELECT statement
and specify the name of the function and any input parameters.
Here is an example of how to call the add_numbers function from the previous
example:
184
Chapter 12 Building Functions and Procedures
This statement calls the add_numbers function with the input parameters 5 and 10
and returns the result of that function (which is 15).
Categories
When creating a PL/pgSQL function, we can specify its volatility category as
IMMUTABLE, STABLE, or VOLATILE. This classification helps the optimizer to minimize
the number of calls made on the result set. For instance, consider a global constant
function called pi(), which always returns 3.14 as output. If we run the pi() function once
or ten times, the result won't change. Thus, there is no need to execute this function
multiple times if we use it in an SQL query. Consider the following example, where we
run the pi() function on a table, where the table has multiple rows:
In the preceding example, a table named test was created, and ten rows were
inserted into it. The pi() function was then called on this table, which should have
been executed ten times, once for each row. However, in PostgreSQL, this function is
not invoked ten times because it is globally constant. Instead, it is executed only once
and the result is cached. This cached result is then utilized in subsequent executions
of the function. This type of function can be classified as an IMMUTABLE function.
Now, Let see what is the classification defined for this function pi() in PostgreSQL.
185
Chapter 12 Building Functions and Procedures
-------------
i
(1 row)
Immutable Functions
To better understand the behavior of the pi() function described earlier, let's create a
regular function (with the default VOLATILE classification), as well as an immutable
function, and observe how many times each function is executed on the test table:
186
Chapter 12 Building Functions and Procedures
187
Chapter 12 Building Functions and Procedures
STABLE Functions
Unlike IMMUTABLE functions, which always produce the same result each time they are
run, STABLE functions maintain a unique result per SQL statement level. That is, the
STABLE function will only execute once per entire SQL result set if the result is always the
same for the given parameter. If the function produces a different result set for the given
input parameters, then the SQL would execute multiple times. Consider the following
example:
NOTICE: test_stable_func() called
NOTICE: test_stable_func() called
NOTICE: test_stable_func() called
NOTICE: test_stable_func() called
NOTICE: test_stable_func() called
NOTICE: test_stable_func() called
NOTICE: test_stable_func() called
NOTICE: test_stable_func() called
NOTICE: test_stable_func() called
NOTICE: test_stable_func() called
t
----
1
2
3
...
...
...
(10 rows)
The function test_stable_func is called with the table's column name t, which
contains different values. However, the function always returns the value 1 regardless of
the input value. Therefore, the optimizer needs to execute the STABLE function multiple
times, as the input parameter and the result differ each time. In addition, the input
parameter for the function test_stable_func is of type column, with dynamic values.
The optimizer does not receive any indication of whether to execute this function only
once or repeatedly. Therefore, this function will be executed for each row in the table.
Let's take another built-in STABLE function, statement_timestamp, as an example
and observe its behavior when invoked for each row:
189
Chapter 12 Building Functions and Procedures
...
...
...
2023-08-15 12:11:03.55166+05:30
(10 rows)
Since this function is marked as STABLE, PostgreSQL will execute it only once and
cache the result. The cached result will then be used for all other tuples instead of
invoking the function repeatedly. For example, if we have a table with a million rows
and we invoke statement_timestamp on each row, it will not make a million system
time calls. Instead, it will only make one system time call and reuse the value for the
other tuples.
STABLE functions will produce unique results for each SQL statement, but their
behavior will change across multiple SQL statements. For example, suppose we have a
transaction where we execute multiple SQL statements. In that case, each SQL statement
will produce different results. Consider the following example, where multiple SQL
statements produce different results inside a transaction:
190
Chapter 12 Building Functions and Procedures
...
...
2023-08-15 12:23:44.237813+05:30
(10 rows)
postgres=*# END;
COMMIT
From the preceding output, it is apparent that each SQL statement has a unique
timestamp, while different SQL statements have different timestamps.
VOLATILE Functions
Unlike STABLE or IMMUTABLE functions, VOLATILE functions are executed for each
tuple of the SQL result set. This means that they may produce different outputs for
the same input value. By default, each function created in PostgreSQL is a VOLATILE
function. Generally, these functions update tables in the database or process each tuple
and return a different value. VOLATILE functions are not the best candidates for creating
functional indexes. Consider the following example, which demonstrates the behavior of
functional indexes:
191
Chapter 12 Building Functions and Procedures
NOTICE: test_volatile_func() called
NOTICE: test_volatile_func() called
NOTICE: test_volatile_func() called
NOTICE: test_volatile_func() called
NOTICE: test_volatile_func() called
NOTICE: test_volatile_func() called
t
----
1
2
3
...
...
...
(10 rows)
192
Chapter 12 Building Functions and Procedures
NOTICE: process_each_row() called
NOTICE: process_each_row() called
NOTICE: process_each_row() called
NOTICE: process_each_row() called
NOTICE: process_each_row() called
NOTICE: process_each_row() called
NOTICE: process_each_row() called
NOTICE: process_each_row() called
process_each_row
------------------
2
3
4
...
...
...
(10 rows)
In the preceding example, the function traversed each tuple and added the value
1 to each row before returning the value. This is a case where volatile functions are
recommended to be used, as the business logic needs to be invoked for each tuple. Let's
take another built-in VOLATILE function, clock_timestamp, as an example and observe
its behavior when invoked for each row:
193
Chapter 12 Building Functions and Procedures
The preceding output suggests that the clock_timestamp function was executed
for each row in the table, resulting in a different timestamp value for each tuple.
Unlike STABLE and IMMUTABLE functions, VOLATILE functions always produce different
timestamp values inside a transaction.
Procedures
Starting from PostgreSQL 11, PL/pgSQL allows the creation of procedures. Procedures,
like functions, consist of a series of business logic steps that may include the use of
LOOP and control statements. However, unlike functions, procedures do not return any
values to the calling environment. To obtain values from procedures, we can use OUT
parameters.
Procedures cannot be invoked as part of SQL statements. Instead, we must use an
explicit “CALL procedure_name()” statement to execute them. Consider the following
simple example:
In the preceding example, we have created a procedure to reset the test data. When
the procedure is invoked, it deletes all data from the test table, inserts some dummy
rows, and then commits the changes. We cannot achieve the same behavior with
functions because the execution of a function is always an integral part of the same
transaction. Procedure execution is not an integral part of the transaction, as procedures
194
Chapter 12 Building Functions and Procedures
As you can see, we received an error message because the function execution occurs
within an implicit transaction, and a nested COMMIT operation was invoked before
completing the implicit transaction. We will discuss this further in the upcoming chapter
on transaction management.
Temporary Functions/Procedures
In PostgreSQL, temporary functions/procedures can be created that are automatically
dropped when the session is closed. These objects are scoped to the session and are
more advantageous than anonymous functions because they allow for input parameters
and return values to be passed to the calling environment. Temporary objects in
PostgreSQL are created in the pg_temp schema, a special schema that is only visible
and accessible to the session that created them. At the end of the session, these objects
are automatically dropped. This is useful for creating temporary tables, indexes, or
195
Chapter 12 Building Functions and Procedures
other database objects that are only needed for the duration of a session. Consider the
following example:
(1 row)
postgres=# \q
Press any key to continue . . .
The preceding output indicates that an error occurred while executing the temporary
function (which was deleted as the previous session closed) that was created in the
previous session.
VARIADIC Functions/Procedures
In some of the cases, functions or procedures may need to accept an unknown number
of inputs as arguments, and declaring a function with a fixed amount of parameters
would not be ideal. For example, consider the following logging method, which accepts
196
Chapter 12 Building Functions and Procedures
the log level as the first parameter, and a variable number of parameters to log the actual
message:
In the log_message() method shown earlier, the app sends only two parameters if
the log level is 'INFO', but sends more than two parameters if the log level is 'ERROR'.
To support this dynamic number of parameters in procedures/functions, PL/pgSQL
offers VARIADIC parameter types that can accept a dynamic number of input parameters
in functions/procedures. Consider the following example, which demonstrates the
behavior described earlier:
197
Chapter 12 Building Functions and Procedures
Now, let's test this procedure with the log level “INFO,” which only sends a text
message along with it:
Now, try setting the log level to 'ERROR' and providing a greater number of input
parameters, as follows:
Best Practices
When designing and implementing functions and procedures in PostgreSQL, it is important
to follow best practices to ensure that your code is efficient, maintainable, and secure.
Here are some best practices to consider:
• Use descriptive function and procedure names that accurately
describe the operation being performed.
• Use input parameters to make your functions and procedures more
flexible and reusable.
• Use comments to document your functions and procedures and
make them more understandable.
• Follow PostgreSQL security best practices to ensure that your code is
secure and protected from attacks.
• Test your functions and procedures thoroughly to ensure that they
are working correctly and efficiently.
198
Chapter 12 Building Functions and Procedures
By following these best practices, you can create functions and procedures that are
efficient, maintainable, and secure and can help you manage and manipulate your data
more effectively in PostgreSQL.
Summary
In this chapter, we have covered functions and procedures which can improve database
performance by consolidating complex logic into a single, reusable unit. When designing
these functions and procedures, it is important to consider factors such as their scope
of invocation, transaction handling, and parameter handling. PostgreSQL functions are
categorized based on their volatility. IMMUTABLE functions always produce the same
output for the same input. These functions are used in query optimization and can be
cached by the server. STABLE functions produce the same output for the same input
and different results for different inputs. VOLATILE functions always produce different
outputs for the same input. Choosing the appropriate volatility category is crucial for
optimizing query performance.
What’s Next
In the next chapter, we will be covering some key features of return values and
parameters in PL/pgSQL like the following:
• Advanced Return Values: Learn about returning complex data types,
arrays, and record types from PL/pgSQL functions and procedures.
• Output Parameters: Explore using output parameters for returning
multiple values or complex data structures from functions.
• Dynamic Result Sets: Discover how to return dynamic result sets
using refcursor parameters for versatile data retrieval.
• Parameter Modes: Dive into the intricacies of different parameter
modes (IN, OUT, INOUT) and their use cases.
199
CHAPTER 13
Return Values
and Parameters
In the previous chapter, we talked about building functions and procedures. We covered
examples of using IMMUTABLE, STABLE and VOLATILE functions besides to temporary
functions and variadic functions. In this chapter, we will cover the importance of
returning a value in a function and the different types of options available to return the
values in PL/pgSQL. We will walk through some real-world examples of where and when
to use each type of return value based on the requirements.
Return Values
In PL/pgSQL, returning a value means that the function will produce a result that can be
used by other parts of the program. The RETURNS keyword is used to specify the data type
of the value that the function will return. This can be any valid PostgreSQL data type,
such as INTEGER, VARCHAR, or even custom composite types.
When the function is executed, it can use the RETURN statement to provide a value as
output. This value can be derived from any operation or calculation performed within
the function and can be based on input parameters, the state of the database, or any
other relevant factors.
By returning a value, PL/pgSQL functions enable developers to create reusable code
that can perform specific tasks and return results that can be used by other parts of
the application. This can help to streamline programming and reduce redundancy by
allowing common operations to be encapsulated in a single function.
201
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_13
Chapter 13 Return Values and Parameters
In addition to returning simple data types, PL/pgSQL functions can also return
complex data structures, such as arrays, tables, or custom composite types. This allows
developers to create flexible and powerful functions that can generate custom output
based on complex input parameters.
Simple Example
Let’s look at a simple example of a PL/pgSQL function that returns a value:
In this example, the calc_area function takes two integer parameters: length and
width. The function multiplies these parameters to calculate the area of a rectangle,
which is assigned to a local variable called area. Finally, the RETURN statement is used to
return the value of area as the function’s output. When calling this function, the returned
value can be stored in a variable or used directly in an expression.
Let’s run the function:
As you can see, the area of a rectangle with length 20 and width 10 is returned as
200 by the function. By returning a value, PL/pgSQL functions can be used to perform
calculations, manipulate data, or generate custom output based on input parameters.
202
Chapter 13 Return Values and Parameters
RETURNS
This keyword is used to specify the data type of the value that the function will return.
This can be any valid PostgreSQL data type, and the function can use the RETURN
statement to provide a value as output. The following is the syntax:
• Cannot return a set of rows, which limits the flexibility of the function
203
Chapter 13 Return Values and Parameters
RETURNS SETOF
This syntax allows a PL/pgSQL function to define a custom record type as the output
and then return a set of rows, each of which is a record with that specific structure. The
following is the syntax:
RETURNS TABLE
This syntax is similar to RETURNS SETOF, but allows the function to define a custom
table type that contains one or more columns with specific data types. The following is
the syntax:
204
Chapter 13 Return Values and Parameters
OUT
This keyword is used to define output parameters that can be set within the function
using the SELECT INTO statement. These parameters can be used to return values in a
variety of ways, such as returning a single value, a set of values, or a record type. The
following is the syntax:
205
Chapter 13 Return Values and Parameters
The choice of return type depends on the specific requirements of the function and
the application as a whole. RETURNS SETOF may be the most flexible, but it may also be
more difficult to define and work with. RETURNS and OUT parameters may be simpler
to use, but they have their own limitations. RETURNS TABLE provides a clear definition
of the function’s output, but it may be more verbose compared to other return types.
206
Chapter 13 Return Values and Parameters
207
Chapter 13 Return Values and Parameters
As you can see, it returned the value from the variable as expected.
208
Chapter 13 Return Values and Parameters
This SQL statement returned a table with all user_id and name values from the
users table.
By providing RETURN NEXT, PL/pgSQL allows developers to return result sets one row
at a time, which can be useful in situations where it is not practical to return the entire
set at once.
209
Chapter 13 Return Values and Parameters
This SQL statement returned a set of rows, each of which is a record of type users.
210
Chapter 13 Return Values and Parameters
END;
$$ LANGUAGE plpgsql;
This SQL statement returned a set of rows, each of which is an integer value.
211
Chapter 13 Return Values and Parameters
parameter. The INTO statement is used to set the value of the user_row variable to the
corresponding record.
To return the result, we use the RETURN statement to return the user_row variable as
the output of the function.
Let’s run the function:
This SQL statement returned a single row with three columns: name, department,
and salary. By using RETURNS RECORD, PL/pgSQL functions can return a single row
with a custom structure, which can be useful in situations where the data needs to be
manipulated or processed further by other parts of the application.
212
Chapter 13 Return Values and Parameters
This SQL statement returned a set of rows, each of which is a record type.
It is also possible that we can return multiple SQL statement results from a single
function as follows:
213
Chapter 13 Return Values and Parameters
This SQL statement returned a single row with one integer value.
Here is another example where we return multiple rows via multiple OUT
parameters:
214
Chapter 13 Return Values and Parameters
LANGUAGE plpgsql
AS $function$
DECLARE
v_rec RECORD;
BEGIN
FOR v_rec IN (SELECT generate_series(1, 10) i, 'abcd' a ) LOOP
p_output1 := v_rec.i;
p_output2 := v_rec.a;
RETURN NEXT;
END LOOP;
RETURN;
END;
$function$;
CREATE FUNCTION
In the preceding example, we declared two OUT variables named p_output1 and
p_output2. Then, we assigned the column value of the record to each OUT variable type.
After that, we called RETURN NEXT, which keeps the result queue in the heap. Finally, the
RETURN statement returns the result heap to the calling environment.
215
Chapter 13 Return Values and Parameters
The preceding code declares two variables. The p_output1 parameter is declared as
INOUT, while the p_output2 parameter is declared as an OUT parameter. An input value
of 5 is passed to the INOUT parameter, p_output1, which is then updated to a different
output value of 10. This demonstrates the behavior of returning values to the calling
environment via an INOUT parameter.
Summary
In this chapter, we talked about various ways to return values in PL/pgSQL functions,
including using the RETURN statement to return a single value of any valid PostgreSQL
data type, using RETURNS TABLE or RETURNS SETOF to return a set of rows with a specific
216
Chapter 13 Return Values and Parameters
structure, and using OUT parameters to set the value of variables that are returned as
output. We also included examples of functions that return different types of data, such
as integers, record types, and tables.
What’s Next
In the next chapter, we will be covering some key features of exception handling in PL/
pgSQL like the following:
• Advanced Exception Handling: Explore handling multiple
exceptions, nesting exception blocks, and gracefully recovering
from errors.
• Custom Exception Types: Learn how to create and raise custom
exceptions to provide meaningful error messages and context.
• Logging and Debugging: Discover techniques for logging exception
details and debugging your PL/pgSQL code.
• Exception Propagation: Explore strategies for propagating
exceptions to higher levels for centralized error handling.
• Error Recovery: Dive into techniques for recovering from exceptions
and ensuring that your application remains operational.
217
CHAPTER 14
Handling Exceptions
In the previous chapter, we talked about returning complex data types, arrays, and
record types from PL/PGSQL functions and procedures and explored output parameters
for returning multiple values. In this chapter, we will cover different types of exceptions
in PostgreSQL. It will start with the introduction of exceptions and how to use them
within PL/pgSQL code with a simple example. We will walk through some real-world
examples of where and when to use these exceptions using PL/pgSQL functions or
procedures. We will also cover the advantages and disadvantages of exceptions.
Exceptions
Let’s start with exploring how to determine the effect of a command in PL/pgSQL which
helps to return meta-information. This will help with the exceptions in PL/pgSQL to
return necessary information. In PL/pgSQL, there are several ways to retrieve meta-
information about the last command run inside a function or procedure. Some of the
most commonly used methods are GET DIAGNOSTICS and FOUND.
GET DIAGNOSTICS
This command is used to retrieve information about the last executed statement. It can
be used to retrieve the row count, the number of affected rows, and the SQLSTATE of the
last executed statement.
The syntax for GET DIAGNOSTICS is as follows:
219
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_14
Chapter 14 Handling Exceptions
Here, <variable> is the name of a variable that will hold the value of the item being
retrieved, and <item> is the item to be retrieved. The item can be one of the following:
Let’s look at an example where we can get how many rows were updated through an
UPDATE within a function.
Create a table named test_table:
Here is the function with the use of GET DIAGNOSTICS to retrieve the row count of the
last executed statement:
220
Chapter 14 Handling Exceptions
In this example, we are creating a function called test_func. Within the function, we
are updating a table and using GET DIAGNOSTICS to retrieve the row count of the update
statement. We are then returning the row count as the output of the function.
Let’s run this function:
As you can see, it returned 10, which means 10 rows in the table were updated
through this function.
However, if you go into the details, how can we use the row count? Let’s consider an
example of a function which is used to update employee salaries.
Let’s create the employees table with some data:
221
Chapter 14 Handling Exceptions
row_count INTEGER;
BEGIN
UPDATE employees
SET salary = new_salary
WHERE id = employee_id;
In this example, we are using GET DIAGNOSTICS to retrieve the number of rows
affected by an update statement. We first update the salary of an employee with a given
ID and then use GET DIAGNOSTICS to retrieve the number of rows affected by the update.
We then use the IF statement to check if the update affected exactly one row. If it did,
we return TRUE, indicating that the update was successful. If it affected zero or more than
one row, we return FALSE, indicating that the update was not successful.
Let’s run this function:
222
Chapter 14 Handling Exceptions
(1 row)
As you can see, the function returned TRUE as it was able to UPDATE one row as per
the employee ID. Let’s see what happens if we run this function with an employee ID
which does not exist in the table, for example, 5:
FOUND
This command is used to determine if the last executed statement returned any rows. It
can be used in conjunction with other commands, such as SELECT, to determine if the
query returned any rows.
Let’s look at an example of how to use FOUND to determine if a SELECT statement
returned any rows:
223
Chapter 14 Handling Exceptions
ELSE
RETURN FALSE;
END IF;
END;
$$ LANGUAGE plpgsql;
In this example, we are creating a function called test_func. Within the function,
we are using a SELECT statement to retrieve data from a table. We are then using FOUND
to determine if the SELECT statement returned any rows and returning a boolean value
based on the result.
Let’s run the SELECT statement and see if the row exists:
This row exists. So, the function with input second1 should return TRUE:
postgres=# SELECT test_func('second1');
test_func
-----------
t
(1 row)
If we consider another row which does not exist, it will return FALSE:
postgres=# SELECT test_func('second100');
test_func
-----------
f
(1 row)
224
Chapter 14 Handling Exceptions
Exceptions in PL/pgSQL
An exception in PL/pgSQL is an error condition that occurs during the execution of a
program. In PostgreSQL, exceptions are used to handle errors that can occur during
database operations, such as syntax errors, constraint violations, data type mismatches,
and object not found errors.
• Syntax Errors: These are errors that occur when the SQL code
is malformed or incorrect. Examples include missing or extra
parentheses, incorrect syntax for a function or keyword, and
misspelled table and column names.
• Constraint Violations: These are errors that occur when a constraint
is violated, such as a primary key or unique constraint. Examples
include trying to insert a duplicate value into a column with a unique
constraint or trying to delete a row that is referenced by a foreign key
constraint.
• Foreign Key Violations: These are errors that occur when a foreign
key constraint is violated. Examples include trying to insert a value
into a column that does not exist in the referenced table or trying to
delete a row that is still referenced by another table.
• Data Type Mismatch Errors: These are errors that occur when the
data type of a column does not match the data being inserted or
updated. Examples include trying to insert a string into a numeric
column or trying to insert a value that is too large for the column.
• Object Not Found Errors: These are errors that occur when an object
referenced in the SQL code does not exist. Examples include trying to
select data from a table that does not exist or trying to create a trigger
on a nonexistent table.
These are some examples of what kind of errors we can see. However, there would be
a lot more.
225
Chapter 14 Handling Exceptions
BEGIN
-- code block
EXCEPTION
WHEN SQLSTATE '<error_number>' THEN
-- handle the unique constraint violation error
WHEN OTHERS THEN
-- handle all other errors
END;
postgres=# DO $$
DECLARE
v_result INT;
226
Chapter 14 Handling Exceptions
BEGIN
v_result = 1/0;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Got error';
END;
$$;
NOTICE: Got error
DO
postgres=# DO $inline$
DECLARE
v_result INT;
BEGIN
v_result = 1/0;
RAISE NOTICE 'Hello World!';
EXCEPTION
WHEN OTHERS THEN
DO $$
BEGIN
RAISE NOTICE 'Got error';
END;
$$;
END;
$inline$;
NOTICE: Got error
DO
227
Chapter 14 Handling Exceptions
Now, consider an example with the GET DIAGNOSTICS. Create a test table and insert
some rows as follows:
As the salary column is not null, you will see a not-null violation error when you try
to update the salary as null:
Let’s create a function to handle not-null violation errors. From PostgreSQL error
codes, 23502 is the error code for not-null violation:
228
Chapter 14 Handling Exceptions
EXCEPTION
WHEN SQLSTATE '23502' THEN
RAISE NOTICE 'customer error: not-null violation error';
RETURN FALSE;
WHEN OTHERS THEN
RAISE NOTICE 'other error';
RETURN FALSE;
END;
$$ LANGUAGE plpgsql;
From the function, if it is a not-null violation error, it throws the message that we are
passing 'customer error: not-null violation error'. If it’s another error, it will go
to the WHEN OTHERS block and throw the 'other error' message.
Let’s test with updating a null value:
As you can see, it has thrown the custom message from the function for not-null
violation. But let’s try updating a text value to a numeric field:
As you can see, it went to the WHEN OTHERS block and threw the other error
message. However, if you want to know the exact error instead of the custom message
when it went to WHEN OTHERS, you can trap the error using SQLSTATE and SQLERRM.
229
Chapter 14 Handling Exceptions
EXCEPTION
WHEN SQLSTATE '23502' THEN
RAISE NOTICE 'customer error: not-null violation error';
RETURN FALSE;
WHEN OTHERS THEN
err_num := SQLSTATE;
err_msg := SUBSTR(SQLERRM, 100);
RAISE NOTICE 'other error is: %:%', err_num, err_msg;
RETURN FALSE;
END;
$$ LANGUAGE plpgsql;
230
Chapter 14 Handling Exceptions
RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
In this example, if the new salary is outside of the range of $50,000 to $80,000, a
custom exception with the error message "Salary must be between $50,000 and $80,000"
will be raised.
Let’s test this function with a salary which is higher than $80,000 and between
$50,000 and $80,000:
231
Chapter 14 Handling Exceptions
Time: 0.790 ms
postgres=# SELECT * FROM employees WHERE id=1;
id | name | email | salary
----+------+----------------+----------
1 | f1 | [email protected] | 70000.00
(1 row)
As you can see, it has raised an exception with $100,000 as it is higher than $80,000
and updated the value as expected when the salary is $70,000.
Custom Exceptions
Sometimes, an application needs to raise custom exceptions with its own error
messages, codes, and even custom hint messages. PL/pgSQL’s RAISE EXCEPTION clause
allows you to do this by setting exception attributes like MESSAGE, DETAIL, HINT, and
ERRCODE. Consider the following example:
postgres=# DO
$$
BEGIN
RAISE EXCEPTION USING MESSAGE='This is error message', DETAIL='These
are the details about this error', HINT='Hint message which may fix this
error',ERRCODE='P1234';
END;
$$;
ERROR: This is error message
DETAIL: These are the details about this error
HINT: Hint message which may fix this error
CONTEXT: PL/pgSQL function inline_code_block line 3 at RAISE
232
Chapter 14 Handling Exceptions
The preceding output shows that we raised a custom exception with custom
messages and error codes. Since we are raising the plpgsql custom error, the error code
should start with "P". To specify the error codes, we should follow the PostgreSQL error
code formats, which are mentioned in the documentation.
We can use both custom exceptions and predefined PL/pgSQL exceptions to raise
exceptions. The following example raises a predefined exception:
postgres=# DO
$$
BEGIN
RAISE division_by_zero USING MESSAGE='Number zero can not be divisor';
END;
$$;
ERROR: Number zero can not be divisor
CONTEXT: PL/pgSQL function inline_code_block line 3 at RAISE
Rethrow Exceptions
In the preceding example, an anonymous block of code is added within an exception
block to execute whenever an exception occurs. This kind of code flexibility is offered
by PL/pgSQL, helpful in handling exceptions. In the previous example, we learned
how to raise an exception. Now, let's discuss how to catch an exception and then throw
it. Consider the following simple example, which raises a custom exception and then
throws it from the place where it caught the exception:
postgres=# DO $$
DECLARE
v_test INT:=0;
BEGIN
BEGIN
v_test:= 1/0;
RAISE NOTICE 'value is %', v_test;
233
Chapter 14 Handling Exceptions
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'got some error %', SQLERRM;
END;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'exception from outer block %', SQLERRM;
END;
$$;
NOTICE: exception from outer block got some error division by zero
DO
As you can see in the preceding example, we have nested blocks in which a custom
exception is raised from inside the inner block and caught by the outer block.
ASSERT
Assertions are used in PL/pgSQL to enforce business rules or check assumptions
about data. They are used to ensure that the data in the database conforms to certain
requirements. Assertions can be used to check the validity of input values, the
correctness of business logic, or the consistency of data in the database. If an assertion
fails, it will raise an exception, which can be caught and handled by the application. If
an assertion passes, then it won’t raise any exception. Consider the following simple
example, which demonstrates the assertion behavior:
postgres=# DO
$$
BEGIN
ASSERT 1=1, 'this assertion should not raise';
ASSERT 1=0, 'assertion failed, as 1 is not equal to 0';
END;
$$;
ERROR: assertion failed, as 1 is not equal to 0
CONTEXT: PL/pgSQL function inline_code_block line 4 at ASSERT
234
Chapter 14 Handling Exceptions
In the preceding output, we have specified two assert statements. One assert
statement's condition is true, while the other's condition is false. An exception only
occurs when the condition is false.
postgres=# DO $$
DECLARE
v_text TEXT:='';
BEGIN
BEGIN
GET DIAGNOSTICS v_text = PG_CONTEXT;
RAISE NOTICE '%', v_text;
END;
END;
$$;
NOTICE: PL/pgSQL function inline_code_block line 6 at GET DIAGNOSTICS
DO
As shown in the preceding example, we can print the CALL stack using PG_CONTEXT
to determine where the GET DIAGNOSTIC was executed from. Now, extend the same
behavior with the following function calls, and see the function call stack:
235
Chapter 14 Handling Exceptions
(1 row)
236
Chapter 14 Handling Exceptions
EXCEPTION
WHEN SQLSTATE '23502' THEN
RAISE NOTICE 'customer error: not-null violation error';
RETURN FALSE;
WHEN OTHERS THEN
GET STACKED DIAGNOSTICS err_msg = MESSAGE_TEXT;
RAISE NOTICE 'Error: %', err_msg;
RETURN FALSE;
END;
$$ LANGUAGE plpgsql;
In this example, the GET STACKED DIAGNOSTICS statement is used to retrieve the
error message associated with an exception that has been raised. The error message
is stored in the variable err_msg and is printed to the console using the RAISE NOTICE
statement.
237
Chapter 14 Handling Exceptions
As you can see, the exact error has been caught with GET STACKED DIAGNOSTICS
using the MESSAGE_TEXT item. For all available items, you can go through the Trapping
Errors (https://www.postgresql.org/docs/current/plpgsql-control-structures.
html#PLPGSQL-ERRORTRAPPING) documentation.
By using exception handling, users can define exception handlers that catch specific
errors and perform appropriate actions. However, there are both advantages and
disadvantages to using exceptions inside PL/pgSQL.
238
Chapter 14 Handling Exceptions
Summary
In this chapter, we have discussed the types of exceptions in PostgreSQL, their syntax,
and usage. We also covered some real-world examples where these exceptions are
useful. We talked about the advantages and disadvantages of exceptions, which will help
to use them carefully based on the requirements.
239
Chapter 14 Handling Exceptions
What’s Next
In the next chapter, we will be covering some key features of triggers in PL/pgSQL like
the following:
• Trigger Types: Explore advanced trigger scenarios, including
BEFORE and AFTER triggers, row-level and statement-level triggers,
and nested triggers.
• Combining Triggers: Learn techniques for combining multiple
triggers on the same table to implement complex behaviors.
• Trigger Performance: Dive into trigger performance considerations
and optimization techniques for efficient data manipulation.
• Real-Time Data Manipulation: Explore how to use triggers to
maintain real-time data integrity and enforce complex business rules.
240
CHAPTER 15
Triggers
In the previous chapter, we talked about the different types of exceptions available in
PL/pgSQL and how to handle them with some examples. In this chapter, we will cover
triggers in PostgreSQL. It will start with the introduction of triggers and how to create
them using PL/pgSQL code with a simple example. We will then talk about the types of
triggers with some use cases and real-world examples of where and when to use these
triggers using PL/pgSQL functions. We will also cover the advantages and disadvantages
of triggers.
241
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_15
Chapter 15 Triggers
Syntax
You can use the CREATE TRIGGER command to create the trigger:
This is a basic syntax. For a detailed version of the syntax, you can go through the
CREATE TRIGGER (https://www.postgresql.org/docs/current/sql-createtrigger.
html) documentation.
The trigger’s execution timing is determined by the BEFORE and AFTER keywords,
indicating whether it should execute prior to or following the triggering event.
This event can be an INSERT, UPDATE, and DELETE on the table so that it will execute
the trigger operation.
Using the ON keyword, you define the table on which the trigger will be executed.
The FOREACH ROW clause specifies that the trigger will be executed for each individual
row impacted by the triggering event.
The WHEN clause specifies a condition that must be met for the trigger to be executed.
The EXECUTE FUNCTION clause specifies the name of the function that will be executed
when the trigger is triggered.
Make sure that you’re creating a trigger and trigger function that will meet your
needs and serve your intended purpose. It can also help you to avoid mistakes or errors
that might occur if you’re not clear about what you’re trying to accomplish.
When creating a trigger and trigger function, it can be helpful to consider the
following questions:
• What is the purpose of the trigger and trigger function?
242
Chapter 15 Triggers
By answering these questions, you can create a trigger and the associated trigger
function that will be effective and efficient in meeting your needs.
Simple Example
The best examples of triggers in PostgreSQL, or any database system, are referential
integrity triggers. These triggers are implicit in the database system and take care of
handling the relationship between child and parent tables created using primary and
foreign keys. Consider the following example.
Create a table test and its child table test_child with PRIMARY KEY and
FOREIGN KEY:
To view the internal referential integrity constraints, query the pg_trigger catalog
table. These triggers are created and maintained by PostgreSQL internally:
The eid column is an auto-incrementing integer and serves as the primary key. The
name column is a string that cannot be null, and the salary column is a numeric value
with two decimal places that cannot be null. The last_modified column is a timestamp
that will be updated automatically by the trigger.
Let’s create a trigger function and a trigger on the table:
The trigger function will automatically update the last_modified column of the
table to the current timestamp whenever a row is updated. The trigger will execute the
trigger function before an update operation on the table.
This will allow us to track changes to the employees table and ensure that the last_
modified column is always up to date without having to manually update it every time a
row is updated.
The terms NEW and OLD refer to the new and old values of a row impacted by an
INSERT, UPDATE, or DELETE operation that triggered a trigger function.
When a trigger function is executed, the NEW variable contains the new values of the
row that was impacted by the triggering operation, while the OLD variable contains the
old values of the row, before the triggering operation was executed.
244
Chapter 15 Triggers
Since we did not specify a value for the last_modified column, it will be null for
each row.
You can test the trigger by updating a row and checking that the last_modified
column has been automatically updated:
You should see that the last_modified column has been automatically updated with
the current timestamp:
In this example, the purpose of the trigger is clear that will automatically update the
last_modified column of a table whenever a row is updated. This will allow us to track
changes to the table and ensure that the last_modified column is always up to date.
• Row-level triggers
• Statement-level triggers
• Event triggers
In this section, we’ll explain each type in detail, along with code examples.
245
Chapter 15 Triggers
Row-Level Triggers
For each row impacted by the triggering event, like an insert, update, or delete operation,
a row-level trigger is executed. Row-level triggers can be used to enforce data constraints
or replicate data across tables.
Next, let’s insert some sample data into the orders table:
246
Chapter 15 Triggers
Now, let’s create a trigger function called update_order_value that will update the
order_value column to reflect the discount whenever a row is inserted:
Now, whenever the customers table gets inserted with a new row, the update_
order_value function will automatically update the order_value column to reflect the
discount.
For example, if we insert a new row with an order value of 150.00 and a discount of
15.00, the order_value column will be automatically updated to 135.00:
As you can see, order_value has been updated as 135.00 after the discount of 15.00.
247
Chapter 15 Triggers
In the preceding example, we created two triggers (a_trigger, b_trigger) on the test
table for the same AFTER INSERT event. When we inserted data into the test table, the
Trigger A function was invoked first, followed by Trigger B. If more than one trigger is
defined for the same event on the same relation, the triggers will be fired in alphabetical
order by trigger name. As PostgreSQL supports multiple pluggable languages like PL/
V8, PL/TCL, and PL/Python, we can attach any of these functions as trigger invocation
methods.
249
Chapter 15 Triggers
WHEN 2 THEN
RAISE NOTICE 'This is the first nested trigger, and stop further
nested calls';
RETURN NEW;
ELSE
RAISE NOTICE 'This is a nested trigger at depth %',
nested_trigger_depth;
END CASE;
As you can see in the preceding example, we fixed the issue of nested trigger
invocation exceeding the allowed level by using the pg_trigger_depth() function.
Next, we will create a separate reporting table called customer_reports that we will
use to replicate data from the customers table. The customer_reports table will have the
same columns as the customers table, but with an additional “report_date” column to
track when the data was replicated.
Create the customer_reports table:
Now that we have our two tables, we can create a trigger function that will replicate
data from the customers table to the customer_reports table. The trigger function will
be called replicate_customer_data and will take one argument: the “NEW” row that
triggered the trigger.
Create the replicate_customer_data trigger function:
251
Chapter 15 Triggers
As you can see, the trigger function simply inserts the new row from the customers
table into the customer_reports table, using the “NEW” keyword to reference the row
that triggered the trigger.
Now, we can create the row-level trigger that will call the replicate_customer_data
function whenever the customers table gets inserted with a new row. The row-level
trigger will be called insert_customer_report and will be executed “AFTER INSERT” on
the customers table.
Create the insert_customer_report row-level trigger:
Now, whenever the customers table gets inserted with a new row, the insert_
customer_report row-level trigger will be called, which in turn will call the replicate_
customer_data trigger function to insert the new row into the customer_reports table.
As you can see, the data inserted in the customers table is replicated to the
customer_reports table.
252
Chapter 15 Triggers
INSTEAD OF Triggers
An INSTEAD OF trigger in PostgreSQL allows for the execution of a different action than
the default action when a specific event occurs. This type of trigger is commonly used
for views, which cannot be updated in the same way as tables. Instead, the trigger can
execute a stored procedure that performs the update in a different way. INSTEAD OF
triggers are useful when dealing with complex views that require more than one table to
be updated. The trigger can execute a stored procedure that updates the necessary tables
in the correct order. Additionally, this type of trigger can be used to enforce business
rules and data constraints when inserting or updating data in a view. Consider the
following example:
253
Chapter 15 Triggers
As you can see, we were able to perform DML operations on views with the help of
INSTEAD OF triggers.
Statement-Level Triggers
Regardless of the number of rows affected, a statement-level trigger executes only once
for each triggering event. Statement-level triggers can be used to log changes to a table or
database.
254
Chapter 15 Triggers
The id column is for the log entry, the operation is the operation that triggered the
log entry (insert, update, or delete), the timestamp is the log entry time, and the order_
id is the order that was affected.
Create a trigger function to log the changes:
This trigger will execute the log_order_changes function after each DML operation
on the orders table. It executes the function for each affected row individually.
Now, whenever a DML operation is performed on the orders table, a log entry
will be inserted in the order_log table with the operation type, timestamp, and order
ID. This can be useful for tracking changes to the table and auditing user activity.
Let’s try inserting a row in the orders table and check the rows in the order_
log table:
As you can see, the data inserted in the orders table is updated to the order_
log table.
There are a few other trigger arguments like TG_OP:
• TG_OP: A string indicating the operation for which the trigger was
fired. The trigger operation can be INSERT, UPDATE, DELETE, or
TRUNCATE, based on how the trigger is defined. This variable helps
you determine the type of action that triggered the trigger.
• TG_NAME: This refers to a variable holding the name of the trigger that
was executed. It allows you to identify which specific trigger function
is currently executing, useful when multiple triggers are defined on
the same table.
• TG_WHEN: A string representing the timing of the trigger’s execution,
which can be one of BEFORE, AFTER, or INSTEAD OF. The timing
specifies whether the trigger action occurs before the operation, after
the operation, or instead of the operation itself.
• TG_LEVEL: A string denoting the execution level of the trigger, which
can take two values: ROW or STATEMENT. When defined as a row-
level trigger, it executes for each affected row individually. In contrast,
as a statement-level trigger, it executes once for the entire statement.
• TG_TABLE_NAME: The table name assists in identifying the specific
table where the triggering event took place.
• TG_RELNAME: Similar to TG_TABLE_NAME, it also provides the name
of the table that caused the trigger invocation.
• TG_RELID: This object or table ID is useful if you need to perform
dynamic queries or operations on the triggering table.
• TG_TABLE_SCHEMA: This variable provides the name of the schema for
the table involved in the trigger event.
257
Chapter 15 Triggers
Event Triggers
An event trigger is a special type of trigger that is not associated with a specific table.
Instead, it is executed in response to specific user object events, such as table create/
drop/alter.
258
Chapter 15 Triggers
This table records the type of event that occurred, the timestamp of the event, the
schema and name of the object that was affected by the event, and the DDL statement
that was executed.
Create a function to log the DDL events:
rec := pg_event_trigger_ddl_commands();
INSERT INTO ddl_log (event_type, event_time, object_name,
statement)
VALUES (tg_tag, current_timestamp, rec.object_identity, current_
query());
END;
$$ LANGUAGE plpgsql;
Whenever a DDL event occurs on the database, the log_ddl_event function will be
executed, and a new log entry will be inserted into the ddl_log table with the type of
event that occurred, the timestamp of the event, the schema and name of the object that
was affected by the event, and the DDL statement that was executed.
Create the event trigger to execute the function:
This event trigger will execute the log_ddl_event function whenever a DDL event
occurs on the database. The ON ddl_command_end clause specifies that the trigger should
be executed after the DDL statement has been executed.
Now, whenever a DDL statement is executed on the database, a log entry will be
created in the ddl_log table with the type of event that occurred, the timestamp of the
event, the schema and name of the object that was affected by the event, and the DDL
statement that was executed.
259
Chapter 15 Triggers
Let’s try executing an ALTER on any table in the database and check the rows of the
ddl_log table:
As you can see, ALTER details are updated in the ddl_log table.
Advantages of Triggers
Row-level triggers
Statement-level triggers
• Statement-level triggers provide a simple way to log changes to a
table or database.
• They can be used to enforce database-wide constraints or replicate
data across databases.
Event triggers
• Event triggers provide a simple way to respond to user object events,
such as table creation, alter, or drop.
• They can be used to automate administrative tasks, such as creating
table backups or performing maintenance tasks.
260
Chapter 15 Triggers
Disadvantages of Triggers
• Triggers cause some performance overhead. Make sure you measure
the performance overhead before you implement a trigger on
any table.
• Triggers can be complex and difficult to debug, especially when they
involve complex logic or interactions with other triggers or functions.
• They can cause performance issues if they are poorly written or
executed frequently, leading to slower query execution times or even
database crashes.
• They can be disabled or circumvented, either intentionally or
unintentionally, leading to data inconsistencies or security
vulnerabilities.
DROP Triggers
You can drop row- and statement-level triggers using the DROP TRIGGER command. The
following is the syntax:
For more options, you can go through the DROP TRIGGER documentation.
You can drop the triggers created in one of the examples as follows:
However, to drop event triggers, you need to use the DROP EVENT TRIGGER
command as follows:
261
Chapter 15 Triggers
Summary
In this chapter, we have discussed the types of triggers in PostgreSQL, their syntax and
usage, as well as their advantages and disadvantages with real-world use cases. While
triggers can be used for enforcing data constraints, replicating data, or logging changes,
they should be used with caution, especially when dealing with complex logic or large
datasets. Otherwise, the triggers will severely affect the database performance.
What’s Next
In the next chapter, we will be covering some key features of transaction management in
PL/pgSQL like the following:
• Advanced Transaction Scenarios: Explore more complex
transaction management scenarios, including nested transactions
and savepoints.
• Exception Handling and Rollback: Learn how to handle exceptions
within transactions and perform controlled rollbacks when needed.
262
CHAPTER 16
Transaction Management
In the previous chapter, we talked about triggers in PostgreSQL and how to build trigger
functions in PL/pgSQLPL/pgSQLreturn values/parameters. We covered different types
of triggers with examples and use cases when to use each type of trigger. We explained
advantages and disadvantages of triggers. In this chapter, we will cover transaction
management in PL/pgSQL and how to write proper transactional code. By the end of this
chapter, you will have a better understanding of how transactions work inside PL/pgSQL
blocks and how to write transactional statements inside PL/pgSQL blocks that have
exception handling.
Nested Transactions
PL/pgSQL allows us to run transactional control statements inside BEGIN/END blocks.
However, the behavior of these transactions has its own internal implementation that is
not exposed externally. Consider the following example:
postgres=# DO
$$
DECLARE
v_cnt integer:=3;
BEGIN
FOR i IN 1..v_cnt LOOP
INSERT INTO test (transaction_ids) VALUES (txid_current());
263
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_16
Chapter 16 Transaction Management
END LOOP;
COMMIT;
END;
$$;
DO
In the preceding example, we created a table called “test” to store the current
transaction ID number. To retrieve the current transaction ID, we used PostgreSQL’s
“txid_current()” function, which returns the transaction in which the statements are
being executed. From the preceding output, we can see that the INSERT statement was
executed in a LOOP three times and recorded the same transaction ID 756 because the
entire anonymous block was executed in a single transaction.
Now, change the anonymous code block to use COMMIT after each INSERT operation
as follows, and see the table results:
postgres=# DO
$$
DECLARE
v_cnt integer:=3;
BEGIN
FOR i IN 1..v_cnt LOOP
INSERT INTO test (transaction_ids) VALUES (txid_current());
COMMIT;
END LOOP;
END;
$$;
DO
264
Chapter 16 Transaction Management
In the preceding example, a different transaction ID was generated after each INSERT
operation that was committed. This means that using COMMIT inside PL/pgSQL blocks
will close the current transaction and create a new transaction. This makes sense as
we have multiple COMMIT statements in a loop, which opens new transactions for each
INSERT statement.
Now, let’s run the same example inside a transaction and see the result:
postgres=*# DO
$$
DECLARE
v_cnt integer:=3;
BEGIN
FOR i IN 1..v_cnt LOOP
INSERT INTO test (transaction_ids) VALUES (txid_current());
COMMIT;
END LOOP;
END;
$$;
265
Chapter 16 Transaction Management
Exception Handling
Exception handling is an essential part of transaction behavior. It allows us to handle
errors that occur during the execution of a transaction. In PL/pgSQL, we can use the
EXCEPTION block to catch and handle errors. This block is executed when an error occurs
and provides the opportunity to perform certain actions, such as logging the error
or rolling back the transaction. By using exception handling, we can ensure that our
transactions are executed in a reliable, consistent, and safe manner. We discussed more
about this in Chapter 14.
Now, let’s discuss how transaction handling would work in the case of an exception
being thrown:
CREATE PROCEDURE
266
Chapter 16 Transaction Management
In the previous code, there may be no obvious errors, but when attempting to
execute the procedure, the following exception occurs: “cannot commit while a
subtransaction is active.” As a result, the execution of the CALL statement fails. Now, try to
execute the same procedure without any EXCEPTION block and see the output:
If we remove the EXCEPTION block from the previous PL/pgSQL procedure, the
CALL statement can be executed successfully. This indicates that the presence of the
EXCEPTION block, when there is a COMMIT statement, causes the “cannot commit while
a subtransaction is active” error. To confirm this behavior, execute the preceding
PL/pgSQL code with an EXCEPTION block but without a COMMIT statement, and observe
the output:
267
Chapter 16 Transaction Management
BEGIN
PERFORM 1/1;
RAISE NOTICE 'before commit: %', txid_current();
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Got error : %', SQLERRM;
END;
$procedure$;
CREATE PROCEDURE
The preceding output confirms that COMMIT and EXCEPTION blocks should not be in a
single BEGIN END block in that order. The reason for this behavior is the way transactions
are handled inside the PL/pgSQL blocks. Each BEGIN END block creates an implicit
subtransaction with the help of SAVEPOINT. Consider the following sample PL/pgSQL
template:
BEGIN
--- Statements
<<COMMIT/ROLLBACK>>
EXCEPTION WHEN others THEN
--- Handle exception
END
The preceding code implicitly gets converted into multiple subtransaction calls as
follows:
BEGIN
SAVEPOINT one;
--- Statements
<<COMMIT/ROLLBACK>>
RELEASE SAVEPOINT one;
268
Chapter 16 Transaction Management
CREATE PROCEDURE
postgres=# CALL public.recon_order_payment();
NOTICE: before commit: 777
CALL
269
Chapter 16 Transaction Management
Summary
This chapter covered the behavior of transactions and exception handling in PL/pgSQL.
It emphasizes the importance of understanding the implications of using COMMIT within
PL/pgSQL blocks and how it affects transaction behavior. When COMMIT is used inside
PL/pgSQL blocks, it closes the current transaction and creates a new transaction. This
feature is useful for ensuring that transactions are executed in a reliable, consistent, and
safe manner.
We also discuss the importance of exception handling in transaction behavior. It
explains how the EXCEPTION block can be used to catch and handle errors that occur
during the execution of a transaction. The documentation provides a detailed example
of how exception handling can be used to ensure that transactions are executed in a
reliable, consistent, and safe manner.
Finally, we highlighted the importance of ensuring that COMMIT/ROLLBACK is the
last executable statement in the procedure to avoid the error “cannot commit while a
subtransaction is active.” By doing so, it ensures that the basic transaction principle is
followed, where the transaction must be rolled back during an exception.
What’s Next
In the next chapter, we will be covering some key features of aggregates in PL/pgSQL like
the following:
• Custom Aggregate Functions: Learn how to create your own custom
aggregate functions to perform complex calculations on data.
• Parallel Aggregation: Explore techniques for parallelizing aggregate
computations to improve performance on large datasets.
• State Transition Aggregates: Discover how to create aggregates that
maintain state across rows to perform advanced analytics.
270
CHAPTER 17
Aggregates
In the previous chapter, we discussed transaction management and how transactions
work within an implicit or explicit transaction. This chapter covers the concepts of
writing aggregates in PostgreSQL and demonstrates how to write a custom aggregate. We
also discuss the core elements of an aggregate, such as the state transition function and
final function.
Custom Aggregate
PL/pgSQL offers a wide range of developer-friendly features and capabilities to users.
One of these features is the ability to create custom aggregates, which can perform
specialized calculations on data. Custom aggregates can be created using the CREATE
AGGREGATE statement, and they can incorporate user-defined functions to perform
complex calculations.
Using custom aggregates can provide greater flexibility and control over the
calculations performed on datasets in PostgreSQL. In contrast to standard aggregates
like SUM, AVG, MAX, MIN, and COUNT, custom aggregates can be tailored to specific
use cases and datasets. This can be particularly useful in scenarios where standard
aggregates do not provide the necessary functionality or accuracy.
Custom aggregates can also be used in conjunction with the GROUP BY clause
to perform calculations on subsets of data. Before creating custom aggregates, it is
necessary to first create a set of functions that can help calculate the aggregated results
over a group of selected rows. These functions are a part of the custom aggregate
definition and are invoked implicitly whenever an aggregate function is called on the
dataset.
271
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_17
Chapter 17 Aggregates
Simple Example
PostgreSQL supports several default data aggregate functions, including SUM, MAX,
MIN, and AVG, which can be used with multiple data types. By utilizing these built-in
aggregate functions, we can perform the aforementioned operations on several datasets.
Consider the following example:
The preceding example is quite simple. We created a table named “test,” inserted
ten records into it, and calculated the average of these ten values by computing the sum
of all the values and dividing it by the number of values. Let’s consider a moderately
complex problem where we need to calculate the average rating of a product by
including all possible rating elements, such as 1, 2, 3, 4, and 5. Now, let’s go and create
the data model for the product’s rating and insert some random rating values as follows:
272
Chapter 17 Aggregates
1 | 5
1 | 2
1 | 3
(5 rows)
As you can see in the preceding output, we have inserted 100 randomly generated
product rating values into the table “products_rating”. The next step is to display the
number of ratings that we received for each value (1 through 5) for the product with an
ID of 1. To do this, we can run the following query, which uses the COUNT and FILTER
statements to convert the single “rating” column into five separate columns:
As you can see in the preceding example, the SQL query for fetching product IDs
and their corresponding rating values is readable and perfectly fine. However, we can
simplify this query even further by using the concept of custom aggregates to abstract
all of its COUNT() and FILTER functionality. Before implementing custom aggregates, it is
important to discuss the concepts of state transition function and final function.
273
Chapter 17 Aggregates
ROW 1 State 1
ROW 2
State 2 result
state
ROW 3 State 3
State N
ROW N
In the preceding example, we created four records and used the max function to find
the maximum among them. To accomplish this, there is a state transition function that
executes for each row and stores the current state value. The sample pseudocode for this
would look like the following:
initial_value = 0;
state = initial_value;
function state_trans_func_max(value):
if (row.value > state)
state = row.value
return state
274
Chapter 17 Aggregates
10 State 10
15 State 15
Max state result(55)
25 State 25
State 55
55
Final Function
The final function is another key concept in custom aggregates. It defines how the final
value of the aggregate is calculated once all rows in the group have been processed.
This function takes the final state of the aggregate as an argument and returns the final
value of the aggregate. The final function is specified as part of the CREATE AGGREGATE
statement, and it can be any user-defined function that takes the final state value as
an argument and returns a value. By combining the state transition function and the
final function, we can create powerful custom aggregates that can perform complex
calculations on datasets. Consider the following simple AVG() aggregate as an example:
Not all aggregate functions require a final function, but functions like avg and
variance do require this final function. The final function performs the final calculation
on the result obtained from the state transition function.
Now, let’s abstract away all these internal details and write a simple SQL query as
follows which gives the same result:
276
Chapter 17 Aggregates
The preceding query is much simpler than the previous COUNT() and FILTER()
functions. To achieve this behavior, we need to start by creating the state transition
function. When designing the state transition function, we must keep in mind that its
return type should match the expected result of the SQL statement.
Create Type
Create a data type that holds the state of each row, as well as the return value of the state
transition function:
Let’s create a rating custom type as before, as it is matching the SQL result set.
Inside the function, we put the IF statement to check the value of the rating given
by rollingValue. Depending on the value, it increments the appropriate count in the
rollingState state variable. Finally, the updated rollingState is returned as the
result of the function, which will be used as the accumulating value for the aggregation
process.
Create Aggregate
To create a custom aggregate, we first need a state transition function, which we have
already created. Now, let’s use the CREATE AGGREGATE to create the custom aggregate
and specify the state transition function as follows:
In the preceding CREATE AGGREGATE definition, the input value’s parameter type is
specified as products_rating. That is, we use this aggregate on the products_table.
The SFUNC (state transition function) is specified as rating_agg_transfn, and the STYPE
(state type) is specified as rating. Also, we specified the initial state value of all these
rating members as 0.
Now, let’s execute this aggregate on the products_table and see the results:
The preceding query result matches the SQL query that uses COUNT() and FILTER()
aggregates. However, the result is displayed in a single row. If we need the result to be
displayed in a multicolumn format, we need to rewrite the SQL query as follows:
Final Function
In the previous examples, we displayed individual ratings, and it would be nice if we also
show the overall rating by considering the state transition function output. Now, let’s
write a final function which takes a state transition function input as an argument and
returns all the ratings, including the final rating value:
279
Chapter 17 Aggregates
rating_5 int,
all_rating float
);
CREATE TYPE
In the preceding function, we return the rating values and also calculate the average
rating of the product by adding some weights to each rating value. Now, drop the
previous aggregate and re-create it with this function, which will be evaluated on the
final result of the state transition function:
Now, execute the aggregate on the table and see all the results at once:
280
Chapter 17 Aggregates
Summary
This chapter explained how to create custom aggregates in PostgreSQL using the CREATE
AGGREGATE statement. Aggregates are useful for performing specialized calculations on
datasets that standard aggregates cannot handle. Custom aggregates can be created
using the CREATE AGGREGATE statement and can incorporate user-defined functions to
perform complex calculations.
The chapter also discussed the concepts of state transition function and final
function, which are key components of custom aggregate creation. The state transition
function updates the aggregate state as new rows are added to the group, while the final
function calculates the final value of the aggregate once all rows in the group have been
processed.
In addition to these functions, PostgreSQL allows us to create moving aggregate
functions that help in writing custom window functions. It also supports writing
combined aggregate functions that help in processing the aggregates in parallel. Overall,
custom aggregates provide greater flexibility and control over calculations on datasets in
PostgreSQL.
What’s Next
In the next chapter, we will be covering some key features of the LISTEN and NOTIFY
commands like the following:
• Real-Time Event Handling: Dive into using the LISTEN and NOTIFY
commands for building real-time event-driven applications.
• Advanced Notification Scenarios: Explore more complex use cases
for LISTEN and NOTIFY, including custom payload and multiplexing.
• Interprocess Communication: Discover how to use LISTEN
and NOTIFY for interprocess communication between different
application components.
• Error Handling and Reliability: Learn how to handle notification
errors and ensure reliable delivery of notifications.
• Database Triggers and Notifications: Explore combining database
triggers with NOTIFY to achieve complex data synchronization
scenarios.
281
CHAPTER 18
Simple Example
To initiate a listen for incoming messages, we can use the LISTEN command. Within
a single session, multiple channels can be invoked for incoming messages. When
the session is closed, these listening channels will also be closed. The following is an
example of opening channels for the LISTEN command:
tcn
abcd
(2 rows)
284
Chapter 18 Listen and Notify
285
Chapter 18 Listen and Notify
Name | dblink_get_notify
Result data type | SETOF record
Argument data types | OUT notify_name text, OUT be_pid integer, OUT
extra text
Type | func
postgres=# DO
$$
DECLARE
v_rec RECORD;
BEGIN
PERFORM dblink_connect('host=127.0.0.1
dbname=postgres password=postgres
user=postgres');
PERFORM dblink_exec('LISTEN test_channel');
286
Chapter 18 Listen and Notify
As you can see, session 1 enters into the listening state and prints the messages
whenever it receives a message over the channel from session 2. As mentioned earlier,
we imitate the app polling mechanism using the dblink function dblink_get_notify.
Let’s use this as an example to create a live stream of event notifications for a table that
undergoes frequent modifications from the application. Consider the following example,
which has a table named test and an AFTER trigger that forms the payload in the form of
JSON and sends it to the listeners:
287
Chapter 18 Listen and Notify
ELSE
PERFORM pg_notify('test', (row_to_json(OLD)::jsonb||
'{"OP": "DELETE"}'::jsonb)::text);
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION
As shown in the preceding example, we created a table called test and a trigger
function called test_trg_func() that is attached to the test_trg trigger. The function
converts rows into JSON format and sends that data as a payload to the notifiers.
Table 18-3 describes this behavior.
288
Chapter 18 Listen and Notify
postgres=# DO
$$
DECLARE
v_rec RECORD;
BEGIN
PERFORM dblink_connect('host=127.0.0.1
dbname=postgres password=postgres
user=postgres');
PERFORM dblink_exec('LISTEN test');
WHILE true LOOP
FOR v_rec IN (SELECT * FROM dblink_get_
notify()) LOOP
RAISE NOTICE 'Got record %', v_rec.extra;
END LOOP;
END LOOP;
END;
$$;
289
Chapter 18 Listen and Notify
Session 1 Session 2
In the preceding example, we are able to capture any changes made to the test table
via the listen and notify mechanism. This means that DML operations performed on the
table will be notified to different sessions. We can also receive these notify messages on
multiple sessions, where a single notify will send the message to all active listen sessions.
T CN Extension
TCN is a trusted extension in PostgreSQL that provides a single function called
triggered_change_notification. In contrast to the previous example, which sends the
entire inserted/updated or deleted record as a payload to the listeners, this extension
only sends the key value details along with the table name and operation (INSERT,
UPDATE, and DELETE) codes. By default, this function sends a notification to the tcn
channel. However, you can specify a different channel by providing its name as an
argument to the function.
Consider the following example:
To perform the following actions, follow the same process as in the previous example
and conduct two sessions. Table 18-4 describes this behavior.
postgres=# DO
$$
DECLARE
v_rec RECORD;
BEGIN
PERFORM dblink_connect('host=127.0.0.1
dbname=postgres password=postgres
user=postgres');
PERFORM dblink_exec('LISTEN tcn');
WHILE true LOOP
FOR v_rec IN (SELECT * FROM dblink_
get_notify()) LOOP
RAISE NOTICE 'Got record %', v_rec.extra;
END LOOP;
END LOOP;
END;
$$;
The preceding output indicates that the listener was able to capture the DML
operations on key columns that occurred during session 2.
291
Chapter 18 Listen and Notify
Summary
The PostgreSQL Listen and Notify feature enables developers to create real-time
applications that respond quickly to changes in the database. We have covered the
LISTEN and NOTIFY commands which establish communication channels between
database connections, and the TCN extension provides a function that sends notifications
to the “tcn” channel, which includes only key value details along with the table
name and operation codes. This feature simplifies the process of creating real-time
applications and allows developers to quickly set up a communication channel between
a database and an application.
What’s Next
In the next chapter, we will be covering some key extensions that help with PL/pgSQL
like the following:
• PL/pgSQL Extensions: Explore advanced extensions for PL/pgSQL,
such as plprofiler and plpgsql-check extensions.
• Integrating Extensions: Learn how to integrate these extensions
with your PL/pgSQL code to enhance functionality.
• Extensions and Performance: Dive into considerations for extension
performance and optimization within PL/pgSQL functions.
• Extensions for Complex Operations: Explore extensions that
provide specialized functionality for advanced data manipulation
and analysis.
292
CHAPTER 19
PL/pgSQL Essential
Extensions
In the previous chapter, we talked about the LISTEN and NOTIFY commands for
building real-time event-driven applications and learned to handle notification
errors to ensure reliable delivery of notifications. In this chapter, we will discuss the
extensions that help with PL/pgSQL programming. We will cover a few extensions
like plpgsql_check which will help to compile PL/pgSQL functions or procedures in
syntactical and symmetrical way and plprofiler which will help to troubleshoot
slow-running functions.
plprofiler Extension
plprofiler is an extension for PostgreSQL that allows you to measure the performance
of PL/pgSQL functions through profiling. Profiling involves measuring the execution
time of a function and identifying any areas that may be causing performance issues.
With plprofiler, you can gather information about the function’s runtime, the number
of calls made to different parts of the code, and more.
Using plprofiler, you can identify which parts of your PL/pgSQL function
are taking the most time, providing valuable insights for optimization. This tool
is particularly helpful when working with complex PL/pgSQL code that requires
performance tuning.
293
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4_19
Chapter 19 PL/pgSQL Essential Extensions
To use plprofiler, you must first install the extension and enable it for specific
functions you want to profile. During function execution, plprofiler collects data,
which you can later analyze to understand how the function performs.
294
Chapter 19 PL/pgSQL Essential Extensions
Installation
To install the plprofiler extension, you can follow two ways – install from the source or
install it from the RPMs (RPM Package Manager).
You can follow these steps to install using source code:
1. Retrieve the Git repository and place it within the “contrib”
directory of your PostgreSQL installation:
cd contrib/plprofiler
make install
cd plprofiler
USE_PGXS=1 make install
cd python-plprofiler
python ./setup.py install
shared_preload_libraries='plprofiler'
295
Chapter 19 PL/pgSQL Essential Extensions
https://download.postgresql.org/pub/repos/yum/15/redhat/
rhel-9-x86_64/
shared_preload_libraries='plprofiler'
5. Connect to the database and create an extension using the
following command:
Usage
To use plprofiler, you need to instrument your PL/pgSQL function with the profiler
function. Let’s take an example of the following function:
PERFORM pg_sleep(0.1);
PERFORM pg_sleep(1);
PERFORM pg_sleep(2);
END;
$$ LANGUAGE plpgsql;
The function contains four PERFORM statements that pause for a certain amount of
time using the pg_sleep() function. These pauses simulate work being done in the
function. The function does not return any values, as its return type is VOID.
Let’s run the function and see the execution times:
(1 row)
It ran little over 3 secs. Now generate an explain plan to see what is slow inside this
function:
As you can see, the explain plan of the function does not show what’s exactly slow
inside the function. This is where plprofiler helps us.
Let’s run the plprofiler report for the function using the following command:
297
Chapter 19 PL/pgSQL Essential Extensions
This will generate the report that looks something like this:
This report shows the total time spent in the function (in milliseconds) for each
line of code in the function. The report clearly shows that the pg_sleep(2) function
accounted for most of the time.
Let’s look at another example where we can troubleshoot the function and fix it.
Create two tables: orders and order_items. The orders table has columns id and
total, and the order_items table has columns id, order_id, price, and quantity:
Here’s an example PL/pgSQL function that calculates the total for a given order
by calculating the subtotal of all order items, adding the tax, and then updating the
order total:
298
Chapter 19 PL/pgSQL Essential Extensions
To test this function with plprofiler, you can create some sample data in the orders
and order_items tables:
299
Chapter 19 PL/pgSQL Essential Extensions
Time: 0.346 ms
SELECT calculate_order_totals(500)
-- row1:
calculate_order_totals:
[current]
----
(1 rows)
SELECT 1 (0.838 seconds)
300
Chapter 19 PL/pgSQL Essential Extensions
As the report says, the total execution time of the function is 836 ms in which SELECT
took 254 ms and UPDATE took 582 ms. If you try to generate an explain analyze plan of
both the SQLs:
Time: 792.049 ms
postgres=*# rollback;
ROLLBACK
Time: 0.137 ms
postgres=#
Both plans show a full table scan. Let’s try to fix and generate a new report. If you
think of a design prospective, both tables are missing primary keys on id columns.
order_id of the order_items table should be referred to the id of the orders table as
they both refer to the same order ids. Also, the first SELECT is running based on the
order_id column, so create an index on that column.
302
Chapter 19 PL/pgSQL Essential Extensions
Time: 0.739 ms
postgres=# begin;
BEGIN
Time: 0.099 ms
postgres=*# explain analyze update orders set total =1000 where id=100;
QUERY PLAN
---------------------------------------------------------------------------
Update on orders (cost=0.43..8.45 rows=0 width=0) (actual
time=0.065..0.066 rows=0 loops=1)
-> Index Scan using orders_pkey on orders (cost=0.43..8.45 rows=1
width=38) (actual time=0.012..0.013 rows=1 loops=1)
Index Cond: (id = 100)
Planning Time: 0.260 ms
Execution Time: 1.026 ms
(5 rows)
Time: 1.567 ms
postgres=*# rollback;
ROLLBACK
303
Chapter 19 PL/pgSQL Essential Extensions
As you can see, it picked up index scans, and query timings are much improved. Let’s
generate the report:
As you can see, the function took 1.5 ms in which SELECT is 0.9 ms and UPDATE
took 0.6 ms.
Overall, plprofiler is a nice developer tool that can help you optimize the
performance of your PL/pgSQL functions and improve the overall performance of your
PostgreSQL database. By providing detailed performance information about functions, it
can help identify performance bottlenecks and areas for optimization. With its functions
for analyzing and visualizing performance data, it is a must-have for any developer
working with PL/pgSQL.
304
Chapter 19 PL/pgSQL Essential Extensions
plpgsql_check Extension
plpgsql_check is a PostgreSQL extension designed to enhance the development and
debugging process of PL/pgSQL functions. It offers a collection of features that help
developers identify potential issues, optimize code, and ensure reliable behavior of PL/
pgSQL functions. The extension performs static analysis on PL/pgSQL code, which
means it examines the code without executing it, identifying issues that might not be
apparent during a simple visual review.
plpgsql_check can detect syntax errors, variable misuse, unused variables, incorrect
parameter handling, and more, helping developers catch mistakes before running
the code. It provides insights into code coverage, showing which parts of a PL/pgSQL
function are executed during testing, aiding in verifying test comprehensiveness.
The extension offers an interface for annotating code with special comments that
influence its behavior during analysis, allowing developers to customize checks for their
specific needs.
plpgsql_check is particularly useful in complex systems where PL/pgSQL functions
play a significant role in business logic or data manipulation. By detecting potential
problems early in the development cycle, plpgsql_check helps reduce the likelihood
of bugs making their way into production environments. It can be integrated into
continuous integration and deployment pipelines, ensuring that PL/pgSQL code quality
is maintained throughout the software development life cycle.
Installation
You can follow these steps to install using source code:
cd contrib/plpgsql_check
make
make install
305
Chapter 19 PL/pgSQL Essential Extensions
Usage
You can use the plpgsql_check_function function to check the errors in your functions:
This will return a table with information about any errors in the function. If there are
no errors, the table will be empty.
Let’s look at a couple of examples where one is with error and one without errors:
There is nothing wrong with the preceding example. Let’s execute the plpgsql_check
function:
Time: 0.477 ms
postgres=#
306
Chapter 19 PL/pgSQL Essential Extensions
In the preceding example, the variable y is defined but not used. Let’s run the
plpgsql_check function:
Time: 0.590 ms
postgres=#
307
Chapter 19 PL/pgSQL Essential Extensions
In the preceding example, we created a function called test_func, and from inside
we are trying to invoke a function which does not exist in the database at all. The
function definition was created, but got an error when we tried to execute the function
test_func. See the invocation as follows:
Having a linter for PL/pgSQL functions that provides detailed information about the
function definition up front would be helpful. The plpgsql_check extension can perform
this linting against the given function definition and print error messages. Try this
extension on the preceding function to see the report we get:
As you can see in the preceding output, this extension is complaining about a
function that does not exist. Invoking this method will result in an error. The plpgsql_
check extension is a valuable asset to developers. It helps in identifying errors that can
only be caught during execution.
308
Chapter 19 PL/pgSQL Essential Extensions
Summary
In this chapter, we talked about the extensions that help with PL/pgSQL programming.
We covered the plpgsql_check extension which really helps to compile your functions
to check if there are any identified potential issues, unused variables, and other code-
related problems. We have also explained the plprofiler extension which allows you to
profile the execution of PL/pgSQL functions. Profiling is the process of measuring the
performance of a function to identify potential bottlenecks and optimize its execution.
309
Index
A, B source code, 57, 58
square brackets, 53
Aggregates
rollingState/rollingValue, 278
AVG() function, 275 C
COUNT()/FILTER() operations, Casting consistency
276, 277 castcontext, 154
COUNT/FILTER statements, 273 castfunc, 154
CREATE AGGREGATE, 278, 279 castmethod, 156
data type, 277 casttarget, 154
final function, 275, 276, 279, 280 conversion function, 163
flexibility/control statement, 271 CREATE CAST command, 160
GROUP BY clause, 271 data types, 163–165
key features, 270 implicit type casting, 155
pictorial representation, 274 JSONB, 165–167
state_trans_func_max, 275 key features, 149
state transition function, 273–275, pg_cast, 157, 158, 160
277, 278 pg_cast catalog table, 153
SUM, MAX, MIN, and AVG, 272, 273 pg_class table, 152
Arrays regclass, 151
append elements, 93, 94 temperature values, 161–163
declaration, 53 explicit type casting, 155, 156
dims function, 95 Control statements, 59
FOR loop, 54 CASE statement, 66–68, 81
iterate, 91, 92 complex control statements, 84
key features, 85 IF/ELSE statement
length function, 89–91 cascading statement, 65, 66
merge, 94 conditional flow, 62–65
multidimensional arrays, 94–96 ORDER BY clause, 63
one/zero-based index, 88, 89 SELECT statement, 64
remove duplicate elements, 92, 93 key features, 61
single- and multidimensional arrays, 87 meaningful variable names, 84
311
© Baji Shaik and Dinesh Kumar Chemuduru 2023
B. Shaik and D. K. Chemuduru, Procedural Programming with PostgreSQL PL/pgSQL,
https://doi.org/10.1007/978-1-4842-9840-4
INDEX
312
INDEX
313
INDEX
N syntax, 135
text operators, 134
NOTIFY, see LISTEN/NOTIFY commands
Numerical data types
binary representation, 51 P, Q
bitwise operator, 51
PL/pgSQL
financial/scientific calculations, 51
aggregates, 270, 271
floating-point numbers, 52
anonymous/named code
scientific/complex calculations, 51
blocks, 1
smallint data type, 50
arrays, 85
source code, 55, 56
block-structured language, 6
types, 50
anonymous/unnamed
blocks, 6–10
BEGIN ... END block, 8
O Hello World message, 8
Operators, 131 named blocks, 10, 11
advantages, 148 nested block, 9
arithmetic operators, 133 casting consistency, 151–167
benefits, 142 control statements, 59, 61–85
bitwise operators, 134 cursors, 112–129
case-sensitive, 140–142 data types, 25, 27–41
comparison operators, 133 dynamic SQL, 168–181
concatenation, 138 exception handling, 217
CREATE OPERATOR command, 135 execution flow, 5–7
cust_opr schema, 139, 142 extensions, 292, 293
data classification, 146–148 features, 1
data type math, 142, 143 functions/procedures, 181, 183
dates, 144–146 handling arrays, 87–96
disadvantages, 149 handling exceptions, 219
equality/inequality, 136–140 installation process, 2–4
integers, 133 JSON strings, 96, 97
integer value, 144 key features, 12
interval data type, 145 LISTEN and NOTIFY commands,
key features, 130 281, 283
logical operators, 134 operators, 130–135
set operators, 134 return values/parameters, 199, 201
source code, 131, 132 stored procedures/functions, 2
symbol/keyword, 131 strings, numbers and arrays, 41, 43–58
314
INDEX
315
INDEX
316