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

В 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.



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

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

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

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

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

Клиентские приложения не могут использовать эти функции, пока соединение 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 большого объекта для экспорта, а в аргументе filename — имя файла в операционной системе. Обратите внимание, что файл записывается библиотекой клиентского интерфейса, а не сервером. Возвращает 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, перечислены в Таблице 1.

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

Функция

Описание

Пример(ы)

lo_from_bytea ( loid oid, data bytea ) → oid

Создает большой объект и сохраняет в нем переданные данные (data). Если loid равен нулю, то система выберет свободный OID; в противном случае используется заданный OID (если этот OID уже присвоен какому-то большому объекту, возникает ошибка). В случае успеха возвращается OID созданного большого объекта.

lo_from_bytea(0, '\xffffff00') → 24528

lo_put ( loid oid, offset bigint, data bytea ) → void

Записывает данные (data) в большой объект, начиная с заданного смещения; при необходимости большой объект расширяется.

lo_put(24528, 1, '\xaa') →

lo_get ( loid oid [, offset bigint, length integer ] ) → 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. Части этой программы закомментированы, но оставлены в тексте для удобства читателя.

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

#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 -
 *    импортировать файл "in_filename" в базу данных как большой объект "lobjOid"
 *
 */
static Oid
importFile(PGconn *conn, char *filename)
{
    Oid         lobjId;
    int         lobj_fd;
    char        buf[BUFSIZE];
    int         nbytes,
                tmp;
    int         fd;

    /*
     * открыть файл для чтения
     */
    fd = open(filename, O_RDONLY, 0666);
    if (fd < 0)
    {                           /* ошибка */
        fprintf(stderr, "cannot open unix file\"%s\"\n", filename);
    }

    /*
     * создать большой объект
     */
    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);

    /*
     * прочитать данные из файла Unix и записать их в инверсионный файл
     */
    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;              /* больше данных не нужно? */
    }
    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 -
 *    экспортировать большой объект "lobjOid" в файл "out_filename"
 *
 */
static void
exportFile(PGconn *conn, Oid lobjId, char *filename)
{
    int         lobj_fd;
    char        buf[BUFSIZE];
    int         nbytes,
                tmp;
    int         fd;

    /*
     * открыть большой объект
     */
    lobj_fd = lo_open(conn, lobjId, INV_READ);
    if (lobj_fd < 0)
        fprintf(stderr, "cannot open large object %u", lobjId);

    /*
     * открыть файл для записи
     */
    fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (fd < 0)
    {                           /* ошибка */
        fprintf(stderr, "cannot open unix file\"%s\"",
                filename);
    }

    /*
     * прочитать данные из инверсионного файла и записать их в файл Unix
     */
    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);
}

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];

    /*
     * установить соединение
     */
    conn = PQsetdb(NULL, NULL, NULL, NULL, database);

    /* проверить, что соединение с сервером было успешно установлено */
    if (PQstatus(conn) != CONNECTION_OK)
    {
        fprintf(stderr, "%s", PQerrorMessage(conn));
        exit_nicely(conn);
    }

    /* Задать надежно защищенный путь поиска, чтобы злонамеренные пользователи не
     * могли перехватить управление.
     */
    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;
}