..

Returning Multiple Tables from a PostgreSQL Function using Composite Types

PostgreSQL functions provide a convenient way to perform a set of operations with only one call to the database. Data from multiple tables can be returned by a function, however, complexity is introduced when trying to use this data in an application due to its format. This post explores how to return data from multiple tables and convert the data to a more useful format for an application.

Defining the function

To start, a function is created that finds and returns the students for a particular class, as shown below:

CREATE FUNCTION get_class(class integer) RETURNS SETOF students AS $$
    BEGIN
        RETURN QUERY SELECT *
        FROM students
        WHERE students.class_id = class;
    END;
$$ LANGUAGE plpgsql;

This function, called get_class, has one argument class which represents the identifier for the class. The return data type of the function is set to the composite type of the students table with the SETOF modifier, stating that a set of items is returned.

“Whenever you create a table, a composite type is automatically created, with the same name as the table, to represent the table’s row type.” - postgresql

This function returns the following:

id,name,joined_at,class_id
1,David,2020-11-15 11:02:33.450739+00,1
2,John,2020-11-15 11:02:33.450739+00,1
3,Hannah,2020-11-15 11:02:33.450739+00,1

Returning multiple tables

This function can be expanded to included information on the teacher for a class, with the new teachers table having the same structure as the students table. A composite type called class is created with two fields for the students and teacher data. The teacher field type is set to the teachers composite type, while the students field is set to json, allowing multiple rows of students to be returned alongside the teacher data.

CREATE TYPE class AS (
    teacher teachers,
    students json
);

The get_class() function is changed to provide the required data for its new return type, class. First, the students for the class are gathered and converted to JSON. Second, the teacher data is gathered and assigned to the class_data variable. Finally, the class_data variable is returned by the function.

CREATE FUNCTION get_class(class integer) RETURNS class AS $$
    DECLARE
        teacher_data teachers;
        class_data class;
    BEGIN
        SELECT array_to_json(array_agg(students_result))
        INTO class_data.students
        FROM (
            SELECT *
            FROM students
            WHERE students.class_id = class
        ) students_result;

        SELECT *
        INTO teacher_data
        FROM teachers
        WHERE teachers.class_id = class;

        class_data.teacher := teacher_data;

        RETURN class_data;
    END;
$$ LANGUAGE plpgsql;

The DECLARE statement includes the teacher_data variable, which is used to temporarily house the teacher data while waiting to be assigned to the class_data variable.

This function now returns the following:

teacher,students
(1,Matthew,"2020-11-15 11:25:51.382269+00",1),
[
    {"id":1,"name":"David","joined_at":"2020-11-15T11:02:33.450739+00:00","class_id":1},
    {"id":2,"name":"John","joined_at":"2020-11-15T11:02:33.450739+00:00","class_id":1},
    {"id":3,"name":"Hannah","joined_at":"2020-11-15T11:02:33.450739+00:00","class_id":1}
]

Introducing a third table

A third table can be added, for example the classes table to provide information about the class. First, the class composite type is changed to include the class data.

CREATE TYPE class AS (
    class json,
    teacher json,
    students json
);

Second, the get_class function is changed to also query the classes table:

CREATE FUNCTION get_class(class integer) RETURNS class AS $$
    DECLARE
        class_data class;
    BEGIN
        SELECT
            row_to_json(classes),
            row_to_json(teachers),
            array_to_json(array_agg(students))
        INTO class_data
        FROM classes
        INNER JOIN teachers ON teachers.class_id = classes.id
        INNER JOIN students ON students.class_id = classes.id
        WHERE classes.id = class
        GROUP BY
            classes,
            teachers;

        RETURN class_data;
    END;
$$ LANGUAGE plpgsql;

The query result is grouped by the classes and teachers columns as the array_agg aggregration function is used in the SELECT statement. Additionally, the row_to_json function is used to convert the data for classes and teachers to JSON, making it easier to use in an application.

The returned data from this function is:

class,teacher,students
{"id":1,"name":"History"},
{"id":1,"name":"Matthew","joined_at":"2020-11-15T11:25:51.382269+00:00","class_id":1},
[
    {"id":1,"name":"David","joined_at":"2020-11-15T11:02:33.450739+00:00","class_id":1},
    {"id":2,"name":"John","joined_at":"2020-11-15T11:02:33.450739+00:00","class_id":1},
    {"id":3,"name":"Hannah","joined_at":"2020-11-15T11:02:33.450739+00:00","class_id":1}
]