Большие объекты

В QHB имеется механизм для работы с большими объектами, который обеспечивает потоковый доступ к пользовательским данным, хранящимся в специальной структуре больших объектов. Потоковый доступ полезен при работе со значениями данных, которые слишком велики, чтобы с легкостью обрабатывать их как единое целое.

В этой главе описываются реализация и интерфейсы языка программирования и запросов к данным больших объектов в QHB. В этой главе для примеров используется библиотека libpq для языка C, но ту же функциональность поддерживает большинство программных интерфейсов QHB. Другие интерфейсы могут использовать внутри себя интерфейс больших объектов для обеспечения универсального подхода к работе с большими значениями. Здесь это не описано.

Краткая справка по большим объектам

Все большие объекты хранятся в одной системной таблице с именем pg_largeobject. Каждый большой объект также имеет запись в системной таблице pg_largeobject_metadata. Большие объекты можно создавать, изменять и удалять с помощью API чтения/записи, аналогичного стандартным операциям с файлами.

QHB также поддерживает систему хранения под названием «TOAST», которая автоматически сохраняет значения, превышающие одну страницу базы данных, во вторичную область хранения для каждой таблицы. Из-за этого механизм для работы с большими объектами оказывается отчасти устаревшим. Одно из оставшихся преимуществ больших объектов заключается в том, что они позволяют использовать значения размером до 4 ТБ, в то время как поля в TOAST могут быть размером не более 1 ГБ. Кроме того, чтение и изменение большого объекта можно успешно выполнять по частям, тогда как при большинстве операций с полем в TOAST значение будет считываться или записываться как единое целое.

Особенности реализации

Программная реализация работы с большими объектами состоит в том, что те разбиваются на «фрагменты», которые сохраняются в строках таблиц в базе данных. При произвольном доступе на чтение и запись быстрый поиск правильного номера фрагмента обеспечивает индекс B-дерево.

Фрагменты сохраненного большого объекта необязательно должны быть смежными. Например, если приложение открывает новый большой объект, перемещается к смещению 1000000 байт и записывает там несколько байт, это не приводит к выделению дополнительных 1000000 байт в хранилище; там будут размещены только фрагменты, охватывающие диапазон фактически записанных байт данных. Однако операция чтения будет считывать нули для любых нераспределенных участков, предшествующих последнему существующему фрагменту. Это соответствует обычному поведению «разреженных» файлов в файловых системах Unix.

У больших объектов имеется владелец и набор прав доступа, которыми можно управлять командами GRANT и REVOKE. Для чтения большого объекта требуются права SELECT, а для записи или усечения — права UPDATE. Удалять большой объект, добавлять для него комментарий или менять его владельца может только его владелец (или суперпользователь базы данных). Это поведение можно скорректировать, изменив параметр времени выполнения lo_compat_privileges.

Клиентские интерфейсы

В этом разделе описываются средства, которые библиотека клиентского интерфейса libpq QHB предоставляет для доступа к большим объектам. Интерфейс больших объектов QHB похож на интерфейс файловой системы Unix, с аналогами функций open, read, write, lseek и т. д.

Все операции с большими объектами, использующие эти функции, должны выполняться в блоке транзакций SQL, поскольку дескрипторы файлов больших объектов действительны только в течение транзакции.

Если при выполнении любой из этих функций возникает ошибка, то функция возвращает значение, в ином случае невозможное, обычно это 0 или -1. Сообщение, описывающее ошибку, хранится в объекте соединения и может быть извлечено с помощью PQerrorMessage.

Клиентские приложения, использующие эти функции, должны включать файл заголовка libpq/libpq-fs.h и соединяться с библиотекой libpq.

Создание большого объекта

Функция

Oid lo_creat(PGconn *conn, int mode);

создает новый большой объект. Возвращаемое значение — это OID, присвоенный новому крупному объекту, или InvalidOid (ноль) при сбое. Аргумент mode не используется и игнорируется, однако для обратной совместимости лучше задать в нем значение INV_READ (чтение), INV_WRITE (запись) или INV_READ | INV_WRITE (чтение/запись). (Эти символьные константы определены в заголовочном файле libpq/libpq-fs.х.)

Пример:

inv_oid = lo_creat(conn, INV_READ|INV_WRITE);

Функция

Oid lo_create(PGconn *conn, Oid lobjId);

также создает новый крупный объект. Назначаемый OID может быть задан с помощью lobjId; если этот OID уже используется для некоторого большого объекта, происходит ошибка. Если lobjId имеет значение InvalidOid (ноль), то lo_create назначает неиспользуемый OID (это поведение аналогично lo_creat). Возвращаемое значение — это OID, присвоенный новому крупному объекту, или InvalidOid (ноль) при ошибке.

Пример:

inv_oid = lo_create(conn, desired_oid);

Импорт большого объекта

Чтобы импортировать в качестве большого объекта файл операционной системы, вызовите:

Oid lo_import(PGconn *conn, const char *filename);

в filename указывается имя файла операционной системы, который будет импортирован как большой объект. Возвращаемое значение — это OID, присвоенный новому крупному объекту, или InvalidOid (ноль) при ошибке. Обратите внимание, что файл считывается библиотекой клиентского интерфейса, а не сервером, поэтому он должен существовать в файловой системе клиента и быть доступен для чтения клиентским приложением.

Функция

Oid lo_import_with_oid(PGconn *conn, const char *filename, Oid lobjId);

также импортирует новый крупный объект. Назначаемый OID может быть задан с помощью lobjId; если этот OID уже используется для некоторого большого объекта, то происходит ошибка. Если lobjId имеет значение InvalidOid (ноль), то lo_import_with_oid назначает неиспользуемый OID (это поведение аналогично lo_import). Возвращаемое значение — это OID, присвоенный новому крупному объекту, или InvalidOid (ноль) при ошибке.

Экспорт большого объекта

Чтобы экспортировать большой объект в файл операционной системы, вызовите

int lo_export(PGconn *conn, Oid lobjId, const char *filename);

В аргументе lobjId указывается OID большого объекта для экспорта, а в аргументе имя файла — имя файла в операционной системе. Обратите внимание, что файл записывается библиотекой клиентского интерфейса, а не сервером. Возвращает 1 при успешном выполнении, -1 при ошибке.

Открытие существующего большого объекта

Чтобы открыть существующий большой объект для чтения или записи, вызовите

int lo_open(PGconn *conn, Oid lobjId, int mode);

В аргументе lobjId указывается OID большого объекта для открытия. Биты в аргументе mode определяют, будет ли объект открыт для чтения (INV_READ), записи (INV_WRITE) или и того и другого. (Эти символьные константы определены в заголовочном файле libpq/libpq-fs.х.) lo_open возвращает дескриптор большого объекта (неотрицательный) для последующего использования в lo_read, lo_write, lo_lseek, lo_lseek64, lo_tell, lo_tell64, lo_truncate, lo_truncate64 и lo_close. Дескриптор действителен только во время выполнения текущей транзакции. При ошибке возвращается значение -1.

В настоящее время сервер не различает режимы INV_WRITE и INV_READ | INV_WRITE: читать данные из дескриптора разрешено в любом случае. Однако между этими режимами и одиночным режимом INV_READ есть существенная разница: в режиме INV_READ записывать данные в дескриптор нельзя, а данные, считанные из него, будут отражать содержимое большого объекта во время снимка состояния транзакции, который был активен при выполнении lo_open, независимо от последующих записей, сделанных этой или другими транзакциями. Чтение из дескриптора, открытого с помощью INV_WRITE, возвращает данные, отражающие все операции записи других зафиксированных транзакций, а также операции записи текущей транзакции. Это похоже на отличия режимов REPEATABLE READ и READ COMMITTED для обычных команд SQL SELECT.

Функция lo_open завершится ошибкой, пользователь не имеет права SELECT для большого объекта или если указан режим INV_WRITE и отсутствует право UPDATE. Эти проверки наличия прав можно отключить с помощью параметра времени выполнения lo_compat_privileges.

Пример:

inv_fd = lo_open(conn, inv_oid, INV_READ|INV_WRITE);

Запись данных в большой объект

Функция

int lo_write(PGconn *conn, int fd, const char *buf, size_t len);

записывает len байт из буфера buf (который должен иметь размер len) в дескриптор большого объекта fd. Аргумент fd должен быть возвращен предыдущим вызовом lo_open. Возвращается количество фактически записанных байт (в текущей реализации это значение всегда будет равно len, если только нет ошибки). В случае ошибки возвращаемое значение равно -1.

Хотя параметр len объявляется как size_t, эта функция будет отклонять значения длины, превышающие INT_MAX. На практике все равно лучше всего передавать данные фрагментами не больше нескольких мегабайт.

Чтение данных из большого объекта

Функция

int lo_read(PGconn *conn, int fd, char *buf, size_t len);

читает до len байт из дескриптора большого объекта fd в буфер buf (который должен иметь размер len). Аргумент fd должен быть возвращен предыдущим вызовом lo_open. Возвращается количество фактически прочитанных байт; это число будет меньше len, если большой объект уже был прочитан до конца. В случае ошибки возвращаемое значение равно -1.

Хотя параметр len объявляется как size_t, эта функция будет отклонять значения длины, превышающие INT_MAX. На практике все равно лучше всего передавать данные фрагментами не больше нескольких мегабайт.

Перемещение в большом объекте

Чтобы изменить текущее местоположение чтения или записи, связанное с дескриптором большого объекта, вызовите

int lo_lseek(PGconn *conn, int fd, int offset, int whence);

Эта функция перемещает указатель текущего местоположения для дескриптора большого объекта, идентифицированного с помощью fd, к новому местоположению, заданному аргументом offset. Допустимыми значениями для аргумента whence являются SEEK_SET (перемещение от начала объекта), SEEK_CUR (перемещение с текущей позиции) и SEEK_END (перемещение от конца объекта). Возвращаемое значение — это новый указатель местоположения или -1 при ошибке.

При работе с большими объектами, размер которых может превышать 2 ГБ, вместо этого используйте

pg_int64 lo_lseek64(PGconn *conn, int fd, pg_int64 offset, int whence);

Эта функция ведет себя так же, как и lo_lseek, но может принять значение offset, превышающее 2 ГБ и/или вернуть результат, превышающий 2 ГБ. Обратите внимание, что если новый указатель местоположения больше 2 ГБ, то lo_lseek выдаст ошибку.

Получение текущего положения в большом объекте

Чтобы получить текущее местоположение чтения или записи для дескриптора большого объекта, вызовите

int lo_tell(PGconn *conn, int fd);

Если возникает ошибка, возвращаемое значение равно -1.

При работе с большими объектами, размер которых может превышать 2 ГБ, вместо этого используйте

pg_int64 lo_tell64(PGconn *conn, int fd);

Эта функция ведет себя так же, как и lo_tell, но может вернуть результат, превышающий 2 ГБ. Обратите внимание, что если текущее местоположение чтения/записи больше 2 ГБ, то lo_tell выдаст ошибку.

Усечение большого объекта

Чтобы усечь большой объект до заданной длины, вызовите

int lo_truncate(PGcon *conn, int fd, size_t len);

Эта функция усекает дескриптор большого объекта fd до длины len. Аргумент fd должен быть возвращен предыдущим вызовом lo_open. Если len превышает текущую длину большого объекта, тот расширяется до заданной длины с нулевыми байтами (’\0’). В случае успеха lo_truncate возвращает нуль. При ошибке возвращается значение -1.

Местоположение чтения/записи, связанное с дескриптором fd, не изменяется.

Хотя параметр len объявлен как size_t, lo_truncate будет отклонять значения длины, превышающие INT_MAX.

При работе с большими объектами, размер которых может превышать 2 ГБ, вместо этого используйте

int lo_truncate64(PGcon *conn, int fd, pg_int64 len);

Эта функция ведет себя так же, как и lo_truncate, но может принимать значение len, превышающее 2 ГБ.

Закрытие дескриптора большого объекта

Дескриптор большого объекта можно закрыть, вызвав

int lo_close(PGconn *conn, int fd);

где fd — это дескриптор большого объекта, возвращенный функцией lo_open. В случае успеха lo_close возвращает нуль. При ошибке возвращается значение -1.

Все дескрипторы больших объектов, которые остаются открытыми в конце транзакции, будут закрыты автоматически.

Удаление большого объекта

Чтобы удалить большой объект из базы данных, вызовите

int lo_unlink(PGconn *conn, Oid lobjId);

В аргументе lobjId указывается OID большого объекта, подлежащего удалению. Возвращает 1 в случае успеха и -1 при ошибке.

Серверные функции

Серверные функции, предназначенные для работы с большими объектами из SQL, перечислены в таблице «SQL-ориентированные функции больших объектов».

Таблица: SQL-ориентированные функции больших объектов

ФункцияВозвращаемый типОписаниеПримерРезультат
lo_from_bytea(loid oid, string bytea)oidСоздает большой объект и сохраняет в нем данные, возвращая его OID. Если loid равен нулю, система выберет OID сама.lo_from_bytea(0, ’\\xffffff00’)24528
lo_put(loid oid, offset bigint, str bytea)voidЗаписывает данные по заданному смещению.lo_put(24528, 1, ’\\xaa’)
lo_get(loid oid [, from bigint, for int])byteaИзвлекает содержимое или его подстроку.lo_get(24528, 0, 3)\xffaaff

Каждой из описанных ранее клиентских функций соответствуют дополнительные серверные функции; на самом деле, по большей части клиентские функции представляют собой просто интерфейсы к аналогичным серверным функциям. К функциям, которые так же удобно вызывать с помощью команд SQL, являются lo_creat, lo_create, lo_unlink, lo_import и lo_export. Вот примеры их использования:

CREATE TABLE image (
    name            text,
    raster          oid
);

SELECT lo_creat(-1);       -- возвращает OID нового, пустого большого объекта

SELECT lo_create(43213);   -- пытается создать большой объект с OID 43213

SELECT lo_unlink(173454);  -- удаляет большой объект с OID 173454

INSERT INTO image (name, raster)
    VALUES ('beautiful image', lo_import('/etc/motd'));

INSERT INTO image (name, raster)  -- то же, что выше, но с предопределенным OID
    VALUES ('beautiful image', lo_import('/etc/motd', 68583));

SELECT lo_export(image.raster, '/tmp/motd') FROM image
    WHERE name = 'beautiful image';

Серверные функции lo_import и lo_export ведут себя совершенно иначе, нежели их аналоги на стороне клиента. Эти две функции читают и записывают файлы в файловой системе сервера, используя разрешения пользователя-владельца базы данных. Поэтому по умолчанию их могут использовать только суперпользователи. Напротив, функции импорта и экспорта на стороне клиента считывают и записывают файлы в файловой системе клиента, используя разрешения клиентской программы. Выполнение клиентских функций не требует никаких прав доступа к базе данных, за исключением права на чтение или запись соответствующего большого объекта.

Внимание!
С помощью команды GRANT можно разрешить использование серверных функций lo_import и lo_export не только суперпользователям, но при этом необходимо внимательно рассмотреть последствия для безопасности. Злонамеренный пользователь, имеющий такие права, может с легкостью воспользоваться ими, чтобы стать суперпользователем (например, путем перезаписи файлов конфигурации сервера), или атаковать остальную файловую систему сервера, не утруждаясь получением собственно прав суперпользователя базы данных. Поэтому доступ к ролям, имеющим такие права, следует контролировать так же тщательно, как и доступ к ролям суперпользователя. Тем не менее, если для выполнения некоторых рутинных задач требуется применение серверных функций lo_import или lo_export, безопаснее использовать роль с такими правами, чем с полными правами суперпользователя, так как это помогает снизить риск сбоев от случайных ошибок.

Функциональные возможности lo_read и lo_write также предоставляются через вызовы на стороне сервера, но имена серверных функций, в отличие от имен интерфейсов на стороне клиента, не содержат подчеркиваний. Эти функции следует вызывать как loread и lowrite.

Пример программы

Данный пример — это простая программа, которая показывает, как можно использовать интерфейс больших объектов в libpq. Части этой программы закомментированы, но оставлены в источнике для удобства читателя. Эту программу также можно найти в src/test/examples/testlo.с в исходном дистрибутиве.

Большие объекты с примером программы libpq

/*-------------------------------------------------------------------------
 *
 * testlo.c
 *    test using large objects with libpq
 *
 * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *    src/test/examples/testlo.c
 *
 *-------------------------------------------------------------------------
 */
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include "libpq-fe.h"
#include "libpq/libpq-fs.h"

#define BUFSIZE         1024

/*
 * importFile -
 *    import file "in_filename" into database as large object "lobjOid"
 *
 */
static Oid
importFile(PGconn *conn, char *filename)
{
    Oid         lobjId;
    int         lobj_fd;
    char        buf[BUFSIZE];
    int         nbytes,
                tmp;
    int         fd;

    /*
     * open the file to be read in
     */
    fd = open(filename, O_RDONLY, 0666);
    if (fd < 0)
    {                           /* error */
        fprintf(stderr, "cannot open unix file\"%s\"\n", filename);
    }

    /*
     * create the large object
     */
    lobjId = lo_creat(conn, INV_READ | INV_WRITE);
    if (lobjId == 0)
        fprintf(stderr, "cannot create large object");

    lobj_fd = lo_open(conn, lobjId, INV_WRITE);

    /*
     * read in from the Unix file and write to the inversion file
     */
    while ((nbytes = read(fd, buf, BUFSIZE)) > 0)
    {
        tmp = lo_write(conn, lobj_fd, buf, nbytes);
        if (tmp < nbytes)
            fprintf(stderr, "error while reading \"%s\"", filename);
    }

    close(fd);
    lo_close(conn, lobj_fd);

    return lobjId;
}

static void
pickout(PGconn *conn, Oid lobjId, int start, int len)
{
    int         lobj_fd;
    char       *buf;
    int         nbytes;
    int         nread;

    lobj_fd = lo_open(conn, lobjId, INV_READ);
    if (lobj_fd < 0)
        fprintf(stderr, "cannot open large object %u", lobjId);

    lo_lseek(conn, lobj_fd, start, SEEK_SET);
    buf = malloc(len + 1);

    nread = 0;
    while (len - nread > 0)
    {
        nbytes = lo_read(conn, lobj_fd, buf, len - nread);
        buf[nbytes] = '\0';
        fprintf(stderr, ">>> %s", buf);
        nread += nbytes;
        if (nbytes <= 0)
            break;              /* no more data? */
    }
    free(buf);
    fprintf(stderr, "\n");
    lo_close(conn, lobj_fd);
}

static void
overwrite(PGconn *conn, Oid lobjId, int start, int len)
{
    int         lobj_fd;
    char       *buf;
    int         nbytes;
    int         nwritten;
    int         i;

    lobj_fd = lo_open(conn, lobjId, INV_WRITE);
    if (lobj_fd < 0)
        fprintf(stderr, "cannot open large object %u", lobjId);

    lo_lseek(conn, lobj_fd, start, SEEK_SET);
    buf = malloc(len + 1);

    for (i = 0; i < len; i++)
        buf[i] = 'X';
    buf[i] = '\0';

    nwritten = 0;
    while (len - nwritten > 0)
    {
        nbytes = lo_write(conn, lobj_fd, buf + nwritten, len - nwritten);
        nwritten += nbytes;
        if (nbytes <= 0)
        {
            fprintf(stderr, "\nWRITE FAILED!\n");
            break;
        }
    }
    free(buf);
    fprintf(stderr, "\n");
    lo_close(conn, lobj_fd);
}


/*
 * exportFile -
 *    export large object "lobjOid" to file "out_filename"
 *
 */
static void
exportFile(PGconn *conn, Oid lobjId, char *filename)
{
    int         lobj_fd;
    char        buf[BUFSIZE];
    int         nbytes,
                tmp;
    int         fd;

    /*
     * open the large object
     */
    lobj_fd = lo_open(conn, lobjId, INV_READ);
    if (lobj_fd < 0)
        fprintf(stderr, "cannot open large object %u", lobjId);

    /*
     * open the file to be written to
     */
    fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (fd < 0)
    {                           /* error */
        fprintf(stderr, "cannot open unix file\"%s\"",
                filename);
    }

    /*
     * read in from the inversion file and write to the Unix file
     */
    while ((nbytes = lo_read(conn, lobj_fd, buf, BUFSIZE)) > 0)
    {
        tmp = write(fd, buf, nbytes);
        if (tmp < nbytes)
        {
            fprintf(stderr, "error while writing \"%s\"",
                    filename);
        }
    }

    lo_close(conn, lobj_fd);
    close(fd);

    return;
}

static void
exit_nicely(PGconn *conn)
{
    PQfinish(conn);
    exit(1);
}

int
main(int argc, char **argv)
{
    char       *in_filename,
               *out_filename;
    char       *database;
    Oid         lobjOid;
    PGconn     *conn;
    PGresult   *res;

    if (argc != 4)
    {
        fprintf(stderr, "Usage: %s database_name in_filename out_filename\n",
                argv[0]);
        exit(1);
    }

    database = argv[1];
    in_filename = argv[2];
    out_filename = argv[3];

    /*
     * set up the connection
     */
    conn = PQsetdb(NULL, NULL, NULL, NULL, database);

    /* check to see that the backend connection was successfully made */
    if (PQstatus(conn) != CONNECTION_OK)
    {
        fprintf(stderr, "Connection to database failed: %s",
                PQerrorMessage(conn));
        exit_nicely(conn);
    }

    /* Set always-secure search path, so malicious users can't take control. */
    res = PQexec(conn,
                 "SELECT pg_catalog.set_config('search_path', '', false)");
    if (PQresultStatus(res) != PGRES_TUPLES_OK)
    {
        fprintf(stderr, "SET failed: %s", PQerrorMessage(conn));
        PQclear(res);
        exit_nicely(conn);
    }
    PQclear(res);

    res = PQexec(conn, "begin");
    PQclear(res);
    printf("importing file \"%s\" ...\n", in_filename);
/*  lobjOid = importFile(conn, in_filename); */
    lobjOid = lo_import(conn, in_filename);
    if (lobjOid == 0)
        fprintf(stderr, "%s\n", PQerrorMessage(conn));
    else
    {
        printf("\tas large object %u.\n", lobjOid);

        printf("picking out bytes 1000-2000 of the large object\n");
        pickout(conn, lobjOid, 1000, 1000);

        printf("overwriting bytes 1000-2000 of the large object with X's\n");
        overwrite(conn, lobjOid, 1000, 1000);

        printf("exporting large object to file \"%s\" ...\n", out_filename);
/*      exportFile(conn, lobjOid, out_filename); */
        if (lo_export(conn, lobjOid, out_filename) < 0)
            fprintf(stderr, "%s\n", PQerrorMessage(conn));
    }

    res = PQexec(conn, "end");
    PQclear(res);
    PQfinish(conn);
    return 0;
}