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

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

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

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

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

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

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

Реализация больших объектов разбивает большие объекты на "куски" и сохраняет их в строках таблиц в базе данных. B-дерево гарантирует быстрый поиск правильного номера чанка при выполнении операций чтения и записи с произвольным доступом.

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

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

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

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

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

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

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

Функция

Oid lo_creat(PGconn *conn, int mode);

создает новый большой объект. Возвращаемое значение - это OID, присвоенный новому крупному объекту, или InvalidOid (ноль) при сбое (failure). Битовый аргумент mode определяет, будет ли объект открыт для чтения (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_READ и INV_WRITE: вы можете читать из дескриптора в любом случае. Однако есть существенная разница между этими режимами. С 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 ГБ. Обратите внимание, что lo_lseek произойдет ошибка, если новый указатель местоположения будет больше, чем 2 ГБ.

Получение текущего положения крупного объекта

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

int lo_tell(PGconn *conn, int fd);

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

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

pg_int64 lo_tell64(PGconn *conn, int fd);

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

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

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

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, но он может принять a 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, перечислены в таблице 5.1.

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

Функциявозвращаемый типОписаниеПримерРезультат
lo_from_bytea(oid oid, string bytea)oidСоздайте большой объект и храните там данные, возвращая его OID. Проходить 0 чтобы система могла выбрать 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);       -- returns OID of new, empty large object

SELECT lo_create(43213);   -- attempts to create large object with OID 43213

SELECT lo_unlink(173454);  -- deletes large object with OID 173454

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

INSERT INTO image (name, raster)  -- same as above, but specify OID to use
    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 функции ведут себя значительно иначе, чем их клиентские аналоги. Эти две функции читают и записывают файлы в файловой системе сервера, используя разрешения пользователя-владельца базы данных. Поэтому по умолчанию их использование ограничено суперпользователями. Напротив, функции импорта и экспорта на стороне клиента считывают и записывают файлы в файловой системе клиента, используя разрешения клиентской программы. Клиентские функции не требуют никаких прав доступа к базе данных, за исключением права на чтение или запись большого объекта, о котором идет речь.

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

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

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

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

Пример 5.1. Большие объекты с примером программы 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;
}