SQL - Procedural Programming with PL/PGSQL#

Overview of pl/pgsql#

  • PL/PGSQL is a good example of a programming language tightly integrated with a database, allowing strong DB level of type and error checking and communication.

  • You may find similar programming languages in other DBMSs that communicate with the database and SQL in a similar way. However, these are not standard languages.

    See full documentation here:

    http://www.postgresql.org/docs/17/plpgsql.html

  • pl/pgsql supports the same data types as the database

  • Programs and functions can be compiled and used directly at the db server

  • Main pl/pgsql block has the form:

    [ <<label>> ]
    [DECLARE
    variable declarations ]
    BEGIN
    statement
    END [ label ] ;
    
  • Variable types:

    integer
    numeric(5)
    varchar
    tablename%ROWTYPE
    tablename.columname%TYPE
    RECORD
    

    ROWTYPE and RECORD have subfields, i.e. x.name.

Programming constructs#

  • Conditionals:

    IF ... THEN ... ELSIF ... THEN ... ELSE ... END IF
    Loops:
    [ <<label>> ]
    LOOP
    statements
    END LOOP [ label ];
    
  • Returning a value:

    • pl/pgsql functions do not allow you to modify input variables

    • RETURN will return a value. As a result, you can call it like a constant in the select statement shown below:

    CREATE FUNCTION sales_tax(subtotal real, state varchar) RETURNS real AS $$
    DECLARE
       adjusted_subtotal real ;
    BEGIN
       IF state = 'NY' THEN
           adjusted_subtotal = subtotal * 0.08 ;
       ELSIF state = 'AL' THEN
           adjusted_subtotal = subtotal ;
       ELSE
           adjusted_subtotal = subtotal * 0.06;
       END IF ;
       RETURN adjusted_subtotal ;
    END ;
    $$ LANGUAGE plpgsql ;
    
  • Now, test it:

    select sales_tax(100, 'NY') ;
    
    sales_tax
    -----------
    8
    (1 row)
    
  • Note: The whole body of the function is entered within the two $$ signs.

  • You can also return a table of rows:

    • Return each tuple with RETURN NEXT and finish with RETURN

    • As these return a table, they are called in the FROM clause. See the loop section below for examples.

  • Starting with Version 11, Postgresql also allows procedures that modify input variables. You can read about them in the documentation.

Handling SQL:#

  • We will consider three types of SQL expressions:

    1. Statements that return no output but a status (successful or not, and what was wrong)

    2. Statements that return a single tuple

    3. Statements that return multiple tuples

    CREATE FUNCTION sales_tax(subtotal real) RETURNS boolean AS $$
    DECLARE
       adjusted_subtotal real ;
    BEGIN
       adjusted_subtotal = subtotal * 0.06;
       BEGIN
           INSERT INTO temp VALUES (adjusted_subtotal) ;
           RETURN true ;
       EXCEPTION WHEN unique_violation THEN
           RETURN false ;
       END ;
    END ;
    $$ LANGUAGE plpgsql ;
    
  • Now, when you run this function, a row is inserted into table temp.

Executing queries#

  • When the query returns a single row, then we can read it directly into a variable.

  • Note that when using variables as input/output, pl/pgsql does not need any special delimiters (be careful naming the variables so as not to clash with column names)

  • Example:

      SELECT * INTO myrec FROM emp WHERE empname = myname;
      IF NOT FOUND THEN
      RAISE EXCEPTION 'employee % not found', myname;
      END IF;
    
    - input: myname, output: myrec
    

Executing queries#

  • When the query returns multiple rows, then a loop is needed to go through them one by one.

    • A query returns a stream of tuples, which needs to be processed.

    • Equally important is closing the stream associated with a query if required by the programming language.

  • Example:

    [ <<label>> ]
    FOR target IN query LOOP
    statements
    END LOOP [ label ];
    
    DECLARE
    myRow  RECORD ;
    lastX      INT ;
    yCnt       INT ;
    BEGIN
       lastX = 0 ;
       yCnt = 0 ;
       FOR myRow IN
              SELECT x,y, count(*) as num
              FROM temp GROUP BY x,y ORDER BY x, num ASC LOOP
          yCnt = yCnt + 1;
          IF yCnt < 4 AND lastX = myRow.x THEN
              INSERT INTO temp2 VALUES(myRow.x, myRow.y, myRow.num) ;
          ELSIF lastX <> myRow.x THEN
              lastX = myRow.x ;
              yCnt = 1 ;
              INSERT INTO temp2 VALUES(myRow.x, myRow.y, myRow.num) ;
          END IF ;
       END LOOP ;
    RETURN 1 ;
    END ;
    
  • Note that procedure proc2() computes and inserts the top 3 y values (by count) for each x. Call it as:

    SELECT proc2() ;
    
  • Here is an example of function that returns a table. Note that the table structure has to be defined in advance.

    CREATE TABLE names (name VARCHAR(255)) ;
    
    CREATE FUNCTION allnames() RETURNS SETOF names AS $$
    DECLARE
       row RECORD ;
    BEGIN
       FOR row in SELECT DISTINCT crsname FROM courses LOOP
           RETURN NEXT row ;
       END LOOP ;
       RETURN ;
    END ;
    $$ LANGUAGE plpgsql ;
    
  • Since this function returns a table, we call it in the FROM clause:

    university=> select * from allnames();
              name
    ------------------------
    Spellcasting
    Practical Applications
    Mazes
    Alternate Dimensions
    Minor Mendings
    Transmogrification
    Advanced Spellcasting
    (7 rows)
    
  • You can also use the tables returned by functions in queries:

    ::

    university=> select * from allnames() a, courses c where a.name=c.crsname;

Cursors#

  • A cursor is a query with a handle. Cursors may have input.

  • Cursors may be defined once, and used many times to read tuples.

    A query in a cursor is optimized once, reducing the cost of optimizing the query many times.

  • Functions may return reference to a cursor, allowing a program to read tuples that are returned.

  • Cursors provide a more efficient implementation of queries returning many tuples.

  • First, declare cursors:

    DECLARE curs2 CURSOR FOR SELECT * FROM tenk1;
    
  • Then, execute the associated query by opening them:

    OPEN curs2;
    
  • Then, retrieve tuples in the result using fetch:

    FETCH curs2 INTO foo, bar, baz;
    

or

FOR recordvar IN curs2 LOOP
  • When finished, close the cursor to release allocated memory:

    CLOSE curs2;
    
  • Cursors can also be used for update/delete if it is pointing to a specific tuple (similar to the notion of an updatable view).

Exceptions#

  • When an SQL statement is executed, if it is not successful, it raises an error. This error can be caught in the usual try/catch format:

    BEGIN
    statement
    EXCEPTION WHEN condition THEN
      statement
    END ;
    
  • The exception conditions define integrity violations, statement errors, connection errors, etc.

  • The pl/pgsql statements can also raise exceptions to be caught by the calling statement:

    RAISE NOTICE  ''
    RAISE EXCEPTION ''
    
  • Also uncaught exceptions within the function will be raised when the function fails.