QHB logo

Документация СУБД «Квант-Гибрид» v.1.3.1

СУБД «Квант-Гибрид» представляет собой цифровую платформу общего назначения, применимую для широкого круга разработчиков государственных информационных систем, приложений, а также для корпоративных отделов по цифровой трансформации предприятий крупного и среднего бизнеса

Документация СУБД «Квант-Гибрид» (Quantum Hybrid Base, далее по тексту QHB) представлена в 3 частях и приложениях:

  • Часть I - общее знакомство с внутренним строением QHB
  • Часть II - рассматриваются темы, представляющие интерес для администратора базы данных QHB
  • Часть III - рассматриваются темы, связанные с формальными знаниями языка SQL
  • Приложения

Состояние документирования

Мы, разработчики QHB, с признательностью примем от Вас информацию об ошибках в Документации на почтовый адрес qhb.support@granit-concern.ru. Мы не скрываем, что продукт QHB создавался как форк от свободно распространяемого open source продукта PostgreSQL. Вероятные ошибки в документации могут быть связаны именно с этим.

О состоянии Документации и её перевода мы будем регулярно сообщать в сопроводительной документации к каждому релизу. Недостающие и/или непереведённые главы Вы можете найти на официальной странице документации PostgreSQL по адресу: https://www.postgresql.org/docs/.

Версия документации: 1.3.1
Номер ревизии: 127a8778

Дополнительно

Вы можете скачать полный архив данной документации по ссылкам:

Ознакомьтесь со списком поддерживаемых платформ и замечаниями к релизу QHB.

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

Документация на СУБД «Квант-Гибрид» предыдущих версий:

Юридическое уведомление

Программное обеспечение СУБД «Квант-Гибрид» v.1.3.1

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

Программное обеспечение было создано в результате правомерной переработки Системы Управления Базами Данных PostgreSQL, распространяемой Калифорнийским университетом по открытой лицензии, и в отношении такой исходной программы применимы следующие условия ее использования по открытой лицензии:

Portions copyright (c) 1996-2011, PostgreSQL Global Development Group

Portions Copyright (c) 1994 Regents of the University of California

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

КАЛИФОРНИЙСКИЙ УНИВЕРСИТЕТ НЕ НЕСЕТ НИКАКОЙ ОТВЕТСТВЕННОСТИ ЗА ЛЮБЫЕ ПОВРЕЖДЕНИЯ, ВКЛЮЧАЯ ПОТЕРЮ ДОХОДА, НАНЕСЕННЫЕ ПРЯМЫМ ИЛИ НЕПРЯМЫМ, СПЕЦИАЛЬНЫМ ИЛИ СЛУЧАЙНЫМ ИСПОЛЬЗОВАНИЕМ ДАННОГО ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ЕГО ДОКУМЕНТАЦИИ, ДАЖЕ ЕСЛИ КАЛИФОРНИЙСКИЙ УНИВЕРСИТЕТ БЫЛ ИЗВЕЩЕН О ВОЗМОЖНОСТИ ТАКИХ ПОВРЕЖДЕНИЙ.

КАЛИФОРНИЙСКИЙ УНИВЕРСИТЕТ СПЕЦИАЛЬНО ОТКАЗЫВАЕТСЯ ПРЕДОСТАВЛЯТЬ ЛЮБЫЕ ГАРАНТИИ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ТОЛЬКО ЭТИМИ ГАРАНТИЯМИ: НЕЯВНЫЕ ГАРАНТИИ ПРИГОДНОСТИ ТОВАРА ИЛИ ПРИГОДНОСТИ ДЛЯ ОТДЕЛЬНОЙ ЦЕЛИ. ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ НА ОСНОВЕ ПРИНЦИПА "КАК ЕСТЬ" И КАЛИФОРНИЙСКИЙ УНИВЕРСИТЕТ НЕ ОБЯЗАН ПРЕДОСТАВЛЯТЬ СОПРОВОЖДЕНИЕ, ПОДДЕРЖКУ, ОБНОВЛЕНИЯ, РАСШИРЕНИЯ ИЛИ ИЗМЕНЕНИЯ.

Кроме того, Программное обеспечение содержит модули собственной разработки:

  • самобалансирующийся менеджер кэша дисковых блоков с автоматической компенсацией нагрузки на дисковую систему;
  • QCP (балансировщик сетевой нагрузки предназначенный для оптимального использования серверных подключений);
  • библиотечный кэш разобранных запросов;
  • серверный процесс, организующий фоновую запись на диск;
  • RBytea (модуль для внешнего хранения больших бинарных объектов с сохранением способа их обработки в прикладных системах);
  • QSS (прозрачное шифрование данных с использованием алгоритма ГОСТ Р 3412-15 "Кузнечик" для произвольных объектов, включая внешние большие объекты);
  • подсистема сбора и агрегации метрик;
  • QSQL (пользовательская консоль для выполнения команд базы данных и запросов на языке SQL);
  • QDL (модуль для прямой загрузки больших объёмов данных из текстового представления непосредственно в страницы данных);
  • бинарные утилиты для управления СУБД;
  • подсистема интернационализации i18n.

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

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

Настоящее соглашение регулируется действующим законодательством Российской Федерации.

Правила сообщения об ошибках

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

Следующие шаги призваны помочь вам в формировании отчётов об ошибках, которые могут быть обработаны эффективно. Мы не предлагаем выполнять их досконально, но лучше им следовать.

Мы не можем обещать немедленного исправления каждой ошибки. Если ошибка очевидна, критична или затрагивает большинство пользователей, то велика вероятность, что кто-то ею займется. Иногда мы рекомендуем вам обновить версию, чтобы увидеть, воспроизводится ли ошибка. Мы также можем решить, что данная ошибка не может быть исправлена до того, как будет выпущено какое-либо серьезное, запланированное обновление. Или, может быть, исправление слишком сложно, и на повестке дня есть более важные вещи. Если вам нужна немедленная помощь, подумайте о заключении договора о коммерческой поддержке.

Выявление ошибок

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

Далее приведены лишь некоторые примеры возможных ошибок:

  • Программа завершается аварийным сигналом или сообщением об ошибке операционной системы, указывающем на проблему в программе. (Контрпримером может быть сообщение «Недостаточно места на диске» - эту проблему вы должны решить самостоятельно).

  • Программа отказывается принимать допустимые (согласно документации) данные.

  • Программа принимает недопустимые входные данные без предупреждения или сообщения об ошибке. Но имейте в виду, что данные, которые вы считаете некорректными, мы можем считать приемлемым расширением или совместимым с принятой практикой.

  • Не получается скомпилировать, собрать или установить QHB на поддерживаемых платформах в соответствии с инструкциями.

Здесь под определением «программа» подразумевается любой исполняемый файл, а не только серверный процесс.

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

Что сообщить

Самое важное, что следует помнить о сообщениях об ошибках, - сообщайте все факты и только факты. Не размышляйте о том, что, по вашему мнению, пошло не так, что «казалось, что оно делает» или какая часть программы содержит ошибку. Если вы не знакомы с реализацией, вы, скорее всего, ошибётесь и не поможете нам. Грамотные объяснения являются отличным дополнением к фактам, но не заменяют их. Если мы собираемся исправить ошибку, мы все равно должны будем посмотреть в чём она заключается. Сообщить голые факты относительно просто (можно просто скопировать и вставить текст с экрана), но слишком часто важные детали не упоминаются, потому что считаются незначительными и неважными или кажется, что отчет будет понятен и без них.

В каждом отчете об ошибке следует указать следующую информацию:

  • Точная последовательность действий начиная с запуска программы, необходимая для воспроизведения проблемы. Она должна быть полной; если вывод зависит от данных в таблицах, то недостаточно указать только SELECT без предшествующих операторов CREATE TABLE и INSERT. У нас не будет времени на восстановление вашей схемы базы данных, и если предполагается, что мы должны создать собственные данные, мы, вероятно, пропустим эту проблему.
  • Если ваше приложение использует какой-либо другой клиентский интерфейс, например PHP, попробуйте изолировать ошибочные запросы. Вряд ли мы будем устанавливать веб-сервер для воспроизведения вашей ошибки. В любом случае не забудьте предоставить конкретные входные файлы; не гадайте о том, что проблема возникает для «больших файлов» или «баз данных среднего размера» и т. д., поскольку эта информация слишком расплывчата.

  • Результат выполнения. Пожалуйста, не пишите, что это "не работает" или "сломалось". Если есть сообщение об ошибке, покажите его, даже если оно выглядит непонятным. Если программа завершается ошибкой операционной системы, приведите эту ошибку. Если ничего не происходит, так и напишите. Даже если результатом вашего теста является сбой программы или что-то очевидное - на нашей платформе это может не произойти. Проще всего будет скопировать текст с терминала, если это возможно.

  • Важно описать результат, который вы ожидали получить. Если вы просто напишете "Эта команда выдает мне такой результат" Или "Это не то, что я ожидал", мы можем запустить ваш пример сами, просмотреть вывод и решить, что все в порядке и результат соответствует ожиданиям. Мы вряд ли будем тратить время на расшифровку точного смысла ваших команд. Особенно воздерживайтесь от простого заявления: "Это не то, что делает SQL/Oracle". Выяснение соответствия SQL-стандартам зачастую трудоёмкое и скучное занятие, а логика работы других реляционных баз данных может быть не полностью документирована или отличаться в нюансах. (Если ваша проблема - сбой программы, то вы, очевидно, можете пропустить этот пункт).

  • Все параметры командной строки и другие параметры запуска, включая все связанные переменные окружения или файлы конфигурации, которые вы изменяли со значений по умолчанию. Пожалуйста, предоставьте точную информацию. Если вы используете предварительно упакованный дистрибутив, который запускает сервер базы данных во время загрузки, вы должны попытаться выяснить, как это сделать.

  • Все что вы сделали не так, как написано в инструкции по установке.

  • Версия QHB. Чтобы узнать версию сервера, к которому вы подключены Вы можете запустить команду, SELECT version();. Большинство исполняемых программ также поддерживает опцию --version; по крайней мере qhb --version и qsql --version должно работать. Если функция или параметры не существуют, значит скорее всего вашу версию пора обновить. Если вы запускаете предварительно упакованную версию, такую ​​как RPM, укажите subversion, которую может иметь пакет. Если вы пишете о снимке Git, укажите это, а также хеш коммита.

  • Если ваша версия старее актуальной, мы почти наверняка предложим вам её обновить. В каждом новом выпуске содержится много исправлений и улучшений, поэтому вполне возможно, что ошибка, с которой вы столкнулись в более старой версии QHB, уже исправлена. Мы можем предоставить только ограниченную поддержку для программ, использующих старые версии QHB; если вам этого недостаточно - рассмотрите возможность заключения договора о коммерческой поддержке.

  • Информация о платформе. Включает в себя имя и версию ядра, библиотеку C/RUST, процессор, информацию о памяти и так далее. В большинстве случаев достаточно сообщить поставщика и версию, но не следует предполагать, что все знают, что именно содержит «Debian» или что все работают на x86_64. Если у вас есть проблемы с установкой, то вам также необходима информация о наборе инструментов на вашем компьютере (компилятор, make и т.д.).

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

Не тратьте все свое время, чтобы выяснить, при каких входных данных ошибка исчезает. Скорее всего это не поможет решить проблему. Если окажется, что вам удалось найти решение раньше нас, пожалуйста, уделите немного времени и поделитесь своими решениями. Кроме того, не тратьте свое время на предположения о причине ошибки. Если ошибка воспроизводится, то мы установим причину.

При написании отчета об ошибке избегайте путаницы в терминологии. Официальное наименование Программного пакета - "КВАНТ-ГИБРИД", для краткости в документации скорее всего будет использовано название "QHB". Если вы говорите о бэкэнд-процессе, отметьте это, а не просто говорите «Сбои QHB». Сбой одного бэкэнд-процесса сильно отличается от сбоя родительского процесса "qhb"; пожалуйста, не пишите, что "процесс упал", когда имеется в виду, что один серверный процесс вышел из строя, или наоборот. Кроме того, клиентские программы, такие как интерактивный интерфейс "QSQL", полностью отделены от бэкэнда. Пожалуйста, постарайтесь указать, является ли проблема на стороне клиента или сервера.

Как сообщать об ошибках

Отправляйте отчёты об ошибках по адресу:

qhb.support@granit-concern.ru

В теме письма желательно указать краткое описание проблемы, возможно, включив в неё часть сообщения об ошибке.

Часть I. Внутреннее устройство

Добро пожаловать в руководство по внутреннему устройству СУБД «Квант-Гибрид» (QHB). Основная цель этой части - познакомить вас на практике с основными аспектами системы QHB, без глубокого погружения в рассматриваемые темы.

Освоив это руководство, вы можете перейти к чтению части II для установки и администрирования своего собственного сервера или части III, для получения информации по использованию языка SQL в QHB.

Основы архитектуры

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

В терминах баз данных QHB использует модель клиент / сервер. Сессия QHB состоит из следующих взаимодействующих процессов (программ):

  • Процесс сервера, который управляет файлами базы данных, принимает подключения к базе данных от клиентских приложений и выполняет действия с базой данных от имени клиентов. Программа сервера базы данных называется qhb.

  • Клиентское приложение пользователя, которое будет выполнять операции с базой данных. Клиентские приложения могут быть очень разнообразными по своей природе: клиент может быть текстовым инструментом, графическим приложением, веб-сервером, который обращается к базе данных для отображения веб-страниц, или специализированным инструментом обслуживания базы данных. Некоторые клиентские приложения поставляются с дистрибутивом QHB, но большинство разработано пользователями.

Обычно для клиент-серверных приложений, клиент и сервер находятся на разных хостах. В этом случае они общаются через сетевое соединение по протоколу TCP/IP. Следует помнить об этом, поскольку файлы, к которым можно получить доступ на клиентском компьютере, могут быть недоступны (или могут быть доступны только с использованием другого имени файла) на сервере базы данных.

Сервер QHB может обрабатывать несколько одновременных подключений от клиентов. Для этого запускается (при помощи системного вызова "fork") новый процесс для каждого соединения. С этого момента клиент и новый серверный процесс обмениваются данными без вмешательства главного процесса qhb. Таким образом, главный процесс сервера всегда работает, ожидая клиентских подключений, тогда как процессы клиента и связанных серверов создаются и удаляются. (Все это происходит, конечно, прозрачно для пользователя.)

Модуль безопасного хранения QSS

Описание

Модуль безопасного хранения «КВАНТ-ГИБРИД» (Quantum Secure Storage, QSS) позволяет создавать таблицы, которые шифруются при записи на диск.

При старте сервера СУБД главный процесс QHB читает с диска мастер-ключ шифрования и сохраняет его в общей памяти, доступной дочерним процессам.

При чтении\записи блока\страницы на диск этот ключ используется для расшифровки\шифрования блока

Использование

Использование модуля в SQL: create table t_qss(c1 int, c2 varchar) USING qss;

В qhb.conf добавляется параметр qss_mode: int с возможными режимами:

  • 0 - отключено: попытки создания новых таблиц с using qss или чтение\запись в уже существующие приводит к ошибкам,
  • 1 - включено: при старте сервера считывается qss.toml и производится загрузка актуального мастер-ключа с его расшифровкой, при любых ошибках сервер останавливается

Файлы, используемые в QSS:

.
└── PGDATA
    ├── base
    ├── qhb.conf
    └── qss
        ├── 0.key
        ├── 1.key
        └── qss.toml

В qss.toml хранятся общие настройки qss (например, путь к динамической библиотеке работы с крипто-токеном) и дополнительная информация по ключам, например, режим расшифровки ключа:

  • не зашифрован,
  • ключ расшифровки находится в файловой системе, например, на примонтированном usb, с указанием пути
  • ключ расшифровки находится на криптотокене, с указанием его идентификатора

Бинарная версии ключа (файл .key) состоит из заголовка постоянного размера с информацией: версия (1), режим расшифровки (должен совпадать с записью в файле-конфигурации) и 32 байтов зашифрованного мастер-ключа

Утилита qss_mgr и управление ключами

Для управления ключами используется утилита qss_mgr со следующими командами:

bash-4.2$ qss_mgr --help
qss_mgr 1.1.0
qss configures secured storage in QHB database

USAGE:
    qss_mgr [FLAGS] [OPTIONS] <SUBCOMMAND>

FLAGS:
    -h, --help       
            Prints help information

        --new        
            Work with key group for key replacement

    -V, --version    
            Prints version information


OPTIONS:
    -d, --data-dir <data-dir>    
            Specifies the directory with database data [env: PGDATA=/tmp/qhb-data/]


SUBCOMMANDS:
    add        Add new version of master key encrypted with other secret key
    del        Remove key
    help       Prints this message or the help of the given subcommand(s)
    init       Initialize config and first encrypted master key
    use-new    Backup current key set and replace it with new
    verify     Verify key or keys

Общий флаг --new позволяет работать с "новым" набором ключей переключиться на которой можно командой use-new

Инициализация с добавлением первого мастер-ключа

Примечание!!!
в режиме pkcs11 для ввода пин-кода используется утилита qss-pinpad, запущенная на этом же компьютере в другом терминале.

Initialize config and first encrypted master key
USAGE:
    qss_mgr init [OPTIONS] [master-key]

OPTIONS:
    -k, --key <key>                                    Key for encryption
    -f, --key-format <key-format>                      Format of key for encryption: bin, base64, armored [default: bin]
        --master-key-format <master-key-format>        Source master key format: bin, base64, armored [default: bin]
    -m, --mode <mode>                                  Master key encrypt mode: fs, pkcs11 [default: pkcs11]
        --module <pkcs11-module-path>                  PKCS11 module
        --token-key-id <token-key-id>                  User key ID on token for pkcs11
        --token-serial-number <token-serial-number>    Token serial number for pkcs11

ARGS:
    <master-key>    Source master key file

Добавление мастер ключа, зашифрованного ключом другого пользователя

Add new version of master key encrypted with other secret key
USAGE:
    qss_mgr add [OPTIONS] [master-key]

OPTIONS:
    -k, --key <key>                                    Key for encryption
    -f, --key-format <key-format>                      Format of key for encryption: bin, base64, armored [default: bin]
        --master-key-format <master-key-format>        Source master key format: bin, base64, armored [default: bin]
    -m, --mode <mode>                                  Master key encrypt mode: fs, pkcs11 [default: pkcs11]
    -n, --num <num>                                    Previous key index to load master key from
        --token-key-id <token-key-id>                  User key ID on token for pkcs11
        --token-serial-number <token-serial-number>    Token serial number for pkcs11

ARGS:
    <master-key>    Source master key file

Удаление ключа

Remove key

USAGE:
    qss_mgr del [FLAGS] --num <num>

FLAGS:
    -q, --quiet      Quiet mode: don't ask for confirmation

OPTIONS:
    -n, --num <num>    Key index

Переключение на новый набор ключей

Backup current key set and replace it with new

USAGE:
    qss_mgr use-new

Проверка ключей

Verify key or keys. If checking all keys, check that master keys is same
USAGE:
    qss_mgr verify [OPTIONS]

OPTIONS:
    -n, --num <num>              Key index, if not specified, tries to check all keys

Утилита qss_recrypt используется для перешифрования таблиц БД новым набором ключей

Список команд

qss_reqcrypt 1.1.0
qss recrypt QHB cluster with new master key

USAGE:
    qss_recrypt [FLAGS] --data-dir <data-dir> <SUBCOMMAND>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information
    -v, --verbose    Sets logging level to Debug [default: Info]

OPTIONS:
        --data-dir <data-dir>    Specifies the directory with database data [env: PGDATA=/tmp/qhb-data/]

SUBCOMMANDS:
    add        Scan database and add it to recrypt's config
    help       Prints this message or the help of the given subcommand(s)
    recrypt    Do database reencryption

Сканирование БД и сохранение данных о ее зашифрованных таблицах в файл конфигурации

Scan database and add it to recrypt's config

USAGE:
    qss_recrypt --data-dir <data-dir> add [FLAGS] [OPTIONS]

FLAGS:
        --no-timeout    Run app with no timeout, use instead of -t 0s

OPTIONS:
    -d, --dbname <dbname>        Specifies the name of the database to connect to [env: PGDATABASE=]
    -h, --host <host>            Specifies the host name of the machine on which the server is running. If the value
                                 begins with a slash, it is used as the directory for the Unix-domain socket [env:
                                 PGHOST=/home/evgen/work/db/build/dbsockets]
    -p, --port <port>            Specifies the TCP port or the local Unix-domain socket file extension on which the
                                 server is listening for connections [env: PGPORT=]  [default: 5432]
    -t, --timeout <timeout>      Seconds to wait when attempting connection, supports "human time", -t 3s
    -U, --username <username>    Connect to the database as the user username instead of the default [env: PGUSER=]

Перешифрование кластера (экземпляра QHB)

При старте будет выполнена расшифровка старого мастер ключа (с запросом пина криптотокена при необходимости), а потом нового.

Do database reencryption

USAGE:
    qss_recrypt --data-dir <data-dir> recrypt

Утилита qss_pinpad предназначена для ввода пина криптотокена при использовании ключей в режиме pkcs11

Запускать следует в отдельном терминале перед стартом сервера или использованием qss_mgr для добавления или проверки ключей

Утилита magma_key_gen

magma_key_gen предназначена для генерации пользовательских ключей QSS на крипто-токенах, поддерживающих аппаратное шифрование по ГОСТ 34.12-2018 и ГОСТ 34.13-2018

Утилита поставляется в пакете qhb-contrib

magma_key_gen принимает следующие аргументы командной строки:

АргументОписание
--id idидентификатор создаваемого ключа
--module pkcs11-module-pathпуть к библиотеке крипто-токена. По-умолчанию используется /usr/lib64/librtpkcs11ecp.so

Если ключ с таким идентификатором уже существует, программа выводит об этом сообщение и завершает работу.

Пример создания и включения шифрованной таблицы

Получение тестовых ключей

Создать мастер ключ и ключ пользователя:

head -c 32 /dev/urandom > master_key.bin
head -c 32 /dev/urandom > user1.bin

Инициализация QSS в режиме мастер ключа подписанного пользовательским ключем на файловой системе

qss_mgr init \
  --module=/usr/lib64/librtpkcs11ecp.so \
  --mode fs \
  --key user1.bin \
  master_key.bin

Инициализация QSS в режиме мастер ключа подписанного пользовательскимм ключем на крипто-токене

qss_mgr init \
  --module=/usr/lib64/librtpkcs11ecp.so \
  --mode pkcs11 \
  --token-serial-number 3c4c6444 \
  --token-key-id 1234 \
  master_key.bin

Добавление возможности запуска сервера с другим пользовательским ключем

Должен быть доступен ключ первого пользователя.

qss_mgr add \
  -mode fs \
  -k user2.bin \
  -n 0

Включение QSS на сервере

echo "qss_mode = 1" >> "${PGDATA}/qhb.conf"
qhb-ctl restart

Создание зашифрованной таблицы

create table t_qss(c1 int, c2 varchar) USING qss;

Подготовка к перешифровке БД новым мастер-ключем

Добавление нового мастер-ключа

qss_mgr --new init \
  --module=/usr/lib64/librtpkcs11ecp.so \
  --mode fs \
  --key user1.bin \
  new_master_key.bin

Сбор информации о зашифрованных таблицах в БД

Выполняется при запущенном кластере, надо повторить для всех БД в кластере, содержащих шифрованные таблицы

qss_recrypt --data-dir "${PGDATA}" add \
  --dbname my

Запуск перешифровки кластера

Выполняется на остановленном кластере

qss_recrypt --data-dir "${PGDATA}" recrypt

Переключение на новый мастер-ключ

Выполняется на остановленном кластере. Переносит текущий набор ключей в папку old, заменяя его подготовленным набором ключей из папки new

qss_mgr --data-dir "${PGDATA}" use-new

Индексы

Индексы являются распространенным способом повышения производительности базы данных. Индекс позволяет серверу базы данных находить и извлекать определенные строки гораздо быстрее, чем без индекса. Но индексы также увеличивают нагрузку на систему баз данных в целом, поэтому их следует использовать разумно.

Краткая справка по индексам

Предположим, у нас есть таблица, подобная этой:

CREATE TABLE test1 (
    id integer,
    content varchar
);

и приложение делает много запросов вида

SELECT content FROM test1 WHERE id = constant;

Без предварительной подготовки система должна будет сканировать всю таблицу test1, строка за строкой, чтобы найти все соответствующие записи. Если в test1 есть много строк и только несколько из них (возможно, ноль или одна) будут возвращены таким запросом, то это явно неэффективный метод. Но если система получила указание поддерживать индекс для столбца id, она может использовать более эффективный метод для поиска подходящих строк. Например, достаточно будет пойти на несколько уровней вглубь дерева поиска.

Подобный подход используется в большинстве научно-популярных книг: термины и понятия, которые часто ищут читатели, собраны в алфавитном указателе в конце книги. Заинтересованный читатель может сравнительно быстро просканировать указатель и перейти к соответствующей странице (страницам), а не читать всю книгу целиком, чтобы найти интересующий материал. Подобно тому, как задача автора состоит в том, чтобы предвидеть элементы, которые читатели могут искать, задача программиста базы данных — предвидеть, какие индексы будут полезны.

Следующая команда может использоваться для создания подобного индекса для столбца id:

CREATE INDEX test1_id_index ON test1 (id);

Имя test1_id_index можно выбрать любое, но в идеале оно должно напоминать вам о назначении индекса.

Чтобы удалить индекс, используйте команду DROP INDEX. Индексы могут быть добавлены и удалены из таблиц в любое время.

После создания индекса дальнейшее вмешательство не требуется: система будет обновлять индекс при изменении таблицы и будет использовать индекс в запросах, когда считает, что это будет более эффективно, чем последовательное сканирование таблицы. Но вам, возможно, придется регулярно запускать команду ANALYZE для обновления статистики, чтобы планировщик запросов мог принимать обоснованные решения. См. главу Советы по производительности для получения информации о том, как узнать, используется ли индекс, и когда и почему планировщик может предпочесть не использовать индекс.

Индексы также могут использоваться командами UPDATE и DELETE с условиями фильтрации. Кроме того, индексы могут использоваться при соединении таблиц, поэтому индекс, определенный для столбца, который является частью условия соединения, также может значительно ускорить запросы с соединениями.

Создание индекса для большой таблицы может занять много времени. По умолчанию QHB позволяет выполнять чтение (SELECT) из таблицы параллельно с созданием индекса, но модификации (INSERT, UPDATE, DELETE) блокируются до завершения построения индекса. В нагруженной системе это часто недопустимо. Можно разрешить модификации параллельно с созданием индекса, но следует учитывать несколько моментов — для получения дополнительной информации см. раздел Неблокирующее построение индексов.

После создания индекса система должна постоянно синхронизировать его с таблицей. Это увеличивает накладные расходы на операции с данными. Поэтому индексы, которые редко или никогда не используются в запросах, должны быть удалены.

Типы индексов

QHB предоставляет несколько типов индексов: B-дерево, Hash, GiST, SP-GiST, GIN и BRIN. Каждый тип индекса использует свой алгоритм, который лучше всего подходит для разных типов запросов. По умолчанию команда CREATE INDEX создает B-дерево, потому что оно подходит в наиболее распространенных ситуациях.

B-деревья могут обрабатывать запросы на равенство, неравенство и принадлежность интервалу значений. В частности, планировщик запросов QHB будет рассматривать возможность использования индекса типа B-дерево всякий раз, когда индексированный столбец участвует в условии с одним из этих операторов:

<
<=
=
>=
>

Конструкции, эквивалентные комбинациям этих операторов, такие как BETWEEN и IN, также могут быть реализованы с помощью поиска по B-дереву. Кроме того, B-дерево может быть использовано при запросе условия IS NULL или IS NOT NULL на столбец индекса.

Оптимизатор также может использовать B-дерево для запросов, включающих операторы сопоставления с образцом LIKE и ~, если шаблон является константой и привязан к началу строки; например, col LIKE 'foo%' or col ~ '^foo', но не col LIKE '%bar'. Однако, если ваша база данных использует локаль, отличную от C, вам нужно будет создать индекс со специальным классом операторов для поддержки индексации запросов на сопоставление с образцом; см. раздел Классы операторов и семейства операторов ниже. Также возможно использовать B-дерево для ILIKE и ~*, но только если шаблон начинается с символов, для которых нет верхнего и нижнего регистра, например, цифр.

Индексы типа B-дерево также можно использовать для извлечения данных в отсортированном порядке. Это не всегда быстрее, чем простое сканирование и сортировка, но часто полезно.

Хеш-индексы могут обрабатывать только простые сравнения на равенство. Планировщик запросов рассмотрит возможность использование хеш-индекса, когда индексируемый столбец участвует в сравнении с использованием оператора =. Следующая команда создает хеш-индекс:

CREATE INDEX name ON table USING HASH (column);

Индексы GiST — это не единая категория индексов, а скорее инфраструктура, с помощью которой может быть реализовано множество различных стратегий индексации. Соответственно, конкретные операторы, с которыми может использоваться индекс GiST, варьируются в зависимости от стратегии индексации (класса операторов). Например, стандартный дистрибутив QHB включает классы операторов GiST для нескольких двумерных геометрических типов данных, которые поддерживают индексированные запросы с использованием следующих операторов:

<< &< &> >> <<| &<| |&> |>> @> <@ ~= &&

(Значение этих операторов см. в разделе Геометрические функции и операторы).

Многие другие классы операторов GiST доступны в коллекции contrib или в виде отдельных проектов.

Индексы GiST могут оптимизировать поиск «ближайшего соседа», например, такой запрос

SELECT * FROM places ORDER BY location <-> point '(101,456)' LIMIT 10;

находит десять мест, ближайших к заданной целевой точке. Возможность сделать это зависит от конкретного класса операторов, использованного при построении индекса.

Индексы SP-GiST, так же как и индексы GiST, предлагают инфраструктуру, которая поддерживает различные виды поиска. SP-GiST позволяет реализовать широкий спектр различных несбалансированных дисковых структур данных, таких как дерево квадрантов, k-мерные деревья и префиксные деревья (Tries). Например, стандартный дистрибутив QHB включает классы операторов SP-GiST для точек двумерного пространства, которые позволяют использовать индекс для запросов с использованием следующих операторов:

<< >> ~= <@ <^ >^

(Значение этих операторов см. в разделе Геометрические функции и операторы).

Как и GiST, SP-GiST поддерживает поиск «ближайшего соседа».

Индексы GIN — это «инвертированные индексы», которые подходят для индексации столбцов, значение которых представляет из себя коллекцию элементов, и последующего поиска по отдельным элементам. Инвертированный индекс содержит отдельную запись для каждого элемента и может эффективно обрабатывать запросы, которые проверяют наличие определенных элементов.

Подобно GiST и SP-GiST, GIN может поддерживать множество различных пользовательских стратегий индексирования, и конкретные операторы, с которыми может использоваться индекс GIN, различаются в зависимости от стратегии индексирования. Например, стандартный дистрибутив QHB включает класс операторов GIN для массивов, который поддерживает индексированные запросы с использованием операторов:

<@ @> = &&

Значение этих операторов см. в разделе Функции и операторы массива. Классы операторов GIN, включенные в стандартную поставку, а также входящие в коллекцию contrib, перечислены в разделе Встроенные классы операторов GIN.

Индексы BRIN (сокращение от Block Range INdexes, индекс диапазона блоков) хранят сводные данные о значениях, хранящихся в последовательных диапазонах физических блоков таблицы. Как и GiST, SP-GiST и GIN, BRIN может поддерживать множество различных стратегий индексирования, и конкретные операторы, с которыми может использоваться индекс BRIN, различаются в зависимости от стратегии индексирования. Для типов данных, имеющих линейный порядок сортировки, индекс хранит минимальные и максимальные значения в столбце для каждого диапазона блоков. Это позволяет использовать индекс для запросов, использующих следующие операторы < <= = >= >. Для получения дополнительной информации см. главу Индексы BRIN.

Многоколоночные индексы

Индекс может быть определен для более чем одного столбца таблицы. Например, если у вас есть такая таблица:

CREATE TABLE test2 (
  major int,
  minor int,
  name varchar
);

(предположим, что вы так храните каталог /dev в базе данных), и вы часто делаете запросы вида

SELECT name FROM test2 WHERE major = constant AND minor = constant;

то может быть целесообразно создать индекс по столбцам major и minor вместе, например:

CREATE INDEX test2_mm_idx ON test2 (major, minor);

В настоящее время только индексы типа B-tree, GiST, GIN и BRIN поддерживают многоколоночные индексы. Можно указать до 32 столбцов.

Многоколоночный индекс типа B-дерево может использоваться с условиями запроса, которые включают любое подмножество столбцов индекса, но индекс наиболее эффективен, когда заданы ограничения на ведущие (крайние левые) столбцы.

Точное правило такое: должно быть ограничение равенства для нескольких (0+) первых столбцов индекса, плюс, возможно, ограничение неравенства на 1 следующий столбец — такие условия (т.н. "предикат поиска") позволяют сканировать узкий диапазон индекса.

Ограничения на прочие столбцы индекса ("дополнительный фильтр поиска") проверяются при сканировании индекса прямо в нем, экономя обращения к таблице, но эти ограничения не уменьшают диапазон сканирования индекса. Например, если есть индекс по (a, b, c), а условие запроса WHERE a = 5 AND b <= 50 AND c = 100, то индекс будет сканироваться от первой записи a = 5 до последней записи a = 5 AND b <= 50, и для каждой записи этого диапазона будет проверяться дополнительное условие с = 100. Этот индекс в принципе может быть использован и для запросов, которые имеют ограничения на b и/или c без ограничения на a — но в этом случае будет сканироваться весь индекс, и скорее всего планировщик предпочтет последовательное сканирование таблицы такому использованию индекса.

Многоколоночный индекс GiST может использоваться с условиями запроса, которые включают любое подмножество столбцов индекса. Условия для дополнительных столбцов ограничивают записи, возвращаемые индексом, но условие для первого столбца является наиболее важным для определения того, какую часть индекса необходимо сканировать. Индекс GiST будет относительно неэффективным, если его первый столбец имеет мало вариантов различных значений, даже если в дополнительных столбцах много вариантов значений.

Многоколоночный индекс GIN может использоваться с условиями запроса, которые включают любое подмножество столбцов индекса. В отличие от B-дерева или GiST, эффективность поиска по индексу одинакова независимо от того, какие столбцы индекса входят в условие запроса.

Многоколоночный индекс BRIN может использоваться с условиями запроса, которые включают любое подмножество столбцов индекса. Подобно GIN и в отличие от B-дерева или GiST, эффективность поиска по индексу одинакова независимо от того, какие столбцы индекса входят в условие запроса. Единственная причина иметь несколько индексов BRIN в одной таблице вместо одного многоколоночного индекса BRIN — это задать для нескольких индексов разное значение параметра pages_per_range.

Конечно, каждый столбец должен использоваться с операторами, соответствующими типу индекса; если в условии используется другой оператор, то для него индекс не может быть использован.

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

Индексы и ORDER BY

В дополнение к просто поиску строк, удовлетворяющих запросу, индекс может выдать их в определенном отсортированном порядке. Это позволяет реализовать указание ORDER BY без отдельного шага сортировки. Из всех типов индексов, поддерживаемых в настоящее время QHB, только B-дерево умеет сортированный вывод — другие типы индексов возвращают строки в неопределенном, зависящем от реализации порядке.

Даже если есть индекс, умеющий выдавать результаты в порядке, требуемом в ORDER BY, у планировщика всегда есть другой вариант: получить выборку другим способом, а потом отсортировать. Например, если в выборку попадает большая часть таблицы, будет выгоднее использовать сканирование таблицы с последующей досортировкой (последовательное чтение таблицы быстрее, чем хождение по ссылкам из индекса).

Важным частным случаем является ORDER BY в сочетании с LIMIT n: использование индекса позволит сразу отсечь n строк в порядке ORDER BY, а при использовании сканирования таблицы придётся достать и отсортировать всю выборку, чтобы получить первые n строк.

По умолчанию индексы B-дерева хранят свои записи в порядке возрастания, значения NULL после всех остальных (в случае равенства ссылка на строку в куче (TID) определяет порядок). Это означает, что прямое сканирование индекса по столбцу x приводит к выводу, удовлетворяющему ORDER BY x (точнее, ORDER BY x ASC NULLS LAST). Тот же индекс также можно сканировать в обратном направлении, получая выходные данные, удовлетворяющие ORDER BY x DESC (или, точнее, ORDER BY x DESC NULLS FIRST, именно такой порядок противоположен предыдущему).

Вы можете настроить порядок индекса B-дерево, включив опции ASC/DESC, NULLS FIRST/NULLS LAST при создании индекса, например:

CREATE INDEX test2_info_nulls_low ON test2 (info NULLS FIRST);
CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);

Индекс, созданный как ASC NULLS FIRST может удовлетворять либо ORDER BY x ASC NULLS FIRST либо ORDER BY x DESC NULLS LAST в зависимости от того, в каком направлении он сканируется.

Вы можете спросить, зачем предлагать все четыре варианта, когда два варианта вместе с возможностью обратного сканирования будут охватывать все варианты ORDER BY. В одноколоночных индексах параметры действительно избыточны, но в многоколоночных индексах они могут быть полезны. Рассмотрим индекс из двух столбцов для (x, y) : он подходит для ORDER BY x, y если мы сканируем вперед, или ORDER BY x DESC, y DESC, если мы сканируем назад. Но он не может поддержать порядок ORDER BY x ASC, y DESC. Для такого порядка годится индекс (x ASC, y DESC) или (x DESC, y ASC)

Очевидно, что индексы с порядками сортировки, отличными от заданных по умолчанию, являются довольно специализированной функцией, но иногда они могут привести к значительному ускорению для определенных запросов. Стоит ли поддерживать такой индекс, зависит от того, как часто вы используете запросы, требующие специального порядка сортировки.

Объединение нескольких индексов

Простое сканирование индекса может использоваться для условий на отдельные столбцы индекса, объединённых по AND. Например, при наличии индекса (a, b) условие запроса WHERE a = 5 AND b = 6 может использовать индекс, но запрос, например, WHERE a = 5 OR b = 6 не может напрямую использовать индекс.

К счастью, QHB имеет возможность комбинировать несколько индексов (включая многократное использование одного и того же индекса) для обработки случаев, которые не могут быть реализованы при сканировании одного индекса. Система может реализовать условия AND и OR за нескольких сканирований индекса. Например, запрос типа WHERE x = 42 OR x = 47 OR x = 53 OR x = 99 можно реализовать через сканирование четырех отдельных диапазонов индекса по x, каждое по одному из условий x = ?. Результаты этих сканирований затем объединяются для получения результата. Другой пример: если у нас есть отдельные индексы для x и y, одна из возможных реализаций запроса WHERE x = 5 AND y = 6 состоит в том, чтобы использовать каждый индекс для соответствующего условия, а затем посчитать пересечение двух множеств строк.

Чтобы объединить несколько индексов, система сканирует каждый необходимый индекс и сохраняет множество строк, подходящий под эту часть условия, в памяти в виде битовой карты. Затем битовые карты можно объединять или пересекать в соответствии с запросом. Наконец, фактические строки таблицы посещаются и возвращаются. Строки таблицы посещаются в физическом порядке, потому что так они лежат в битовой карте; это означает, что любой порядок выдачи исходных индексов потерян, и поэтому потребуется отдельный шаг сортировки, если в запросе есть предложение ORDER BY. По этой причине, а также потому, что каждое дополнительное сканирование индекса добавляет дополнительное время, планировщик иногда предпочитает использовать простое сканирование индекса, даже если доступны дополнительные индексы, которые также можно было бы использовать.

Во всех приложениях, кроме самых простых, могут быть полезны различные комбинации индексов, и разработчик базы данных должен найти компромисс, какие индексы иметь. Иногда лучше использовать многоколоночные индексы, а иногда лучше создавать отдельные индексы и полагаться на функцию комбинирования индексов. Например, если ваша рабочая нагрузка включает в себя набор запросов, которые иногда содержат условие только на столбец x, иногда только на столбец y, а иногда на оба столбца, вы можете создать два отдельных индекса для x и y, полагаясь на комбинацию индексов для обработки запросов, которые используйте оба столбца. Вы также можете создать многоколоночный индекс для (x, y). Этот индекс эффективнее, чем комбинация индексов, для запросов, включающих оба столбца, но, как обсуждалось в разделе Многоколоночные индексы, он почти бесполезен для запросов, включающих только y, поэтому он не должен быть единственным индексом. Комбинация многоколоночного индекса и отдельного индекса по y будет неплохим вариантом. Для запросов, включающих только x, можно использовать многоколонный индекс, хотя он будет больше и, следовательно, медленнее, чем индекс только для x. Третий вариант заключается в создании всех трех индексов, но это разумно, только если поиск в таблице происходит гораздо чаще, чем модификации, и все три типа запросов одинаково частые. Если один из типов запросов встречается значительно реже, чем другие, то лучше создать только два индекса, которые лучше всего соответствуют двум более частым запросам.

Уникальные индексы

Индексы также можно использовать для обеспечения уникальности значения столбца или комбинации из нескольких столбцов.

CREATE UNIQUE INDEX name ON table (column [, ...]);

В настоящее время только индексы типа B-дерево могут быть объявлены уникальными.

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

QHB автоматически создает уникальный индекс, когда для таблицы определено уникальное ограничение или первичный ключ. Индекс охватывает столбцы, которые составляют первичный ключ или уникальное ограничение (это может быть многоколонный индекс, если необходимо), и он используется при проверке ограничения.

Заметка
Не надо вручную создавать индексы для уникальных столбцов; это создаст копию автоматически созданного индекса.

Индексы по выражениям

Столбец индекса необязательно должен быть просто столбцом базовой таблицы, но может быть функцией или скалярным выражением, вычисляемым от одного или нескольких столбцов таблицы. Эта опция полезна для быстрого доступа к таблице по результатам вычисления выражения.

Например, распространенный способ сравнения без учета регистра состоит в использовании функции lower:

SELECT * FROM test1 WHERE lower(col1) = 'value';

Этот запрос может использовать индекс, если он был определен по функции lower(col1) :

CREATE INDEX test1_lower_col1_idx ON test1 (lower(col1));

Если объявить этот индекс уникальным, то это не позволит вставить в таблицу несколько строк, значение col1 которых отличается только регистром. Т.о. индексы по выражениям позволяют задавать нетривиальные ограничения уникальности. Например, следующий уникальный индекс предотвращает сохранение чисел с одинаковой целой частью в столбце с действительным числами:

CREATE UNIQUE INDEX test1_uniq_int ON tests ((floor(double_col)));

В качестве другого примера, если вы часто делаете запросы вроде

SELECT * FROM people WHERE (first_name || ' ' || last_name) = 'John Smith';

тогда, возможно, стоит создать такой индекс:

CREATE INDEX people_names ON people ((first_name || ' ' || last_name));

Синтаксис команды CREATE INDEX обычно требует записи круглых скобок вокруг выражения, как показано в последнем примере. Скобки могут быть опущены, когда выражение является просто вызовом функции, как в первом примере.

Индексы по выражениям относительно дороги в обслуживании, поскольку производные выражения должны вычисляться для каждой строки после вставки и каждого изменения. Однако выражения индекса не пересчитываются во время поиска по индексу, поскольку результат вычисления уже хранятся в индексе. В обоих приведенных выше примерах система видит запрос как WHERE indexed_column = ’constant’, поэтому скорость поиска эквивалентна любому другому простому запросу индекса. Таким образом, индексы по выражениям полезны, когда скорость поиска важнее скорости вставки и обновления.

Частичные индексы

Частичный индекс — это индекс, построенный на подмножестве таблицы; подмножество определяется условным выражением (называемым предикатом частичного индекса). Индекс содержит записи только для тех строк таблицы, которые удовлетворяют предикату. Частичные индексы являются специализированной функцией, но есть несколько ситуаций, в которых они полезны.

Одна из основных причин использования частичного индекса — избегать индексации наиболее популярных значений. Поскольку запрос, выполняющий поиск популярного значения (на которое приходится более скольких-то процентов от всей таблицы), все равно будет использовать сканирование таблицы, а не индекс, то нет смысла вообще сохранять эти строки в индексе. Это уменьшает размер индекса, что ускорит те запросы, которые используют индекс. Это также ускорит многие операции обновления таблиц, поскольку индекс нужно обновлять не всегда. Пример ниже показывает возможное применение этой идеи.

Пример. Настройка частичного индекса для исключения частых значений

Предположим, вы храните журналы доступа веб-сервера в базе данных. Большинство обращений происходит из диапазона IP-адресов вашей организации, но некоторые из других источников (например, сотрудники, подключающиеся по телефонной линии). Если вы ищете по IP-адресу в основном для внешних обращений, вам, вероятно, не нужно индексировать диапазон IP-адресов, который соответствует подсети вашей организации.

Предположим, что таблица такая:

CREATE TABLE access_log (
    url varchar,
    client_ip inet,
    ...
);

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

CREATE INDEX access_log_client_ip_ix ON access_log (client_ip)
WHERE NOT (client_ip > inet '192.168.100.0' AND
           client_ip < inet '192.168.100.255');

Типичный запрос, который может использовать этот индекс:

SELECT *
FROM access_log
WHERE url = '/index.html' AND client_ip = inet '212.78.10.32';

Здесь IP-адрес из запроса покрывается частичным индексом. Следующий запрос не может использовать частичный индекс, так как он использует IP-адрес, который исключен из индекса:

SELECT *
FROM access_log
WHERE url = '/index.html' AND client_ip = inet '192.168.100.23';

Заметьте, что этот тип частичного индекса требует, чтобы общие значения были предопределены, поэтому такие частичные индексы лучше всего использовать для данных, распределение которых не меняются. Такие индексы можно время от времени пересоздавать, чтобы приспособиться к новым распределениям данных, но это добавляет усилий на обслуживание.

Другое возможное использование частичного индекса — включение в индекс только тех значений, которые участвуют в типичной рабочей нагрузке (как показано в следующем примере). Это дает преимущества, описанные выше, однако не позволяет использовать индекс при поиске по "нетипичным" значениям. Очевидно, что выбор частичных индексов в таком сценарии потребует большой осторожности и экспериментов.

Пример. Настройка частичного индекса для исключения «неинтересных» значений

Если у вас есть таблица, которая содержит как оплаченные, так и неоплаченные заказы, где неоплаченные заказы занимают небольшую долю в общей таблице, и при этом эти строки являются наиболее востребованными, вы можете повысить производительность, создав индекс только для неоплаченных заказов. Команда создания индекса будет выглядеть так:

CREATE INDEX orders_unbilled_index ON orders (order_nr)
    WHERE billed is not true;

Возможный запрос, использующий этот индекс:

SELECT * FROM orders WHERE billed is not true AND order_nr < 10000;

Однако индекс также может использоваться в запросах, которые вообще не включают order_nr, например:

SELECT * FROM orders WHERE billed is not true AND amount > 5000.00;

Это не так эффективно, как частичный индекс по столбцу amount, поскольку система должна сканировать весь индекс. Тем не менее, если неоплаченных заказов относительно мало, использование этого частичного индекса для того, чтобы найти неоплаченные заказы с любым дополнительным условием может быть выигрышным.

Обратите внимание, что такой запрос не может использовать этот индекс:

SELECT * FROM orders WHERE order_nr = 3501;

Заказ 3501 может быть и оплачен, поэтому нельзя ограничиться поиском в частичном индексе.

Пример 3.2 также иллюстрирует, что индексированный столбец и столбец, используемый в предикате, не обязаны совпадать. QHB поддерживает частичные индексы с произвольными предикатами, при условии, что задействованы только столбцы индексируемой таблицы. Однако имейте в виду, что предикат должен соответствовать условиям, используемым в запросах, которые должны использовать индекс. Чтобы быть точным, частичный индекс может использоваться в запросе, только если система может аналитически распознать, что из условия WHERE всегда следует предикат индекса. QHB не имеет сложного средства проверки теорем, способного распознавать математически эквивалентные выражения, написанные в разных формах. (Мало того, что такое средство чрезвычайно трудно создать, оно, вероятно, будет работать слишком медленно, чтобы использоваться в планировщике запросов.) Система может распознавать простые следствия из неравенства, например, x &lt; 1 подразумевает x &lt; 2 . А в общем случае, лучше бы предиката индекса точно соответствовал части WHERE, иначе индекс не будет признан пригодным для использования. Сопоставление происходит во время планирования запроса, а не во время выполнения. Как следствие, параметризованные запросы могут не работать с частичным индексом. Например, подготовленный запрос с параметром может иметь условие WHERE x &lt; ?, а предикат индекса x &lt; 2 — индекс не будет использоваться для этого запроса, т.к. для некоторых значений параметра это приводило бы к неправильным результатам.

Третье возможное использование для частичных индексов — не для запросов, а для поддержания уникальности. Идея заключается в том, чтобы создать уникальный индекс для подмножества таблицы, как в следующем примере. Это обеспечивает уникальность среди строк, которые удовлетворяют предикату индекса, без ограничения для тех, которые не удовлетворяют.

Пример. Настройка частичного уникального индекса

Предположим, у нас есть таблица с описанием результатов теста. Мы хотим убедиться, что для данной комбинации субъекта и цели хранится только одна «успешная» запись, а записей о «неудачных» испытаниях может храниться много. Вот один из способов сделать это:

CREATE TABLE tests (
    subject text,
    target text,
    success boolean,
    ...
);

CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target)
    WHERE success;

Это особенно эффективный подход, когда мало успешных тестов и много неудачных.

Следующий индекс запрещает создание более одной строки со значением NULL в данном столбце, используя предложение частичного индекса для обработки только значений нулевого столбца и используя выражение для перевода NULL в true:

CREATE UNIQUE INDEX tests_target_one_null ON tests ((target IS NULL)) WHERE target IS NULL;

Наконец, частичный индекс также можно использовать, чтобы запретить планировщику использовать этот индекс для конкретных запросов. Иногда особое распределение данных может привести к тому, что система будет использовать индекс, когда это на самом деле не нужно. В этом случае индекс можно настроить таким образом, чтобы он не был доступен для некорректного запроса. Обычно QHB делает разумный выбор в отношении использования индекса (например, он не использует при низкоселективной выборке, поэтому предыдущие примеры в первую очередь экономят место, занимаемое индексом, а не отваживают планировщик от использования индекса), а крайне неправильный выбор плана является, вероятно, ошибкой в системе.

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

Более подробную информацию о частичных индексах можно найти у Stonebraker, Seshadri.

Сканирование только по индексу и покрывающие индексы

Все индексы в QHB являются вторичными, то есть каждый индекс хранится отдельно от основной области данных таблицы (которая в терминологии QHB называется кучей(heap) таблицы). Это означает, что при обычном сканировании индекса каждый поиск строки требует извлечения данных как из индекса, так и из кучи. Кроме того, хотя записи индекса, которые соответствуют заданному индексируемому условию WHERE, обычно близки друг к другу в индексе, строки таблицы, на которые они ссылаются, могут находиться где угодно в куче. И та часть доступа по индексу, которая состоит из обращения к строкам в куче, включает в себя много произвольного доступа, который может быть медленным, особенно на традиционных вращающихся носителях. (Как описано в Разделе Объединение нескольких индексов, сканирования-на-битовых-картах пытаются уменьшить эту стоимость, делая доступ к куче последовательным, но он все равно производится.)

Чтобы решить эту проблему производительности, QHB поддерживает сканирование только по индексу, которое может реализовывать запросы только на основании индекса без какого-либо доступа к куче. Основная идея состоит в том, чтобы возвращать значения непосредственно из записи индекса, а не обращаться к соответствующей записи кучи. Существуют два фундаментальных ограничения на использование этого метода:

  1. Тип индекса должен поддерживать сканирование только по индексу. Индексы B-дерева поддерживают. Индексы GiST и SP-GiST поддерживают сканирование только по индексу для некоторых классов операторов, но не для всех. Остальные типы индексов не поддерживают. Основным требованием является то, что индекс должен физически хранить или иметь возможность восстановить исходное значение данных для каждой записи индекса. В качестве контрпримера, индексы GIN не могут поддерживать сканирование только по индексу, поскольку каждая запись индекса содержит только часть исходного значения столбца.

  2. Запрос должен ссылаться только на столбцы, хранящиеся в индексе. Например, предполагая индекс по столбцам x и y таблицы, которая также имеет столбец z, эти запросы могут использовать сканирование только по индексу:

    SELECT x, y FROM tab WHERE x = 'key';
    SELECT x FROM tab WHERE x = 'key' AND y < 42;
    

    а эти запросы не могут:

    SELECT x, z FROM tab WHERE x = 'key';
    SELECT x FROM tab WHERE x = 'key' AND z < 42;
    

    (Для индексов по выражению и частичных индексов все сложнее, см. ниже.)

Если эти два фундаментальных требования выполнены, то все значения данных, требуемые запросом, доступны из индекса, поэтому физически возможно сканирование только по индексу. Но для любого сканирования таблицы в QHB есть дополнительное требование: система должна убедиться, что каждая извлеченная строка «видима» для MVCC-снимка текущего запроса, как описано в главе Параллельный контроль. Информация о видимости не хранится в записях индекса, а только в записях кучи; так что на первый взгляд может показаться, что для любого поиска в любом случае потребуется доступ к куче. И это действительно так, если строка таблицы была недавно изменена. Однако для редко меняющихся данных есть способ обойти эту проблему. QHB отслеживает для каждой страницы в куче таблицы, все ли строки, хранящиеся на этой странице, достаточно стары, чтобы быть видимыми для всех текущих и будущих транзакций. Эта информация сохраняется в виде одного бита в карте видимости таблицы. Сканирование только по индексу после нахождения подходящей записи индекса проверяет бит видимости для соответствующей страницы кучи. Если он установлен, строка определенно видимая, и поэтому данные могут быть возвращены без обращения к куче. Если он не установлен, запись кучи должна быть посещена, чтобы выяснить, видна ли строка, поэтому никакого преимущества в производительности по сравнению со стандартным сканированием индекса не достигается. Даже в успешном случае этот подход требует обращения к карте видимости; но поскольку карта видимости на четыре порядка меньше описываемой ею кучи, для доступа к ней требуется гораздо меньше операций ввода-вывода. В большинстве случаев карта видимости постоянно хранится в памяти.

Короче говоря, хотя сканирование только по индексу возможно с учетом двух фундаментальных требований, оно будет выигрышным, только если для значительной части страниц кучи таблицы установлены биты видимости. Такие таблицы, в которых большая часть строк неизменна, достаточно распространены, чтобы сделать этот тип сканирования очень полезным на практике.

Чтобы эффективно использовать функцию сканирования только по индексу, вы можете создать покрывающий индекс, т.е. индекс, специально включающий лишние столбцы, которые требуются конкретному запросу, который вы часто выполняете. Поскольку запросам обычно требуется получать больше столбцов, чем только те, по которым они осуществляют поиск, QHB позволяет создавать индекс, в котором некоторые столбцы являются просто «дополнительной нагрузкой», а не частью ключа поиска. Это делается путем добавления указания INCLUDE со списком дополнительных столбцов. Допустим, вы часто делаете запрос вида

SELECT z FROM tab WHERE x = 'key';

Традиционный подход к ускорению таких запросов заключается в создании индекса только по x. Тем не менее, индекс, заданный как

CREATE INDEX tab_x_z ON tab(x) INCLUDE (z);

может обрабатывать эти запросы как сканирование только по индексу, потому что z можно получить из индекса, не посещая кучу.

Поскольку столбец z не является частью ключа поиска индекса, он не обязательно должен иметь тип данных, который может обрабатывать индекс; он просто хранится в индексе и не обрабатывается механизмом индекса. Кроме того, если индекс является уникальным, то есть

CREATE UNIQUE INDEX tab_x_z ON tab(x) INCLUDE (z);

условие уникальности применяется только к столбцу x, а не к комбинации x и z. (При описании UNIQUE и PRIMARY KEY тоже можно задать INCLUDE, это альтернативный синтаксис для создания такого индекса.)

Добавлять в индекс неключевые столбцы дополнительной нагрузки следует обдуманно, особенно если эти столбцы широкие. Если кортеж индекса превысит максимально допустимый размер для данного типа индекса, вставка данных завершится неудачно. В любом случае неключевые столбцы дублируют данные из таблицы и увеличивают размер индекса, что потенциально замедляет поиск. И помните, что нет никакого смысла включать столбцы полезной нагрузки в индекс, если таблица изменяется часто, т.к. все равно потребуется обращаться к куче. Если есть обращение к строке в куче, получить значение всех столбцов столбца можно оттуда, и это ничего не стоит. Другие ограничения заключаются в том, что в качестве дополнительных столбцов нельзя включать выражения, и что только B-деревья и GiST в настоящее время поддерживают включенные столбцы.

До того, как в QHB появилась функция INCLUDE, люди иногда создавали покрывающие индексы, записывая столбцы полезной нагрузки как обычные столбцы индекса, то есть

CREATE INDEX tab_x_y ON tab(x, y);

даже если они не собирались использовать y как часть WHERE. Это прекрасно работает, если дополнительные столбцы являются конечными столбцами; делать их ведущими столбцами неразумно по причинам, изложенным в разделе Многоколоночные индексы. Однако этот метод не подходит для уникальных индексов.

Усечение суффиксов всегда удаляет неключевые столбцы с верхних уровней B-Дерева. Как столбцы дополнительной нагрузки, они никогда не используются при сканировании индекса. Процесс усечения в принципе удаляет один или несколько конечных столбцов ключа из не-листьев B-Дерева, когда они не требуются для обхода дерева. Поэтому на практике покрытие индексов даже без предложения INCLUDE часто позволяет избежать хранения столбцов, которые являются дополнительной нагрузкой, на верхних уровнях. Однако явное определение столбцов дополнительной нагрузки в качестве неключевых столбцов надежно сохраняет кортежи на верхних уровнях небольшими.

Теоретически, сканирование только по индексу может использоваться с индексами по выражениям. Например, имея индекс по f(x), где x — столбец таблицы, можно выполнить запрос

SELECT f(x) FROM tab WHERE f(x) < 1;

как сканирование только по индексу; и это очень привлекательно, если f() дорогая для вычисления функция. Однако планировщик QHB в настоящее время не очень разбирается в таких случаях. Он считает, что запрос потенциально может быть выполнен при сканировании только по индексу, только когда из индекса доступны все столбцы, необходимые для запроса. В этом примере x не требуется, кроме как в контексте f(x), но планировщик этого не замечает и приходит к выводу, что сканирование только по индексу невозможно. Если очень надо, то это ограничение можно обойти, добавив x в качестве включенного столбца, например

CREATE INDEX tab_f_x ON tab (f(x)) INCLUDE (x);

Дополнительное предостережение. Если цель состоит в том, чтобы избежать повторного вычисления f(x), то есть ещё одна проблема. Не факт, что планировщик додумается использовать значение, взятое из индекса, для всех вхождений f(x). Например, в запросе

SELECT B.name FROM tab join B on B.id = f(x) WHERE f(x) < 1;

даже если будет использовано сканирование только по индексу, значение f(x) для соединения будет вычислено заново (x возьмут из дополнительной колонки индекса). Этот недостаток может быть исправлен в будущих версиях QHB.

Частичные индексы также интересны при сканировании только по индексам. Рассмотрим частичный индекс из примера выше:

CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target)
    WHERE success;

Можем ли мы выполнить сканирование только по индексу, чтобы удовлетворить следующий запрос?

SELECT target FROM tests WHERE subject = 'some-subject' AND success;

Есть проблема: WHERE содержит success, который не является столбцом индекса. Тем не менее, сканирование только по индексу возможно, потому что плану не нужно перепроверять эту часть WHERE во время выполнения: все записи, найденные в индексе, обязательно имеют success = true поэтому в плане это не нужно явно проверять.

Классы операторов и семейства операторов

Определение индекса может указывать класс оператора для каждого столбца индекса.

CREATE INDEX name ON table (column opclass [sort options] [, ...]);

Класс операторов определяет операторы, которые будут использоваться индексом для этого столбца. Например, индекс B-дерева для типа int4 будет использовать класс int4_ops ; этот класс операторов включает функции сравнения для значений типа int4. На практике класс операторов по умолчанию для типа данных столбца обычно достаточен. Основная причина наличия классов операторов заключается в том, что для некоторых типов данных может быть более одного осмысленного поведения индекса. Например, мы могли бы захотеть отсортировать тип данных комплексного числа или по абсолютному значению или по реальной части. Мы могли бы сделать это, определив два класса операторов для типа данных, а затем выбрав подходящий класс при создании индекса. Класс оператора определяет основной порядок сортировки (который затем можно изменить, добавив параметры сортировки COLLATE, ASC/DESC, NULLS FIRST/NULLS LAST).

Есть также несколько встроенных классов операторов, кроме классов по умолчанию:

  • Классы операторов text_pattern_ops, varchar_pattern_ops и bpchar_pattern_ops поддерживают индексы B-дерева для типов text, varchar и char соответственно. Отличие от классов операторов по умолчанию состоит в том, что значения сравниваются строго символ за символом, а не в соответствии с правилами сортировки, специфичными для локали. Это делает эти классы операторов пригодными для использования в запросах, включающих выражения сопоставления с образцом (регулярные выражения LIKE или POSIX), когда база данных использует локаль, отличную от «C». Например, вы можете проиндексировать столбец varchar следующим образом:

    CREATE INDEX test_index ON test_table (col varchar_pattern_ops);
    

    Обратите внимание, что вам придется создать еще и индекс с классом операторов по умолчанию, если вы хотите, чтобы запросы, включающие обычные сравнения <, <=, > или >= использовали индекс. Такие запросы не могут использовать операторы класса xxx_pattern_ops (сравнения на равенство — могут). Можно создать несколько индексов про один столбец, но с разными классами операторов. Если вы используете локаль C, вам не нужен индекс с операторами класса xxx_pattern_ops, т.к. в локали C индекс с операторами по умолчанию можно использовать и для запросов сопоставления с образцом.

Следующий запрос показывает все известные классы операторов:

SELECT am.amname AS index_method,
       opc.opcname AS opclass_name,
       opc.opcintype::regtype AS indexed_type,
       opc.opcdefault AS is_default
    FROM pg_am am, pg_opclass opc
    WHERE opc.opcmethod = am.oid
    ORDER BY index_method, opclass_name;

Класс операторов — это просто подмножество более крупной структуры, называемой семейством операторов. В тех случаях, когда несколько типов данных имеют сходное поведение, часто бывает полезно определить операторы над семейством типов данных и разрешить им работать с индексами. Для этого классы операторов для каждого из типов должны быть сгруппированы в одно и то же семейство операторов. Операторы семейства типов являются членами семейства, но не привязаны к конкретному классу в семействе.

Эта расширенная версия предыдущего запроса показывает семейство операторов, к которому принадлежит каждый класс операторов:

SELECT am.amname AS index_method,
       opc.opcname AS opclass_name,
       opf.opfname AS opfamily_name,
       opc.opcintype::regtype AS indexed_type,
       opc.opcdefault AS is_default
    FROM pg_am am, pg_opclass opc, pg_opfamily opf
    WHERE opc.opcmethod = am.oid AND
          opc.opcfamily = opf.oid
    ORDER BY index_method, opclass_name;

Этот запрос показывает все определенные семейства операторов и все операторы, включенные в каждое семейство:

SELECT am.amname AS index_method,
       opf.opfname AS opfamily_name,
       amop.amopopr::regoperator AS opfamily_operator
    FROM pg_am am, pg_opfamily opf, pg_amop amop
    WHERE opf.opfmethod = am.oid AND
          amop.amopfamily = opf.oid
    ORDER BY index_method, opfamily_name, opfamily_operator;

Индексы и правила сортировки

Индекс может использовать только одно правило сортировки (COLLATION) на столбец индекса. Если требуется поиск по столбцу с разными правилами сортировки, придется создать несколько индексов.

Рассмотрим следующие команды:

CREATE TABLE test1c (
    id integer,
    content varchar COLLATE "x"
);

CREATE INDEX test1c_content_index ON test1c (content);

Индекс унаследует правила сортировки от базового столбца content. Так что запрос вида

SELECT * FROM test1c WHERE content > constant;

может использовать индекс, потому что сравнение столбца с константой по умолчанию использует правило сортировки столбца, и это соответствует индексу. Однако этот индекс не может ускорить запросы, в которых задано другое правило сортировки, например, такой:

SELECT * FROM test1c WHERE content > constant COLLATE "y";

Если такие запросы тоже нужны, то можно создать дополнительный индекс, который будет поддерживать правило сортировки "y", например:

CREATE INDEX test1c_content_y_index ON test1c (content COLLATE "y");

Анализ использования индексов

Хотя индексы в QHB не нуждаются в обслуживании или настройке, все же важно проверить, какие индексы действительно используются реальной рабочей нагрузкой запросов. Изучение использования индекса для отдельного запроса выполняется командой EXPLAIN; его применение для этой цели иллюстрируется в разделе Использование EXPLAIN. Также возможно собрать общую статистику об использовании индекса на работающем сервере, как описано в разделе Сборщик статистики.

Сложно сформулировать общую процедуру выбора индексов для создания. Существует ряд типичных случаев, которые были показаны в примерах в предыдущих разделах. Часто требуется много экспериментов. Остаток этого раздела содержит несколько советов на эту тему:

  • В первую очередь запустите ANALYZE. Эта команда собирает статистику о распределении значений в таблице. Эта информация необходима для оценки количества строк, возвращаемых запросом, которое необходимо планировщику для реалистичной оценки затрат для каждого возможного плана запроса. При отсутствии какой-либо реальной статистики предполагаются некоторые значения по умолчанию, которые почти наверняка будут неточными. Поэтому изучение планов выполнения без запуска ANALYZE является заведомо бесперспективным. См. разделы Обновление статистики планировщика и Процесс «Автовакуум» для получения дополнительной информации.

  • Используйте реальные данные для экспериментов. Использование тестовых данных для настройки индексов скажет вам, какие индексы вам нужны для тестовых данных, но не более того.

    Особенно фатально использовать очень маленькие наборы тестовых данных. Выборка 1000 из 100000 строк может быть кандидатом на использование индекса, но если на тестовых данных вы выбираете 1 из 100 строк, то вы получите совсем другой результат. Все 100 строк, вероятно, помещаются на одной странице, и последовательное чтение всей таблицы будет безусловно быстрее на таком объеме,

    Также будьте осторожны при генерации синтетических тестовых данных (это часто неизбежно, когда приложение еще не работает). Однородные значения, полностью случайные значения или значения, вставленные в порядке возрастания, исказят статистику по сравнению с реальными данными.

  • Когда индексы не используются, для тестирования может быть полезно форсировать их использование. Существуют параметры времени выполнения, которые могут отключать различные типы планов (см. раздел Конфигурация метода планирования). Например, отключение последовательного сканирования (enable_seqscan) и последовательных соединений (enable_nestloop), которые являются наиболее простыми планами, заставит систему использовать другой план. Если система все еще выбирает последовательное сканирование или последовательное соединение, то, вероятно, существует более фундаментальная причина, по которой индекс не используется, например, условие запроса не соответствует индексу. (Какой запрос можно использовать какой тип индекса объясняется в предыдущих разделах.)

  • Если искусственными ограничениями удалось заставить планировщик использовать индекс, то есть две варианта: либо планировщик прав, и использование индекса плохо подходит, либо оценки затрат планов запросов не отражают реальность. Посмотрите оценки стоимости планов с индексом и без индекса командой EXPLAIN ANALYZE, а также сравните реальное время выполнения.

  • Если окажется, что оценки затрат ошибочны, опять-таки есть несколько вариантов. Общая стоимость вычисляется умножением оценки количества строк на стоимость операций, производимых с каждой строкой. Стоимость операций можно скорректированы с помощью параметров (описано в разделе Константы стоимости планировщика). Неточная оценка количества строк(селективности) обычно связана с неточной статистикой. Это можно исправить, настроив параметры сбора статистики (см. ALTER TABLE).

    Если вам не удастся скорректировать оценки стоимостей, чтобы они соответствовали реальности, вам, возможно, придется силой заставить планировщик использовать индекс. Вы также можете связаться с разработчиками QHB для изучения проблемы.

Индексы B-деревья

QHB включает в себя реализацию стандартной структуры данных индекса B-дерева (многонаправленного сбалансированного дерева — btree). Любой тип данных, который можно отсортировать в четком линейном порядке, может быть загружен в индекс B-дерево. Единственное ограничение заключается в том, что запись индекса не может превышать приблизительно одной трети страницы (после сжатия TOAST, если применимо).

Поскольку любой класс операторов B-дерева определяет порядок сортировки на своем типе данных, классы операторов B-дерева (точнее, на самом деле семейства операторов) стали использоваться в QHB для общего представления и понимания семантики сортировки. Поэтому они приобрели некоторые функции, которые выходят за рамки того, что было бы необходимо только для поддержки индексов B-деревьев, и их используют части системы, которые довольно далеки от методов доступа B-дерева.

Поведение классов операторов B-дерева

Класс операторов B-дерева должен предоставлять пять операторов сравнения: <, <=, =, >= и >. Можно было бы ожидать, что в эти операторы также должен входить <>, но это не так, потому что практически никогда не имеет смысл использовать <> в предложении WHERE для поиска по индексу. (Для некоторых целей планировщик обрабатывает <> как связанный с классом операторов B-дерева; но он находит данный оператор через отрицание оператора =, а не обращаясь к pg_amop).

Если несколько типов данных имеют почти одинаковую семантику сортировки, их классы операторов можно сгруппировать в семейство операторов. Это выгодно, потому что позволяет планировщику делать выводы о межтиповых сравнениях. Каждый класс операторов в семействе должен содержать однотиповые операторы (и связанные с ними вспомогательные функции) для своего типа входных данных, в то время как межтиповые операторы и вспомогательные функции «слабо» связаны с семейством. Рекомендуется включать в семейство полный набор межтиповых операторов, таким образом гарантируя, что планировщик сможет вызвать любые условия сравнения, которые выведет из транзитивности.

Существует несколько основных предположений, которым должно удовлетворять семейство операторов для B-деревьев:

  • Оператор = должен быть отношением эквивалентности, то есть для любых отличных от NULL значений A, B, C определенного типа данных:

    • A = A истинно (рефлексивность)

    • A = B влечет B = A (симметричность)

    • если A = B и B = C, то A = C (транзитивность)

  • Оператор < должен представлять из себя отношение строгого порядка, то есть для любых отличных от NULL значений A, B, C:

    • A < A ложно (антирефлексивность)

    • если A < B и B < C, то A < C (транзитивность)

  • Более того, порядок должен быть полным; то есть для любых отличных от NULL значений A, B должно быть верно ровно 1 утверждение из 3: A < B, либо B < A, либо A = B (закон трихотомии)

    (Конечно, закон трихотомии обосновывает определение вспомогательной функции сравнения.)

Остальные три оператора определяются через операторы = и < очевидным образом и должны работать согласованно с последними.

Для семейства операторов, поддерживающих несколько типов данных, вышеуказанные законы должны выполняться, когда A, B, C берутся из любых типов данных в семействе. Закон транзитивности обеспечить сложнее всего, поскольку в ситуациях с разными типами это зависит от согласованности поведения двух или трех различных операторов. Для примера, нельзя поместить операторы для float8 и numeric в одно семейство, по крайней мере, не в сегодняшней ситуации, когда для сравнения с float8 значения numeric тоже преобразуются в тип float8. Из-за ограничения точности типа float8 различные значения numeric при приведении к float8 превращаются в одно и то же число, тем самым нарушая закон транзитивности.

Другое требование для семейства с несколькими типами данных заключается в том, что любые существующие неявные преобразования между типами данных, входящими в семейство операторов, должны сохранять результат сравнения.

Должно быть достаточно очевидно, почему индекс B-дерево требует выполнения вышеприведенных законов внутри одного типа данных: без этого просто не будет четкого упорядочивания ключей. Кроме того, индексный поиск, использующий сравнение с ключом другого типа данных, требует, чтобы сравнения вели себя разумно между двумя типами данных. Расширение семейства до трех или более типов данных не является обязательным для самого механизма индекса B-дерево, но планировщик полагается на них в целях оптимизации.

Вспомогательные функции B-дерева

B-дерево определяет одну обязательную и четыре факультативные вспомогательные функции:

order

Для каждой комбинации типов данных, для которых семейство операторов
B-дерева предоставляет операторы сравнения, оно должно предоставить и
вспомогательную функцию сравнения, зарегистрированную в ***pg\_amproc*** как вспомогательная функция номер 1
со свойствами *amproclefttype/amprocrighttype*, равными левому и правому
типу данных сравнения (т. е. тем же типам данных, с которыми
зарегистрированы соответствующие операторы в ***pg\_amop***). Функция
сравнения должна принимать отличные от *NULL* значения **A** и **B** и возвращать
целое число, которое < 0, 0 или > 0, когда **A < B**, **A = B** или **A > B** соответственно.
Результат *NULL* не допускается: все значения типа данных должны быть
сравнимы.

Если сравниваемые значения имеют сортируемый тип данных, то во
вспомогательную функцию сравнения будет передан соответствующий OID сортировки,
используя стандартный механизм **PG\_GET\_COLLATION()**.

<!-- not open source
Примеры реализации order ищите в файле *src/backend/access/nbtree/nbtcompare.c* -->    

sortsupport

Опционально семейство операторов B-дерева может предоставлять *вспомогательную(ые) функцию(и) сортировки*, регистрируемую как
вспомогательная функция номер 2. Эти функции позволяют
реализовывать сравнения для целей сортировки более эффективно, чем
при простом вызове вспомогательной функции сравнения.

<!-- not open source
API, участвующие в этом, определены в: *src/include/utils/sortsupport.h*.
-->
<!--  "(или несколько функций)" как их различает система, если они все №2 ?! -->

in_range

Опционально семейство операторов B-дерева может предоставлять вспомогательную(ые) функцию(и) *in\_range*, регистрируемую как
вспомогательная функция номер 3.
Такие функции не используется при операциях B-дерева; они
расширяют семантику семейства операторов, чтобы оно могло поддерживать оконные предложения, содержащие типы границ рамки **RANGE *смещение* PRECEDING** и
**RANGE *смещение* FOLLOWING** (см. раздел [Вызовы оконных функций]). По сути, дополнительная информация позволяет добавлять или вычитать значение ***смещения*** способом, соответствующим принятому в семействе порядку сортировки.

Функция *in\_range* должна иметь сигнатуру

```sql
in_range(значение type1, база type1, смещение type2, вычитание bool, меньше bool)
returns bool
```

***Значение*** и ***база*** должны быть одинакового типа, одного из поддерживаемых семейством операторов
(т. е. типом, для которого задается порядок). Однако ***смещение*** может быть другого типа,
который никаким другим образом данным семейством может и не поддерживаться.
Например, встроенное семейство операторов *time\_ops*  предоставляет
функцию *in\_range* которая имеет параметр ***смещение*** типа *interval*. Семейство может
иметь функцию *in\_range* для любого поддерживаемого типа и одного или нескольких типов ***смещения***.
Каждая вспомогательная функция *in\_range* должна быть
зарегистрирована в ***pg\_amproc*** с *amproclefttype* равным *type1* и *amprocrighttype*
равным *type2*.

Основополагающая семантика функции *in\_range* зависит от двух логических флаговых параметров. Она должна прибавить или вычесть   из ***базы смещение*** и после этого сравнить результат со ***значением*** следующим образом:

-   если !***вычитание*** и !***меньше***, возвращается ***значение*** >= (***база + смещение***)

-   если !***вычитание*** и ***меньше***, возвращается ***значение*** <= (***база + смещение***)

-   если ***вычитание*** и !***меньше***, возвращается ***значение*** >= (***база - смещение***)

-   если ***вычитание*** и ***меньше***, возвращается ***значение*** <= (***база - смещение***)

Прежде чем делать это, функция должна проверить знак ***смещения***:
если оно отрицательное, выдавать ошибку *ERRCODE\_INVALID\_PRECEDING\_OR\_FOLLOWING\_SIZE* (22013) с текстом ошибки «invalid preceding or following size in window function». (Этого требует стандарт SQL, хотя нестандартные семейства операторов, вероятно, могут проигнорировать это ограничение, т. к., по-видимому, в нем нет особой семантической необходимости.)
Эта проверка возложена на *in_range*, чтобы основному коду не нужно было понимать, что для конкретного типа данных означает «отрицательное».

Дополнительно ожидается, что функции *in_range* должны, в частности, избегать возникновения ошибки в случае переполнения при вычислении ***база + смещение*** или ***база - смещение***. Правильный результат сравнения можно получить, даже если это значение выходит за границы диапазона типа данных.
Обратите внимание, что если тип данных включает такие понятия, как «бесконечность» или «NaN», может понадобиться повышенная осторожность для обеспечения согласованности результатов *in_range*
с обычным порядком сортировки этого семейства операторов.

Результаты функции *in_range* должны соответствовать порядку сортировки, установленному семейством операторов. Выражаясь точнее,
при любых фиксированных значениях ***смещения*** и ***вычитания*** справедливо следующее:

- Если *in_range* с ***меньше*** = true возвращает *true* для некоторого ***значения1*** и ***базы***, *true* должно возвращаться
для каждого ***значения2*** <= ***значению1*** с той же ***базой***.

- Если *in_range* с ***меньше*** = true возвращает *false* для некоторого ***значения1*** и ***базы***, *false* должно возвращаться
для каждого ***значения2*** >= ***значению1*** с той же ***базой***.

- Если *in_range* с ***меньше*** = true возвращает *true* для некоторого ***значения*** и ***базы1***, *true* должно возвращаться
для каждой ***базы2*** >= ***базе1*** с тем же ***значением***.

- Если *in_range* с ***меньше*** = true возвращает *false* для некоторого ***значения*** и ***базы1***, *false* должно возвращаться
для каждой ***базы2*** <= ***базе1*** с тем же ***значением***.

При ***меньше*** = false должны выполнятся аналогичные утверждения с противоположными условиями.

Если упорядочиваемый тип (*type1*) является сортируемым, функции *in_range* с помощью стандартного механизма *PG\_GET\_COLLATION()* будет передан OID соответствующего правила сортировки.

Функции *in_range* не обязаны обрабатывать значение аргументов *NULL* и обычно помечаются как строгие.

equalimage

Опционально семейство операторов В-дерева может предоставить вспомогательные функции *equalimage* («равенство подразумевает равенство образов»), регистрируемые как вспомогательная функция номер 4. Эти функции позволяют основному коду определять, безопасно ли применять оптимизацию с дедупликацией в В-дереве. На данный момент функции *equalimage* вызываются только при построении или перестроении индекса.

Функция *equalimage* должна иметь сигнатуру

```SQL
equalimage(opcintype oid) returns bool
```

Возвращаемое значение является статической информацией о классе операторов и правиле сортировки. Результат *true* показывает, что функция *order* для класса операторов гарантирует возвращать 0 («аргументы равны»), только когда его аргументы ***А*** и ***В*** также взаимозаменяемы без потери семантической информации. Если функция *equalimage* не зарегистрирована или возвращает *false*, это означает, что нельзя предполагать выполнение данного условия.

Аргумент ***opcintype*** является *pg_type.oid* типа данных, который индексируется данным классом операторов. Это удобство позволяет повторно использовать в разных классах операторов одну и ту же нижележащую функцию *equalimage*. Если ***opcintype*** относится к сортируемому типу данных, функции *equalimage* с помощью стандартного механизма *PG\_GET\_COLLATION()* будет передан OID соответствующего правила сортировки.

С точки зрения класса операторов результат *true* означает, что дедупликация безопасна (или безопасна для правила сортировки, чей OID был передан его функции *equalimage*). Однако основной код будет считать дедупликацию безопасной для индекса, только если **каждый** индексируемый столбец использует класс операторов, регистрирующий функцию *equalimage*, и все эти функции при вызове действительно возвращают *true*.

Равенство образов является условием, **почти** равнозначным простому битовому равенству. Есть лишь одно небольшое различие: при индексировании типа данных *valerna* представление двух равных образов на диске может отличаться в битовом отношении вследствие несогласованного применения сжатия TOAST к входным данным. Строго говоря, когда функция *equalimage* класса операторов возвращает *true*, безопасно предположить, что функция на С *datum_image_eq()* всегда будет согласована с функцией *order* класса операторов (при условии, что обеим функциям передан одинаковый OID правила сортировки).

Основной код совершенно не способен сделать какие-либо выводы относительно статуса класса операторов «равенство подразумевает равенство образов» в семействе операторов для множества типов данных на основе сведений о других классах операторов в том же семействе. Также семейству операторов нет смысла регистрировать межтиповую функцию *equalimage*, а попытка сделать это приведет к ошибке. Причина этого в том, что статус «равенство подразумевает равенство образов» зависит не только от семантик сортировки/равенства, которые более или менее определены на уровне семейства операторов. В целом, эти семантики, которые реализует один конкретный тип данных, должны рассматриваться по отдельности.

Для классов операторов, включенных в базовый продукт QHB, принято соглашение регистрировать стандартную универсальную функцию *equalimage*. Большинство классов операторов регистрирует функцию *btequalimage()*, которая указывает, что дедупликация безопасна без каких-либо условий. Классы операторов для сортируемых типов данных, таких как *text*, регистрируют функцию *btvarstrequalimage()*, которая указывает, что дедупликация безопасна с детерминированными правилами сортировки. Для сохранения контроля в сторонних расширениях наилучшим решением будет регистрировать их собственные специальные функции *equalimage*.

options

Опционально семейство операторов В-дерево может предоставлять вспомогательные функции *options* («параметры класса операторов»), регистрируемые как вспомогательная функция номер 5. Эти функции определяют набор видимых пользователю параметров, которые управляют поведением класса операторов.

Вспомогательная функция *options* должна иметь сигнатуру

```sql
options(relopts local_relopts *) returns void
```

Эта функция передает указатель на структуру ***local_relopts***, в которую нужно внести набор параметров класса операторов. К этим параметрам можно обращаться из других вспомогательных функций при помощи макросов *PG_HAS_OPCLASS_OPTIONS()* и *PG_GET_OPCLASS_OPTIONS()*.

На данный момент ни у одного класса операторов В-дерево нет вспомогательной функции *options*. В отличие от GiST, SP-GiST, GIN и BRIN, В-дерево не допускает гибкое представление ключей. Так что, вероятно, в актуальном методе доступа к индексу В-дереву у функции *options* нет практического применения. Тем не менее, эта вспомогательная функция была добавлена в В-дерево в целях единообразия и, возможно, станет полезной при дальнейшем развитии реализации В-дерева в QHB.

Реализация

В этом разделе изложена подробная информация о реализации индекса В-дерева, которая может быть полезна для специалистов.

Структура В-дерева

В QHB индексы В-деревья — это многоуровневые древовидные структуры, где каждый уровень дерева можно использовать в качестве двусвязного списка страниц. Единственная метастраница хранится в фиксированном положении в начале первого файла сегмента индекса. Все остальные страницы делятся на листовые и внутренние. Листовые страницы находятся на самом нижнем уровне дерева. Все остальные уровни состоят из внутренних страниц. Каждая листовая страница содержит кортежи, которые указывают на строки таблицы. Каждая внутренняя страница содержит кортежи, которые указывают на следующий уровень вниз по дереву. Обычно листовые страницы составляют более 99% всех страниц. Как внутренние, так и листовые страницы используют стандартный формат страницы, описанный в разделе Внутренняя структура страницы базы данных.

Новые листовые страницы добавляются в индекс В-дерево, когда существующая листовая страница не может вместить поступающий кортеж. Операция разделения страницы освобождает место для элементов, которые изначально принадлежали переполнившейся странице, перенося какую-то их часть на новую страницу. Разделение страницы также вставляет в родительскую страницу новую ссылку вниз, на новую страницу, что, в свою очередь, может вызвать разделение родительской страницы. Страницы разделяются «каскадно вверх» рекурсивным образом. Когда же в итоге и корневая страница не может вместить новую ссылку вниз, выполняется операция разделения корневой страницы. Это добавляет к структуре дерева новый уровень путем создания новой корневой страницы, которая находится на уровень выше исходной корневой страницы.

Дедупликация

Дубликат — это кортеж листовой страницы (кортеж, который указывает на строку таблицы), в котором все ключевые столбцы индекса содержат значения, совпадающие с соответствующими значениями столбца из по крайней мере еще одного кортежа листовой страницы в том же индексе. На практике дублирующиеся кортежи встречаются довольно часто. Индексы В-деревья могут использовать для дубликатов особое экономящее пространство представление при включении факультативного механизма — дедупликации.

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

Примечание
Дедупликация В-дерева также эффективна при работе с «дубликатами», которые содержат значение NULL, несмотря на то, что значения NULL, согласно операторам = из любого класса операторов В-дерева, не считаются равными друг другу. С точки зрения любой части реализации, которая понимает дисковую структуру В-дерева, NULL — это просто еще одно значение из домена значений индекса.

Процесс дедупликации запускается только по необходимости, когда вставляется новый элемент, который не помещается на существующую листовую страницу. Это предотвращает (или, по крайней мере, откладывает) разделение листовой страницы. В отличие от кортежей со списком идентификаторов GIN, в B-дереве этим кортежам не нужно расширяться всякий раз, когда вставляется новый дубликат; они просто являются альтернативным физическим представлением исходного логического содержимого листовой страницы. При смешанной нагрузке типа чтение/запись такая модель ставит во главу угла согласованную производительность. Для большинства клиентских приложений дедупликация должна обеспечить как минимум удовлетворительное увеличение производительности. По умолчанию она включена.

Команды CREATE INDEX и REINDEX используют дедупликацию, чтобы создать кортежи со списком идентификаторов, хотя применяемая ими стратегия слегка отличается. Каждая группа обычных дублирующихся кортежей, обнаруженных во взятых из таблицы отсортированных входных данных, объединяется в кортеж со списком идентификаторов до того, как добавляется на текущую ожидающую листовую страницу. В каждый такой кортеж упаковывается максимально возможное количество идентификаторов TID. Листовые страницы записываются обычным способом, без каких-либо отдельных проходов для исключения дубликатов. Эта стратегия очень подходит командам CREATE INDEX и REINDEX, поскольку они относятся к разовым групповым операциям.

При рабочих нагрузках с преобладанием записи, которые не получают преимуществ от дедупликации из-за малого количества или отсутствия дублированных значений в индексах, ее применение приведет к небольшому постоянному снижению производительности (если только она явно не отключена). Чтобы отключить дедупликацию в отдельных индексах, можно воспользоваться параметром хранения deduplicate_items. При рабочих нагрузках только на чтение производственных издержек не возникает, поскольку кортежи со списком идентификаторов читаются по меньшей мере так же эффективно, как и кортежи в стандартном представлении. Поэтому обычно в таких случаях отключать дедупликацию бесполезно.

Непосредственно индексы В-деревья не учитывают, что в среде MVCC может быть несколько сохранившихся версий одной логической строки таблицы; для индекса каждый кортеж является независимым объектом, которому требуется отдельный элемент индекса. Иногда «версионные дубликаты» могут накапливаться и негативно влиять на время отклика и скорость обработки запросов. Обычно это случается при нагрузках с преобладанием UPDATE, где большая часть отдельных обновлений не может применить оптимизацию HOT (обычно вследствие того, что как минимум один столбец индекса подвергается изменению, требующему новый набор версий индексного кортежа — по одному новому кортежу для каждого индекса). В действительности дедупликация В-дерева устраняет разбухание индекса, вызванное тиражированием версий. Обратите внимание, что из-за тиражирования версий даже кортежи уникального индекса не обязательно физически уникальны при хранении на диске. К уникальным индексам оптимизация путем дедупликации применяется выборочно и нацеливается на те страницы, на которых могут содержаться версионные дубликаты. Глобальная же цель — дать команде VACUUM больше времени на выполнение, прежде чем из-за тиражирования версий произойдет «ненужное» разделение страницы.

Совет
Для определения того, следует ли дедупликации выполняться в уникальном индексе, применяется специальный эвристический алгоритм. Зачастую он может перейти напрямую к разделению листовой страницы без расходов на лишние циклы холостых проходов дедупликации. Если вас беспокоят издержки на бесполезную дедупликацию, можете выборочно задать значение deduplicate_items = off для отдельных индексов. В уникальных же индексах дедупликацию вполне можно оставить включенной — потери от этого будут невелики.

Из-за ограничений на уровне реализации дедупликацию можно использовать не везде. Безопасность ее применения определяется при выполнении команды CREATE INDEX или REINDEX.

Обратите внимание, что дедупликация считается небезопасной и не может применяться в следующих случаях, при которых имеются значительные семантические различия между равными значениями:

  • Дедупликация не может применяться с типами text, varchar и char при использовании недетерменированного правила сортировки, т. к. среди равных значений должны сохраняться различия в регистре и диакритических знаках.

  • Дедупликация не может применяться с типом numeric, т. к. среди равных значений должен сохраняться масштаб числового отображения.

  • Дедупликация не может применяться с типом jsonb, т. к. внутри класса операторов В-дерева jsonb используется тип numeric.

  • Дедупликация не может применяться с типами float4 и float8, т. к. у этих типов разное представление для значений -0 и 0, которые при этом все равно считаются равными. Это различие должно быть сохранено.

Есть еще одно дополнительное ограничение на уровне реализации, которое может быть убрано в будущих версиях QHB:

  • Дедупликация не может применяться с типами-контейнерами (такими как составные или диапазонные типы или массивы).

Есть еще одно дополнительное ограничение на уровне реализации, которое действует независимо от используемого класса оператора или правила сортировки:

  • Дедупликация не может применяться в индексах с INCLUDE.

Индексы GiST

Введение

GiST означает "обобщенное поисковое дерево" (Generalized Search Tree). Это сбалансированный, древовидный метод доступа, который работает как основа для произвольной схемы индексирования. B-деревья, R-деревья и многие другие схемы индексирования могут быть реализованы с помощью GiST.

Одним из преимуществ GiST является то, что он позволяет разрабатывать пользовательские типы данных вместе с соответствующими методами доступа эксперту предметной области без участия специалиста по базам данных.

Встроенные классы операторов

Основной дистрибутив QHB включает классы операторов GiST, показанные в следующей таблице:

ИмяИндексируемый тип данныхИндексируемые операторыОператоры сортировки
box_opsbox&& &> &< &<| >> << <<| <@ @> @ |&> | >> ~ ~=<->
circle_opscirc&& &> &< &<| >> << <<| <@ @> @ |&> | >> ~ ~=<->
inet_opsinet, cidr&& >> >>= > >= <> << <<= < <= =
point_opspoint>> >^ << <@ <@ <@ <^ ~=<->
poly_opspolygon&& &> &< &<| >> << <<| <@ @> @ |&> | >> ~ ~=<->
range_opsлюбой диапазонный тип&& &> &< >> << <@ -|- = @> @>
tsquery_opstsquery<@ @>
tsvector_opstsvector@@

По историческим причинам, класс операторов inet_ops не является классом по умолчанию для типов inet и cidr. Чтобы использовать его, указывайте имя класса явно в CREATE INDEX, например

CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);

Расширяемость

Традиционно, внедрение нового метода доступа к индексам означало много сложной работы. Необходимо было разобраться во внутренней работе базы данных: принципах работы менеджера блокировок и журнала упреждающей записи. Интерфейс GiST имеет высокий уровень абстракции, требующий от автора метода доступа только реализации семантики типа данных, к которому происходит обращение. Сам слой GiST заботится о параллелизме, журналировании и поиске в древовидной структуре.

Эту расширяемость не следует путать с расширяемостью других стандартных поисковых деревьев в плане типов данных, которые они могут обрабатывать. Например, QHB поддерживает расширяемые B-деревья и хэш-индексы. Это означает, что в QHB вы можете построить B-дерево или хэш-индекс над любым типом данных, каким захотите. Но B-деревья поддерживают только предикаты диапазона (<, =, >), а хэш-индексы только равенства.

Таким образом, если вы проиндексируете, скажем, коллекцию изображений с помощью B-дерева, вы сможете делать только запросы вида “изображение А равно изображению Б?”, “изображение А меньше изображения Б?” и т.п. В зависимости от того, как вы определяете “равно”, “меньше” и “больше” в этом контексте, это может иметь какой-то смысл. Однако, используя индекс на основе GiST, вы можете поддержать запросы, специфичные для предметной области, например "найти все изображения лошадей” или "найти все засвеченные фотографии".

Все, что требуется для запуска метода доступа GiST, — это реализовать несколько пользовательских методов, которые определяют поведение ключей в дереве. Конечно, эти методы должны быть довольно причудливыми, чтобы поддерживать причудливые запросы, но для всех стандартных запросов (B-деревья, R-деревья и т. д.) они довольно прямолинейны. Короче говоря, GiST сочетает в себе расширяемость с универсальностью, переиспользованием кода и чистым интерфейсом.

Существует пять методов, которые должен предоставить класс оператора индекса для GiST, и четыре, которые являются необязательными. Корректность индекса обеспечивается правильной реализацией методов same, consistent и union, а эффективность (размер и скорость работы) определяется методами penalty и picksplit.

Два из опциональных методов: compress и decompress позволяют индексу хранить во внутренних (т.е. нелистовых) вершинах дерева данные другого типа, нежели индексируемый тип. Листья в любом случае будут хранить данные такого же типа, как и индексируемый столбец, а другие узлы могут хранить произвольную структуру (C-style-struct), но все же в рамках общих ограничений QHB на типы данных (про данные переменной длины см. varlena). Если тот тип данных, что будет лежать в промежуточных вершинах дерева, существует на уровне SQL, то можно задать ему свойство STORAGE в команде CREATE OPERATOR CLASS. На самом деле, можно иметь много разных типов содержимого вершин. Есть главный тип, в котором производятся все вычисления при построении и навигации индекса, а при сохранении в вершины индекса вызывается compress, и в compress можно преобразовать в любой бинарный формат, в том числе с потерей информации. Если compress нет, то главный тип и содержимое всех вершин совпадает с индексируемым типом.

Необязательный метод №8 — distance, который нужен, если класс операторов хочет поддержать упорядоченные сканирования (поиск N ближайших соседей). Девятый метод fetch нужен, если класс операторов хочет поддерживать сканирование только по индексу, но при этом хранит альтернативный тип в промежуточных вершинах (использует compress/decompress).

  • consistent

    Для значения p в вершине индекса и поискового значения q, эта функция возвращает, является ли запрос q "согласованным" cо значением p. Для листовых записей индекса это эквивалентно проверке индексируемого условия, а для внутренних вершин дерева это определяет, необходимо ли сканировать соответствующее поддерево. Когда функция возвращает истину, она должна также заполнить флаг перепроверки recheck. Это указывает, является ли q безусловно подходящим или только возможно подходящим. Выставьте recheck = false, когда индекс полностью проверил условие предиката, а если recheck = true, то эта строка является только кандидатом на совпадение. В этом случае система будет перепроверять запрос на значении, взятом из строки. Такое соглашение позволяет GiST поддерживать как точные (lossless) так и грубые (lossy) индексы.

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE OR REPLACE FUNCTION my_consistent(internal, data_type, smallint, oid, internal)
    RETURNS bool
    AS 'MODULE_PATHNAME'
    LANGUAGE C STRICT;
    

    Примерный шаблон реализации на C:

    PG_FUNCTION_INFO_V1(my_consistent);
    
    Datum
    my_consistent(PG_FUNCTION_ARGS)
    {
        GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
        data_type  *query = PG_GETARG_DATA_TYPE_P(1);
        StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
        /* Oid subtype = PG_GETARG_OID(3); */
        bool       *recheck = (bool *) PG_GETARG_POINTER(4);
        data_type  *key = DatumGetDataType(entry->key);
        bool        retval;
    
        /*
        * Рассчитать возвращаемое значение как функцию от стратегии, ключа и запроса.
        *
        * Используйте GIST_LEAF(entry) чтобы понять, в каком месте дерева вас вызвали.
        * Это очень полезно. Например, при реализации оператора =
        *  вы можете во внутренних вершинах проверять, что пересечение не пусто,
        *  а в листьях проверять на точное равенство.
        */
    
        *recheck = true;        /* ну или false, если проверка была 100%-ная */
    
        PG_RETURN_BOOL(retval);
    }
    

    Здесь, key является элементом в индексе, а query — значение, которое ищут. Параметр StrategyNumber указывает, какой из операторов вашего класса применяется: он соответствует номер одного из оператора из команды CREATE OPERATOR CLASS.

    В зависимости от того, какие операторы вы включили в класс, тип данных query может быть разными, в том числе разным для разных операторов. В любом случае, он будет соответствовать типу второго аргумента оператора, а первый аргумент оператора будет главного типа содержимого индекса. (Приведенная выше заготовка кода предполагает, что все запросы будет одинакового типа data_type; если это не так, то сначала надо узнать тип второго параметра оператора, а потом доставать значение из query.) В SQL-объявлении функции consistent рекомендуется указывать тип данных query совпадающим с индексируемым типом данных, даже если реально передаваемый тип данных может быть другим в зависимости от оператора.

  • union

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

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE OR REPLACE FUNCTION my_union(internal, internal)
    RETURNS storage_type
    AS 'MODULE_PATHNAME'
    LANGUAGE C STRICT;
    

    Примерный шаблон реализации на C:

    PG_FUNCTION_INFO_V1(my_union);
    
    Datum
    my_union(PG_FUNCTION_ARGS)
    {
        GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
        GISTENTRY  *ent = entryvec->vector;
        data_type  *out,
                *tmp,
                *old;
        int         numranges,
                    i = 0;
    
        numranges = entryvec->n;
        tmp = DatumGetDataType(ent[0].key);
        out = tmp;
    
        if (numranges == 1)
        {
            out = data_type_deep_copy(tmp);
    
            PG_RETURN_DATA_TYPE_P(out);
        }
    
        for (i = 1; i < numranges; i++)
        {
            old = out;
            tmp = DatumGetDataType(ent[i].key);
            out = my_union_implementation(out, tmp);
        }
    
        PG_RETURN_DATA_TYPE_P(out);
    }
    

    Как вы можете видеть, в этой заготовке мы считаем, что для нашего типа данных union (X, Y, Z) = union(union(X, Y), Z). Но поддержать типы данных, для которых это не так, тоже будет несложно.

    Результатом функции объединения должно быть значение главного типа содержимого индекса (как говорилось выше, он может отличаться или не отличаться от индексируемого типа). Функция union должна возвращать указатель на кусок памяти, выделенной с помощью palloc. Даже если результат совпадает с одним из входных аргументов, вы не может просто вернуть на него указатель, вы должны склонировать содержимое.

    Как видно из примера, первый внутренний (internal) аргумент функции union — это указатель GistEntryVector. Второй аргумент является указателем на целочисленную переменную, которую можно игнорировать.

  • compress

    Преобразует элемент данных в формат, подходящий для физического хранения на странице индекса. Если метод отсутствует, то элементы данных хранятся в индексе без изменений.

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE OR REPLACE FUNCTION my_compress(internal)
    RETURNS internal
    AS 'MODULE_PATHNAME'
    LANGUAGE C STRICT;
    

    Примерный шаблон реализации на C:

    PG_FUNCTION_INFO_V1(my_compress);
    
    Datum
    my_compress(PG_FUNCTION_ARGS)
    {
        GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
        GISTENTRY  *retval;
    
        if (entry->leafkey)
        {
            // заменить entry->key заархивированной версией
    
            compressed_data_type *compressed_data = palloc(sizeof(compressed_data_type));
    
            /* здесь заполнить *compressed_data на основании entry->key ... */
    
            retval = palloc(sizeof(GISTENTRY));
            gistentryinit(*retval, PointerGetDatum(compressed_data),
                        entry->rel, entry->page, entry->offset, FALSE);
        }
        else
        {
            // Как правило, для нелистовых вершин ничего не надо делать
            retval = entry;
        }
    
        PG_RETURN_POINTER(retval);
    }
    

    В этом коде нужно заменить compressed_data_type на индексируемый тип, так как содержимое листовых узлов должно быть такого типа.

  • decompress

    Преобразует сохраненное представление данных в главный тип содержимого, которым могут манипулировать другие методы GiST в классе операторов. Если метод декомпрессии отсутствует, предполагается, что другие методы GiST могут работать непосредственно на сохраненном формате данных. Декомпрессия не обязательно противоположна компрессии; в частности, если сжатие с потерями, для декомпрессии невозможно точно восстановить исходные данные. Поведение decompress и fetch также могут различаться т.к. первое возвращает главный тип содержимого индекса (например, тип первого аргумента consistent), а второе — индексируемый тип, и эти типы могут различаться.

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE OR REPLACE FUNCTION my_decompress(internal)
    RETURNS internal
    AS 'MODULE_PATHNAME'
    LANGUAGE C STRICT;
    

    Тривиальная реализация на C:

    PG_FUNCTION_INFO_V1(my_decompress);
    
    Datum
    my_decompress(PG_FUNCTION_ARGS)
    {
        PG_RETURN_POINTER(PG_GETARG_POINTER(0));
    }
    

    Приведенный выше пример подходит для случая, когда никакая декомпрессия не требуется. (Но в таком случае лучше, конечно, вообще отказаться от этого метода.)

  • penalty

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

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE OR REPLACE FUNCTION my_penalty(internal, internal, internal)
    RETURNS internal
    AS 'MODULE_PATHNAME'
    LANGUAGE C STRICT;  -- в некоторых случаях у вас получится нестрогая функция
    

    Шаблон реализации на C:

    PG_FUNCTION_INFO_V1(my_penalty);
    
    Datum
    my_penalty(PG_FUNCTION_ARGS)
    {
        GISTENTRY  *origentry = (GISTENTRY *) PG_GETARG_POINTER(0);
        GISTENTRY  *newentry = (GISTENTRY *) PG_GETARG_POINTER(1);
        float      *penalty = (float *) PG_GETARG_POINTER(2);
        data_type  *orig = DatumGetDataType(origentry->key);
        data_type  *new = DatumGetDataType(newentry->key);
    
        *penalty = my_penalty_implementation(orig, new);
        PG_RETURN_POINTER(penalty);
    }
    

    По историческим причинам, penalty не возвращает результат типа float, а кладет его по указателю, переданному третьим аргументом. А собственно возвращаемое значение функции игнорируется; но принято возвращать указатель на результат как в примере.

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

  • picksplit

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

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE OR REPLACE FUNCTION my_picksplit(internal, internal)
    RETURNS internal
    AS 'MODULE_PATHNAME'
    LANGUAGE C STRICT;
    

    Шаблон реализации на C:

    PG_FUNCTION_INFO_V1(my_picksplit);
    
    Datum
    my_picksplit(PG_FUNCTION_ARGS)
    {
        GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
        GIST_SPLITVEC *v = (GIST_SPLITVEC *) PG_GETARG_POINTER(1);
        OffsetNumber maxoff = entryvec->n - 1;
        GISTENTRY  *ent = entryvec->vector;
        int         i,
                    nbytes;
        OffsetNumber *left,
                *right;
        data_type  *tmp_union;
        data_type  *unionL;
        data_type  *unionR;
        GISTENTRY **raw_entryvec;
    
        maxoff = entryvec->n - 1;
        nbytes = (maxoff + 1) * sizeof(OffsetNumber);
    
        v->spl_left = (OffsetNumber *) palloc(nbytes);
        left = v->spl_left;
        v->spl_nleft = 0;
    
        v->spl_right = (OffsetNumber *) palloc(nbytes);
        right = v->spl_right;
        v->spl_nright = 0;
    
        unionL = NULL;
        unionR = NULL;
    
        /* Initialize the raw entry vector. */
        raw_entryvec = (GISTENTRY **) palloc(entryvec->n * sizeof(void *));
        for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i))
            raw_entryvec[i] = &(entryvec->vector[i]);
    
        for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i))
        {
            int         real_index = raw_entryvec[i] - entryvec->vector;
    
            tmp_union = DatumGetDataType(entryvec->vector[real_index].key);
            Assert(tmp_union != NULL);
    
            /*
            * Выбрать, куда положить элемент индекса и соответственно изменить unionL и unionR
            * Добавить его в v->spl_left, либо в v->spl_right, и счетчики тоже изменить соответственно.
            */
    
            if (my_choice_is_left(unionL, curl, unionR, curr))
            {
                if (unionL == NULL)
                    unionL = tmp_union;
                else
                    unionL = my_union_implementation(unionL, tmp_union);
    
                *left = real_index;
                ++left;
                ++(v->spl_nleft);
            }
            else
            {
                // то же самое для направо...
            }
        }
    
        v->spl_ldatum = DataTypeGetDatum(unionL);
        v->spl_rdatum = DataTypeGetDatum(unionR);
        PG_RETURN_POINTER(v);
    }
    

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

    Как и penalty, функция picksplit имеет решающее значение для хорошей работы индекса. Придумать, как будут работать penalty и picksplit — самое сложное в дизайне GiST-индекса.

  • same

    Возвращает true, если две записи индекса идентичны, в противном случае false.

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE OR REPLACE FUNCTION my_same(storage_type, storage_type, internal)
    RETURNS internal
    AS 'MODULE_PATHNAME'
    LANGUAGE C STRICT;
    

    Шаблон реализации на C:

    PG_FUNCTION_INFO_V1(my_same);
    
    Datum
    my_same(PG_FUNCTION_ARGS)
    {
        prefix_range *v1 = PG_GETARG_PREFIX_RANGE_P(0);
        prefix_range *v2 = PG_GETARG_PREFIX_RANGE_P(1);
        bool       *result = (bool *) PG_GETARG_POINTER(2);
    
        *result = my_eq(v1, v2);
        PG_RETURN_POINTER(result);
    }
    

    По историческим причинам, функция same не просто возвращает логический результат, а помещает его по указателю, переданному третьим аргументом. Возвращаемое значение функции игнорируется, но принято возвращать этот же указатель как показано в примере.

  • distance

    Для значения p в вершине индекса и поискового значения q, эта функция возвращает "расстояние" от q до p. Для листовой вершины это скорее всего точное расстояние между двумя точками, а для нелистовой вершины следует понимать это как расстояние от q до ближайшего элемента из поддерева (возможно, заниженное, но не завышенное). Эта функция должна быть предоставлена, если класс операторов содержит какие-либо операторы упорядочивания. Запрос, использующий оператор упорядочивания, будет реализован путем возврата записей индекса с наименьшими значениями "расстояния", поэтому работа distance должна быть согласованы с семантикой этого оператора.

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE OR REPLACE FUNCTION my_distance(internal, data_type, smallint, oid, internal)
    RETURNS float8
    AS 'MODULE_PATHNAME'
    LANGUAGE C STRICT;
    

    Примерный шаблон реализации на C:

    PG_FUNCTION_INFO_V1(my_distance);
    
    Datum
    my_distance(PG_FUNCTION_ARGS)
    {
        GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
        data_type  *query = PG_GETARG_DATA_TYPE_P(1);
        StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
        /* Oid subtype = PG_GETARG_OID(3); */
        /* bool *recheck = (bool *) PG_GETARG_POINTER(4); */
        data_type  *key = DatumGetDataType(entry->key);
        double      retval;
    
        /*
        * Вычисление результата как функции от стратегии(оператора), ключа и поискового значения
        */
    
        PG_RETURN_FLOAT8(retval);
    }
    

    Аргументы у distance такие же, как и у consistent.

    Допускаются неточности при вычислении расстояния, главное вернуть не больше, чем реальное расстояние. Например, в геометрических приложениях расстояние до группы обычно считают как расстояние до ограничивающего группу параллелипипеда. Для внутренней вершины дерева возвращаемое расстояние не должно быть больше расстояния до любого из дочерних вершин. Если возвращенное расстояние не является точным, функция должна установить *recheck в true. (Это не обязательно для внутренних вершин дерева; для них вычисление всегда предполагается неточным). В этом случае движок вычислит точное расстояние после извлечения строки из кучи и при необходимости пересортирует строки.

    Если функция расстояния хотя бы иногда возвращает *recheck = true, то тип (float8/float4) и масштаб значений должны быть такие же как и у исходного оператора упорядочивания, потому что будут сортировать результаты distance с результатами оператора вперемешку. А если всегда *recheck = false, то результаты distance могут быть любыми float8 значениями, несогласованными с оператором, т.к. их будут сравнивать только между собой. (Бесконечность и минус бесконечность зарезервированы для обработки специальных случаев, таких как NULL, поэтому не надо возвращать такие значения).

  • fetch

    Преобразует сжатое индексное представление элемента данных в индексируемый тип данных для сканирования только по индексу. Возвращаемые данные должны быть точной копией исходного индексированного значения без искажений.

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE OR REPLACE FUNCTION my_fetch(internal)
    RETURNS internal
    AS 'MODULE_PATHNAME'
    LANGUAGE C STRICT;
    

    Аргумент является указателем на структуру GISTENTRY. На входе, поле key — содержимое листа дерева в сжатом виде (т.е. результат работы compress), NOT NULL. Возвращаемое значение — другая GISTENTRY, чье поле key содержит те же данные в исходном, несжатом виде. Если функция compress класса операторов предостаелена, но ничего не делает для листовых записей, то метод fetch может возвращать аргумент без изменений. Если же функции compress вообще нет, тогда и fetch не нужна.

    Шаблон реализации на C:

    PG_FUNCTION_INFO_V1(my_fetch);
    
    Datum
    my_fetch(PG_FUNCTION_ARGS)
    {
        GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
        input_data_type *in = DatumGetPointer(entry->key);
        fetched_data_type *fetched_data;
        GISTENTRY  *retval;
    
        retval = palloc(sizeof(GISTENTRY));
        fetched_data = palloc(sizeof(fetched_data_type));
    
        /*
        * Собственно преобразовать fetched_data в Datum исходного индексируемого типа
        */
    
        /* fill *retval from fetched_data. */
        gistentryinit(*retval, PointerGetDatum(converted_datum),
                    entry->rel, entry->page, entry->offset, FALSE);
    
        PG_RETURN_POINTER(retval);
    }
    

    Если функция compress сохраняет данные в листах с искажением (с потерей информации), то класс оператора не может поддерживать сканирование только по индексу и не должен определять функцию fetch.

Все методы поддержки GiST обычно вызываются в короткоживущих контекстах памяти, то есть CurrentMemoryContext сбрасывается после обработки каждого кортежа. Это позволяет не беспокоиться о вызовах pfree. Однако в некоторых случаях хотелось бы иметь долгоживущий объект для кэширования данных при повторных вызовах. Чтобы сделать это, выделите память в контексте fcinfo->flinfo->fn_mcxt, и положите указатель в fcinfo->flinfo->fn_extra. Время жизни такого объекта — одна индексная операция (одно сканирование, одна вставка или построение индекса). Если вы заменяете fcinfo->flinfo->fn_extra, а там было ненулевое значение, то вы должны его освободить, иначе будут утечки.

Реализация

Построение больших индексов GiST путем простой последовательной вставки всех кортежей бывает медленным, потому что, когда индекс большой и вытесняется из памяти на диск, вставка очередного кортежа происходит в случайное место индекса и вызывает чтение/запись на диск случайных страниц. QHB поддерживает более эффективный, буферизованный метод построения индексов GiST, что может значительно уменьшить количество операций ввода-вывода при обработке неупорядоченного набора данных. Для упорядоченных наборов данных преимущество меньше или отсутствует, т.к. для них последовательная вставка тоже прекрасно работает (в каждый момент времени работа ведется только в нескольких страницах, которые влезают в память).

Однако для построения индекса с буферизацией будет больше вызовов penalty, т.е. больше нагрузка на процессор. Кроме того, требуется дополнительное временное пространство на диске, примерно равное итоговому размеру индекса. Также построение с буферизацией и без может приводить к разным по качеству индексам. Это зависит от реализации penalty, picksplit и операторов. Более качественный (сбалансированный) индекс может получиться как при одном, так и при другом алгоритме построения индекса.

Глобально поведение управляется параметром effective_cache_size: когда размер индекса превышает или явно собирается превысить effective_cache_size, построение GiST-индекса переключается на буферизованный метод. Также буферизацию можно включить/выключить явно для конкретного индекса. Для этого используется опция buffering команды CREATE INDEX. Поведение по умолчанию подходит для большинства случаев, но отключение буферизации может ускорить построение, если вам известно, что входные данные упорядочены.

Примеры

Исходный дистрибутив QHB содержит несколько примеров индексных методов, реализованных на основе GiST. В основной дистрибутив входит полнотекстовый поиск (типы tsvector и tsquery), а также R-Деревья для некоторых встроенных типов геометрических данных.

Следующие модули из contrib также содержат классы операторов GiST:

МодульОписание функционала
btree_gistФункциональность, эквивалентная B-дереву для некоторых типов данных
cubeИндексация многомерных кубов
hstoreМодуль для хранения пар (ключ, значение)
intarrayRD-дерево для одномерного массива int4
ltreeИндексирование древовидных путей
pg_trgmСтепень сходства текстов в метрике триграмм
segИндексирование интервалов действительных чисел

Индексы SP-GiST

Введение

SP-GiST — это сокращение от Space-partitioned GiST (обобщённое дерево по разбитому на партиции пространству). SP-GiST представляет собой дерево поиска, ветви которого не пересекаются (в отличие от GiST). В таком виде представимы многие несбалансированные структуры данных, в том числе деревья квадрантов, K-мерные деревья и префиксное деревья (Tries). Общая особенность этих структур заключается в том, что они многократно разбивают пространство поиска на непересекающиеся партиции, которые не обязательно должны быть одинакового размера. Поиск, хорошо соответствующий правилу разбиения, может быть очень быстрым.

Эти популярные структуры данных были первоначально разработаны для использования в оперативной памяти. В основной памяти они обычно проектируются как множество динамически выделенных узлов, связанных указателями. В отличие от B-Дерева у каждой вершины бывает всего по 2-4 дочерних, а высота дерева получается соответственно большая; большая высота — это проход большого количества вершин при поиске. Если хранить это прямо в таком виде на диске, то будет много чтений произвольного доступа. Для хранения на диске деревья должны наоборот сильно ветвиться. Основная задача, решаемая SP-GiST, заключается в размещении вершин поискового дерева на дисковых страницах таким образом, чтобы поиск требовал доступа только к нескольким дисковым страницам, даже если он проходит через много вершин.

Как и GiST, SP-GiST предназначена для разработки пользовательских типов данных с соответствующими методами доступа экспертом в прикладной области типа данных.

Встроенные классы операторов

Основной дистрибутив QHB включает классы операторов SP-GiST, показанные в таблице.

ИмяИндексированный Тип ДанныхИндексируемые ОператорыОператоры сортировки
kd_point_opspoint<< <@ <^ >> >^ ~=<->
quad_point_opspoint<< <@ <^ >> >^ ~=<->
range_opsлюбой диапазонный тип&& &< &> -|- << <@ = >> @>
box_opsbox<< &< && &> >> ~= @> <@ &<| <<| |>> |&><->
poly_opspolygon<< &< && &> >> ~= @> <@ &<| <<| |>> |&><->
text_opstext< <= = > >= ~<=~ ~<~ ~>=~ ~>~ ^@
inet_opsinet, cidr&& >> >>= > >= <> << <<= < <= =

Из двух классов операторов для типа point, quad_point_ops — это класс по умолчанию. kd_point_ops поддерживает те же операторы, но использует другую структуру данных индекса, которая может обеспечить лучшую производительность в некоторых приложениях.

Классы операторов quad_point_ops, kd_point_ops и poly_ops поддерживают оператор упорядочения <->, который позволяет выполнять поиск k ближайших соседей (k-NN search) среди проиндексированных точек или многоугольников.

Расширяемость

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

Листовые вершины SP-GiST-дерева содержат значения того же типа данных, что и индексируемый столбец. Однако они могут хранить значение не целиком, а только суффикс. В этом случае вспомогательные функции класса операторов должны уметь восстанавливать полное значение, собирая значения в родительских вершинах при проходе через них от корня к листу.

Содержимое внутренние вершин более сложное. Каждая внутренняя вершина содержит набор из одного или нескольких узлов (nodes), каждый из которых является ветвью дерева (= областью пространства). Узел содержит либо ссылку на внутреннюю вершину (если ветвь дерева большая), либо короткий список листьев, все из которых располагаются на одной странице индекса (если ветвь маленькая). Каждый узел обычно имеет метку (label) которая описывает его; например, в префиксном дереве метка узла может быть следующей буквой строкового значения. (Класс оператора может опустить метки узлов, если он работает с фиксированным набором узлов для всех внутренних вершин, см. SP-GiST без меток узлов).

Внутренняя вершина опционально может иметь префикс (prefix), описывающее все его члены. В префиксном дереве это может быть общий префикс представленных строк. Значение prefix не обязательно является действительно префиксом, но может быть любыми данными, необходимыми классу операторов; например, в дереве квадрантов в prefix хранится центральная точка, по которой происходит разбиение на квадранты. (А 4 узла соответствуют 4-ем областям пространства после разбиения.). Зачем существуют и префиксы, и метки? Метка нужна для того, чтобы решить, в какую из дочерних вершин пойти, не обращаясь к этим вершинам; метки хранятся в родительской вершине. Префикс наоборот хранится в самой вершине, и позволяет делать некоторые операции к ней, не обращаясь к родителю.

Некоторые алгоритмы работы с деревом требуют знания уровня ("глубины") текущей вершины, поэтому ядро SP-GiST предоставляет классам операторов возможность управлять подсчетом уровней при спуске по дереву. Как говорилось выше, есть поддержка восстановления значений по кусочкам, если это необходимо. И наконец, при спуске по дереву можно передавать дополнительный объект любого типа, он называется traverse value.

Примечание
Ядро SP-GiST берет на себя заботу о NULL-значения. Хотя индексы SP-GiST хранят записи для проиндексированных NULL'ов, это скрыто от кода класса оператора индекса: индексируемые NULL-значения никогда не будут переданы методам класса оператора, равно как и NULL-запросы. (Предполагается, что операторы SP-GiST являются строгими (STRICT), и NULL-значения заведомо не подходят под условия.) По этой причине NULL-значения больше не будут обсуждаться в этом разделе.

Существует пять пользовательских методов, которые должен предоставить класс оператора индекса для SP-GiST, и один необязательный. Все пять обязательных методов принимают 2 аргумента типа internal, это указатели на структуры, в первой расположены все входные значения, а во вторую надо поместить выходные значения. Возвращаемое значение из 4-ех методов void, а метод leaf_consistent возвращает bool. Методы не должны изменять какие-либо поля своих входных структур. Во всех случаях выходная структура инициализируется нулями перед вызовом пользовательского метода. Необязательный шестой метод compress принимает единственный аргумент datum — значение, которое индексируем, и возвращает его же в формате для хранения в листовой вершине.

Пять обязательных пользовательских методов:

  • config

    Возвращает статическую информацию о реализации индекса, включая OID'ы типа данных префикса и типа данных меток узлов.

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE FUNCTION my_config(internal, internal) RETURNS void ...
    

    Первый аргумент — это указатель на структуру spgConfigIn, содержащую входные данные для функции. Второй аргумент является указателем на структуру spgConfigOut, которую функция должна заполнить результирующими данными.

    typedef struct spgConfigIn
    {
        Oid         attType;        /* Индексируемый тип данных*/
    } spgConfigIn;
    
    typedef struct spgConfigOut
    {
        Oid         prefixType;     /* Какой будет тип данных префикса */
        Oid         labelType;      /* Какой будет тип данных метки */
        Oid         leafType;       /* Какой будет тип данных значений в листах */
        bool        canReturnData;  /* Умеет реконструировать значения */
        bool        longValuesOK;   /* Умеет обрабатывать длинные значения */
    } spgConfigOut;
    

    Значение attType будет вам интересно, если ваш класс операторов умеет работать с несколькими типами данных.

    Для классов операторов, которые не используют префиксы, prefixType следует задать VOIDOID. Аналогично, для классов операторов, которые не используют метки узлов, labelType следует задать VOIDOID. canReturnData должно быть установлено в true, если класс оператора способен восстанавливать исходное значение проиндексированного поля (для сканирования только по индексу). longValuesOK должно быть установлено true, только если attType имеет переменную длину, и класс оператора умеет нарезать длинные значения для размещения в нескольких узлах (см. Ограничения SP-GiST).

    leafType обычно используют равный attType (оставить leafType нулевым работает так же, но лучше явно выставьте leafType = attType). Если attType и leafType различаются, должен быть предоставлен необязательный метод compress. Метод compress переводит индексируемые значения из attType в leafType. Примечание: leaf_consistent и inner_consistent получают в качестве аргументов attType, leafType на них не влияет.

  • choose

    Решает, как именно будем вставлять новое значение в ветку дерева.

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE FUNCTION my_choose(internal, internal) RETURNS void ...
    

    Первый аргумент — это указатель на структуру spgConfigIn, содержащую входные данные для функции. Второй аргумент является указателем на структуру spgConfigOut, которую функция должна заполнить результирующими данными.

    typedef struct spgChooseIn
    {
        Datum       datum;          /* индексируемое значение */
        Datum       leafDatum;      /* данные для записи в лист (все или суффикс) */
        int         level;          /* текущая глубина в дереве, считая с 0 */
    
        /* Содержимое текущей вершины */
        bool        allTheSame;     /* вершина помечена all-the-same? */
        bool        hasPrefix;      /* у вершины есть prefix? */
        Datum       prefixDatum;    /* если да, то значение prefix */
        int         nNodes;         /* количество дочерних узлов */
        Datum      *nodeLabels;     /* label'ы дочерних узлов в виде линейного массива */
    } spgChooseIn;
    
    typedef enum spgChooseResultType
    {
        spgMatchNode = 1,           /* давайте спустимся в один из дочерних узлов */
        spgAddNode,                 /* давайте добавим новый дочерний узел этой вершине */
        spgSplitTuple               /* давайте разобьем текущую вершину (поменяет ее prefix) */
    } spgChooseResultType;
    
    typedef struct spgChooseOut
    {
        spgChooseResultType resultType;     /* тип действия, 3 варианта, см. выше */
        union
        {
            struct                  /* если тип spgMatchNode */
            {
                int         nodeN;      /* номер узла, в который пойдем (от 0) */
                int         levelAdd;   /* прибавить вот столько к глубине */
                Datum       restDatum;  /* новое значение leafDatum */
            }           matchNode;
            struct                  /* если тип spgAddNode */
            {
                Datum       nodeLabel;  /* label нового узла */
                int         nodeN;      /* позиция вставки (от 0) */
            }           addNode;
            struct                  /* если тип spgSplitTuple */
            {
                /* Информация для создания верхней из 2 вершин после разбиения */
                bool        prefixHasPrefix;    /* вершина будет иметь prefix? */
                Datum       prefixPrefixDatum;  /* если да, то значение prefix */
                int         prefixNNodes;       /* число дочерних узлов */
                Datum      *prefixNodeLabels;   /* их label'ы */
                int         childNodeN;         /* номер дочернего узла, куда класть вторую вершину,
                                                *   образующуюся при разбиении (от 0)
                                                */
    
                /* Информация для создания нижней из 2 вершин (делается из текущей вершины и получает ее содержимое) */
                bool        postfixHasPrefix;   /* вершина будет иметь prefix? */
                Datum       postfixPrefixDatum; /* если да, то значение prefix */
            }           splitTuple;
        }           result;
    } spgChooseOut;
    

    datum — это исходное значение типа attType, которое должно был быть вставлено в индекс. Если есть метод compress, то leafDatum — это значение типа leafType, которое сперва получают вызовом compress, а потом при спуске по дереву методы choose или picksplit меняют его; когда процесс вставки достигает конечной страницы, текущее значение параметра leafDatum это то, что будет сохранено во вновь созданной листовой вершине. Если нет метода compress, то leafDatum не меняется при спуске по дереву и совпадает с datum. level — текущая глубина в дереве (прибавляется не всегда на +1, а вы ее меняете по своему усмотрению), для корня дерева 0. allTheSame имеет значение true, если текущая внутренняя вершина помечена как содержащая несколько эквивалентных узлов (см. Внутренние вершины "all-the-same"). hasPrefix имеет значение true, если текущая внутренняя вершина содержит префикс; и если содержит, то prefixDatum является его значением. nNodes — это число дочерних узлов, содержащихся в вершине, а nodeLabels — это массив значений их меток, или NULL, если нет никаких меток.

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

    Если новое значение соответствует одному из существующих дочерних узлов, установите resultType равным spgMatchNode. Установите nodeN равным индексу (с нуля) этого узла в массиве узлов. levelAdd — сколько прибавить к глубине, например, если вашим методам глубина не интересна, то оставляйте ноль, и ее не будут считать. Если ваши методы реализуют хранение только суффикса, то в restDatum положите остаток суффикса, а если нет, то положите туда тоже, что и пришло (leafDatum).

    Если необходимо добавить новый дочерний узел, установите resultType равным spgAddNode. Положите в nodeLabel метку, которая будет использоваться для нового узла, а в nodeN индекс (от нуля), в который необходимо вставить узел в массив узлов. После добавления узла метод choose будет вызван снова для той же самой вершины, и на этот раз должен привести к результату spgMatchNode.

    Если новое значение не соответствует префиксу вершины, установите resultType равным spgSplitTuple. Это действие перемещает все существующие узлы в новую внутреннюю вершину более низкого уровня, а текущую вершину превращает в вершину, имеющую одну нисходящую ссылку, указывающую на новую вершину более низкого уровня. Установите prefixHasPrefix, чтобы указать, должна ли новая верхняя вершина иметь префикс, и если да, то заполните prefixPrefixDatum значением префикса. Это новое значение префикса должно быть менее строгим, чем исходное, достаточно нестрогим, чтобы новое значение подходило. Установите prefixNNodes равным числу узлов, необходимых в новой вершине, и сразу же задайте им всем метки: в prefixNodeLabels поместите указатель на массив, выделенный с помощью palloc, содержащий их метки, или, если метки не требуются, то prefixNodeLabels = NULL. Обратите внимание, что общий размер новой верхней вершины не должен превышать общего размера вершины, которую он замещает; это ограничивает длину нового префикса и новых меток. Задайте childNodeN — номер дочернего узла, соответствующего нижней вершине, образовавшейся при разбиении. Для нижнего узла нужно задать только postfixHasPrefix и postfixPrefixDatum. Сочетание префиксов двух вершин и, возможно, метки узла должно иметь такое же семантическое значение, что и префикс вершины до её разбиения, потому что нет возможности пойти в дочерние узлы и там что-то поправить. После того, как узел был разделен, то функция choose будет вызвана снова для верхней из двух вершин. Этот вызов скорее всего вернет результат spgAddNode: раз старая вершина не годилась для вставки, то после разбиения вы захотите создать ей сестринскую.

    Итого, вы можете строить дерево, либо добавляя детей в текущую вершину, либо вставляя вершину над текущей, и добавлять детей уже к ней. Перебалансировку сделать нельзя.

  • picksplit

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

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE FUNCTION my_picksplit(internal, internal) RETURNS void ...
    

    Первый аргумент — это указатель на структуру spgConfigIn, содержащую входные данные для функции. Второй аргумент является указателем на структуру spgConfigOut, которую функция должна заполнить результирующими данными.

    typedef struct spgPickSplitIn
    {
        int         nTuples;        /* кол-во листовых вершин */
        Datum      *datums;         /* их значения (линейный массив длины nTuples) */
        int         level;          /* текущая глубина (считая от 0) */
    } spgPickSplitIn;
    
    typedef struct spgPickSplitOut
    {
        bool        hasPrefix;      /* нужен ли prefix? */
        Datum       prefixDatum;    /* если да, то значение prefix'а */
    
        int         nNodes;         /* кол-во дочерних узлов */
        Datum      *nodeLabels;     /* их label'ы (или NULL, если не надо меток) */
    
        int        *mapTuplesToNodes;   /* раскладка листьев по дочерним узлам */
        Datum      *leafTupleDatums;    /* новые значения листовых узлов */
    } spgPickSplitOut;
    

    nTuples — количество листовых вершин, которые сейчас раскладываем. datums — это массив их значений типа leafType level это текущий уровень (глубина), одинаковый у всех листовых вершин, и у новой внутренней вершины, предположительно, будет такой же.

    Установите hasPrefix, чтобы указать, должна ли новая внутренняя вершина иметь префикс, и если да, то заполните prefixDatum значением префикса. Поместите в nNodes количество дочерних узлов у новой вершины, а в nodeLabels массив их меток или NULL, если метки узлов не требуются. Задайте, в какой узел поместить каждую из листовых вершин — mapTuplesToNodes, длина равна nTuples, каждый элемент — номер узла (от 0). Заполните leafTupleDatums значениями, хранимыми в листах (если вы не храните только суффиксы для компактности, это будет тоже самое, что и входные данные datums, но все равно надо сделать копию). Обратите внимание, что picksplit должна выделить память под nodeLabels, mapTuplesToNodes и leafTupleDatums с помощью palloc.

    Если во входных аргументах больше одного листа, то ожидается, что picksplit их как-то отклассифицирует и разделит на несколько узлов (picksplit вызывают с этой целью). Если picksplit поместит все предложенные листы в один узел, то ядро SP-GiST делает вывод, что они условно одинаковые, разделит их на узлы случайным образом (делить всё-таки надо из-за ограничения на количество дочерних листов). Эти несколько узлов все получат одинаковую метку (ту, которую вернула picksplit) и флаг allTheSame чтобы показать, что это произошло. Функции choose и inner_consistent должны проявлять надлежащую осторожность с такими внутренними вершинами. Дополнительную информацию смотрите в Внутренние вершины "all-the-same".

    Функция picksplit может быть запущена для 1 листа только в том случае, если функция config установила longValuesOK = true, и встретилось значение длиннее, чем влезает на страницу. В этом случае смысл операции состоит в том, чтобы побить значение на префикс (который сохранят во внутренней вершине) и суффикс, который сохранят в листе или, если он всё ещё слишком длинный, снова вызовут picksplit. Дополнительную информацию смотрите в Ограничения SP-GiST.

  • inner_consistent

    Возвращает набор ветвей, в которые следует сходить при поиске.

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE FUNCTION my_inner_consistent(internal, internal) RETURNS void ...
    

    Первый аргумент — это указатель на структуру spgConfigIn, содержащую входные данные для функции. Второй аргумент является указателем на структуру spgConfigOut, которую функция должна заполнить результирующими данными.

    typedef struct spgInnerConsistentIn
    {
        /* Свойства исходного поискового запроса */
        ScanKey     scankeys;       /* массив значений, которые ищем, и предикатов (операторов) */
        ScanKey     orderbys;       /* массив операторов упорядочивания и значений для них */
        int         nkeys;          /* длина массива scankeys */
        int         norderbys;      /* длина массива orderbys */
    
        /* Состояние алгоритма обхода дерева */
        Datum       reconstructedValue;     /* кумулятивный префикс родителей */
        void       *traversalValue; /* накопительный объект класса операторов */
        MemoryContext traversalMemoryContext;   /* контекст памяти, на котором аллокировать traversalValue */
        int         level;          /* текущая глубина (от 0) */
        bool        returnData;     /* нужно ли восстанавливать оригинальное значение? */
    
        /* Свойства текущей внутренней вершины */
        bool        allTheSame;     /* она помечена all-the-same? */
        bool        hasPrefix;      /* у нее есть prefix? */
        Datum       prefixDatum;    /* если да, то какой */
        int         nNodes;         /* количество дочерних узлов */
        Datum      *nodeLabels;     /* метки дочерних узлов (NULL, если нет меток) */
    } spgInnerConsistentIn;
    
    typedef struct spgInnerConsistentOut
    {
        int         nNodes;         /* в сколько узлов (веток) надо сходить */
        int        *nodeNumbers;    /* массив с номерами узлов длины nNodes */
        int        *levelAdds;      /* на сколько увеличить глубину при входе в узел (массив длины nNodes) */
        Datum      *reconstructedValues;    /* кумулятивный префикс при входе в узел (массив длины nNodes)*/
        void      **traversalValues;        /* накопительные объекты (массив длины nNodes) */
        double    **distances;      /* массив из nNodes массивов из norderbys расстояний до выбранных узлов */
    } spgInnerConsistentOut;
    

    Массив scankeys длины nkeys описывает условия поиска в индексе. Эти условия объединяются по И(AND) — только строки, удовлетворяющие всем из них считаются подходящими. (Обратите внимание, что возможен вариант nkeys = 0, и это означает, что условия нет, и нужны просто все записи индекса). Обычно inner_consistent волнуют только поля sk_strategy и sk_argument каждой записи массива scankeys, которые соответственно дают оператор и значение сравнения. В частности, нет необходимости проверять sk_flags на предмет, является ли значение сравнения NULL, потому что основной код SP-GiST отфильтрует такие условия. Массив orderbys длины norderbys описывает операторы упорядочивания (если таковые имеются) аналогичным образом.

    reconstructedValue — аккумулятор префикса, накапливаемый при проходе от корня (из префиксов вершин и, возможно, меток узлов). При заходе в корень это 0, а дальше зависит от выходных значений inner_consistent. Если ваш класс операторов не использует восстановление значений по префиксам внутренних вершин, то используйте 0 на всех уровнях. reconstructedValue всегда типа leafType.

    traversalValue — указатель на любой ваш объект, который вы вернули из inner_consistent при заходе в родителя; для корня traversalValue == NULL. traversalMemoryContext — это контекст памяти, в котором надо аллокировать элементы traversalValues (см. ниже). level — текущая глубина. returnData имеет значение true, если в рамках запроса требуются восстановленные данные (если config вернула !canReturnData, то их не затребуют). allTheSame — свойство текущей вершины, в этом случае все узлы имеют одинаковую метку (если таковая имеется), и поэтому либо все, либо ни один из них не соответствует запросу (см. Внутренние вершины "all-the-same" ). hasPrefix + prefixDatum — префикс текущей вершины. nNodes — это число дочерних узлов, а nodeLabels — их метки.

    В результате работы nNodes устанавливается в число дочерних узлов (ветвей дерева), которые надо посетить, потому что там могут быть результаты, подходящем под запрос. Часто это всего один узел, но потенциально может быть много, и это усложняет интерфейс: все выходные результаты являются массивами из nNodes элементов, индекс в которых — это номер узла среди рекомендуемых. nodeNumbers — номера рекомендуемых узлов среди дочерних узлов вершины. levelAdds — приращение глубины при спуске в данный узел; обычно это +1 для всех узлов, но вы можете решить, что какие-то узлы "особо заглубленные", например, на основании метки; если никакие методы вашего класса операторов не интересуется глубиной, то можете оставить указатель levelAdds нулевым. Если ваш класс операторов использует восстановление значений по префиксам внутренних вершин, то вам нужно заполнить reconstructedValues для передачи в дочерние вершины. Если происходит поиск ближайших соседей, заполните distances расстояниями до узлов, которые рекомендуете посетить (внешний массив по номеру узла, внутренний по номеру метрики из orderbys), если нет, то оставьте указатель distances нулевым; узлы с меньшим расстоянием будут посещены в первую очередь.

    Если вам нужно передать какую-то дополнительную информацию в методы вашего класса операторов, заполните traversalValues, иначе оставьте его нулевым. Учтите, что массивы nodeNumbers, levelAdds, distances, reconstructedValues, и traversalValues надо выделять palloc'ом в текущем контексте, а элементы traversalValues надо выделять в контексте аллокации traversalMemoryContext, при этом каждый элемент должен быть отдельной единицей аллокации (нельзя использовать 1 единицу аллокации и положить в массив traversalValues указатель на ее середину).

  • leaf_consistent

    Возвращает true, если листовая вершина удовлетворяет запросу.

    SQL-объявление функции должно выглядеть следующим образом:

    CREATE FUNCTION my_leaf_consistent(internal, internal) RETURNS bool ...
    

    Первый аргумент — это указатель на структуру spgConfigIn, содержащую входные данные для функции. Второй аргумент является указателем на структуру spgConfigOut, которую функция должна заполнить результирующими данными.

    typedef struct spgLeafConsistentIn
    {
        /* Свойства исходного поискового запроса */
        ScanKey     scankeys;       /* массив значений, которые ищем, и предикатов (операторов) */
        ScanKey     orderbys;       /* массив операторов упорядочивания и значений для них */
        int         nkeys;          /* длина массива scankeys */
        int         norderbys;      /* длина массива orderbys */
    
        /* Состояние алгоритма обхода дерева */
        Datum       reconstructedValue;     /* кумулятивный префикс родителей */
        void       *traversalValue; /* накопительный объект класса операторов */
        int         level;          /* текущая глубина (от 0) */
        bool        returnData;     /* нужно ли возвращать оригинальное значение столбца? */
    
        /* Свойства текущей листовой вершины */
        Datum       leafDatum;      /* данные, хранящиеся в листе */
    } spgLeafConsistentIn;
    
    typedef struct spgLeafConsistentOut
    {
        Datum       leafValue;        /* оригинальное значение столбца, если надо */
        bool        recheck;          /* true, если надо перепроверить условие по значению из строки кучи */
        bool        recheckDistances; /* true, если расстояние надо уточнить по значению из строки кучи */
        double     *distances;        /* массив из norderbys расстояний до данного листа  */
    } spgLeafConsistentOut;
    

    Массив scankeys длины nkeys описывает условия поиска в индексе. Эти условия объединяются по И(AND) — только строки, удовлетворяющие всем из них считаются подходящими. (Обратите внимание, что возможен вариант nkeys = 0, и это означает, что любой лист подходит). Обычно leaf_consistent волнуют только поля sk_strategy и sk_argument каждой записи массива scankeys, которые соответственно дают оператор и значение сравнения. В частности, нет необходимости проверять sk_flags на предмет, является ли значение сравнения NULL, потому что основной код SP-GiST отфильтрует такие условия. Массив orderbys длины norderbys описывает операторы упорядочивания (если таковые имеются) аналогичным образом.

    reconstructedValue — аккумулятор префикса, накопленный при проходе от корня. Может быть NULL, если префикс не собирали; имеет тип leafType. traversalValue указатель на любой ваш объект, который вы вернули из inner_consistent при заходе в родителя. level — глубина листа (если вы ее считали). returnData имеет значение true, если нужно восстановить исходные данные столбца (если config вернула !canReturnData, то их не затребуют). leafDatum — это значение в листовой вершине типа leafType.

    Функция должна возвращать true, если лист соответствует запросу, иначе false. Если результат true, и returnData true, тогда надо заполнить leafValue значением столбца (типа attType). Это может быть в точности leafDatum или комбинация из reconstructedValue и leafDatum. Также, recheck может быть установлено в true, если соответствие является не стопроцентным, и надо перепроверить условие на значении, взятом из строки кучи. Если выполняется упорядоченный поиск, заполните массив distances для массива значений расстояний в соответствии с массивом orderbys. В противном случае оставьте его NULL. Если хотя бы одно из возвращенных расстояний не является точным, установите recheckDistances в true; в этом случае исполнитель вычислит точные расстояния после извлечения строки из кучи и при необходимости переупорядочит строки.

Необязательный пользовательский метод:

  • compress

    Преобразует индексируемое значение (типа attType) в формат для физического хранения в листовой вершине индекса (типа leafType). Выходное значение не должно быть помещенным в TOAST.

    SQL-объявление функции может выглядеть следующим образом:

    CREATE FUNCTION my_compress(internal) RETURNS internal ...
    

Все вспомогательные методы SP-GiST обычно вызываются в контексте кратковременной памяти; то есть, CurrentMemoryContext сбрасывается после обработки каждой вершины, поэтому можно не освобождать память. Метод config является исключением: он должен стараться избегать утечки памяти. Но обычно методу config незачем выделять память.

Если к типу индексируемого столбца применимы правила сортировки, то действующее для индекса правило сортировки будет передано всем вспомогательным методам через стандартный механизм PG_GET_COLLATION().

Реализация

В этом разделе рассматриваются детали реализации и другие хитрости, которые могут быть полезны для разработчиков классов операторов SP-GiST.

Ограничения SP-GiST

Ограничения вытекают из того, что любая листовая или внутренняя вершина должна помещаться на одной странице индекса (по умолчанию 8кБ). При индексировании столбцов переменной длины это может вызывать ошибку, когда встретится конкретное особо длинное значение. Ваш класс операторов может поддержать хранение длинных значений, если устроит что-то вроде префиксного дерева: исходное значение столбца делится между всеми вершинами на пути от корня до листа, при этом каждый отдельный кусок достаточно короткий, чтобы влезть на страницу; для этого можно искусственно создать цепочку из вершин с единственным потомком. Если ваш класс операторов готов всё это делать, то верните longValuesOK из config.

Для промежуточных вершин это ограничение означает, что не может быть слишком много дочерних узлов, и у них не могут быть слишком длинные префиксы. Далее, если узел внутренней вершины указывает на набор листов, эти листы должны находиться на одной странице (такое проектное решение принято для экономии места и более кучного размещения ветви в дисковой странице). Если набор дочерних узлов становится слишком большим, то добавляется новая внутренняя вершина, а листы разбиваются на несколько узлов этой вершины. На этом шаге вызывается picksplit для набора листов, которая должна их разбить, иначе ядро SP-GiST прибегает к чрезвычайным мерам, описанным в Внутренние вершины "all-the-same".

SP-GiST без меток узлов

Некоторые древовидные алгоритмы используют фиксированный набор узлов для каждой внутренней вершины; например, в дереве квадрантов всегда есть ровно четыре узла, соответствующие четырем квадрантам вокруг центральной точки внутренней вершины. В таком случае код обычно работает с узлами по номеру, и нет необходимости в явных метках узлов. Чтобы подавить метки узлов (и тем самым сэкономить место), функция picksplit может возвращать значение NULL для массива nodeLabels, и аналогично функция choose может возвращать значение NULL для массива prefixNodeLabels во время действия spgSplitTuple. Это, в свою очередь, приведет к тому, что nodeLabels станет NULL во время последующих вызовов choose и inner_consistent. Теоретически можно иметь часть внутренних вершин с метками узлов, а часть без.

Если у внутренней вершины нет меток узлов, то метод choose не должен возвращать spgAddNode, поскольку предполагается, что набор узлов в таких случаях является фиксированным.

Внутренние вершины "all-the-same"

Ядро SP-GiST может переопределять результаты вызова функции picksplit, если picksplit не удается разделить предоставленные листовые значения по крайней мере на две категории узлов. Когда это происходит, создается новая внутрення вершина с несколькими узлами, каждый из которых имеет ту же метку, что picksplit дал одному узлу, который он вернул, а конечные значения делятся случайным образом между этими эквивалентными узлами. Во внутренней вершине устанавливается флаг allTheSame, чтобы предупредить функции choose и inner_consistent о том, что разбиение внутренней вершины на узлы не такое, как они ожидают.

При работе choose с allTheSame-вершиной результат выбора spgMatchNode интерпретируется как "вставить в любой из узлов", значение nodeN игнорируется. Выбор spgAddNode вообще не допустим и вызовет ошибку (т.к. получилось бы, что часть узлов "all-the-same", а новый узел особенный).

При работе inner_consistent с allTheSame-вершиной она должна выбирать все узлы или не одного (т.к. формально они все одинаковые). Для этого может потребоваться или не потребоваться какой-либо специальный код.

Индексы GIN

Введение

GIN расшифровывается как «Generalized Inverted Index» (Обобщённый инвертированный индекс). GIN предназначается для случаев, когда индексируемые значения являются составными, а запросы, на обработку которых рассчитан индекс, ищут по наличию элементов в этих составных объектах. Например, такими объектами могут быть документы, а запросы могут выполнять поиск документов, содержащих определённые слова.

В этом разделе мы используем термин объект или элемент, говоря о составном значении, которое индексируется, и термин ключ, говоря о включённом в него элементе. GIN всегда хранит и ищет ключи, а не объекты как таковые.

Индекс GIN хранит пары (ключ => набор-строк), где набор-строк содержит идентификаторы строк, в которых есть данный ключ. Один и тот же идентификатор строки может фигурировать в нескольких списках, так как объект может содержать больше одного ключа. Значение каждого ключа хранится только один раз, так что индекс GIN очень компактен в случаях, когда один ключ встречается много раз.

GIN является обобщённым в том смысле, что ядро GIN не знает о конкретных операциях, которые он ускоряет. Конкретная семантика определяется вспомогательными методами (стратегиями), которые управляют тем, как извлекаются ключи из индексируемых объектов и условий запросов, и как установить, действительно ли удовлетворяет запросу строка, содержащая некоторые значения ключей. Как и GiST, GIN позволяет разрабатывать дополнительные типы данных с соответствующими методами доступа экспертам в предметной области типа данных.

Встроенные классы операторов

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

ИмяИндексированный Тип ДанныхИндексируемые Операторы
array_opsanyarray&& = @<
jsonb_opsjsonb? ?& ? @> @? @@
jsonb_path_opsjsonb@> @? @@
tsvector_opstsvector@@ @@@

Из двух классов операторов для типа jsonb, jsonb_ops это значение по умолчанию. jsonb_path_ops поддерживает меньшее количество операторов, но обеспечивает лучшую производительность для этих операторов. Дополнительную информацию смотрите в разделе Индексация jsonb.

Расширяемость

Интерфейс GIN имеет высокий уровень абстракции, требующий от автора метода доступа только реализации семантики типа данных, с которым происходит работа. Слой GIN сам заботится о параллелизме, журналировании и поиске в древовидной структуре.

Все, что требуется для работы метода доступа GIN — это реализовать несколько пользовательских методов, которые определяют поведение ключей в дереве и отношения между ключами, индексируемыми элементами и индексируемыми запросами. Короче говоря, GIN сочетает расширяемость с универсальностью, повторным использованием кода и чистым интерфейсом.

Два метода, которые обязан предоставить класс операторов для GIN:

  • extractValue

    Прототип метода на C:

    Datum *extractValue(Datum itemValue, int32 *nkeys, bool **nullFlags)
    

    Возвращает массив ключей (выделенный с помощью palloc) индексируемого объекта. Количество возвращаемых ключей должно быть сохранено в *nkeys. Если какой-то из ключей может быть NULL, то необходимо также заполнить nullFlags (это массив типа bool длиной *nkeys, true означает, что этот ключ NULL; аллокировать тоже palloc'ом). Если все ключи не NULL, то можно оставить *nullFlags нулевым. Возвращаемое значение тоже может быть нулевым, если объект вообще не содержит ключей.

  • extractQuery

    Прототип метода на C:

    Datum *extractQuery(Datum query, int32 *nkeys, StrategyNumber n, bool **pmatch,
                        Pointer **extra_data, bool **nullFlags, int32 *searchMode)
    

    Возвращает массив ключей (выделенный с помощью palloc) для запроса. query — это правый аргумент индексируемого оператора, где левый аргумент — это индексируемый столбец. Аргумент n задаёт номер стратегии оператора в классе операторов (см. раздел Стратегии методов индексов). Разные операторы могут иметь разный тип правой части, и, соответственно, query может быть разного типа — и даже при одинаковом типе запрос может интерпретироваться по-разному для разных операторов. Для различения и нужен параметр n.

    Количество возвращаемых ключей должно быть записано в *nkeys. Если какой-то из ключей может быть NULL, то необходимо также заполнить nullFlags (это массив типа bool длиной *nkeys, true означает, что этот ключ NULL; аллокировать тоже palloc'ом). Если все ключи не NULL, то можно оставить *nullFlags нулевым. Возвращаемое значение тоже может быть нулевым, если объект вообще не содержит ключей.

    Выходной аргумент searchMode позволяет функции extractQuery выбрать режим, в котором должен выполняться поиск. Если *searchMode установить в GIN_SEARCH_MODE_DEFAULT (это значение уже установлено перед вызовом), подходящими кандидатами считаются объекты, которые соответствуют хотя бы одному из возвращённых ключей. Если *searchMode установить в GIN_SEARCH_MODE_INCLUDE_EMPTY, то в дополнение к объектам с минимум одним совпадением ключа, подходящими кандидатами будут считаться и объекты, вообще не содержащие ключей. (Этот режим полезен для реализации, например, оператора является-подмножеством). Если *searchMode установить в GIN_SEARCH_MODE_ALL, подходящими кандидатами считаются все отличные от NULL объекты в индексе, независимо от того, встречаются ли в них возвращаемые ключи. (Этот режим намного медленнее других, так как он по сути требует сканирования всего индекса, но он может быть необходим для корректной обработки крайних случаев. Оператор, который часто выбирает этот режим, скорее всего не подходит для реализации в классе операторов GIN).

    Выходной аргумент pmatch используется, когда поддерживаются частичные ключи в запросах. Выделите массив из *nkeys элементов типа bool и задайте значение true для тех ключей, для которых выделенный ключ является частичным. Если *pmatch нулевой (а он нулевой перед вызовом), GIN полагает, что все ключи точные, а не частичные.

    Выходной аргумент extra_data используется, чтобы привязать к каждому ключу дополнительную информацию, которая будет передана в методы consistent и comparePartial. Поместите в *extra_data массив из из *nkeys указателей на объекты производного типа. Перед вызовом указатель *extra_data нулевой, поэтому просто игнорируйте его, если не надо никакой дополнительной информации. Если массив *extra_data задан, то он передаётся в метод consistent целиком, а в comparePartial передаётся один соответствующий элемент.

Класс операторов должен также предоставить функцию для проверки, соответствует ли индексированный объект запросу. Поддерживаются две её вариации: булевская consistent и triConsistent с трехзначным результатом. Если предоставлена только consistent, то некоторые оптимизации, построенные на отбраковывании объектов до выборки всех ключей, отключаются. Если предоставлена только triConsistent, то она будет использоваться и вместо consistent, т.к. является более общей. Однако, если вычисление булевской вариации дешевле, то имеет смысл реализовать обе.

  • consistent

    Прототип метода на C:

    bool consistent(bool check[], StrategyNumber n, Datum query, int32 nkeys,
                    Pointer extra_data[], bool *recheck, Datum queryKeys[], bool nullFlags[])
    

    Возвращает true, если индексированный элемент удовлетворяет оператору запроса с номером стратегии n (или "возможно удовлетворяет", если дополнительно ставится флаг перепроверки). Проиндексированное значение в этом методе не доступно (GIN их нигде не хранит), есть только информация, какие из ключей запроса встретились в данном кортеже индекса. В метод передаётся всё, что вернула extractQuery при разборе запроса: массив ключей queryKeys, какие из них NULL — nullFlags, дополнительную информацию extra_data, но главное — какие из этих ключей присутствуют в текущем кортеже индекса: check. Все эти массивы имеют длину nkeys, если ненулевые. На всякий случай передаётся и оригинальный запрос query.

    Если extractQuery выдала NULL-ключ, и в кортеже тоже есть NULL-ключ, то считается, что ключ найден (что является нетипичным при обработке NULL-значений, и похоже на семантику IS NOT DISTINCT FROM). Если с точки зрения вашего класса операторов это особенная ситуация, то, встретив check[i] == true, вы можете проверить nullFlags[i], чтобы понять, было ли это попадание в NULL-ключ. В случае положительного результата флаг *recheck следует оставить false, если вы уверены, что строка подходит, или выставить true, чтобы оператор перепроверили на данных, взятых из строки таблицы из кучи.

  • triConsistent

    Прототип метода на C:

    GinTernaryValue triConsistent(GinTernaryValue check[], StrategyNumber n, Datum query,
                                int32 nkeys, Pointer extra_data[], Datum queryKeys[],
                                bool nullFlags[])
    

    Метод похож на consistent, но результат и входной аргумент check имеют тип GinTernaryValue, который состоит из 3-х вариантов: GIN_TRUE, GIN_FALSE и GIN_MAYBE. GIN_MAYBE означает "может быть подходит". Вы должны руководствоваться общими правилами тернарной логики: возвращать GIN_TRUE, только если строка подходит под запрос в предположении "худшего случая" для ключей, которые GIN_MAYBE, т.е. предполагая, что они все не подходят. И наоборот, возвращать GIN_FALSE, только если строка не подходит, даже в "лучшем случае" для GIN_MAYBE-ключей.

    Выходного параметра recheck нет, вместо этого результат GIN_MAYBE трактуется как "надо перепроверить", а GIN_TRUE как "точно подходит".

Далее, ядру GIN нужно уметь сортировать ключи. Вы можете предоставить функцию сортировки compare. Если вы этого не сделаете, то GIN будет использовать сортировку по умолчанию для соответствующего типа данных.

  • compare

    Прототип метода на C:

    int compare(Datum a, Datum b)
    

    Сравнивать два ключа (а не исходных индексируемых значения) и вернуть целое число < 0/0/> 0, когда первый ключ меньше/равен/больше второго. NULL-ключи никогда не передаются в эту функцию.

И ещё один необязательный метод:

  • comparePartial

    Прототип метода на C:

    int comparePartial(Datum partial_key, Datum key, StrategyNumber n, Pointer extra_data)
    

    Осуществляет проверку соответствия ключа индекса частичному ключу запроса. Результат 0 означает, что ключ подходит; меньше нуля — не подходит, но следует продолжить сканировать ключи; больше нуля — следует прекратить сканирование набора ключей, т.к. все остальные точно не подходят. NULL-ключи никогда не передаются в эту функцию.

    Частичный ключ не обязательно является префиксом ключа, но так или иначе согласован с порядком сортировки, что позволяет выполнять не полное сканирование всех ключей, а только сканирование диапазона. Зная порядок сканирования ключей, вы можете прекратить сканирование досрочно, вернув положительное значение.

    Для поддержки нечётких запросов класс операторов должен предоставлять comparePartial, а extractQuery должен выставлять флаг pmatch для частичных ключей. Дополнительную информацию смотрите в разделе Алгоритм нечёткого поиска.

Фактические типы переменных типа Datum, указанных выше, варьируются для разных классов операторов. Входное значение extractValue всегда имеет тип индексируемого столбца, а значения всех ключей имеют тип STORAGE класса операторов. Параметр query, передаваемый в extractQuery, consistent, triConsistent, будет такого типа, как правый аргумент поискового оператора; это не обязательно такой же тип, как у индексируемого столбца, главное, чтобы из него можно было извлечь ключи для поиска. Однако в SQL-объявлениях функций extractQuery, consistent, triConsistent рекомендуется указывать тип query совпадающим с типом столбца, если для разных операторов разный тип правой части.

Реализация

Внутри индекс GIN содержит B-дерево, построенное по ключам, где каждый ключ является элементом одного или нескольких проиндексированных объектов (например, элемент массива), и где каждая листовая вершина содержит либо массив("posting list") указателей на строки в куче (если этот массив достаточно маленький, чтобы поместиться в вершине индекса), либо указатель на B-дерево указателей кучи ("posting tree", “дерево рассылки”). На рис. 1 показаны эти компоненты индекса GIN.

В индекс может быть явно включён ключ со значением NULL. Кроме того, ключ NULL считается содержащимся во всех строках, в которых индексируемый столбец имеет значение NULL, либо если ExtractValue не выделила из значения ни одного ключа. Это сделано, чтобы можно было искать пустые значения.

Многоколоночные индексы GIN реализуются путем построения единого B-дерева, значения которого — пары (номер столбца, ключ), причём тип ключа может быть разным для разных столбцов.

Рисунок 1. Внутренняя структура GIN

Компоненты GIN

Быстрое обновление GIN

Обновление индекса GIN, как правило, происходит медленно, и это неотъемлемое свойство инвертированных индексов: вставка или обновление одной строки таблицы может вызвать множество вставок в индекс (по одной для каждого ключа, извлеченного из индексируемого элемента). Ядро GIN способно отложить большую часть этой работы, вставляя новые кортежи во временный, несортированный список ожидающих записей. Когда происходит VACUUM или VACUUM ANALYZE, или когда явно вызывается функция gin_clean_pending_list, или когда длина списка ожидания превышает значение параметра gin_pending_list_limit, все кортежи из списка ожидания помещаются в основное дерево, используя метод групповой вставки, такой же как и при первичном построении индекса. Это значительно увеличивает скорость обновления индекса GIN, даже считая дополнительные накладные расходы на вакуум. Кроме того, перестроение индекса можно вынести в отдельный фоновый процесс, чтобы избежать подвисания клиента во время вставки.

Главным недостатком этого подхода является замедление поиска. Кроме обычного поиска в индексе требуется также сканировать список ожидания, поэтому большой список ожидания значительно замедлит поиск. Еще один недостаток заключается в том, что, хотя амортизированное время модификации снижается, некоторое конкретное обновление может вызывать переполнение списка ожидания и подвиснуть надолго. Обе проблемы можно свести к минимуму, правильно настроив автовакуум, или вовремя запуская gin_clean_pending_list из другого служебного процесса.

Если максимальное время модификации важнее, чем среднее, использование отложенных записей можно отключить, изменив параметр fastupdate индекса GIN. Дополнительные сведения см. в разделе Параметры хранения индекса.

Алгоритм нечёткого поиска

GIN может поддерживать запросы "нечёткого поиска", в которых вместо конкретного ключа указывается диапазон ключей для поиска. Диапазон всегда согласованный с порядком сортировки ключей (определяемым методом compare, либо сортировкой по умолчанию для типа ключа), и, желательно, чтобы он не был слишком широким. Как бы ни звучало указание нечёткого поиска в исходном запросе, extractQuery должна установить флаг pmatch, а в значение ключа поместить нижнюю границу диапазона ключей. Одновременно это должно быть поддержано методом comparePartial, который должен определить верхнюю границу диапазона поиска (например, её можно передать через extra_data). Дерево ключей сканируется начиная с нижней границы, и пока comparePartial не вернёт > 0; все ключи, отобранные comparePartial, считаются подходящими.

Пример для поиска ключа по префиксу очень понятный: нижняя граница диапазона ключей совпадает с префиксом, метод comparePartial получает partial_key, равный этому префиксу, и его реализация тривиальна. В общем случае нижняя и верхняя граница, и условие поиска — разные вещи, и надо как-то их все передать в comparePartial. Например, поиск слов, получающихся из 'спорт' перестановкой двух соседних букв, может потребовать сканирования диапазона ['опрст'; 'тсрпо'] или ['о'; 'у').

GIN советы и хитрости

Создание или вставка

Если вы собираетесь вставить много данных, рассмотрите вариант удаления индекса и построения его с нуля. Этот совет верен для большинства индексов, но вставка в GIN-индекс может быть особенно медленной. См. также раздел Быстрое обновление GIN.

maintenance_work_mem

Время построения GIN-индекса сильно зависит от параметра maintenance_work_mem; рассмотрите вариант увеличения этого параметра в сессии, где выполняется создание индекса.

gin_pending_list_limit

Как уже было сказано в разделе Быстрое обновление GIN, параметр gin_pending_list_limit является основным средством настройки производительности GIN-индекса (вторым по значимости средством является частота запуска автовакуума). Параметр gin_pending_list_limit можно задать отдельно для каждого индекса, изменив его параметры хранения. Общее правило, что для редко меняющихся таблиц gin_pending_list_limit должен быть меньше.

gin_fuzzy_search_limit

Основной целью разработки GIN-индексов был полнотекстовый поиск. Полнотекстовый поиск часто возвращает огромное количество строк, особенно если искать частые слова. Прочитать такое количество строк с диска и отсортировать (отранжировать) их в памяти обычно неприемлемо в нагруженной системе. Чтобы облегчить контроль таких запросов, GIN имеет настраиваемый верхний предел на количество возвращаемых строк: параметр gin_fuzzy_search_limit. По умолчанию он установлен в 0 (то есть без ограничений). Если задано ненулевое ограничение, то все результаты поиска после заданного количества молча отбрасываются. Ограничение на количество результатов является "мягким" в том смысле, что фактическое число возвращаемых результатов может несколько отличаться от указанного предела в зависимости от запроса и качества генератора случайных чисел системы.

По опыту, значения в тысячах (например, 5000 — 20000) работают хорошо.

Ограничения

GIN предполагает, что индексируемые операторы являются строгими(STRICT). Это означает, что если значение столбца NULL, то extractValue не вызывается (но создаётся запись индекса, что эта строка содержит NULL-ключ); если поисковое значение NULL, то extractQuery не вызывается и результат поиска считается пустым. Однако запрос может содержать в себе что-то, что класс операторов истолкует как поиск NULL-ключа, например, пустую строку.

Примеры

Основной дистрибутив QHB включает классы операторов GIN, ранее перечисленные в таблице в разделе Встроенные классы операторов. Следующие расширения из contrib также содержат классы операторов GIN:

  • btree_gin

    Функциональность B-дерева для некоторых типов данных

  • hstore

    Расширение для хранения пар (ключ, значение)

  • intarray

    Расширенная поддержка для int[ ]

  • pg_trgm

    Сходство текста, основанное на сопоставлении триграмм

Индексы BRIN

Введение

BRIN обозначает индекс диапазона блоков (Block Range Index). BRIN предназначен для обработки очень больших таблиц, в которых некоторые столбцы имеют естественную корреляцию с их физическим расположением в таблице. Диапазон блоков — это группа страниц, которые физически соседствуют в таблице; для каждого диапазона блоков в индексе хранится некоторая сводная информация. Например, таблица, хранящая заказы на поставку, может иметь столбец даты, в который был помещен каждый заказ, и в большинстве случаев записи для более ранних заказов лежат раньше в таблице. Таблица, хранящая адреса, и имеющая столбец почтового индекса, будет хранить адреса одного города подряд, и почтовые индексы строк тоже окажутся сгруппированными.

Индексы BRIN отвечают на запросы посредством сканирования-на-битовых-картах, возвращая все строки всех страниц диапазона, если диапазон целиком был признан совместимым с условиями запросом. Исполнитель запроса (query executor) отвечает за перепроверку всех строк и удаление тех, которые не соответствуют условиям запроса — другими словами, эти индексы являются очень грубыми (большая ошибка первого рода). Но поскольку индекс BRIN очень мал, сканирование индекса добавляет лишь немного накладных расходов по сравнению с последовательным сканированием, при этом помогая избежать сканирования каких-то частей таблицы, если по BRIN-индексу понятно, что они точно не содержат подходящих строк.

Конкретные данные, которые будет хранить индекс BRIN, а также конкретные запросы, которые индекс сможет удовлетворить, зависят от класса оператора, выбранного для каждого столбца индекса. Например, типы данных, имеющие линейный порядок сортировки, могут иметь классы операторов, которые хранят минимальное и максимальное значение в каждом диапазоне блоков. Геометрические типы могут хранить ограничивающую рамку для всех объектов в диапазоне блоков.

Размер диапазона блоков определяется во время создания индекса параметром pages_per_range. Количество записей индекса будет равно размеру таблицы в страницах, деленному на выбранное значение pages_per_range. Чем меньше число, тем больше размер индекса, но и тем выше его точность.

Обслуживание индекса

В момент создания все существующие страницы кучи сканируются, и для каждого диапазона (включая неполный диапазон в конце) создается кортеж в индексе, содержащий сводку по этому диапазону.

При вставке новых строк требуется обновление сводной информации в BRIN-индексе. При вставке в "старые" диапазоны обновление происходит немедленно, а при вставке в "новые" диапазоны обновление индекса откладывается (в эти диапазоны вставляют постоянно, и пересчитывать кортеж индекса после каждой новой строчки было бы накладно). При поиске "новые" необсчитанные диапазоны считаются потенциально содержащими все что угодно.

Обсчёт "новых" диапазонов происходит при работе VACUUM. Вакуум можно запустить вручную, также обсчёт можно инициировать вызовом функции brin_summarize_new_values(regclass). Если включить параметр autosummarize, то "новые" диапазоны будут пересчитываться в фоне процессом Автовакуума. Если он не успевает это делать, то в журнале сервера могут появиться сообщения вида

LOG:  request for BRIN range summarization for index "brin_wi_idx" page 128 was not recorded

По умолчанию параметр autosummarize выключен, т.к. это увеличивает нагрузку системы.

При удалении строк из таблицы не происходит пересчёта BRIN-индекса (это было бы слишком долго). После массовых удалений можно пересчитать BRIN-индекс по конкретным диапазонам, выполнив brin_desummarize_range (regclass, bigint) + brin_summarize_range (regclass, bigint), или по всей таблице, перестроив индекс целиком. Если этого не сделать, индекс будет говорить, что в таком-то диапазоне возможно есть такие-то данные даже после того, как вы удалите все такие данные.

Встроенные классы операторов

Основной дистрибутив QHB включает классы операторов BRIN, показанные в следующей таблице.

Классы операторов minmax хранят минимальные и максимальные значения среди значений индексируемого столбца по всем строкам диапазона. Классы операторов inclusion хранят значение, которое включает("обрамляет") все значения столбца в пределах диапазона.

ИмяИндексируемый тип данныхПоддерживаемые операторы при поиске
int8_minmax_opsbigint< <= = >= >
bit_minmax_opsbit< <= = >= >
varbit_minmax_opsbit varying< <= = >= >
bytea_minmax_opsbytea< <= = >= >
bpchar_minmax_opscharacter< <= = >= >
char_minmax_ops"char"< <= = >= >
date_minmax_opsdate< <= = >= >
float8_minmax_opsdouble precision< <= = >= >
inet_minmax_opsinet< <= = >= >
int4_minmax_opsinteger< <= = >= >
interval_minmax_opsinterval< <= = >= >
macaddr_minmax_opsmacaddr< <= = >= >
macaddr8_minmax_opsmacaddr8< <= = >= >
name_minmax_opsname< <= = >= >
numeric_minmax_opsnumeric< <= = >= >
pg_lsn_minmax_opspg_lsn< <= = >= >
oid_minmax_opsoid< <= = >= >
float4_minmax_opsreal< <= = >= >
int2_minmax_opssmallint< <= = >= >
text_minmax_opstext< <= = >= >
tid_minmax_opstid< <= = >= >
date_minmax_opsdate< <= = >= >
timestamp_minmax_opstimestamp without time zone< <= = >= >
timestamptz_minmax_opstimestamp with time zone< <= = >= >
time_minmax_opstime without time zone< <= = >= >
timetz_minmax_opstime with time zone< <= = >= >
uuid_minmax_opsuuid< <= = >= >
box_inclusion_opsbox<< &< && &> >> ~= @> <@ &<| <<| |>> |&>
network_inclusion_opsinet&& >>= <<= = >> <<
range_inclusion_opsлюбой тип-диапазон<< &< && &> >> @> <@ -|- = < <= = > >=

Расширяемость

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

Все, что требуется, чтобы BRIN-индекс заработал, - это реализовать несколько пользовательских методов, которые определят поведение сводных значений, хранящихся в индексе, и их взаимодействие с ключами сканирования (= значениями в столбце таблицы). У BRIN-индексов четкий интерфейс, дающий хорошую расширяемость и переиспользование кода.

Существует четыре метода, которые должен предоставить класс оператора для использования в BRIN:

  • BrinOpcInfo *opcInfo(Oid type_oid)
    

    Возвращает внутреннюю информацию о сводных данных индексированных столбцов. Конкретнее, должна вернуть указатель на структуру BrinOpcInfo, выделенную с помощью palloc. Определение структуры такое:

    typedef struct BrinOpcInfo
    {
        /* Количество совместно проиндексированных столбцов */
        uint16      oi_nstored;
    
        /* Приватные данные класса оператора */
        void       *oi_opaque;
    
        /* Элементы кеша типов для проиндексированных столбцов */
        TypeCacheEntry *oi_typcache[FLEXIBLE_ARRAY_MEMBER];
    } BrinOpcInfo;
    

    Brinpcinfo::oi_opaque используется для передачи информации между методами класса операторов во время сканирования индекса.

  • bool consistent(BrinDesc *bdesc, BrinValues *column, ScanKey key)
    

    Вернуть, входит ли key (то, что ищут) в сводное значение column из индекса. key->sk_attno содержит номер столбца — это важно, если у вас многоколоночный индекс

  • bool addValue(BrinDesc *bdesc, BrinValues *column, Datum newval, bool isnull)
    

    Обновить сводную информацию диапазона column с учетом нового значения столбца newval. Вернуть true, если column изменилось

  • bool unionTuples(BrinDesc *bdesc, BrinValues *a, BrinValues *b)
    

    Объединить две сводки: изменить сводку a, включив в нее сводку b. Не надо менять сводку b! Вернуть false, если не потребовалось менять a (т.к. уже a полностью включало b).

Способы создания нового класса операторов, совместимого с BRIN:

  1. Стандартный дистрибутив включает поддержку двух семейств классов операторов: minmax и inclusion. Есть реализация соответствующих классов операторов для всех встроенных типов данных. Аналогичные классы операторов для других типов могут быть выведены без написания какого-либо кода. Достаточно просто объявить этот класс в системном каталоге. Обратите внимание, что в код вспомогательных функций этих семейств встроены некоторые предположения о семантике стратегий операторов.

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

  3. Использовать вспомогательные функции от семейства minmax, вместе с набором операторов для типа данных.

    Для создания класса операторов для типа данных, на котором определен полный порядок, можно использовать вспомогательные функции от minmax совместно с соответствующими операторами, как показано в следующей таблице. Все члены класса операторов (функции и операторы) являются обязательными.

    Член класса операторовКакой объект использовать
    Вспомогательная функция 1внутренняя функция brin_minmax_opcinfo()
    Вспомогательная функция 2внутренняя функция brin_minmax_add_value()
    Вспомогательная функция 3внутренняя функция brin_minmax_consistent()
    Вспомогательная функция 4внутренняя функция brin_minmax_union()
    Стратегия 1оператор меньше
    Стратегия 2оператор меньше-или-равно
    Стратегия 3оператор равно
    Стратегия 4оператор больше-или-равно
    Стратегия 5оператор больше
  4. Для создания класса оператора для сложного типа данных, наборы значений которого "обрамляются" "рамками" некоторого другого типа, можно использовать вспомогательные функции от семейства inclusion совместно с соответствующими операторами и дополнительными функциями, как показано в следующей таблице. Из них только одна дополнительная функция, которая может быть написана на любом языке, является обязательной. Реализация необязательных дополнительных функций позволяет некоторые оптимизации при работе индекса. Все операторы необязательные, но для некоторых операторов нужно реализовать другой для комплектности, как показано в таблице

    Член класса операторовКакой объект использоватьТребует наличия
    Вспомогательная функция 1внутренняя функция brin_inclusion_opcinfo() 
    Вспомогательная функция 2внутренняя функция brin_inclusion_add_value() 
    Вспомогательная функция 3внутренняя функция brin_inclusion_consistent() 
    Вспомогательная функция 4внутренняя функция brin_inclusion_union() 
    Вспомогательная функция 11функция для объединения двух элементов (обязательная) 
    Вспомогательная функция 12дополнительная функция для проверки возможности слияния двух элементов 
    Вспомогательная функция 13дополнительная функция, чтобы проверить, если элемент содержится в другом 
    Вспомогательная функция 14необязательная функция для проверки, является ли элемент пустым 
    Стратегия 1оператор левееСтратегия 4
    Стратегия 2оператор не-выпирает-справаСтратегия 5
    Стратегия 3оператор перекрывается 
    Стратегия 4оператор не-выпирает-слеваСтратегия 1
    Стратегия 5оператор правееСтратегия 2
    Стратегия 6, 18оператор такой-же-или-равенСтратегия 7
    Стратегия 7, 13, 16, 24, 25оператор охватывает-или-равен
    Стратегия 8, 14, 26, 27оператор содержится-в-или-равенСтратегия 3
    Стратегия 9оператор не-выпирает-сверхуСтратегия 11
    Стратегия 10оператор нижеСтратегия 12
    Стратегия 11оператор вышеСтратегия 9
    Стратегия 12оператор не-выпирает-снизуСтратегия 10
    Стратегия 20оператор меньшеСтратегия 5
    Стратегия 21оператор меньше-или-равноСтратегия 5
    Стратегия 22оператор большеСтратегия 1
    Стратегия 23оператор больше-или-равноСтратегия 1

    Номера вспомогательных функций 1-10 зарезервированы для внутренних функций BRIN, поэтому функции уровня SQL начинаются с числа 11. Вспомогательная функция номер 11 является главной, обязательной для построения индекса. Она принимает два аргумента того же типа, что и класс оператора, и возвращать их объединение. Класс оператора семейства inclusion может хранить результат объединения в другом типа данных, он задается параметром STORAGE. Возвращаемое значение функции №11 должно быть типа STORAGE.

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

И minmax, и inclusion поддерживают операторы, работающие с разными типами данных, хотя с ними зависимости становятся более сложными. Класс операторов minmax требует, чтобы был определен полный набор операторов с обоими аргументами, имеющими один и тот же тип. Это позволяет поддерживать дополнительные типы данных путем определения дополнительных наборов операторов. Стратегии класса операторов inclusion требуют, чтобы оператор принимал первый аргумент типа STORAGE, а второй аргумент типа данных столбца таблицы. Смотрите float4_minmax_ops в качестве примера расширения minmax, а также box_inclusion_ops в качестве примера расширения inclusion.

Параллельный контроль

В этой главе описывается поведение СУБД QHB, когда две или более сессий пытаются одновременно обратиться к одним и тем же данным. Задачи, стоящие перед СУБД в такой ситуации: обеспечить высокопроизводительный доступ к данным и сохранить их целостность. Материал данной статьи будет полезен всем разработчикам приложений баз данных.

Многоверсионная модель

QHB предоставляет разработчикам богатый набор инструментов для управления конкурентным доступом к данным. Внутренняя согласованность данных поддерживается с помощью многоверсионной модели (Multiversion Concurrency Control, MVCC). Она устроена таким образом, что в ходе исполнения каждой SQL-команды СУБД видит данные базы как бы "замороженными", определённой версии. Из этого следует, что те данные, которые были изменены или добавлены в ходе параллельной работы других транзакций, не нарушают целостности нашего представления о данных. MVCC, отказываясь от методологий блокировки традиционных систем баз данных, сводит к минимуму конфликты блокировок, чтобы обеспечить разумную производительность в многопользовательских средах.

Основное преимущество использования MVCC-модели управления параллелизмом, а не блокировок, состоит в том, что в MVCC блокировки, полученные для чтения данных, не конфликтуют с блокировками, взятыми для записи данных, а потому чтение никогда не блокирует запись, а запись чтение. QHB обеспечивает эту гарантию даже при использовании самого строгого уровня изоляции транзакций за счет использования Serializable Snapshot Isolation (SSI).

Средства блокировки на уровне таблиц и строк также доступны в QHB для приложений, которые обычно не нуждаются в полной изоляции транзакций и предпочитают явно управлять конкретными точками конфликта. Однако правильное использование MVCC обычно обеспечивает лучшую производительность, чем блокировки. Кроме того, определяемые приложением консультативные блокировки предоставляют механизм для получения блокировок, которые не привязаны к одной транзакции.

Изоляция транзакций

Стандарт SQL определяет четыре уровня изоляции транзакций. Наиболее строгим является Serializable, который определяется таким образом, что любое одновременное выполнение набора из нескольких Serializable транзакций гарантированно даст тот же эффект, что и запуск их по одному. Три других уровня определены в терминах явлений, возникающих в результате взаимодействия между параллельными транзакциями, которые не должны происходить на каждом уровне. Стандарт отмечает, что из-за определения Serializable, ни одно из этих явлений невозможно на этом уровне. (В этом нет ничего удивительного -- если эффект транзакций должен соответствовать тому, что они выполнялись по одному, как вы можете наблюдать какие-либо явления, вызванные их взаимодействием?)

Явления, которые запрещены на разных уровнях:

  • Грязное чтение (Dirty read). Транзакция считывает данные, которые были записаны в результате выполнения параллельной незафиксированной транзакции.
  • Неповторяемое чтение (Non-repeatable read). Транзакция считывает ранее прочитанные данные и замечает, что данные были изменены другой транзакцией (завершённой после первого чтения).
  • Фантомное чтение (Phantom read). Транзакция повторно выполняет запрос, возвращающий набор строк для некоторого условия и обнаруживает, что набор строк, удовлетворяющих условию, изменился из-за транзакции, завершившейся за это время.
  • Аномалии сериализации (Serialization anomaly). Результат успешной фиксации (commiting) группы транзакций оказывается несогласованным (inconsistent), отличающимся от результата полученного в ходе последовательного выполнения этих транзакций, независимо от порядка их выполнения. Результат успешной фиксации (commiting) группы транзакций конкурентно оказывается отличным от результата успешной фиксации группы транзаций выполнявшихся последовательно.

Уровни изолияции транзакций, описанные в стандарте SQL и реализованные в QHB:

Уровень изоляцииГрязное чтениеНеповторимое чтениеФантомное чтениеАномалия сериализации
Read uncommittedРазрешено, но не в QHBВозможноВозможноВозможно
Read committedНевозможноВозможноВозможноВозможно
Repeatable readНевозможноНевозможноРазрешено, но не в QHBВозможно
SerializableНевозможноНевозможноНевозможноНевозможно

В QHB вы можете запросить любой из четырех стандартных уровней изоляции транзакций, но внутренне реализованы только три различных уровня изоляции, то есть режим Read Uncommitted в QHB ведет себя как Read Committed. Это связано с тем, что это единственный разумный способ сопоставить стандартные уровни изоляции с архитектурой многоверсионного управления QHB.

Из таблицы также видно, что реализация Repeatable Read в QHB не позволяет выполнять фантомные чтения. Стандарт SQL допускает более строгое поведение: четыре уровня изоляции определяют только то, какие явления не должны происходить, а какие нет. Поведение доступных уровней изоляции подробно описано в следующих подразделах.

Чтобы установить уровень изоляции транзакции, используйте команду SET TRANSACTION.

Важно
Некоторые типы данных и функции QHB имеют специальные правила, касающиеся поведения транзакций. В частности, изменения, внесенные в последовательность (и, следовательно, счетчик столбца, объявленного с использованием serial), сразу видны всем другим транзакциям и не отменяются, если транзакция, которая внесла изменения, прерывается. См. главы Функции управления последовательностями и Серийные типы.

Уровень изоляции Read Committed

Read Committed - уровень изоляции по умолчанию в QHB. Когда транзакция использует этот уровень изоляции, запрос SELECT (без предложения FOR UPDATE/SHARE) видит только данные, зафиксированные до начала запроса; он никогда не видит ни незафиксированные данные, ни изменения, зафиксированные во время выполнения запроса параллельными транзакциями. По сути, запрос SELECT видит снимок базы в моменте, данные с начала выполнения запроса. Однако SELECT видит результаты предыдущих обновлений, выполненных в его собственной транзакции, даже если они еще не зафиксированы (commited). Также обратите внимание, что две последовательные команды SELECT могут видеть разные данные, даже если они находятся в пределах одной транзакции, если другие транзакции производят изменения данных после запуска первого SELECT и до запуска второго SELECT.

Команды UPDATE, DELETE, SELECT FOR UPDATE и SELECT FOR SHARE ведут себя так же, как и SELECT, в плане поиска целевых строк: они будут находить только те целевые строки, которые были зафиксированы на момент запуска команды. Однако такая целевая строка, возможно, уже была обновлена, удалена или заблокирована другой параллельной транзакцией к моменту ее обнаружения. В этом случае запланированное изменение будет дожидаться фиксации (commit) конкурентной трнзакции или отмены (rollback) если та ещё выполняется. Если конкурирующая транзакция откатывается (rollback), текущая транзакция может продолжить изменения полученной строки (конкурирующая её не изменила). Если конкурирующая транзакция зафиксировалась, но в результате её работы строка была удалена -- она будет проигнорирована; в противном случае она будет получена заново с повторной проверкой условия WHERE. Применительно к SELECT FOR UPDATE и SELECT FOR SHARE это означает, что обновлённая версия строки блокируется и возвращается клиенту.

INSERT с предложением ON CONFLICT DO UPDATE ведёт себя схожим образом: в режиме Read Commited каждая строка, предлагаемая для вставки, будет либо вставлена, либо изменена. Если не возникает несвязных ошибок, гарантируется один из двух исходов: если конфликт вызван конкурирующей транзакцией, результат который пока недоступен INSERT, UPDATE подействует на эту строку несмотря на то, что эта команда не должна видеть никакую версию этой строки.

INSERT с предложением ON CONFLICT DO NOTHING может привести к тому, что вставка не будет продолжена для строки из-за результата другой транзакции, эффекты которой не видны для снимка INSERT. Опять же, это характерно только для уровня Read Committed.

В силу вышеприведенных правил, команда обновления может увидеть несогласованное (inconsistent) состояние: она может видеть результаты выполнения конкурирующей команды. Вследствие этого, уровень Read Commited не подходит для команд со сложным сценарием поиска; однако, он вполне пригоден для простых случаев:

BEGIN;
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534;
COMMIT;

Если две такие транзакции одновременно пытаются изменить остаток на счете 12345, мы явно хотим, чтобы вторая транзакция началась с обновленной версии строки счета. Поскольку каждая команда влияет только на заранее определенную строку, ее отображение в обновленной версии строки не создает несогласованности.

Более сложное использование может привести к нежелательным результатам в режиме Read Committed. Например, рассмотрим команду DELETE работающую с данными, которые добавляются и удаляются согласно условиям другой командой, например, предположим, что website представляет собой таблицу из двух строк, где website.hits равны 9 и 10 :

BEGIN;
UPDATE website SET hits = hits + 1;
-- run from another session:  DELETE FROM website WHERE hits = 10;
COMMIT;

DELETE не сможет произвести удаление записей до момента фиксации транзакции. Запись с website.hits = 9 до выполнения UPDATE не будет подходить под условие DELETE, а вторая запись, с website.hits = 10 будет заблокирована до момента фиксации. После фиксации первой транзакции, первая запись получит website.hits = 10 и будет удалена во второй транзации.

Поскольку режим Read Committed запускает каждую команду с новым моментальным снимком, который включает в себя все транзакции, зафиксированные до этого момента, последующие команды в той же транзакции в любом случае будут видеть результаты совершенной параллельной транзакции.

Частичная изоляция транзакций, обеспечиваемая режимом Read Committed, подходит для многих приложений, и этот режим быстр и прост в использовании; однако, этого недостаточно для всех случаев. Приложениям, которые выполняют сложные запросы и обновления, может потребоваться более строго согласованное представление базы данных, чем обеспечивает режим Read Committed.

Уровень изоляции Repeatable Read

Уровень изоляции Repeatable Read видит только данные, зафиксированные до начала транзакции; он никогда не видит ни незафиксированные данные, ни изменения, зафиксированные во время выполнения транзакций параллельными транзакциями. (Тем не менее, запрос видит результаты предыдущих обновлений, выполненных в его собственной транзакции, даже если эти изменения еще не зафиксированы.) Это более надежная гарантия, чем требуется стандартом SQL для этого уровня изоляции, и предотвращает все явления описано в таблице 1 за исключением аномалий сериализации. Как упомянуто выше, это специально разрешено стандартом, который описывает только минимальную защиту, которую должен обеспечивать каждый уровень изоляции.

Этот уровень отличается от Read Committed тем, что запрос в повторяемой транзакции чтения видит моментальный снимок в начале первого оператора, не являющегося элементом управления транзакцией в транзакции, а не в начале текущего оператора в транзакции. Таким образом, последовательные команды SELECT в одной транзакции видят одни и те же данные, т.е. они не видят изменений, внесенных другими транзакциями, зафиксированными после запуска их собственной транзакции.

Приложения, использующие этот уровень, должны быть готовы повторить транзакции из-за ошибок сериализации.

Команды UPDATE, DELETE, SELECT FOR UPDATE и SELECT FOR SHARE ведут себя так же, как и команды SELECT с точки зрения поиска целевых строк: они найдут только целевые строки, которые были зафиксированы на момент начала транзакции. Однако такая целевая строка, возможно, уже была обновлена (или удалена, или заблокирована) другой параллельной транзакцией к моменту ее обнаружения. В этом случае повторяемая транзакция чтения будет ожидать, когда первая обновляющая транзакция будет зафиксирована или откатана (если она все еще выполняется). Если первый модуль обновления откатывается назад, то его эффекты сводятся на нет, и повторяемая транзакция чтения может продолжить обновление первоначально найденной строки. Но если первый обновитель фиксирует (и фактически обновил или удалил строку, а не просто заблокировал ее), то повторяемая транзакция чтения будет откатана с сообщением

ERROR:  could not serialize access due to concurrent update

потому что повторяемая транзакция чтения не может изменять или блокировать строки, измененные другими транзакциями после начала повторяемой транзакции чтения.

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

Обратите внимание, что только обновление транзакций может потребоваться повторить; Транзакции только для чтения никогда не будут иметь конфликтов сериализации.

Режим повторяемого чтения обеспечивает строгую гарантию того, что каждая транзакция видит полностью стабильное представление базы данных. Тем не менее, это представление не всегда будет соответствовать последовательному (по одному) выполнению параллельных транзакций одного и того же уровня. Например, даже транзакция только для чтения на этом уровне может видеть контрольную запись, обновленную, чтобы показать, что пакет был завершен, но не увидеть одну из подробных записей, которая логически является частью пакета, потому что она прочитала более раннюю версию контрольной записи., Попытки обеспечить выполнение бизнес-правил транзакциями, выполняющимися на этом уровне изоляции, вряд ли будут работать правильно без осторожного использования явных блокировок для блокировки конфликтующих транзакций.

Уровень изоляции Serializable

Уровень изоляции Serializable обеспечивает самую строгую изоляцию транзакций. Этот уровень эмулирует выполнение последовательных транзакций для всех зафиксированных транзакций; как если бы транзакции были выполнены одна за другой, поочередно, а не одновременно. Однако, как и уровень повторяемого чтения, приложения, использующие этот уровень, должны быть готовы повторить транзакции из-за сбоев сериализации. Фактически, этот уровень изоляции работает точно так же, как и Repeatable Read, за исключением того, что он отслеживает условия, которые могут привести к тому, что выполнение параллельного набора сериализуемых транзакций будет вести себя не так, как все возможные последовательные (по одному) выполнения этих транзакций. Этот мониторинг не вводит каких-либо блокировок, кроме присутствующих в повторяющемся чтении, но есть некоторые накладные расходы на мониторинг, и обнаружение условий, которые могут вызвать аномалию сериализации, вызовет сбой сериализации .

В качестве примера рассмотрим таблицу mytab, изначально содержащую:

class | value
------+-------
    1 |    10
    1 |    20
    2 |   100
    2 |   200

Предположим, что сериализуемая транзакция A вычисляет:

SELECT SUM(value) FROM mytab WHERE class = 1;

а затем вставляет результат 30 в качестве value в новую строку с class = 2. Одновременно сериализуемая транзакция B вычисляет:

SELECT SUM(value) FROM mytab WHERE class = 2;

и получает результат 300, который он вставляет в новую строку с class = 1. Затем обе транзакции пытаются зафиксировать. Если какая-либо транзакция выполняется на уровне изоляции Repeatable Read, обеим будет разрешено зафиксировать; но поскольку последовательный порядок выполнения не согласуется с результатом, использование сериализуемых транзакций позволит зафиксировать одну транзакцию и откатит другую с этим сообщением:

ERROR:  could not serialize access due to read/write dependencies among transactions

Это потому, что если бы A выполнилась до B, B вычислила бы сумму 330, а не 300, и аналогично другой порядок привел бы к другой сумме, вычисленной A.

Если для предотвращения аномалий Вы используете уровень изоляции Serializable, то важно помнить, что данные постоянной(persistant) пользовательской таблицы не явлются валидными до того момента, пока не произойдёт фиксация транзакцией. Это верно в том числе и для транзакций, которые выполняют только чтение, за исключением ситуаций, когда данные читаются отложенной(deffer)

Если вы используете уровень изоляции Serializable для предотвращения аномалий, важно, чтобы любые данные, считанные из постоянной пользовательской таблицы, не считались действительными до тех пор, пока производящая их транзакция не была успешно зафиксирована. Это верно даже для read-only транзакций, за исключением того, что данные, считанные в отложенной транзакции только для чтения, как известно, действительны, как только они прочитаны, потому что такая транзакция ожидает, пока она не сможет получить моментальный снимок, гарантированный свободный от такого проблемы, прежде чем начать читать какие-либо данные. Во всех других случаях приложения не должны зависеть от результатов, прочитанных во время транзакции, которая впоследствии была прервана; вместо этого они должны повторить транзакцию, пока она не завершится успешно.

Чтобы гарантировать истинную сериализуемость, QHB использует блокировку предикатов, что означает, что он сохраняет блокировки, которые позволяют ему определять, когда запись повлияла бы на результат предыдущего чтения из параллельной транзакции, если бы она выполнялась первой. В QHB эти блокировки не вызывают блокировку и, следовательно, не могут играть какую-либо роль в возникновении тупика. Они используются для идентификации и пометки зависимостей между параллельными сериализуемыми транзакциями, которые в определенных комбинациях могут привести к аномалиям сериализации. В отличие от транзакции Read Committed или Repeatable Read, которая хочет обеспечить согласованность данных, может потребоваться снять блокировку всей таблицы, что может заблокировать других пользователей, пытающихся использовать эту таблицу, или использовать SELECT FOR UPDATE или SELECT FOR SHARE который не только может заблокировать другие транзакции, но и вызвать доступ к диску.

Блокировки предикатов в QHB, как и в большинстве других систем баз данных, основаны на данных, фактически доступных транзакции. Они будут отображаться в системном представлении pg_locks в mode SIReadLock . Конкретные блокировки, полученные во время выполнения запроса, будут зависеть от плана, используемого запросом, и несколько более мелких блокировок (например, блокировок кортежей) могут быть объединены в меньшее количество более грубых блокировок (например, блокировок страниц) в течение транзакция для предотвращения исчерпания памяти, используемой для отслеживания блокировок. Транзакция READ ONLY может освободить свои блокировки SIRead до завершения, если обнаружит, что по-прежнему не может возникнуть конфликтов, которые могут привести к аномалии сериализации. Фактически транзакции READ ONLY часто могут установить этот факт при запуске и избежать каких-либо предикатных блокировок. Если вы явно запросите SERIALIZABLE READ ONLY DEFERRABLE, она будет блокирована, пока не сможет установить этот факт. (Это единственный случай, когда сериализуемые транзакции блокируют, а повторяющиеся транзакции чтения не делают.) С другой стороны, блокировки SIRead часто необходимо сохранять после принятия транзакции до тех пор, пока не завершатся перекрывающиеся транзакции чтения-записи.

Последовательное использование сериализуемых транзакций может упростить разработку. Гарантия того, что любой набор успешно совершенных параллельных сериализуемых транзакций будет иметь такой же эффект, как если бы они выполнялись по одной за раз, означает, что если вы сможете продемонстрировать, что отдельная транзакция, как написано, будет работать правильно при запуске сама по себе, вы может быть уверен, что он будет действовать правильно в любой комбинации сериализуемых транзакций, даже без какой-либо информации о том, что эти другие транзакции могут сделать, или он не будет успешно зафиксирован. Важно, чтобы среда, в которой используется этот метод, имела обобщенный способ обработки ошибок сериализации (которые всегда возвращаются со значением SQLSTATE ’40001’), поскольку будет очень сложно предсказать, какие именно транзакции могут способствовать чтению / записи. зависимости и необходимо откатить, чтобы предотвратить сериализацию аномалий. Мониторинг зависимостей чтения / записи имеет свою стоимость, также как и перезапуск транзакций, которые завершаются с ошибкой сериализации, но сбалансированы с затратами и блокировками, связанными с использованием явных блокировок и SELECT FOR UPDATE или SELECT FOR SHARE Сериализуемые транзакции лучший выбор производительности для некоторых сред.

Хотя уровень изоляции Sergizable в QHB позволяет фиксировать параллельные транзакции только в том случае, если он может доказать, что существует последовательный порядок выполнения, который даст тот же эффект, он не всегда предотвращает возникновение ошибок, которые не возникнут при истинном последовательном выполнении., В частности, можно увидеть уникальные нарушения ограничений, вызванные конфликтами с перекрывающимися сериализуемыми транзакциями, даже после явной проверки отсутствия ключа перед попыткой его вставки. Этого можно избежать, убедившись, что все сериализуемые транзакции, которые вставляют потенциально конфликтующие ключи, явно проверяют, могут ли они сделать это в первую очередь. Например, представьте приложение, которое запрашивает у пользователя новый ключ, а затем проверяет, что его еще не существует, сначала пытаясь выбрать его, или генерирует новый ключ, выбирая максимально существующий ключ и добавляя его. Если некоторые сериализуемые транзакции вставляют новые ключи напрямую, не следуя этому протоколу, нарушения уникальных ограничений могут быть зарегистрированы даже в тех случаях, когда они не могут произойти при последовательном выполнении параллельных транзакций.

Для обеспечения оптимальной производительности при использовании параллельных транзакций для управления параллелизмом следует учитывать следующие проблемы:

  • Объявите транзакции READ ONLY когда это возможно.

  • Управляйте количеством активных соединений, используя пул соединений, если это необходимо. Это всегда важный фактор производительности, но он может быть особенно важен в загруженной системе с использованием сериализуемых транзакций.

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

  • Не оставляйте соединения висящими « бездействующими в транзакции » дольше, чем это необходимо. Параметр конфигурации idle_in_transaction_session_timeout может использоваться для автоматического отключения длительных сеансов.

  • Устраните явные блокировки, SELECT FOR UPDATE и SELECT FOR SHARE где они больше не нужны, благодаря защитам, автоматически предоставляемым сериализуемыми транзакциями.

  • Когда система вынуждена объединять несколько блокировок предикатов на уровне страниц в одну блокировку предикатов на уровне отношений, поскольку в таблице блокировок предикатов не хватает памяти, может произойти увеличение частоты сбоев сериализации. Вы можете избежать этого, увеличив max_pred_locks_per_transaction, max_pred_locks_per_relation и / или max_pred_locks_per_page.

  • Последовательное сканирование всегда требует блокировки предиката на уровне отношений. Это может привести к увеличению частоты сбоев сериализации. Может быть полезно поощрять использование сканирования индекса путем уменьшения random_page_cost и / или увеличения cpu_tuple_cost. Обязательно сопоставьте любое уменьшение откатов транзакций и перезапусков с любым общим изменением времени выполнения запроса.

Явная блокировка

QHB предоставляет различные режимы блокировки для управления одновременным доступом к данным в таблицах. Эти режимы могут использоваться для управляемой приложением блокировки в ситуациях, когда MVCC не дает желаемого поведения. Кроме того, большинство команд QHB автоматически получают блокировки соответствующих режимов, чтобы гарантировать, что ссылочные таблицы не будут удалены или изменены несовместимыми способами во время выполнения команды. (Например, TRUNCATE не может безопасно выполняться одновременно с другими операциями в той же таблице, поэтому он получает эксклюзивную блокировку для таблицы, чтобы обеспечить это.)

Чтобы просмотреть список текущих незавершенных блокировок на сервере базы данных, используйте системное представление pg_locks. Для получения дополнительной информации о мониторинге состояния подсистемы диспетчера блокировки см. Главу 27 .

Блокировки на уровне таблицы

В приведенном ниже списке показаны доступные режимы блокировки и контексты, в которых они автоматически используются QHB. Вы также можете получить любую из этих блокировок явно с помощью команды LOCK. Помните, что все эти режимы блокировки являются блокировками на уровне таблицы, даже если имя содержит слово « строка » ; Названия режимов блокировки являются историческими. В некоторой степени имена отражают типичное использование каждого режима блокировки - но семантика все та же. Единственное реальное различие между одним режимом блокировки и другим - это набор режимов блокировки, с которыми конфликтует каждый (см. Таблицу 2). Две транзакции не могут одновременно удерживать блокировки конфликтующих режимов на одной и той же таблице. (Однако транзакция никогда не конфликтует сама с собой. Например, она может получить блокировку ACCESS EXCLUSIVE а затем получить блокировку ACCESS SHARE для той же таблицы.) Бесконфликтные режимы блокировки могут одновременно поддерживаться многими транзакциями. Обратите внимание, в частности, на то, что некоторые режимы блокировки являются конфликтующими друг с другом (например, блокировка ACCESS EXCLUSIVE не может удерживаться более чем одной транзакцией одновременно), в то время как другие не являются конфликтующими друг с другом (например, блокировка ACCESS SHARE может удерживаться несколько транзакций).

Режимы блокировки на уровне таблицы

ACCESS SHARE

Конфликтует только с режимом блокировки ACCESS EXCLUSIVE .

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

ROW SHARE

Конфликты с режимами блокировки EXCLUSIVE и ACCESS EXCLUSIVE .

Команды SELECT FOR UPDATE и SELECT FOR SHARE получают блокировку этого режима для целевых таблиц (в дополнение к блокировкам ACCESS SHARE для любых других таблиц, на которые есть ссылки, но не выбранных FOR UPDATE/FOR SHARE ).

ROW EXCLUSIVE

Конфликты с режимами блокировки SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE .

Команды UPDATE, DELETE и INSERT получают этот режим блокировки для целевой таблицы (в дополнение к блокировкам ACCESS SHARE для любых других ссылочных таблиц). В общем, этот режим блокировки будет активирован любой командой, которая изменяет данные в таблице.

SHARE UPDATE EXCLUSIVE

Конфликты с режимами блокировки SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим защищает таблицу от одновременных изменений схемы и VACUUM .

Приобретен VACUUM (без FULL ), CREATE INDEX CONCURRENTLY, REINDEX CONCURRENTLY CREATE INDEX CONCURRENTLY, REINDEX CONCURRENTLY CREATE STATISTICS, а также некоторые варианты ALTER INDEX и ALTER TABLE (для полной информации см. ALTER INDEX и ALTER TABLE ).

SHARE

Конфликты с режимами блокировки ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим защищает таблицу от одновременных изменений данных.

Приобретен по CREATE INDEX (без одновременного CONCURRENTLY ).

SHARE ROW EXCLUSIVE

Конфликты с режимами блокировки ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим защищает таблицу от одновременных изменений данных и является самоисключающим, так что только один сеанс может удерживать его одновременно.

Приобретено CREATE TRIGGER и некоторыми формами ALTER TABLE (см. ALTER TABLE ).

EXCLUSIVE

Конфликты с режимами блокировки ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим допускает только одновременные блокировки ACCESS SHARE, т. ACCESS SHARE Только чтение из таблицы может выполняться параллельно с транзакцией, удерживающей этот режим блокировки.

Приобретен REFRESH MATERIALIZED VIEW CONCURRENTLY .

ACCESS EXCLUSIVE

Конфликтует с блокировками всех режимов ( ACCESS SHARE, ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE ). Этот режим гарантирует, что держатель является единственной транзакцией, которая имеет доступ к таблице любым способом.

Получено REINDEX DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL и REFRESH MATERIALIZED VIEW (без CONCURRENTLY ). Многие формы ALTER INDEX и ALTER TABLE также получают блокировку на этом уровне. Это также режим блокировки по умолчанию для операторов LOCK TABLE которые не указывают режим явно.

Заметка
Только блокировка ACCESS EXCLUSIVE блокирует SELECT (без FOR UPDATE/SHARE).

После получения блокировка обычно удерживается до конца транзакции. Но если блокировка получена после установления точки сохранения, блокировка снимается немедленно, если точка отката возвращается к. Это согласуется с принципом, что ROLLBACK отменяет все эффекты команд, начиная с точки сохранения. То же самое относится и к блокировкам, полученным в блоке исключений PL/pgSQL: сбой ошибки из блока освобождает блокировки, полученные внутри него.

Таблица 2. Конфликтующие режимы блокировки

Запрошенный
режим
блокировки

Текущий режим блокировки

ACCESS
SHARE

ROW
SHARE

ROW
EXCLUSIVE

SHARE
UPDATE
EXCLUSIVE

SHARE

SHARE
ROW
EXCLUSIVE

EXCLUSIVE

ACCESS
EXCLUSIVE

ACCESS SHARE               X
ROW SHARE             X X
ROW EXCLUSIVE         X X X X
SHARE UPDATE EXCLUSIVE       X X X X X
SHARE     X X   X X X
SHARE ROW EXCLUSIVE     X X X X X X
EXCLUSIVE   X X X X X X X
ACCESS EXCLUSIVE X X X X X X X X

Блокировки на уровне строк

В дополнение к блокировкам на уровне таблиц существуют блокировки на уровне строк, которые перечислены ниже с контекстами, в которых они автоматически используются QHB. См. Таблицу 3 для полной таблицы конфликтов блокировки на уровне строк. Обратите внимание, что транзакция может содержать конфликтующие блокировки в одной и той же строке, даже в разных субтранзакциях; но кроме этого, две транзакции не могут содержать конфликтующие блокировки в одной строке. Блокировки на уровне строк не влияют на запросы данных; они блокируют только писателей и шкафчиков в одном ряду.

Режимы блокировки на уровне строк

FOR UPDATE

FOR UPDATE блокирует строки, извлеченные SELECT как если бы они были обновлены. Это предотвращает их блокировку, изменение или удаление другими транзакциями до завершения текущей транзакции. То есть другие транзакции, в которых предпринимаются попытки UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR NO KEY UPDATE, SELECT FOR SHARE или SELECT FOR KEY SHARE этих строк, будут заблокированы до завершения текущей транзакции; и наоборот, SELECT FOR UPDATE будет ожидать параллельной транзакции, которая выполнила любую из этих команд в той же строке, а затем заблокирует и вернет обновленную строку (или не строку, если строка была удалена). Однако в REPEATABLE READ или SERIALIZABLE сообщение об ошибке, если строка, подлежащая блокировке, изменилась с момента запуска транзакции. Для дальнейшего обсуждения см. Раздел "Проверка согласованности данных на уровне приложения".

Режим блокировки FOR UPDATE также вызывается любым DELETE в строке, а также UPDATE который изменяет значения в определенных столбцах. В настоящее время для случая UPDATE рассматривается набор столбцов с уникальным индексом, который можно использовать во внешнем ключе (поэтому частичные индексы и экспрессивные индексы не учитываются), но это может измениться в будущем.

FOR NO KEY UPDATE
Ведет себя аналогично FOR UPDATE, за исключением того, что полученная блокировка слабее: эта блокировка не будет блокировать команды SELECT FOR KEY SHARE которые пытаются получить блокировку в тех же строках. Этот режим блокировки также активируется любым UPDATE которое не получает блокировку FOR UPDATE .
FOR SHARE
Ведет себя аналогично FOR NO KEY UPDATE, за исключением того, что он получает общую блокировку, а не монопольную блокировку для каждой извлеченной строки. Общая блокировка блокирует выполнение другими транзакциями UPDATE, DELETE, SELECT FOR UPDATE или SELECT FOR NO KEY UPDATE в этих строках, но не мешает им выполнять SELECT FOR SHARE или SELECT FOR KEY SHARE .
FOR KEY SHARE
Ведет себя аналогично FOR SHARE, за исключением того, что блокировка слабее: SELECT FOR UPDATE заблокирован, но не SELECT FOR NO KEY UPDATE. Совместная блокировка ключом блокирует выполнение другими транзакциями DELETE или любого UPDATE который изменяет значения ключа, но не другого UPDATE, и это также не предотвращает SELECT FOR NO KEY UPDATE, SELECT FOR SHARE или SELECT FOR KEY SHARE .

QHB не запоминает какую-либо информацию об измененных строках в памяти, поэтому не существует ограничений на количество строк, заблокированных за один раз. Однако блокировка строки может привести к записи на диск, например, SELECT FOR UPDATE изменяет выбранные строки, чтобы пометить их как заблокированные, и, следовательно, приведет к записи на диск.

Таблица 3. Конфликтующие блокировки на уровне строк

Запрошенный режим блокировки Текущий режим блокировки
FOR KEY SHARE FOR SHARE FOR NO KEY UPDATE FOR UPDATE
FOR KEY SHARE       X
FOR SHARE     X X
FOR NO KEY UPDATE   X X X
FOR UPDATE X X X X

Блокировки на уровне страницы

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

Взаимные блокировки

Использование явной блокировки может увеличить вероятность взаимоблокировок, при этом каждая из двух (или более) транзакций удерживает блокировки, которые нужны другой. Например, если транзакция 1 получает эксклюзивную блокировку для таблицы A, а затем пытается получить эксклюзивную блокировку для таблицы B, в то время как транзакция 2 уже имеет эксклюзивную блокировку таблицы B и теперь хочет получить эксклюзивную блокировку для таблицы A, ни одна из них не может продолжить, QHB автоматически обнаруживает тупиковые ситуации и разрешает их, прерывая одну из задействованных транзакций, позволяя другим завершить. (Точно, какая транзакция будет прервана, трудно предсказать, и на нее не следует полагаться.)

Обратите внимание, что взаимные блокировки также могут возникать в результате блокировок на уровне строк (и, следовательно, они могут возникать, даже если явная блокировка не используется). Рассмотрим случай, когда две параллельные транзакции изменяют таблицу. Первая транзакция выполняет:

UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 11111;

Это получает блокировку на уровне строк в строке с указанным номером учетной записи. Затем вторая транзакция выполняет:

UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;

Первый оператор UPDATE успешно получает блокировку на уровне строк в указанной строке, поэтому он успешно обновляет эту строку. Однако второй оператор UPDATE обнаруживает, что строка, которую он пытается обновить, уже заблокирована, поэтому он ожидает завершения транзакции, получившей блокировку. Вторая транзакция теперь ожидает завершения первой транзакции, прежде чем продолжить выполнение. Теперь транзакция 1 выполняет:

UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;

Транзакция 1 пытается получить блокировку на уровне строки в указанной строке, но не может: транзакция 2 уже удерживает такую блокировку. Поэтому он ожидает завершения транзакции 2. Таким образом, транзакция 1 блокируется в транзакции 2, а транзакция 2 блокируется в транзакции 1: условие взаимоблокировки. QHB обнаружит эту ситуацию и прервет одну из транзакций.

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

До тех пор, пока не будет обнаружена ситуация взаимоблокировки, транзакция, ищущая блокировку на уровне таблицы или на уровне строки, будет бесконечно долго ожидать освобождения конфликтующих блокировок. Это означает, что для приложений плохая идея держать транзакции открытыми в течение длительных периодов времени (например, во время ожидания ввода данных пользователем).

Консультативные блокировки

QHB предоставляет средства для создания блокировок, которые имеют значения, определенные приложением. Они называются консультативными блокировками, потому что система не навязывает их использование - это зависит от приложения, чтобы использовать их правильно. Консультативные блокировки могут быть полезны для стратегий блокировки, которые неудобно подходят для модели MVCC. Например, обычное использование консультативных блокировок состоит в том, чтобы эмулировать стратегии пессимистической блокировки, типичные для так называемых систем управления данными « плоских файлов ». Хотя флаг, хранящийся в таблице, можно использовать для той же цели, консультативные блокировки быстрее, избегают раздувания таблиц и автоматически очищаются сервером в конце сеанса.

Есть два способа получить консультативную блокировку в QHB : на уровне сеанса или на уровне транзакции. После получения на уровне сеанса консультативная блокировка удерживается до тех пор, пока явно не будет снята или сеанс не завершится. В отличие от стандартных запросов на блокировку, запросы на консультативную блокировку на уровне сеанса не учитывают семантику транзакции: блокировка, полученная во время обратной транзакции, будет по-прежнему удерживаться после отката, и аналогично разблокировка эффективна, даже если вызывающая транзакция завершится неудачей позже. Блокировка может быть получена несколько раз в процессе ее владения; для каждого выполненного запроса на блокировку должен быть соответствующий запрос на разблокировку до фактического снятия блокировки. С другой стороны, запросы на блокировку на уровне транзакции ведут себя больше как обычные запросы на блокировку: они автоматически освобождаются в конце транзакции и явной операции разблокировки не выполняется. Такое поведение часто более удобно, чем поведение на уровне сеанса, для кратковременного использования консультативной блокировки. Запросы на уровне сеанса и на уровне транзакции для одного и того же идентификатора консультативной блокировки будут блокировать друг друга ожидаемым образом. Если сеанс уже удерживает данную консультативную блокировку, дополнительные запросы от него всегда будут успешными, даже если другие сеансы ожидают блокировки; это утверждение верно независимо от того, находится ли существующая блокировка блокировки и новый запрос на уровне сеанса или уровне транзакции.

Как и все блокировки в QHB, полный список консультативных блокировок, удерживаемых в настоящее время любым сеансом, можно найти в системном представлении pg_locks .

Как консультативные, так и обычные блокировки хранятся в пуле общей памяти, размер которой определяется переменными конфигурации max_locks_per_transaction и max_connections. Необходимо соблюдать осторожность, чтобы не исчерпать эту память, иначе сервер вообще не сможет предоставить никаких блокировок. Это накладывает верхний предел на количество консультативных блокировок, предоставляемых сервером, обычно в пределах от десятков до сотен тысяч в зависимости от того, как настроен сервер.

В некоторых случаях, используя рекомендательные методы блокировки, особенно в запросах, включающих явное упорядочение и предложения LIMIT, необходимо соблюдать осторожность, чтобы контролировать блокировки, полученные из-за порядка, в котором оцениваются выражения SQL. Например:

SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; -- ok
SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; -- danger!
SELECT pg_advisory_lock(q.id) FROM
(
  SELECT id FROM foo WHERE id > 12345 LIMIT 100
) q; -- ok

В приведенных выше запросах вторая форма опасна, поскольку не гарантируется применение LIMIT до выполнения функции блокировки. Это может привести к получению некоторых блокировок, которые приложение не ожидает, и, следовательно, не сможет освободить (пока не завершится сеанс). С точки зрения приложения, такие блокировки были бы висячими, хотя все еще видимыми в pg_locks.

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

Проверка согласованности данных на уровне приложения

Очень сложно обеспечить соблюдение бизнес-правил в отношении целостности данных с использованием транзакций Read Committed, поскольку представление данных смещается с каждым оператором, и даже один оператор не может ограничиваться снимком оператора, если возникает конфликт записи.

Хотя транзакция Repeatable Read имеет стабильное представление данных на протяжении всего своего выполнения, существует небольшая проблема с использованием моментальных снимков MVCC для проверок согласованности данных, связанных с так называемыми конфликтами чтения / записи. Если одна транзакция записывает данные, а параллельная транзакция пытается прочитать те же данные (до или после записи), она не может увидеть работу другой транзакции. Читатель тогда, кажется, выполнил сначала независимо от того, который начался первым или который совершил первым. Если это так далеко, это не проблема, но если считыватель также записывает данные, которые считываются параллельной транзакцией, то теперь есть транзакция, которая, кажется, выполнялась перед любой из ранее упомянутых транзакций. Если транзакция, которая, по-видимому, выполнила последнюю, на самом деле фиксируется первой, цикл очень легко отобразить на графике порядка выполнения транзакций. Когда появляется такой цикл, проверки целостности не будут работать правильно без посторонней помощи.

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

Обеспечение согласованности с сериализуемыми транзакциями

Если уровень изоляции Сериализуемой транзакции используется для всех записей и для всех операций чтения, которые требуют согласованного просмотра данных, никаких других усилий не требуется для обеспечения согласованности. Программное обеспечение из других сред, написанное для использования сериализуемых транзакций для обеспечения согласованности, должно « просто работать » в этом отношении в QHB .

При использовании этого метода, это позволит избежать ненужной нагрузки для прикладных программистов, если прикладное программное обеспечение проходит через среду, которая автоматически повторяет транзакции, которые откатываются с ошибкой сериализации. Это может быть хорошей идеей, чтобы установить default_transaction_isolation в serializable. Также было бы целесообразно предпринять некоторые действия, чтобы гарантировать, что никакой другой уровень изоляции транзакции не используется, либо непреднамеренно, либо для подрыва проверок целостности, посредством проверок уровня изоляции транзакции в триггерах.

Смотрите главу Подзапросы для предложений по производительности.

!!! Предупреждение

Этот уровень защиты целостности с использованием сериализуемых
транзакций еще не распространяется на режим горячего резервирования.
Из-за этого те, кто использует горячее резервирование,
могут хотеть использовать *Repeatable Read* и явную блокировку на главном.

Обеспечение согласованности с помощью явных блокирующих блокировок

Когда возможны несериализуемые записи, чтобы обеспечить текущую достоверность строки и защитить ее от одновременных обновлений, необходимо использовать SELECT FOR UPDATE, SELECT FOR SHARE или соответствующую инструкцию LOCK TABLE. ( SELECT FOR UPDATE и SELECT FOR SHARE блокируют только возвращенные строки от одновременных обновлений, в то время как LOCK TABLE блокирует всю таблицу.) Это следует учитывать при переносе приложений на QHB из других сред.

Для тех, кто конвертирует из других сред, также следует отметить тот факт, что SELECT FOR UPDATE не гарантирует, что параллельная транзакция не будет обновлять или удалять выбранную строку. Для этого в QHB вы должны обновить строку, даже если значения не нужно менять. SELECT FOR UPDATE временно блокирует другие транзакции от получения той же блокировки или выполнения UPDATE или DELETE которые могут повлиять на заблокированную строку, но как только транзакция, удерживающая эту блокировку, фиксирует или откатывает назад, заблокированная транзакция продолжит конфликтующую операцию, если только фактическое UPDATE ряда был выполнен, пока замок удерживался.

Глобальные проверки достоверности требуют дополнительного внимания при несериализуемых MVCC. Например, банковское приложение может захотеть проверить, что сумма всех кредитов в одной таблице равна сумме дебетов в другой таблице, когда обе таблицы активно обновляются. Сравнение результатов двух последовательных команд SELECT sum(...) не будет надежно работать в режиме Read Committed, поскольку второй запрос, скорее всего, будет включать результаты транзакций, не учитываемые первой. Выполнение двух сумм в одной повторяемой транзакции чтения даст точную картину только последствий транзакций, совершенных до начала повторяемой транзакции чтения, но можно с полным основанием задаться вопросом, актуален ли ответ к моменту его доставки. Если повторяемая транзакция чтения сама применила некоторые изменения перед попыткой проверки согласованности, полезность проверки становится еще более дискуссионной, поскольку теперь она включает некоторые, но не все изменения после запуска транзакции. В таких случаях осторожный человек может захотеть заблокировать все таблицы, необходимые для проверки, чтобы получить бесспорную картину текущей реальности. Блокировка в режиме SHARE (или выше) гарантирует отсутствие незафиксированных изменений в заблокированной таблице, кроме изменений в текущей транзакции.

Также обратите внимание, что если кто-то полагается на явную блокировку для предотвращения одновременных изменений, следует либо использовать режим Read Committed, либо в режиме Repeatable Read, будьте осторожны, чтобы получить блокировки перед выполнением запросов. Блокировка, полученная повторяемой транзакцией чтения, гарантирует, что никакие другие транзакции, модифицирующие таблицу, все еще не запущены, но если моментальный снимок, видимый транзакцией, предшествует получению блокировки, он может предшествовать некоторым уже зафиксированным изменениям в таблице. Снимок повторяющейся транзакции чтения фактически замораживается в начале первого запроса или команды изменения данных ( SELECT, INSERT, UPDATE или DELETE), поэтому можно явно получить блокировки до того, как моментальный снимок будет заморожен.

Ограничения

Некоторые команды DDL, в настоящее время только TRUNCATE и формы переписывания таблиц ALTER TABLE, не являются безопасными для MVCC. Это означает, что после фиксации усечения или перезаписи таблица будет отображаться пустой для одновременных транзакций, если они используют снимок, сделанный до принятия команды DDL. Это будет проблемой только для транзакции, которая не обращалась к рассматриваемой таблице до запуска команды DDL - любая сделавшая транзакция содержала бы по меньшей мере блокировку таблицы ACCESS SHARE, которая блокировала бы команду DDL до завершения этой транзакции. Таким образом, эти команды не вызовут каких-либо явных несоответствий в содержании таблицы для последовательных запросов к целевой таблице, но они могут вызвать видимое несоответствие между содержимым целевой таблицы и другими таблицами в базе данных.

Поддержка уровня изоляции Serializable транзакций еще не была добавлена к целям репликации с горячим резервированием. Самым строгим уровнем изоляции, поддерживаемым в настоящее время в режиме горячего резервирования, является Repeatable Read. Хотя выполнение всех постоянных операций записи в базу данных в рамках сериализуемых транзакций на главном сервере гарантирует, что все резервные серверы в конечном итоге достигнут согласованного состояния, транзакция повторного чтения, выполняемая в режиме ожидания, может иногда видеть переходное состояние, несовместимое с любым последовательным выполнением транзакций на мастере.

Блокировка и индексы

Хотя QHB обеспечивает блокирующий доступ на чтение / запись к данным таблицы, блокирующему доступ на чтение / записи в настоящее время не предлагаются для каждого метода доступа индекса осуществляется в QHB. Различные типы индексов обрабатываются следующим образом:

ИндексыОписание
ИндексыКраткосрочные блокировки / эксклюзивные блокировки на уровне страниц используются для доступа на чтение / запись. Блокировки снимаются сразу после извлечения или вставки каждой строки индекса. Эти типы индексов обеспечивают максимальный параллелизм без условий взаимоблокировки.
Хеш-индексыСовместно используемые / эксклюзивные блокировки на уровне хеш-сегмента используются для доступа на чтение / запись. Замки снимаются после обработки всего ведра. Блокировки уровня сегмента обеспечивают лучший параллелизм, чем блокировки уровня индекса, но возможна взаимоблокировка, так как блокировки удерживаются дольше, чем одна операция индекса.
GIN индексыКраткосрочные блокировки / эксклюзивные блокировки на уровне страниц используются для доступа на чтение / запись. Блокировки снимаются сразу после извлечения или вставки каждой строки индекса. Но обратите внимание, что вставка GIN-индексированного значения обычно приводит к нескольким вставкам индексного ключа на строку, поэтому GIN может выполнять существенную работу для вставки одного значения.

В настоящее время B-деревья предлагают лучшую производительность для параллельных приложений; поскольку они также имеют больше возможностей, чем хеш-индексы, они являются рекомендуемым типом индекса для параллельных приложений, которым необходимо индексировать скалярные данные. При работе с нескалярными данными B-деревья бесполезны, и вместо них следует использовать индексы GiST, SP-GiST или GIN.

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

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

Расширение SQL

В следующих разделах мы обсудим, как вы можете расширить язык запросов SQL, добавив:

Как работает расширяемость

QHB хорошо расширяем, потому что его работа основана на каталоге. Если вы знакомы со стандартными системами реляционных баз данных, вы знаете, что они хранят информацию о базах данных, таблицах, столбцах и т. д. в так называемых системных каталогах (иногда называемых словарём базы данных). Каталоги доступны пользователю в виде таблиц, похожих на любые другие таблицы, но СУБД хранит в них свою внутреннюю бухгалтерию. Одним из ключевых отличий между QHB и большинством систем баз данных является то, что QHB хранит гораздо больше информации в своих каталогах: не только информацию о таблицах и столбцах, но также информацию о всех существующих типах данных, функциях, методах доступа и так далее. Эти таблицы могут быть изменены пользователем, а поскольку QHB основывает свою работу на этих таблицах, то это означает, что QHB может быть расширен пользователями. Для сравнения, большинство систем баз данных могут быть расширены только путем изменения жестко закодированных процедур в их исходном коде или путем загрузки модулей, специально написанных поставщиком СУБД.

Кроме того, сервер QHB может включать в себя написанный пользователем код посредством динамической загрузки. То есть пользователь может указать файл с исполняемым кодом (например, разделяемую библиотеку), который реализует новый тип или функцию, и QHB загрузит его при необходимости. Код, написанный на SQL, еще проще добавить на сервер. Эта способность изменять свою работу «на лету» делает QHB прекрасно подходящим для быстрого создания прототипов новых приложений и систем хранения.

Система типов QHB

Типы данных QHB можно разделить на базовые, контейнерные, доменные типы и псевдотипы.

Базовые типы

Базовые типы, такие как integer, реализуются ниже уровня языка SQL (на низкоуровневом языке, например, C, Rust). Они обычно соответствуют тому, что называют абстрактными типами данных.

Встроенные базовые типы описаны в главе Типы данных.

Перечисления (enum) могут рассматриваться как подкатегория базовых типов. Но они могут быть созданы с использованием только SQL, без какого-либо низкоуровневого программирования. Обратитесь к разделу Перечисляемые типы за дополнительной информацией.

Контейнерные типы

QHB имеет три «контейнерных» типа, т.е. типа, который содержат внутри себя несколько значений другого типа. Это массивы, составные типы и диапазоны.

Массивы могут содержать несколько значений одного типа. Тип массива создается автоматически для каждого базового, составного, диапазонного и доменного типов. А вот массивов массивов нет. С точки зрения системы типов, многомерные массивы не отличаются от одномерных. Обратитесь к разделу Массивы за дополнительной информацией.

Составные типы(composite type), или кортежи — такой тип имеет одна строка таблицы. Для каждой таблицы автоматически создаётся тип её строки (его имя совпадает с именем таблицы), но составной тип может существовать и без привязки к таблице, его можно создать командой CREATE TYPE. Составной тип — это набор именованых полей некоторых других типов. Значением составного типа является строка или кортеж. Обратитесь к разделу Составные типы за дополнительной информацией.

Диапазонный тип состоит из двух значений одного типа, которые являются нижней и верхней границами диапазона. Диапазонные типы создаются пользователем, хотя существует несколько встроенных. Обратитесь к разделу Диапазонные типы за дополнительной информацией.

Доменные типы

Доменный тип основывается на конкретном базовом типе и во многих случаях взаимозаменяем с его базовым типом. Однако у домена могут быть ограничения, ограничивающие его допустимые значения подмножеством того, что допускает базовый тип. Домены создаются с помощью команды SQL CREATE DOMAIN. Обратитесь к разделу Доменные типы за дополнительной информацией.

Псевдо-типы

Есть несколько «псевдотипов» для специальных целей. Псевдотипы не могут быть столбцами таблиц или компонентами контейнерных типов, но их можно использовать для объявления аргументов и типов результатов функций. Это обеспечивает механизм в системе типов для идентификации особенного поведения функций. В таблице 27 перечислены существующие псевдотипы.

Полиморфные типы

Пять псевдотипов, представляющих особый интерес: anyelement, anyarray, anynonarray, anyenum и anyrange, которые в совокупности называются полиморфными типами. Любая функция, объявленная с использованием этих типов, называется полиморфной функцией. Полиморфная функция может работать со многими различными типами данных, причем конкретный тип выводится из данных, фактически переданных ей в конкретном вызове.

Полиморфные аргументы и результаты разрешаются в конкретный тип данных при разборе запроса, вызывающего полиморфную функцию. Они связаны друг с другом следующим образом: хотя anyelement может быть любого типа, все anyelement в конкретной функции должны быть того же типа. Аргументы и результаты, объявленные как anyarray/anyrange должны быть массивом/диапазоном, базовый тип (тип элементов) которых такой же, как и anyelement других аргументов.

anynonarray обрабатывается точно так же, как anyelement, но добавляет дополнительное ограничение, что фактический тип не должен быть типом массива. anyenum обрабатывается точно так же, как anyelement, но добавляет дополнительное ограничение, что фактический тип должен быть типом enum.

Таким образом, когда несколько аргументов объявлены с полиморфным типом, выбирается только 1 конкретный тип, и все anyelement/anyenum/anynonarray будут этого типа, а все anyarray будут массивами этого типа. Например, функция, объявленная как equal(anyelement, anyelement) будет принимать любые два входных значения, но одинакового типа. Функция equal2(anyelement, anyenum) тоже принимает два аргумента одинакового типа, а значит оба из них должны быть одинаковыми перечислениями (enum).

Если возвращаемое значение функции объявляется как полиморфный тип, должен быть хотя бы один входной аргумента полиморфного типа, чтобы была возможность вывести конкретный тип из входных аргументов при вызове функции. Например, мы можем написать функцию индексирования массива (получение i-го элемента), её заголовок будет таким:

my_subscript(anyarray, integer) returns anyelement

Это объявление требует, чтобы первый аргумент был массивом и позволяет анализатору выводить правильный тип результата из фактического типа первого аргумента. А если объявить функцию как f(anyarray) returns anyenum, то она будет принимать только массивы перечисляемых типов.

Функция с переменным числом аргументов (которая принимает переменное число аргументов, см. Функции SQL с переменным числом аргументов) тоже может быть полиморфной: для этого надо объявить ее последний параметр как VARIADIC anyarray. При сопоставлении аргументов и определения фактического типа такая функция ведет себя так же, как если бы вы записали соответствующее число параметров типа anynonarray.

Пользовательские функции

В QHB есть четыре вида функций:

Каждый тип функции может принимать базовые типы, составные типы или их комбинации в качестве аргументов (параметров). Кроме того, каждый тип функции может возвращать базовый тип, или составной тип, или множество базовых/составных типов (т.е. много строк, фактически таблицу).

Многие виды функций могут принимать или возвращать определенные псевдотипы (например, полиморфные типы); поддержка псевдотипов зависит от языка программирования, на котором написана функция, обратитесь к описанию каждого вида функций для более подробной информации.

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

Пользовательские процедуры

Процедура — это объект базы данных, похожий на функцию. Различия следующие:

  • процедура не может возвращать значения (функция тоже может не возвращать, если не хочет);

  • функции вызываются как часть запроса или вообще любого DML, а процедура вызывается явно с помощью оператора CALL, также есть API для вызова именно процедуры, отдельное от API для выполнения произвольного DML;

  • в процедуре можно начать/зафиксировать/откатить транзакцию;

  • кроме этих моментов, функции можно использовать вместо процедур: в QHB функции могут иметь почти любые побочные эффекты.

Рекомендации по созданию пользовательских функций в оставшейся части этой главы также применимы к процедурам, за исключением того, что надо использовать команду CREATE PROCEDURE, нет возвращаемого значения и некоторые другие опции, такие как STRICT, неприменимы.

Функции и процедуры вместе также называются рутинами (ROUTINE). Существуют команды ALTER ROUTINE и DROP ROUTINE, которые можно применить, даже когда неизвестно, процедура это или функция. А команды CREATE ROUTINE нет, нужно выбрать, что именно хотите создать.

Функции на языке запросов (SQL)

Функции SQL выполняют произвольный список операторов SQL, возвращая результат последней выборки (SELECT) в списке. Если функция не помечена как возвращающая множество (SETOF), то будет возвращена только первая строка результата последней выборки. (Если не задан ORDER BY, то порядок строк не определён, и первая строка может выбираться недетерминировано.) Если последний запрос вообще не вернул строк, то результатом функции будет NULL. Также последняя команда может быть модификацией (INSERT, UPDATE или DELETE) с указанием RETURNING, опять же будет возвращена одна первая строка.

Второй вариант — функции, возвращающие SETOF sometype или, что тоже самое, TABLE(columns). В этом случае возвращаются все строки результата последней выборки (или модификации с указанием RETURNING). Более подробная информация приведена ниже.

Тело функции SQL должно быть списком операторов SQL, разделенных точкой с запятой. Точка с запятой после последнего оператора является необязательной. Если не объявлено, что функция возвращает void, последним оператором должен быть SELECT или INSERT, UPDATE или DELETE с указанием RETURNING.

Любой набор команд на языке SQL можно собрать вместе и объявить функцией. Помимо выборок SELECT, команды могут включать модификации (INSERT, UPDATE и DELETE), а также другие команды SQL. (В SQL-функциях вы не можете использовать команды управления транзакциями, например, COMMIT, SAVEPOINT и некоторые служебные команды, например, VACUUM). Однако последняя команда должна быть командой SELECT или иметь указание RETURNING, и что вернёт эта команд, то и будет возвращаемым значением функции. Можно сделать функцию, которая что-то делает, но ничего не возвращает, в этом случае объявите её как возвращающую void. Например, эта функция удаляет строки с отрицательными зарплатами из таблицы emp:

CREATE FUNCTION clean_emp() RETURNS void AS '
    DELETE FROM emp
        WHERE salary < 0;
' LANGUAGE SQL;

SELECT clean_emp();

 clean_emp
-----------
(1 row)

Замечание
Тело SQL-функции анализируется целиком, прежде чем выполнить что-либо. Хотя функция SQL может содержать команды, которые изменяют системные каталоги (например, CREATE TABLE), эффекты таких команд не будут видны во время парсинга последующих команд в функции. Так, например, если функция начинается так:
CREATE TABLE foo (...);
INSERT INTO foo VALUES(...);
— она не запустится, потому что она содержит обращение к несуществующей таблице foo, а таблица не создастся, потому что функция невалидна целиком и не запускается. В такой ситуации рекомендуется переписать функцию на PL/pgSQL.

Синтаксис команды CREATE FUNCTION требует, чтобы тело функции было записано как строковая константа. Обычно для строковой константы наиболее удобно использовать знаки доллара (см. раздел Строковые константы с экранированием знаками доллара). Если вы решите использовать обычный синтаксис строковой константы с одинарными кавычками, следует удваивать одинарные кавычки () и обратную косую черту (\) в теле функции (согласно правилам экранирования, см. раздел Строковые константы).

Аргументы для функций SQL

На аргументы функции SQL можно ссылаться в теле функции, используя имена или номера. Примеры обоих методов приведены ниже.

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

В более старом "нумерованном" подходе на аргументы ссылаются с использованием синтаксиса $n; тогда $1 относится к первому входному аргументу, $2 ко второму и т. д. Это будет работать независимо от того, был ли конкретный аргумент объявлен с именем.

Если аргумент имеет составной тип, то к полям можно обратиться через точку (т.н. dot notation), например, argname.fieldname или $1.fieldname. Здесь опять могут быть проблемы с совпадением имени, и может потребоваться указать имя аргумента с именем функции в качестве префикса, чтобы однозначно сослаться на поле аргумента.

Аргументы функции SQL могут использоваться только как значения данных, но не как идентификаторы. Так, например, это разумно:

INSERT INTO mytable VALUES ($1);

а это не сработает

INSERT INTO $1 VALUES (42);

Функции SQL c базовыми типами

Простейшая возможная функция SQL не имеет аргументов и просто возвращает значение базового типа, например, integer:

CREATE FUNCTION one() RETURNS integer AS $$
    SELECT 1 AS rrres;
$$ LANGUAGE SQL;

-- Альтернативный синтаксис строкового литерала:
CREATE FUNCTION one() RETURNS integer AS '
    SELECT 1 AS rrres;
' LANGUAGE SQL;

SELECT one();

 one
-----
   1

Обратите внимание, что внутри функции мы определили псевдоним rrres для столбца результата, но этот псевдоним не виден вне функции, и результат обозначен как one вместо rrres.

SQL-функции, которые принимают базовые типы в качестве аргументов тоже легко писать:

-- Вариант с именованными аргументами:
CREATE FUNCTION add_em(x integer, y integer) RETURNS integer AS $$
    SELECT x + y;
$$ LANGUAGE SQL;

-- Альтернативый вариант с нумерованными аргументами:
CREATE FUNCTION add_em(integer, integer) RETURNS integer AS $$
    SELECT $1 + $2;
$$ LANGUAGE SQL;

SELECT add_em(1, 2) AS answer;

 answer
--------
      3

Вот более полезная функция, которая может использоваться для дебетования банковского счета:

CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS numeric AS $$
    UPDATE bank
        SET balance = balance - debit
        WHERE accountno = tf1.accountno;
    SELECT 1;
$$ LANGUAGE SQL;

Пользователь может выполнить эту функцию для дебетования счета 17 на 100 рублей следующим образом:

SELECT tf1(17, 100.0);

В этом примере мы выбрали имя accountno для первого аргумента, но оно совпадает с именем столбца в таблице bank. В команде UPDATE accountno ссылается на столбец bank.accountno, а для ссылки на аргумент необходимо использовать tf1.accountno. Конечно, мы могли бы избежать этого, использовав другое имя для аргумента.

На практике, вероятно, хотелось бы вернуть из функции что-то более полезное, чем константа 1, поэтому более вероятное определение:

CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS numeric AS $$
    UPDATE bank
        SET balance = balance - debit
        WHERE accountno = tf1.accountno;
    SELECT balance FROM bank WHERE accountno = tf1.accountno;
$$ LANGUAGE SQL;

Функция корректирует баланс и возвращает новый баланс. То же самое можно сделать одной командой, используя RETURNING:

CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS numeric AS $$
    UPDATE bank
        SET balance = balance - debit
        WHERE accountno = tf1.accountno
    RETURNING balance;
$$ LANGUAGE SQL;

Результат последнего SELECT или RETURNING не обязан в точности соответствовать типу результата. QHB может автоматически привести результат к нужному типу, если это можно сделать неявным преобразованием (implicit cast) или присваивающим преобразованием (assignment cast). Либо вы можете использовать явное преобразование типа. Например, предположим, что мы хотим, чтобы предыдущая функция add_em возвращала тип float8. Это не сработает:

CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
    SELECT $1 + $2;
$$ LANGUAGE SQL;

потому что нет неявного преобразования integer во float. Нужно добавить явное преобразование:

CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
    SELECT ($1 + $2)::float8;
$$ LANGUAGE SQL;

Функции SQL с составными типами

При написании функций, работающих с аргументами составных типов, нам придётся обращаться к конкретным полям аргумента. Например, предположим, что emp является таблицей, содержащей данные о сотрудниках и, следовательно, имя составного типа строки таблицы тоже emp. Вот функция double_salary которая вычисляет, какой была бы чья-то зарплата, если её удвоить:

CREATE TABLE emp (
    name        text,
    salary      numeric,
    age         integer,
    cubicle     point
);

INSERT INTO emp VALUES ('Bill', 4200, 45, '(2,1)');

CREATE FUNCTION double_salary(emp) RETURNS numeric AS $$
    SELECT $1.salary * 2 AS salary;
$$ LANGUAGE SQL;

SELECT name, double_salary(emp.*) AS dream
    FROM emp
    WHERE emp.cubicle ~= point '(2,1)';

 name | dream
------+-------
 Bill |  8400

Обратите внимание на использование синтаксиса $1.salary для выбора одного поля из аргумента-строки. Также обратите внимание, как вызывающая команда SELECT использует table_name.*, чтобы взять всю текущую строку таблицы в качестве значения составного типа. На строку таблицы можно сослаться, используя только имя таблицы, например так:

SELECT name, double_salary(emp) AS dream
    FROM emp
    WHERE emp.cubicle ~= point '(2,1)';

но этот альтернативный способ не рекомендуется. Вариант table_name.* лучше читается, сразу видно, что передаётся строка таблицы, а не какая-то колонка. (См. раздел Использование составных типов в запросах для получения подробной информации об этих двух обозначениях для составного значения строки таблицы).

Иногда удобно создавать значение составного аргумента на лету. Это можно сделать с помощью конструктора типа ROW. Например, мы можем подправить данные перед передачей в функцию:

SELECT name, double_salary(ROW(name, salary * 1.1, age, cubicle)) AS dream
    FROM emp;

Можно создать функцию, которая возвращает составной тип. Вот пример функции, которая возвращает одну строку emp:

CREATE FUNCTION new_emp() RETURNS emp AS $$
    SELECT text 'None' AS name,
        1000.0 AS salary,
        25 AS age,
        point '(2,2)' AS cubicle;
$$ LANGUAGE SQL;

В этом примере мы заполняем все поля константами, но вместо них могли быть произвольные вычисления.

Обратите внимание на две важные вещи в реализации функции:

  • Порядок значений в списке выборки должен точно соответствовать порядку полей составного типа (= порядку столбцов в таблице). Алиасы, которые мы дали константам в new_emp(), ни на что не влияют.

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

ERROR:  function declared to return emp returns varchar instead of text at column 1
Как и в случае с базовым типом, функция будет автоматически
применять только неявные и присваивающие преобразования.

Другой способ определить ту же функцию:

CREATE FUNCTION new_emp() RETURNS emp AS $$
    SELECT ROW('None', 1000.0, 25, '(2,2)')::emp;
$$ LANGUAGE SQL;

Здесь мы создали значение анонимного составного типа, а потом привели его к нужному типу emp. В данном примере это ни чем не лучше, но в некоторых случаях это удобная альтернатива — например, если мы возвращаем кортеж, который нам вернула другая функция. Другой пример: если возвращаемое значение нашей функции — доменный тип на основе составного, то мы обязаны сделать выброрку 1 составного значения, а не отдельных полей, т.к. в последнем случае не пройдёт преобразование в доменный тип.

Функцию, возвращающую составное значение, можно использовать как значение, и как таблицу:

-- Кортеж как значение
SELECT new_emp();

         new_emp
--------------------------
 (None,1000.0,25,"(2,2)")

-- Кортеж как таблица
SELECT * FROM new_emp();

 name | salary | age | cubicle
------+--------+-----+---------
 None | 1000.0 |  25 | (2,2)

Второй способ более подробно описан в разделе Использование результата функции как таблицы.

Когда вы вызываете функцию, которая возвращает составной тип, вам может потребоваться взять одно поле из ее результата. Вы можете сделать это с помощью следующего синтаксиса:

SELECT (new_emp()).name;

 name
------
 None

Дополнительные скобки необходимы, чтобы не запутать парсер. Без скобок вы получите что-то вроде этого:

SELECT new_emp().name;
ERROR:  syntax error at or near "."
LINE 1: SELECT new_emp().name;
                        ^

Другой вариант — использовать функциональную нотацию для извлечения поля:

SELECT name(new_emp());

 name
------
 None

Как объяснено в разделе Использование составных типов в запросах, эти две нотации эквивалентны.

Что ещё можно сделать с результатом работы такой функции? Можно передать его в другую функцию, которая как раз ожидает аргумент подходящего составного типа:

CREATE FUNCTION getname(emp) RETURNS text AS $$
    SELECT $1.name;
$$ LANGUAGE SQL;

SELECT getname(new_emp());
 getname
---------
 None
(1 row)

Функции SQL с выходными параметрами

Альтернативный способ описания результатов функции — с помощью выходных параметров, как в этом примере:

CREATE FUNCTION add_em (IN x int, IN y int, OUT sum int)
AS 'SELECT x + y'
LANGUAGE SQL;

SELECT add_em(3,7);
 add_em
--------
     10
(1 row)

Это существенно не отличается от версии add_em показанной в предыдущем разделе. Реальное назначение выходных параметров заключается в том, чтобы предоставлять удобный способ определения функций, которые возвращают несколько значений. Например,

CREATE FUNCTION sum_n_product (x int, y int, OUT sum int, OUT product int)
AS 'SELECT x + y, x * y'
LANGUAGE SQL;

 SELECT * FROM sum_n_product(11,42);
 sum | product
-----+---------
  53 |     462
(1 row)

По сути, мы здесь создали анонимный составной тип для результата функции. Приведенный выше пример имеет тот же конечный результат, что и

CREATE TYPE sum_prod AS (sum int, product int);

CREATE FUNCTION sum_n_product (int, int) RETURNS sum_prod
AS 'SELECT $1 + $2, $1 * $2'
LANGUAGE SQL;

но только без мороки с определением отдельного составного типа. Обратите внимание, что имена выходных параметров определяют имена столбцов анонимного составного типа. (Если вы опустите имя для выходного параметра, система выберет имя самостоятельно).

Также обратите внимание, что выходные параметры не указываются при вызове такой функции из SQL; cнаружи функции выходные параметры ведут себя в точности как возвращаемые значения. Сигнатура функции в QHB определяется только входными параметрами, поэтому, например, вы можете удалить функцию sum_n_product любым из следующих вариантов:

DROP FUNCTION sum_n_product (x int, y int, OUT sum int, OUT product int);
DROP FUNCTION sum_n_product (int, int);

Параметры могут быть помечены как IN (по умолчанию), OUT, INOUT или VARIADIC. INOUT-параметр служит как входным параметром (частью списка входных аргументов), так и выходным параметром (частью кортежа результата). VARIADIC-параметр являются входными параметром, но обрабатываются особым образом, это описано в следующем разделе.

Замечание
Если у функции есть OUT- или INOUT-параметры, то у неё не может быть возвращаемого значения (RETURNS) и наоборот. Это два разных стиля описания возвращаемых значений; OUT-параметры — универсальный способ, а RETURNS — более красивый и наглядный.

Функции SQL с переменным числом аргументов

Функции SQL могут быть объявлены так, чтобы принимать переменное число аргументов, при условии, что все «необязательные» аргументы имеют один и тот же тип. Необязательные аргументы будут переданы функции в виде массива. Для этого последний аргумент функции должен быть массивом и помечен как VARIADIC. Например:

CREATE FUNCTION mleast(VARIADIC arr numeric[]) RETURNS numeric AS $$
    SELECT min($1[i]) FROM generate_subscripts($1, 1) g(i);
$$ LANGUAGE SQL;

SELECT mleast(10, -1, 5, 4.4);
 mleast
--------
     -1
(1 row)

Технически все предоставленные аргументы от позиции VARIADIC и дальше собираются в одномерный массив, как если бы вы написали

SELECT mleast(ARRAY[10, -1, 5, 4.4]);    -- прямо в таком виде не сработает

Однако вы не можете написать так — или, по крайней мере, это не будет соответствовать определению этой функции. VARIADIC-параметр ожидает один или несколько элементов базового типа массива, а не значение-массив.

Иногда полезно иметь возможность передавать уже имеющий массив в функцию с переменным числом аргументов; это особенно удобно, когда одна функция с переменным числом аргументов хочет передать свой переменный аргумент другой такой функции. Это можно сделать, воспользовавшись тем же ключевым словом VARIADIC:

SELECT mleast(VARIADIC ARRAY[10, -1, 5, 4.4]);

Массив, помеченный как VARIADIC может быть только последним аргументом вызова функции, и он сопоставится только с VARIADIC-аргументом функции, передавая массив. Указание VARIADIC в вызове также является единственным способом передачи пустого массива в функцию с переменным числом аргументов:

SELECT mleast(VARIADIC ARRAY[]::numeric[]);

Простое написание SELECT mleast() не работает, поскольку VARIADIC-аргумент должен соответствовать хотя бы одному фактическому значению. (Вы можете определить вторую функцию с именем mleast без параметров, если хотите разрешить такие вызовы).

Замечание по безопасности
Если есть схема в которой несознательные пользователи могут создавать функции, и вы вызываете функцию из этой схемы, и у неё есть VARIADIC-параметр, то всегда вызывайте её с использование синтаксиса VARIADIC ARRAY[...]. Так вы можете быть уверены, что вызываете именно эту функцию (если её не удалят, конечно). Если вы передаёте просто несколько чисел через запятую в VARIADIC-параметр, то злоумышленник может создать функцию с тем же именем, но с конкретным набором параметров, и ваш код начнёт вызывать эту функцию.

Даже если у VARIADIC-параметра есть имя, это имя не просто использовать для сопоставления параметров по именам при вызове функции (см. раздел Вызов функции). Например, следующие варианты все не работают:

SELECT mleast(arr => 10);
SELECT mleast(arr => 10, arr => 20);
SELECT mleast(arr => ARRAY[10, -1, 5, 4.4]);

Единственный работающий вариант опять использует ключевое слово VARIADIC:

SELECT mleast(VARIADIC arr => ARRAY[10, -1, 5, 4.4]);

Функции SQL со значениями аргументов по умолчанию

Функции могут быть объявлены со значениями по умолчанию для некоторых или всех входных аргументов. Значения по умолчанию подставляются, когда функция вызывается с недостаточным количеством фактических аргументов. Поскольку аргументы могут быть пропущены с конца списка, все параметры после параметра со значением по умолчанию также должны иметь значения по умолчанию. (Хотя при использовании связывания аргументов по именам при вызове функции можно задать любое подмножество аргументов, не только с конца, но надо всё-таки, чтобы позиционное задание аргументов тоже работало.)

QHB разрешает создание функций, у которых совпадает набор обязательных входных аргументов, а различается только набор факультативных (имеющих значение по умолчанию)! При вызове неоднозначности разрешаются определённым образом. Из-за этого нужно соблюдать меры предосторожности при вызове функций в базе данных, где несознательные пользователи могут создавать функции.

Примеры:

CREATE FUNCTION foo(a int, b int DEFAULT 2, c int DEFAULT 3)
RETURNS int
LANGUAGE SQL
AS $$
    SELECT $1 + $2 + $3;
$$;

SELECT foo(10, 20, 30);
 foo
-----
  60
(1 row)

SELECT foo(10, 20);
 foo
-----
  33
(1 row)

SELECT foo(10);
 foo
-----
  15
(1 row)

SELECT foo();  -- не работает, потому что для первого аргумента нет умолчания
ERROR:  function foo() does not exist

Вместо ключевого слова DEFAULT можно также использовать =:

CREATE FUNCTION foo(a int, b int = 2, c int = 3)

Использование результата функции как таблицы

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

Вот пример:

CREATE TABLE foo (fooid int, foosubid int, fooname text);
INSERT INTO foo VALUES (1, 1, 'Joe');
INSERT INTO foo VALUES (1, 2, 'Ed');
INSERT INTO foo VALUES (2, 1, 'Mary');

CREATE FUNCTION getfoo(int) RETURNS foo AS $$
    SELECT * FROM foo WHERE fooid = $1;
$$ LANGUAGE SQL;

SELECT *, upper(fooname) FROM getfoo(1) AS t1;

 fooid | foosubid | fooname | upper
-------+----------+---------+-------
     1 |        1 | Joe     | JOE
(1 row)

Как показывает пример, мы можем работать со столбцами результата функции так же, как если бы они были столбцами обычной таблицы.

Обратите внимание, что мы получили только одну строку из функции. Это потому, что мы не использовали SETOF. Это описано в следующем разделе.

Функции SQL, возвращающие множество строк

Когда функция SQL объявляется как возвращающая SETOF sometype, последня выборка функции выполняется полностью, и каждая строка, которую она выводит, возвращается как элемент набора результатов.

Такие функции обычно используется в предложении FROM. В этом случае каждая строка, возвращаемая функцией, становится строкой псевдотаблицы, обрабатываемой запросом. Например, предположим, что таблица foo имеет то же содержимое, что и выше, и мы говорим:

CREATE FUNCTION getfoo(int) RETURNS SETOF foo AS $$
    SELECT * FROM foo WHERE fooid = $1;
$$ LANGUAGE SQL;

SELECT * FROM getfoo(1) AS t1;

fooid | foosubid | fooname
-------+----------+---------
    1 |        1 | Joe
    1 |        2 | Ed
(2 rows)

Если функция возвращает результат посредством выходных параметров, тоже можно вернуть много строк:

CREATE TABLE tab (y int, z int);
INSERT INTO tab VALUES (1, 2), (3, 4), (5, 6), (7, 8);

CREATE FUNCTION sum_n_product_with_tab (x int, OUT sum int, OUT product int)
RETURNS SETOF record
AS $$
    SELECT $1 + tab.y, $1 * tab.y FROM tab;
$$ LANGUAGE SQL;

SELECT * FROM sum_n_product_with_tab(10);
 sum | product
-----+---------
  11 |      10
  13 |      30
  15 |      50
  17 |      70
(4 rows)

Ключевым моментом здесь является то, что вы должны написать RETURNS SETOF record, чтобы указать, что функция возвращает несколько строк, а не одну. Если имеется только один выходной параметр, то вместо record напишите его тип.

Часто бывает полезно сконструировать результат выборки, вызывая функцию, возвращающую таблицу, несколько раз, подставляя её аргументы из результатов подзапроса. Предпочтительный способ сделать это - использовать ключевое слово LATERAL, которое описано в разделе Подзапросы LATERAL. Вот пример использования возвращающей таблицу функции для перечисления элементов древовидной структуры:

SELECT * FROM nodes;
   name    | parent
-----------+--------
 Top       |
 Child1    | Top
 Child2    | Top
 Child3    | Top
 SubChild1 | Child1
 SubChild2 | Child1
(6 rows)

CREATE FUNCTION listchildren(text) RETURNS SETOF text AS $$
    SELECT name FROM nodes WHERE parent = $1
$$ LANGUAGE SQL STABLE;

SELECT * FROM listchildren('Top');
 listchildren
--------------
 Child1
 Child2
 Child3
(3 rows)

SELECT name, child FROM nodes, LATERAL listchildren(name) AS child;
  name  |   child
--------+-----------
 Top    | Child1
 Top    | Child2
 Top    | Child3
 Child1 | SubChild1
 Child1 | SubChild2
(5 rows)

Этот пример не делает ничего такого, чего мы не могли бы сделать LATERAL-соединением без функции, но в более сложных случаях, когда для соединения требуются сложные вычисления, удобно поместить их в функцию.

Функции, возвращающие табличные результаты, также можно вызывать в списке выборки. Для каждой строки, которую запрос генерирует сам по себе, вызывается функция, возвращающая множество строк, и для каждого элемента этого множества генерируется выходная строка. Предыдущий пример также можно выполнить с помощью таких запросов:

SELECT listchildren('Top');
 listchildren
--------------
 Child1
 Child2
 Child3
(3 rows)

SELECT name, listchildren(name) FROM nodes;
  name  | listchildren
--------+--------------
 Top    | Child1
 Top    | Child2
 Top    | Child3
 Child1 | SubChild1
 Child1 | SubChild2
(5 rows)

В последнем SELECT обратите внимание, что для Child2, Child3 и т.д. нет выходных строк. Это происходит потому, что listchildren возвращает 0 строк для этих аргументов, поэтому строки результата не генерируются. Это такое же поведение, которое мы имели бы при внутреннем соединении с результатом функции при использовании синтаксиса LATERAL.

Поведение QHB для функции, возвращающей множество в списке выбора запроса, почти такое же, как если бы функция, возвращающая множество, была в LATERAL FROM. Например,

SELECT x, generate_series(1,5) AS g FROM tab;

почти эквивалентно

SELECT x, g FROM tab, LATERAL generate_series(1,5) AS g;

Разница проявляется только в свободе выбора планировщика. Во втором варианте планировщик может поместить g во внешнюю цикл соединения вложенными циклами, поскольку g на самом деле не зависит от tab. Это приведет к другому порядку строк в итоговом результате. Табличные функции в списке выборки всегда будут выполнятся самым внутренним циклом соединения, т.е. результат функции (функций) пробегается до конца, а только потом переходят к следующей строке FROM.

Если в списке выбора запроса несколько функций, возвращающих множество строк, то мы получим не декартово произведение их результатов, как можно было подумать, а совмещение результатов функций по номерам: для каждой строки из базового запроса есть выходная строка, использующая первый результат из каждой функции, затем выходная строка, использующая второй результат из каждой функции и так далее; если результаты какой-то функция кончились раньше, вместо них будут NULL, и так пока не кончится самая "результативная" функция. Для каждой строки из базового запроса будет столько строк, сколько строк в самом большом из результатов функций. Такое поведение аналогично тому, что вы получите, поместив эти функции в один LATERAL ROWS FROM( ... )

Табличные функции могут вызывать друг друга в списке выборки, хотя это не разрешено в секции FROM. В таких случаях каждый уровень вложенности обрабатывается отдельно, генерируя отдельные внутренний цикл, как если бы это был отдельный LATERAL ROWS FROM( ... ). Например, в

SELECT srf1(srf2(x), srf3(y)), srf4(srf5(z)) FROM tab;

функции возврата srf2, srf3 и srf5 будут запускаться в первом шаге для каждой строки tab, а затем для каждой строки того, что получилось, будут запускаться srf1 и srf4.

Функции, возвращающие множество, нельзя использовать в конструкциях условного вычисления, таких как CASE или COALESCE. Например, рассмотрим

SELECT x, CASE WHEN x > 0 THEN generate_series(1, 5) ELSE 0 END FROM tab;

Может показаться, что это должно привести к пяти повторениям входных строк, которые имеют x > 0, и одному повторению прочих; но на самом деле, поскольку generate_series(1, 5) будет выполняться в неявном элементе LATERAL FROM до того, как выражение CASE начнёт вычислять, запрос выдаст пять повторений каждой строки из tab. Чтобы избежать путаницы, такие конструкции запрещены, и вызывают ошибку разбора.

Заметка
Если последней командой функции является INSERT, UPDATE или DELETE с RETURNING, эта команда всегда будет выполняться до конца, даже если функция не объявлена с помощью SETOF или вызывающий запрос не извлечёт все строки результата (например, у него есть LIMIT). Любые дополнительные строки, созданные указанием RETURNING отбрасываются, но изменения в заданной таблице производятся (и все завершаются до возврата из функции).

Функции SQL, возвращающие таблицы

Есть еще один способ объявить функцию как возвращающую набор: использовать синтаксис RETURNS TABLE(columns). Это эквивалентно использованию одного или нескольких OUT-параметров плюс маркировке функции как возвращающей SETOF record (или, если столбец единственный, SETOF тип-столбца). Нотация RETURNS TABLE(columns) указана в последних версиях стандарта SQL и, следовательно, может быть более переносимой, чем использование SETOF.

Например, предыдущий пример суммы и произведения может быть переписан так:

CREATE FUNCTION sum_n_product_with_tab (x int)
RETURNS TABLE(sum int, product int) AS $$
    SELECT $1 + tab.y, $1 * tab.y FROM tab;
$$ LANGUAGE SQL;

Не допускается использование явных параметров OUT или INOUT с указанием RETURNS TABLE, надо выбрать один из вариантов нотации.

Полиморфные функции SQL

SQL-функции могут принимать и возвращать полиморфные типы anyelement, anyarray, anynonarray, anyenum и anyrange, обсуждавшиеся в разделе Полиморфные типы. Вот пример полиморфной функции make_array, которая создает массив из двух элементов произвольного типа данных:

CREATE FUNCTION make_array(anyelement, anyelement) RETURNS anyarray AS $$
    SELECT ARRAY[$1, $2];
$$ LANGUAGE SQL;

SELECT make_array(1, 2) AS intarray, make_array('a'::text, 'b') AS textarray;
 intarray | textarray
----------+-----------
 {1,2}    | {a,b}
(1 row)

Обратите внимание на использование приведения типа 'a'::text чтобы указать, что аргумент имеет тип text. Это необходимо, потому что строковый литерал по умолчанию не относится к какому-то конкретному строковому типу (имеет тип unknown). Без приведения типа вы получите такую ошибку:

ERROR:  could not determine polymorphic type because input has type "unknown"

Разрешается иметь полиморфные аргументы и фиксированный тип результата, но не наоборот:

CREATE FUNCTION is_greater(anyelement, anyelement) RETURNS boolean AS $$
    SELECT $1 > $2;
$$ LANGUAGE SQL;

SELECT is_greater(1, 2);
 is_greater
------------
 f
(1 row)

CREATE FUNCTION invalid_func() RETURNS anyelement AS $$
    SELECT 1;
$$ LANGUAGE SQL;
ERROR:  cannot determine result data type
DETAIL:  A function returning a polymorphic type must have at least one polymorphic argument.

Функции с выходными аргументами вполне могут использовать полиморфизм:

CREATE FUNCTION dup (f1 anyelement, OUT f2 anyelement, OUT f3 anyarray)
AS 'select $1, array[$1,$1]' LANGUAGE SQL;

SELECT * FROM dup(22);
 f2 |   f3
----+---------
 22 | {22,22}
(1 row)

Полиморфизм может также использоваться функциями с переменным числом аргументов:

CREATE FUNCTION anyleast (VARIADIC anyarray) RETURNS anyelement AS $$
    SELECT min($1[i]) FROM generate_subscripts($1, 1) g(i);
$$ LANGUAGE SQL;

SELECT anyleast(10, -1, 5, 4);
 anyleast
----------
       -1
(1 row)

SELECT anyleast('abc'::text, 'def');
 anyleast
----------
 abc
(1 row)

CREATE FUNCTION concat_values(text, VARIADIC anyarray) RETURNS text AS $$
    SELECT array_to_string($2, $1);
$$ LANGUAGE SQL;

SELECT concat_values('|', 1, 4, 2);
 concat_values
---------------
 1|4|2
(1 row)

Функции SQL и правила сортировки

Когда SQL-функция имеет один или несколько параметров такого типа, для которого применимы правила сортировки (COLLATION), правила сортировки выбираются при каждом вызове на основании фактических аргументов функции. Алгоритм выбора описан в разделе Поддержка сортировки. Если правила сортировки успешно выбраны (т.е. нет конфликтов между неявно установленными правилами сортировки аргументов), то все параметры функции считаются имеющими такое правило сортировки, и обрабатываются соответственно. Это повлияет на поведение операций, связанных со сравнением и сортировкой, внутри функции. Например, для функции anyleast описанной выше, результат

SELECT anyleast('abc'::text, 'ABC');

будет зависеть от правил сортировки базы данных по умолчанию. В локали C результат будет ABC, но во многих других локалях это будет abc.

Правило сортировки можно установить принудительно при вызове функции, добавив указание COLLATE к любому из аргументов, например:

SELECT anyleast('abc'::text, 'ABC' COLLATE "C");

Либо, если вы хотите, чтобы функция работала по конкретным правилам, независимо от свойств аргументов, можно задавать правила сортировки внутри функции: при выполнении операции, а не при объявлении аргумента. Следующая версия anyleast всегда будет использовать локаль en_US для сравнения строк:

CREATE FUNCTION anyleast (VARIADIC anyarray) RETURNS anyelement AS $$
    SELECT min($1[i] COLLATE "en_US") FROM generate_subscripts($1, 1) g(i);
$$ LANGUAGE SQL;

К сожалению, при вызове такой функций для не текстового типа будет ошибка при попытке применить к нему COLLATE...

Если среди фактических аргументов не могут быть выбраны общие правила сортировки, то параметры будут считаться имеющими правила сортировки по умолчанию для своих типов данных (которые обычно являются правилами сортировки по умолчанию базы данных, но могут отличаться для параметров доменных типов).

Поведение сопоставляемых параметров можно рассматривать как ограниченную форму полиморфизма, применяющуюся только к текстовым типам данных.

Перегрузка функций

С одним и тем же SQL-именем может быть определено несколько функций, если они принимают разные наборы входных аргументов. Другими словами, имена функций могут быть перегружены.

Это требует дополнительных мер предосторожности при работе в базах данных, где несознательные пользователи могут создавать функции. Например, вы можете вызвать совсем другую функцию, ошибившись в типе аргумента.

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

При создании семейства перегруженных функций следует соблюдать осторожность, чтобы не создавать двусмысленности выбора. Например, если есть функции

CREATE FUNCTION test(int, real) RETURNS ...
CREATE FUNCTION test(smallint, double precision) RETURNS ...

то не понятно, какая функция вызовется при тривиальном запросе SELECT test(1, 1.5).

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

Имя функции, принимающей один аргумент составного типа, не должно совпадать с одним из полей этого составного типа! Дело в том, что если кортеж типа T имеет поле attribute, то attribute(t) означает тоже самое, что и t.attribute. Если вы создадите свою функцию attribute(T), то вместо вашей функции будет молча вызываться t.attribute. Пользовательскую функцию всё-таки можно вызвать, используя квалифицированное имя schema.attribute(t), но лучше в такое не ввязываться.

Другой возможный конфликт — между вариативной и невариативной функциями. Например, можно создать foo(numeric) и foo(VARIADIC numeric[]). В этом случае неясно, какая из них должна вызываться в случае foo(10.1). Правило состоит в том, что используется функция, появляющаяся ранее в пути поиска, или, если две функции находятся в одной и той же схеме, выбирается невариативная.

При перегрузке функций на нативном языке существует дополнительное ограничение: имена связывания нативных функций не должны повторяться, и не должны совпадать с именами внутренних функций QHB, иначе это приведёт к неоднозначности при вызове функции, получится платформозависимое поведение: вы либо получите ошибку подгрузки библиотеки во время работы, либо вызовется не та функция (при совпадение с именем внутренней функции, скорее всего, вызовется внутренняя). По умолчанию имя связывания совпадает с именем функции, но это можно переопределить альтернативной формой указания AS команды CREATE FUNCTION:

CREATE FUNCTION test(int) RETURNS int
    AS 'filename', 'test_1arg'
    LANGUAGE C;
CREATE FUNCTION test(int, int) RETURNS int
    AS 'filename', 'test_2arg'
    LANGUAGE C;

test_2arg — имя нативной функции; такой формат имени — лишь одно из многих возможных соглашений, нет чётких правил на этот счёт.

Категории изменчивости функций

Каждая функция имеет категорию изменчивости с возможными вариантами VOLATILE, STABLE или IMMUTABLE. VOLATILE является значением по умолчанию, если в CREATE FUNCTION не указать явно другую категорию. Категория изменчивости — это обещания оптимизатору касательно поведения функции:

  • Функция VOLATILE может делать все, что угодно, включая изменение базы данных. Она может возвращать разные результаты при последовательных вызовах с одинаковыми аргументами. Оптимизатор не делает никаких предположений о поведении таких функций. Запрос, использующий изменчивую функцию, выполняет функцию для каждой строки, каждый раз, где она встречается.

  • Функция STABLE не может изменить данные и гарантированно возвращает одинаковые результаты при одинаковых входных значения — в рамках одного запроса. Эта категория позволяет оптимизатору заменить несколько вызовов функции с одними и теми же аргументами на один вызов. В частности, если стабильная функция содержится в "правой части" условия фильтрации, то можно вычислить функцию один раз, и потом искать результат в индексе. (Для изменчивых функций это недопустимо, потому что значение условия потенциально может меняться, и надо его вычислять для каждой строки).

  • Функция IMMUTABLE не может изменять данные и гарантированно будет возвращать одни и те же результаты, если всегда будут одни и те же аргументы. Эта категория позволяет оптимизатору предварительно оценить функцию, когда запрос вызывает ее с постоянными аргументами. Например, запрос типа SELECT ... WHERE x = 2 + 2 может быть упрощен на вид до SELECT ... WHERE x = 4, потому что функция, лежащая в основе оператора сложения целых чисел, помечена как IMMUTABLE.

Для достижения наилучших результатов оптимизации вы должны пометить свои функции категорией самой строгой изменчивости, какая для них выполняется.

Любая функция с побочными эффектами должна быть помечена как VOLATILE, чтобы оптимизатор не мог убрать ее вызов. Даже функция без побочных эффектов должна быть помечена как VOLATILE если ее значение может измениться в течение выполнения одного запроса: например, random(), currval(), timeofday(). Другим важным примером является то, что семейство функций current_timestamp квалифицируется как STABLE, поскольку их значения не изменяются в пределах транзакции.

Разница между категориями STABLE и IMMUTABLE почти незаметна, если рассуждать о простых интерактивных запросах, которые планируются и выполняются немедленно: не имеет большого значения, выполняется ли функция один раз во время планирования или один раз во время запуска запроса. Разница, если план будет сохранён и использован позже. Обозначение функции IMMUTABLE, когда это неправда, может привести к преждевременной оптимизации вызова на этапе планирования, а потом к повторному использованию устаревшего значения при использовании плана. Это опасно при использовании подготовленных операторов или внутри процедур, план которых кешируется (например, процедуры на PL/pgSQL).

Для функций, написанных на SQL или на одном из стандартных процедурных языков, существует еще одно важное свойство, определяемое категорией изменчивости, а именно видимость любых изменений данных, которые уже были внесены командой SQL, вызывающей функцию. Функция VOLATILE увидит такие изменения, функция STABLE или IMMUTABLE нет. Это поведение реализовано с использованием моментальных снимков MVCC: функции STABLE и IMMUTABLE используют моментальный снимок, созданный в начале вызывающего запроса, тогда как функции VOLATILE получают новый снимок в начале запроса, который они выполняют внутри себя.

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

Из-за такого поведения моментального снимка функция, результат которой зависит от данных в таблицах, может считаться стабильной, даже если она берёт данные из таблиц, которые могут правиться параллельно выполняющимися запросами (если, конечно, нет других причин, почему она нестабильная). QHB будет выполнять все команды STABLE-функции, используя снимок, созданный для вызывающего запроса, и, таким образом, функция будет видеть фиксированное состояние базы данных в течение всего этого запроса.

Такое же поведение снимка используется для IMMUTABLE-функций. Однако, если результат вашей функции зависит от содержимого таблиц, то она, по-видимому, не IMMUTABLE! (Результат изменится, когда поменяют содержимое таблицы). Однако QHB не запретит вам объявить такую функцию IMMUTABLE.

Распространенной ошибкой является пометка функции IMMUTABLE когда ее результаты зависят от параметра конфигурации. Например, функция, которая манипулирует временем, вполне может выдавать результаты, которые зависят от настройки TimeZone. В целях безопасности такие функции должны быть помечены как STABLE.

Замечание
QHB проверяет, чтобы функции, помеченные STABLE или IMMUTABLE, не содержали никаких команд SQL, кроме SELECT, т.к. функции, меняющие данные явно нестабильные. Однако не проверят и не предотвращает вызвать изменчивую функцию (формально, если функция вызывает изменчивую, то она тоже волатильная). Если вы попробуете сделать это, то заметите, что изменения данных, сделанные вызванной волатильной функцией, не видны внешней стабильной функции: она работает с моментальным снимком базы.

Функции на процедурном языке

QHB позволяет писать пользовательские функции на различных интерпретируемых("скриптовых") языках. Эти языки обычно называются процедурными языками (PL). Процедурные языки не встроены в сервер QHB; они реализованы в подгружаемых модулях. См. главу Процедурные языки и последующие главы для получения дополнительной информации.

Внутренние функции

Внутренние функции - это функции, написанные на C или на Rust, которые статически скомпонованы в сервер QHB. Определение функции состоит из её имени на C, которое не обязательно совпадает с именем, объявленным для использования в SQL. (Из соображений обратной совместимости пустое "тело" функции воспринимается как то, что имя функции на C совпадает с SQL-именем).

Обычно все внутренние функции, присутствующие на сервере, объявляются во время инициализации кластера базы данных (см. раздел Создание кластера базы данных), но пользователь может использовать CREATE FUNCTION для создания дополнительного псевдонима внутренней функции. Внутренние функции объявляются в CREATE FUNCTION с LANGUAGE internal. Например, создадим псевдоним для функции sqrt:

CREATE FUNCTION square_root(double precision) RETURNS double precision
    AS 'dsqrt'
    LANGUAGE internal
    STRICT;

Большинство внутренних функций «строгие» (STRICT).

Замечание
Не все предопределенные функции являются «внутренними» в вышеописанном смысле. Некоторые предопределенные функции написаны на SQL.

Функции на нативном языке

Пользовательские нативные функции могут быть написаны на C, Rust, С++ или других компилируемых unmanaged языках. Функции должны следовать соглашению о вызове C ("extern C", все означенные языки имеют возможность следовать такому соглашению) и соглашению QHB об именах функций, экспортируемых из библиотек. Это соглашение об именах называется "Version 1", и для разработки на языке C предоставляется макрос PG_FUNCTION_INFO_V1() для правильного оформления вашей функции (см. ниже).

Пользовательские функции компилируются в динамически загружаемые объекты (также называемые разделяемыми библиотеками) и загружаются сервером по требованию. Функция динамической загрузки — это то, что отличает «нативные» функции от «внутренних», а сами функции устроены одинаково.

Динамическая загрузка

При первом вызове пользовательской функции в конкретном загружаемом объектном файле в сеансе динамический загрузчик загружает этот объектный файл в память, чтобы вызвать функцию. Поэтому функция CREATE FUNCTION для пользовательской функции C должна указывать две части информации для функции: имя загружаемого объектного файла и C-имя (символ ссылки) конкретной функции, вызываемой в этом объектном файле. Если C-имя не указано явно, предполагается, что оно совпадает с именем функции SQL.

Следующий алгоритм используется для поиска файла общего объекта на основе имени, указанного в команде CREATE FUNCTION:

  1. Если имя является абсолютным путем, данный файл загружается.

  2. Если имя начинается со строки $libdir, эта часть заменяется именем каталога библиотеки пакетов QHB, которое определяется во время сборки.

  3. Если имя не содержит часть каталога, поиск файла производится по пути, указанному в переменной конфигурации dynamic_library_path, см. dynamic_library_path.

  4. В противном случае (файл не найден в пути или он содержит не абсолютную часть каталога), динамический загрузчик попытается принять имя, как указано, что, скорее всего, не удастся. (Ненадежно зависеть от текущего рабочего каталога).

Если эта последовательность не работает, к указанному имени добавляется расширение имени файла общей библиотеки для конкретной платформы (часто .so), и эта последовательность повторяется. Если это также не удается, загрузка не удастся.

Рекомендуется размещать разделяемые библиотеки относительно $libdir или по пути динамической библиотеки. Это упрощает обновление версий, если новая установка находится в другом месте. Действительный каталог, $libdir стоит $libdir можно узнать с помощью команды pg_config --pkglibdir.

Идентификатор пользователя, на котором работает сервер QHB, должен проходить путь к файлу, который вы собираетесь загрузить. Создание файла или каталога более высокого уровня нечитабельным и/или не исполняемым пользователем qhb является распространенной ошибкой.

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

Заметка
QHB не будет компилировать функцию C/Rust автоматически. Объектный файл должен быть скомпилирован перед тем, как на него будет ссылаться команда CREATE FUNCTION. См. раздел Компиляция и связывание динамически загружаемых функций для получения дополнительной информации.

Чтобы гарантировать, что динамически загружаемый объектный файл не загружается на несовместимый сервер, QHB проверяет, что файл содержит «магический блок» с соответствующим содержимым. Это позволяет серверу обнаруживать очевидные несовместимости, такие как код, скомпилированный для другой основной версии QHB. Чтобы включить магический блок, запишите это в один (и только один) из исходных файлов модуля после включения заголовка fmgr.h:

PG_MODULE_MAGIC;

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

При желании динамически загружаемый файл может содержать функции инициализации и финализации. Если файл содержит функцию с именем _PG_init, эта функция будет вызываться сразу после загрузки файла. Функция не получает параметров и должна возвращать void. Если файл содержит функцию с именем _PG_fini, эта функция будет вызвана непосредственно перед выгрузкой файла. Аналогично, функция не получает параметров и должна возвращать void. Обратите внимание, что _PG_fini будет вызываться только во время выгрузки файла, а не во время завершения процесса. (В настоящее время выгрузки отключены и никогда не произойдут, но это может измениться в будущем).

Базовые типы в функциях языка C/RUST

Чтобы знать, как писать функции на языке C/RUST, вам необходимо знать, как QHB внутренне представляет базовые типы данных и как их можно передавать в функции и из функций. Внутри QHB рассматривает базовый тип как «блок памяти». Пользовательские функции, которые вы определяете для типа, в свою очередь, определяют способ работы QHB с ним. То есть QHB будет хранить и извлекать данные только с диска и использовать ваши пользовательские функции для ввода, обработки и вывода данных.

Базовые типы могут иметь один из трех внутренних форматов:

  • передача по значению с фиксированной длиной

  • передача по ссылке, фиксированной длины

  • передача по ссылке, переменной длины

Типы по значению могут иметь длину только 1, 2 или 4 байта (также 8 байтов, если sizeof(Datum) равен 8 на вашем сервере). Вы должны быть осторожны, чтобы определить ваши типы так, чтобы они были одинакового размера (в байтах) на всех архитектурах. Например, тип long опасен, потому что на одних машинах он составляет 4 байта, а на других - 8 байтов, тогда как тип int на большинстве машин Unix составляет 4 байта. int4 реализация типа int4 на машинах Unix может быть:

/* 4-byte integer, passed by value */
typedef int int4;

(Фактический код QHB C вызывает этот тип int32, потому что в C существует соглашение, согласно которому int XX означает XX бит . Поэтому обратите внимание также на то, что тип C int8 имеет размер 1 байт. SQL-тип int8 называется int64 в C. См. также таблицу 1).

С другой стороны, типы фиксированной длины любого размера могут передаваться по ссылке. Например, вот пример реализации типа QHB:

/* 16-byte structure, passed by reference */
typedef struct
{
    double  x, y;
} Point;

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

Наконец, все типы переменной длины также должны передаваться по ссылке. Все типы переменной длины должны начинаться с непрозрачного поля длины ровно 4 байта, которое будет установлено SET_VARSIZE; никогда не устанавливайте это поле напрямую! Все данные, которые должны быть сохранены в этом типе, должны быть расположены в памяти сразу после этого поля длины. Поле длины содержит общую длину структуры, то есть включает в себя размер самого поля длины.

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

!!! Предупреждение

Никогда не изменяйте содержимое входного значения передачи по ссылке. Если вы это сделаете, вы, скорее всего, повредите данные на диске, поскольку указанный вами указатель может указывать прямо на буфер диска. Единственное исключение из этого правила объясняется в разделе Пользовательские агрегаты.

В качестве примера мы можем определить тип text следующим образом:

typedef struct {
    int32 length;
    char data[FLEXIBLE_ARRAY_MEMBER];
} text;

Нотация [FLEXIBLE_ARRAY_MEMBER] означает, что фактическая длина части данных не указана в этом объявлении.

При манипулировании типами переменной длины мы должны быть осторожны, чтобы правильно распределить объем памяти и правильно задать поле длины. Например, если мы хотим сохранить 40 байтов в text структуре, мы можем использовать фрагмент кода, подобный этому:

#include "postgres.h"
...
char buffer[40]; /* our source data */
...
text *destination = (text *) palloc(VARHDRSZ + 40);
SET_VARSIZE(destination, VARHDRSZ + 40);
memcpy(destination->data, buffer, 40);
...

VARHDRSZ - это то же самое, что и sizeof(int32), но считается хорошим стилем использовать макрос VARHDRSZ для ссылки на размер служебной информации для типа переменной длины. Кроме того, поле длины должно быть установлено с SET_VARSIZE макроса SET_VARSIZE, а не путем простого присваивания.

Таблица 1 указывает, какой тип C/RUST соответствует какому типу SQL при написании функции на языке C/RUST, использующей встроенный тип QHB. В столбце «Определено в» указан заголовочный файл, который необходимо включить, чтобы получить определение типа. (Фактическое определение может быть в другом файле, который включен в указанный файл. Рекомендуется, чтобы пользователи придерживались определенного интерфейса). Обратите внимание, что вы всегда должны сначала включать postgres.h в любой исходный файл, поскольку он объявляет ряд вещей, которые вам понадобятся в любом случае.

Таблица 1. Эквивалентные типы C для встроенных типов SQL

Тип SQLТип CОпределено в
booleanboolpostgres.h (возможно встроенный компилятор)
boxBOX*utils/geo_decls.h
byteabytea*postgres.h
"char"char(встроенный компилятор)
characterBpChar*postgres.h
cidCommandIdpostgres.h
dateDateADTutils/date.h
smallint ( int2 )int16postgres.h
int2vectorint2vector*postgres.h
integer ( int4 )int32postgres.h
real ( float4 )float4*postgres.h
double precision ( float8 )float8*postgres.h
intervalInterval*datatype/timestamp.h
lsegLSEG*utils/geo_decls.h
nameNamepostgres.h
oidOidpostgres.h
oidvectoroidvector*postgres.h
pathPATH*utils/geo_decls.h
pointPOINT*utils/geo_decls.h
regprocregprocpostgres.h
texttext*postgres.h
tidItemPointerstorage/itemptr.h
timeTimeADTutils/date.h
time with time zoneTimeTzADTutils/date.h
timestampTimestamp*datatype/timestamp.h
varcharVarChar*postgres.h
xidTransactionIdpostgres.h

Теперь, когда мы рассмотрели все возможные структуры для базовых типов, мы можем показать некоторые примеры реальных функций.

Версия 1 Соглашение о вызовах

Соглашение о вызовах версии 1 опирается на макросы, которые подавляют большую часть сложности передачи аргументов и результатов. Объявление C функции версии-1 всегда:

Datum funcname(PG_FUNCTION_ARGS)

Кроме того, вызов макроса:

PG_FUNCTION_INFO_V1(funcname);

должен появиться в том же исходном файле. (Обычно он написан непосредственно перед самой функцией). Этот вызов макроса не требуется для внутренних функций языка, поскольку QHB предполагает, что все внутренние функции используют соглашение версии 1. Это, однако, требуется для динамически загружаемых функций.

В функции версии 1 каждый фактический аргумент выбирается с помощью PG_GETARG_ xxx () который соответствует типу данных аргумента. В нестрогих функциях необходимо предварительно проверить нулевой аргумент, используя PG_ARGNULL_ xxx (). Результат возвращается с использованием PG_RETURN_ xxx () для возвращаемого типа. PG_GETARG_ xxx () принимает в качестве аргумента номер аргумента функции для выборки, где счет начинается с 0. PG_RETURN_ xxx () принимает в качестве аргумента фактическое значение, которое нужно вернуть.

Вот несколько примеров использования соглашения о вызовах версии 1:

#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"

PG_MODULE_MAGIC;

/* by value */

PG_FUNCTION_INFO_V1(add_one);

Datum
add_one(PG_FUNCTION_ARGS)
{
    int32   arg = PG_GETARG_INT32(0);

    PG_RETURN_INT32(arg + 1);
}

/* by reference, fixed length */

PG_FUNCTION_INFO_V1(add_one_float8);

Datum
add_one_float8(PG_FUNCTION_ARGS)
{
    /* The macros for FLOAT8 hide its pass-by-reference nature. */
    float8   arg = PG_GETARG_FLOAT8(0);

    PG_RETURN_FLOAT8(arg + 1.0);
}

PG_FUNCTION_INFO_V1(makepoint);

Datum
makepoint(PG_FUNCTION_ARGS)
{
    /* Here, the pass-by-reference nature of Point is not hidden. */
    Point     *pointx = PG_GETARG_POINT_P(0);
    Point     *pointy = PG_GETARG_POINT_P(1);
    Point     *new_point = (Point *) palloc(sizeof(Point));

    new_point->x = pointx->x;
    new_point->y = pointy->y;

    PG_RETURN_POINT_P(new_point);
}

/* by reference, variable length */

PG_FUNCTION_INFO_V1(copytext);

Datum
copytext(PG_FUNCTION_ARGS)
{
    text     *t = PG_GETARG_TEXT_PP(0);

    /*
     * VARSIZE_ANY_EXHDR is the size of the struct in bytes, minus the
     * VARHDRSZ or VARHDRSZ_SHORT of its header.  Construct the copy with a
     * full-length header.
     */
    text     *new_t = (text *) palloc(VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
    SET_VARSIZE(new_t, VARSIZE_ANY_EXHDR(t) + VARHDRSZ);

    /*
     * VARDATA is a pointer to the data region of the new struct.  The source
     * could be a short datum, so retrieve its data through VARDATA_ANY.
     */
    memcpy((void *) VARDATA(new_t), /* destination */
           (void *) VARDATA_ANY(t), /* source */
           VARSIZE_ANY_EXHDR(t));   /* how many bytes */
    PG_RETURN_TEXT_P(new_t);
}

PG_FUNCTION_INFO_V1(concat_text);

Datum
concat_text(PG_FUNCTION_ARGS)
{
    text  *arg1 = PG_GETARG_TEXT_PP(0);
    text  *arg2 = PG_GETARG_TEXT_PP(1);
    int32 arg1_size = VARSIZE_ANY_EXHDR(arg1);
    int32 arg2_size = VARSIZE_ANY_EXHDR(arg2);
    int32 new_text_size = arg1_size + arg2_size + VARHDRSZ;
    text *new_text = (text *) palloc(new_text_size);

    SET_VARSIZE(new_text, new_text_size);
    memcpy(VARDATA(new_text), VARDATA_ANY(arg1), arg1_size);
    memcpy(VARDATA(new_text) + arg1_size, VARDATA_ANY(arg2), arg2_size);
    PG_RETURN_TEXT_P(new_text);
}

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

CREATE FUNCTION add_one(integer) RETURNS integer
     AS 'DIRECTORY/funcs', 'add_one'
     LANGUAGE C STRICT;

-- note overloading of SQL function name "add_one"
CREATE FUNCTION add_one(double precision) RETURNS double precision
     AS 'DIRECTORY/funcs', 'add_one_float8'
     LANGUAGE C STRICT;

CREATE FUNCTION makepoint(point, point) RETURNS point
     AS 'DIRECTORY/funcs', 'makepoint'
     LANGUAGE C STRICT;

CREATE FUNCTION copytext(text) RETURNS text
     AS 'DIRECTORY/funcs', 'copytext'
     LANGUAGE C STRICT;

CREATE FUNCTION concat_text(text, text) RETURNS text
     AS 'DIRECTORY/funcs', 'concat_text'
     LANGUAGE C STRICT;

Здесь DIRECTORY обозначает каталог файла общей библиотеки (например, каталог учебника по QHB, который содержит код для примеров, используемых в этом разделе). (Лучше было бы использовать просто 'funcs' в предложении AS после добавления DIRECTORY в путь поиска. В любом случае мы можем опустить системное расширение для общей библиотеки, обычно .so ).

Обратите внимание, что мы указали функции как «строгие», что означает, что система должна автоматически принимать нулевой результат, если любое входное значение равно нулю. Делая это, мы избегаем необходимости проверять нулевые входы в коде функции. Без этого нам пришлось бы явно проверять нулевые значения, используя PG_ARGISNULL().

На первый взгляд, соглашения о кодировании в версии 1 могут показаться просто бессмысленным мракобесием по сравнению с использованием простых соглашений о вызовах языка C/RUST. Тем не менее, они позволяют работать с NULL аргументами / возвращаемыми значениями и «поджаренными» (сжатыми или вне строки) значениями.

Макрос PG_ARGISNULL(n) позволяет функции проверять, является ли каждый вход нулевым. (Конечно, делать это необходимо только в функциях, не объявленных как «строгие»). Как и в PG_GETARG_ xxx (), входные аргументы считаются начиная с нуля. Обратите внимание, что следует воздерживаться от выполнения PG_GETARG_ xxx () пока не убедитесь что аргумент не является нулевым. Чтобы вернуть нулевой результат, выполните PG_RETURN_NULL(); это работает как в строгих, так и в нестрогих функциях.

Другие опции, предоставляемые интерфейсом версии 1, - это два варианта PG_GETARG_ xxx (). Первый из них, PG_GETARG_ xxx _COPY(), гарантирует возврат копии указанного аргумента, который является безопасным для записи. (Обычные макросы иногда возвращают указатель на значение, которое физически хранится в таблице, в которую нельзя записывать. Использование PG_GETARG_ xxx _COPY() гарантирует доступный для записи результат). Второй вариант состоит из PG_GETARG_ xxx _SLICE() макроса, который принимает три аргумента. Первый - это номер аргумента функции (как указано выше). Второе и третье - это смещение и длина возвращаемого сегмента. Смещения отсчитываются от нуля, а отрицательная длина требует возврата оставшейся части значения. Эти макросы обеспечивают более эффективный доступ к частям больших значений в том случае, если они имеют тип хранения «внешний». (Тип хранения столбца может быть указан с помощью ALTER TABLE tablename ALTER COLUMN colname SET STORAGE storagetype. storagetype - один из plain, external, extended или main).

Наконец, соглашения о вызовах функций версии 1 позволяют возвращать заданные результаты (раздел Возврат наборов) и реализовывать функции триггера (глава Триггеры)

Написание кода

Прежде чем перейти к более сложным темам, мы должны обсудить некоторые правила кодирования для функций языка C/RUST QHB. Хотя может быть возможно загрузить функции, написанные на языках, отличных от C, в QHB, это обычно сложно (когда это вообще возможно), потому что другие языки, такие как C++, FORTRAN или Pascal, часто не следуют тому же соглашению о вызовах, что и C. То есть другие языки не передают аргумент и возвращают значения между функциями одинаковым образом. По этой причине мы будем предполагать, что ваши функции языка C на самом деле написаны на C/RUST.

Основные правила написания и построения функций C/RUST следующие:

  • Используйте pg_config --includedir-server чтобы узнать, где установлены файлы заголовков сервера QHB в вашей системе (или системе, на которой будут работать ваши пользователи).

  • Компиляция и компоновка вашего кода для его динамической загрузки в QHB всегда требует специальных флагов. См. раздел Компиляция и связывание динамически загружаемых функций для подробного объяснения того, как это сделать для вашей конкретной операционной системы.

  • Не забудьте определить «магический блок» для вашей общей библиотеки, как описано в разделе 6.10.1.

  • При выделении памяти используйте функции QHB palloc и pfree вместо соответствующих библиотек C функции malloc и free. Память, выделенная palloc будет автоматически освобождаться в конце каждой транзакции, предотвращая утечки памяти.

  • Всегда обнуляйте байты ваших структур, используя memset (или выделяйте их сначала с помощью palloc0). Даже если вы назначите каждому полю вашей структуры, могут быть отступы выравнивания (отверстия в структуре), которые содержат значения мусора. Без этого трудно поддерживать хеш-индексы или хеш-объединения, так как для вычисления хеша необходимо выделить только значимые биты вашей структуры данных. Планировщик также иногда полагается на сравнение констант с помощью побитового равенства, поэтому вы можете получить нежелательные результаты планирования, если логически эквивалентные значения не являются побитовыми.

  • Большинство внутренних типов QHB объявлены в postgres.h, в то время как интерфейсы диспетчера функций ( PG_FUNCTION_ARGS и т. д.) Находятся в fmgr.h, поэтому вам нужно будет включить как минимум эти два файла. По причинам переносимости лучше сначала включить postgres.h, а не файлы заголовков других систем или пользователей. В том числе postgres.h также будет включать elog.h и palloc.h для вас.

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

Компиляция и связывание динамически загружаемых функций

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

Для получения информации, выходящей за рамки этого раздела, вам следует прочитать документацию по вашей операционной системе, в частности страницы руководства для компилятора C/RUST, cc и редактора ссылок, ld. Кроме того, исходный код QHB содержит несколько рабочих примеров в каталоге contrib. Однако, если вы будете полагаться на эти примеры, вы сделаете свои модули зависимыми от доступности исходного кода QHB.

Создание общих библиотек обычно аналогично связыванию исполняемых файлов: сначала исходные файлы компилируются в объектные файлы, затем объектные файлы связываются вместе. Объектные файлы должны быть созданы как позиционно-независимый код (PIC), что концептуально означает, что они могут быть размещены в произвольном месте в памяти, когда они загружаются исполняемым файлом. (Объектные файлы, предназначенные для исполняемых файлов, обычно не компилируются таким образом). Команда для связи общей библиотеки содержит специальные флаги, чтобы отличать ее от ссылки на исполняемый файл (по крайней мере, теоретически - в некоторых системах эта практика намного уродливее).

В следующих примерах мы предполагаем, что ваш исходный код находится в файле foo.c и мы создадим общую библиотеку foo.so Промежуточный объектный файл будет называться foo.o если не указано иное. Общая библиотека может содержать более одного объектного файла, но здесь мы используем только один.

Linux

  • Флаг компилятора для создания PIC - -fPIC. Флаг компилятора для создания разделяемой библиотеки - -shared. Полный пример выглядит так:
cc -fPIC -c foo.c
cc -shared -o foo.so foo.o

Полученный файл общей библиотеки затем можно загрузить в QHB . При указании имени файла для команды CREATE FUNCTIONнеобходимо указать имя файла общей библиотеки, а не файла промежуточных объектов. Обратите внимание, что стандартное расширение разделяемой библиотеки системы (обычно .so или .sl ) может быть опущено в команде CREATE FUNCTION и обычно должно быть опущено для лучшей переносимости.

Обратитесь к разделу 6.10.1 о том, где сервер ожидает найти файлы общей библиотеки.

Составные аргументы

Составные типы не имеют фиксированного макета, как структуры C. Экземпляры составного типа могут содержать пустые поля. Кроме того, составные типы, являющиеся частью иерархии наследования, могут иметь поля, отличные от других членов той же иерархии наследования. Следовательно, QHB предоставляет функциональный интерфейс для доступа к полям составных типов из C.

Предположим, мы хотим написать функцию для ответа на запрос:

SELECT name, c_overpaid(emp, 1500) AS overpaid
    FROM emp
    WHERE name = 'Bill' OR name = 'Sam';

Используя соглашения о вызовах версии-1, мы можем определить c_overpaid как:

#include "postgres.h"
#include "executor/executor.h"  /* for GetAttributeByName() */

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(c_overpaid);

Datum
c_overpaid(PG_FUNCTION_ARGS)
{
    HeapTupleHeader  t = PG_GETARG_HEAPTUPLEHEADER(0);
    int32            limit = PG_GETARG_INT32(1);
    bool isnull;
    Datum salary;

    salary = GetAttributeByName(t, "salary", &isnull);
    if (isnull)
        PG_RETURN_BOOL(false);
    /* Alternatively, we might prefer to do PG_RETURN_NULL() for null salary. */

    PG_RETURN_BOOL(DatumGetInt32(salary) > limit);
}

GetAttributeByName - системная функция QHB, которая возвращает атрибуты из указанной строки. Он имеет три аргумента: аргумент типа HeapTupleHeader передаваемый в функцию, имя требуемого атрибута и возвращаемый параметр, который сообщает, является ли атрибут null. GetAttributeByName возвращает значение Datum которое можно преобразовать в правильный тип данных с помощью соответствующего DatumGetXXX(). Обратите внимание, что возвращаемое значение не имеет смысла, если установлен null флаг; всегда проверяйте null флаг, прежде чем пытаться что-либо сделать с результатом.

Существует также GetAttributeByNum, который выбирает целевой атрибут по номеру столбца, а не по имени.

Следующая команда объявляет функцию c_overpaid в SQL:

CREATE FUNCTION c_overpaid(emp, integer) RETURNS boolean
    AS 'DIRECTORY/funcs', 'c_overpaid'
    LANGUAGE C STRICT;

Обратите внимание, что мы использовали STRICT чтобы нам не нужно было проверять, были ли входные аргументы NULL.

Возвращающиеся строки (составные типы)

Чтобы вернуть значение строки или составного типа из функции языка C/RUST, вы можете использовать специальный API, который предоставляет макросы и функции, чтобы скрыть большую часть сложности построения составных типов данных. Чтобы использовать этот API, исходный файл должен включать:

#include "funcapi.h"

Существует два способа создания составного значения данных (далее «кортеж»): вы можете построить его из массива значений Datum или из массива строк C/RUST, которые можно передать во входные функции преобразования столбца кортежа типы данных. В любом случае сначала необходимо получить или создать дескриптор TupleDesc для структуры кортежа. При работе с Datums вы передаете TupleDesc в BlessTupleDesc, а затем вызываете heap_form_tuple для каждой строки. При работе со строками C вы передаете TupleDesc в TupleDescGetAttInMetadata, а затем вызываете BuildTupleFromCStrings для каждой строки. В случае если функция возвращает набор кортежей, все шаги настройки могут быть выполнены один раз во время первого вызова функции.

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

TypeFuncClass get_call_result_type(FunctionCallInfo fcinfo,
                                   Oid *resultTypeId,
                                   TupleDesc *resultTupleDesc)

передача той же самой структуры fcinfo переданной самой вызывающей функции. (Это, конечно, требует использования соглашений о вызовах версии-1). resultTypeId может быть задан как NULL или как адрес локальной переменной для получения OID типа результата функции. resultTupleDesc должен быть адресом локальной переменной TupleDesc. Убедитесь, что результатом является TYPEFUNC_COMPOSITE; если это так, resultTupleDesc был заполнен необходимым TupleDesc. (Если это не так, вы можете сообщить об ошибке в соответствии с «function returning record called in context that cannot accept type record» ).

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

Заметка
get_call_result_type имеет одноуровневый get_expr_result_type, который можно использовать для разрешения ожидаемого выходного типа для вызова функции, представленного деревом выражений. Это может быть использовано при попытке определить тип результата вне самой функции. Существует также get_func_result_type, который можно использовать, когда доступен только OID функции. Однако эти функции не могут работать с функциями, объявленными для возврата record, и get_func_result_type не может разрешать полиморфные типы, поэтому вы должны преимущественно использовать get_call_result_type.

Старые устаревшие функции для получения TupleDesc:

TupleDesc RelationNameGetTupleDesc(const char *relname)

чтобы получить TupleDesc для типа строки именованного отношения, и:

TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)

чтобы получить TupleDesc на основе типа OID. Это может быть использовано для получения TupleDesc для базового или составного типа. Однако она не будет работать для функции, которая возвращает record, и не может разрешать полиморфные типы.

Когда у вас есть TupleDesc, вызовите:

TupleDesc BlessTupleDesc(TupleDesc tupdesc)

если вы планируете работать с Datums, или:

AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)

если вы планируете работать со строками C. Если вы пишете функцию, возвращающую набор, вы можете сохранить результаты этих функций в структуре FuncCallContext - используйте поле tuple_desc или attinmeta соответственно.

При работе с Datums используйте:

HeapTuple heap_form_tuple(TupleDesc tupdesc, Datum *values, bool *isnull)

построить HeapTuple заданных пользовательских данных в форме Datum.

При работе со строками Си используйте:

HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)

построить HeapTuple данных пользовательских данных в форме строки C. values это массив строк C, по одному на каждый атрибут возвращаемой строки. Каждая строка C должна иметь форму, ожидаемую функцией ввода типа данных атрибута. Чтобы вернуть null значение для одного из атрибутов, соответствующий указатель в массиве values должен быть установлен в NULL. Эту функцию нужно будет вызывать снова для каждой возвращаемой строки.

Как только вы построите кортеж для возврата из своей функции, он должен быть преобразован в элемент Datum. Использование:

HeapTupleGetDatum(HeapTuple tuple)

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

Пример появится в следующем разделе.

Возврат наборов

Существует также специальный API, который обеспечивает поддержку возврата наборов (нескольких строк) из функции языка Си. Функция, возвращающая множество, должна следовать соглашениям о вызовах версии-1. Кроме того, исходные файлы должны включать funcapi.h, как указано выше.

Функция возврата набора (SRF) вызывается один раз для каждого возвращаемого элемента. Поэтому SRF должен сохранять достаточно состояния, чтобы помнить, что он делал, и возвращать следующий элемент при каждом вызове. Структура FuncCallContext предназначена для управления этим процессом. Внутри функции fcinfo->flinfo->fn_extra используется для хранения указателя на FuncCallContext во всех вызовах.

typedef struct FuncCallContext
{
    /*
     * Number of times we've been called before
     *
     * call_cntr is initialized to 0 for you by SRF_FIRSTCALL_INIT(), and
     * incremented for you every time SRF_RETURN_NEXT() is called.
     */
    uint64 call_cntr;

    /*
     * OPTIONAL maximum number of calls
     *
     * max_calls is here for convenience only and setting it is optional.
     * If not set, you must provide alternative means to know when the
     * function is done.
     */
    uint64 max_calls;

    /*
     * OPTIONAL pointer to miscellaneous user-provided context information
     *
     * user_fctx is for use as a pointer to your own data to retain
     * arbitrary context information between calls of your function.
     */
    void *user_fctx;

    /*
     * OPTIONAL pointer to struct containing attribute type input metadata
     *
     * attinmeta is for use when returning tuples (i.e., composite data types)
     * and is not used when returning base data types. It is only needed
     * if you intend to use BuildTupleFromCStrings() to create the return
     * tuple.
     */
    AttInMetadata *attinmeta;

    /*
     * memory context used for structures that must live for multiple calls
     *
     * multi_call_memory_ctx is set by SRF_FIRSTCALL_INIT() for you, and used
     * by SRF_RETURN_DONE() for cleanup. It is the most appropriate memory
     * context for any memory that is to be reused across multiple calls
     * of the SRF.
     */
    MemoryContext multi_call_memory_ctx;

    /*
     * OPTIONAL pointer to struct containing tuple description
     *
     * tuple_desc is for use when returning tuples (i.e., composite data types)
     * and is only needed if you are going to build the tuples with
     * heap_form_tuple() rather than with BuildTupleFromCStrings().  Note that
     * the TupleDesc pointer stored here should usually have been run through
     * BlessTupleDesc() first.
     */
    TupleDesc tuple_desc;

} FuncCallContext;

SRF использует несколько функций и макросов, которые автоматически манипулируют структурой FuncCallContext (и ожидают найти ее через fn_extra). Использование:

SRF_IS_FIRSTCALL()

определить, вызывается ли ваша функция в первый или последующий раз. При первом вызове (только) используйте:

SRF_FIRSTCALL_INIT()

инициализировать FuncCallContext. При каждом вызове функции, включая первый, используйте:

SRF_PERCALL_SETUP()

правильно настроить использование FuncCallContext и очистку любых ранее возвращенных данных, оставшихся от предыдущего прохода.

Если ваша функция имеет данные для возврата, используйте:

SRF_RETURN_NEXT(funcctx, result)

вернуть его вызывающему. (результат должен иметь тип Datum, либо одно значение, либо кортеж, подготовленный, как описано выше). Наконец, когда ваша функция закончит возвращать данные, используйте:

SRF_RETURN_DONE(funcctx)

очистить и закончить SRF.

Текущий контекст памяти, когда вызывается SRF, является временным контекстом, который будет очищен между вызовами. Это означает, что вам не нужно вызывать pfree для всего, что вы выделили с помощью palloc; это все равно уйдет. Однако, если вы хотите распределить какие-либо структуры данных между вызовами, вам нужно поместить их в другое место. Контекст памяти, на который ссылается multi_call_memory_ctx, является подходящим местом для любых данных, которые должны сохраняться до завершения работы SRF. В большинстве случаев это означает, что вы должны переключиться на multi_call_memory_ctx при выполнении настройки первого вызова.

!!! Предупреждение

Хотя фактические аргументы функции остаются неизменными между вызовами, если вы сбрасываете значения аргументов (что обычно делается прозрачно с помощью макроса PG_GETARG_xxx) во временном контексте, то очищенные копии будут освобождаться в каждом цикле. Соответственно, если вы сохраняете ссылки на такие значения в вашем user_fctx, вы должны либо скопировать их в multi_call_memory_ctx после удаления, либо убедиться, что вы удалили значения только в этом контексте.

Полный пример псевдокода выглядит следующим образом:

Datum
my_set_returning_function(PG_FUNCTION_ARGS)
{
    FuncCallContext  *funcctx;
    Datum             result;
    further declarations as needed

    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext oldcontext;

        funcctx = SRF_FIRSTCALL_INIT();
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
        /* One-time setup code appears here: */
        user code
        if returning composite
            build TupleDesc, and perhaps AttInMetadata
        endif returning composite
        user code
        MemoryContextSwitchTo(oldcontext);
    }

    /* Each-time setup code appears here: */
    user code
    funcctx = SRF_PERCALL_SETUP();
    user code

    /* this is just one way we might test whether we are done: */
    if (funcctx->call_cntr < funcctx->max_calls)
    {
        /* Here we want to return another item: */
        user code
        obtain result Datum
        SRF_RETURN_NEXT(funcctx, result);
    }
    else
    {
        /* Here we are done returning items and just need to clean up: */
        user code
        SRF_RETURN_DONE(funcctx);
    }
}

Полный пример простого SRF, возвращающего составной тип, выглядит следующим образом:

PG_FUNCTION_INFO_V1(retcomposite);

Datum
retcomposite(PG_FUNCTION_ARGS)
{
    FuncCallContext     *funcctx;
    int                  call_cntr;
    int                  max_calls;
    TupleDesc            tupdesc;
    AttInMetadata       *attinmeta;

    /* stuff done only on the first call of the function */
    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext   oldcontext;

        /* create a function context for cross-call persistence */
        funcctx = SRF_FIRSTCALL_INIT();

        /* switch to memory context appropriate for multiple function calls */
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

        /* total number of tuples to be returned */
        funcctx->max_calls = PG_GETARG_UINT32(0);

        /* Build a tuple descriptor for our result type */
        if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
            ereport(ERROR,
                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                     errmsg("function returning record called in context "
                            "that cannot accept type record")));

        /*
         * generate attribute metadata needed later to produce tuples from raw
         * C strings
         */
        attinmeta = TupleDescGetAttInMetadata(tupdesc);
        funcctx->attinmeta = attinmeta;

        MemoryContextSwitchTo(oldcontext);
    }

    /* stuff done on every call of the function */
    funcctx = SRF_PERCALL_SETUP();

    call_cntr = funcctx->call_cntr;
    max_calls = funcctx->max_calls;
    attinmeta = funcctx->attinmeta;

    if (call_cntr < max_calls)    /* do when there is more left to send */
    {
        char       **values;
        HeapTuple    tuple;
        Datum        result;

        /*
         * Prepare a values array for building the returned tuple.
         * This should be an array of C strings which will
         * be processed later by the type input functions.
         */
        values = (char **) palloc(3 * sizeof(char *));
        values[0] = (char *) palloc(16 * sizeof(char));
        values[1] = (char *) palloc(16 * sizeof(char));
        values[2] = (char *) palloc(16 * sizeof(char));

        snprintf(values[0], 16, "%d", 1 * PG_GETARG_INT32(1));
        snprintf(values[1], 16, "%d", 2 * PG_GETARG_INT32(1));
        snprintf(values[2], 16, "%d", 3 * PG_GETARG_INT32(1));

        /* build a tuple */
        tuple = BuildTupleFromCStrings(attinmeta, values);

        /* make the tuple into a datum */
        result = HeapTupleGetDatum(tuple);

        /* clean up (this is not really necessary) */
        pfree(values[0]);
        pfree(values[1]);
        pfree(values[2]);
        pfree(values);

        SRF_RETURN_NEXT(funcctx, result);
    }
    else    /* do when there is no more left */
    {
        SRF_RETURN_DONE(funcctx);
    }
}

Один из способов объявить эту функцию в SQL:

CREATE TYPE __retcomposite AS (f1 integer, f2 integer, f3 integer);

CREATE OR REPLACE FUNCTION retcomposite(integer, integer)
    RETURNS SETOF __retcomposite
    AS 'filename', 'retcomposite'
    LANGUAGE C IMMUTABLE STRICT;

Другой способ - использовать параметры OUT:

CREATE OR REPLACE FUNCTION retcomposite(IN integer, IN integer,
    OUT f1 integer, OUT f2 integer, OUT f3 integer)
    RETURNS SETOF record
    AS 'filename', 'retcomposite'
    LANGUAGE C IMMUTABLE STRICT;

Обратите внимание, что в этом методе тип вывода функции формально является анонимным типом записи.

Модуль contrib/tablefunc каталога в исходном дистрибутиве содержит больше примеров возвращающих множество функций.

Полиморфные аргументы и возвращаемые типы

Можно объявить функции языка Си, чтобы они принимали и возвращали полиморфные типы anyelement, anyarray, anynonarray, anyenum и anyrange. См. Раздел Полиморфные типы для более подробного объяснения полиморфных функций. Когда аргументы функции или возвращаемые типы определены как полиморфные типы, автор функции не может заранее знать, с каким типом данных он будет вызываться или должен возвращаться. В fmgr.h предусмотрены две подпрограммы, позволяющие функции C версии 1 обнаружить фактические типы данных своих аргументов и тип, который ожидается вернуть. Процедуры называются get_fn_expr_rettype(FmgrInfo *flinfo) и get_fn_expr_argtype(FmgrInfo *flinfo, int argnum). Они возвращают OID типа результата или аргумента или InvalidOid, если информация недоступна. Структура flinfo обычно доступна как fcinfo->flinfo. Параметр argnum основан на zero. get_call_result_type также может использоваться как альтернатива get_fn_expr_rettype. Существует также get_fn_expr_variadic, который можно использовать для определения, были ли переменные аргументы объединены в массив. Это в первую очередь полезно для VARIADIC "any" функций, поскольку такое объединение всегда будет происходить для функций с переменными числами, принимающих обычные типы массивов.

Например, предположим, что мы хотим написать функцию, которая принимает один элемент любого типа, и возвращает одномерный массив этого типа:

PG_FUNCTION_INFO_V1(make_array);
Datum
make_array(PG_FUNCTION_ARGS)
{
    ArrayType  *result;
    Oid         element_type = get_fn_expr_argtype(fcinfo->flinfo, 0);
    Datum       element;
    bool        isnull;
    int16       typlen;
    bool        typbyval;
    char        typalign;
    int         ndims;
    int         dims[MAXDIM];
    int         lbs[MAXDIM];

    if (!OidIsValid(element_type))
        elog(ERROR, "could not determine data type of input");

    /* get the provided element, being careful in case it's NULL */
    isnull = PG_ARGISNULL(0);
    if (isnull)
        element = (Datum) 0;
    else
        element = PG_GETARG_DATUM(0);

    /* we have one dimension */
    ndims = 1;
    /* and one element */
    dims[0] = 1;
    /* and lower bound is 1 */
    lbs[0] = 1;

    /* get required info about the element type */
    get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);

    /* now build the array */
    result = construct_md_array(&element, &isnull, ndims, dims, lbs,
                                element_type, typlen, typbyval, typalign);

    PG_RETURN_ARRAYTYPE_P(result);
}

Следующая команда объявляет функцию make_array в SQL:

CREATE FUNCTION make_array(anyelement) RETURNS anyarray
    AS 'DIRECTORY/funcs', 'make_array'
    LANGUAGE C IMMUTABLE;

Существует вариант полиморфизма, который доступен только для функций языка Си: они могут быть объявлены для получения параметров типа "any". (Обратите внимание, что это имя типа должно быть заключено в двойные кавычки, поскольку оно также является зарезервированным словом SQL). Это работает как любой элемент, за исключением того, что оно не ограничивает различные "any" аргументы одним и тем же типом и не помогает определить результат функции тип. Функция языка Си также может объявить свой последний параметр VARIADIC "any". Это будет соответствовать одному или нескольким фактическим аргументам любого типа (не обязательно того же типа). Эти аргументы не будут собраны в массив, как это происходит с обычными переменными функциями; они просто будут переданы функции отдельно. Макрос PG_NARGS () и методы, описанные выше, должны использоваться для определения количества фактических аргументов и их типов при использовании этой функции. Кроме того, пользователи такой функции могут захотеть использовать ключевое слово VARIADIC в своем вызове функции, ожидая, что функция будет рассматривать элементы массива как отдельные аргументы. Сама функция должна реализовать это поведение, если необходимо, после использования get_fn_expr_variadic, чтобы обнаружить, что фактический аргумент был помечен как VARIADIC.

Общая память и LWLocks

Надстройки могут резервировать LWLocks и распределение общей памяти при запуске сервера. Общая библиотека надстройки должна быть предварительно загружена, указав ее в shared_preload_libraries. Общая память резервируется путем вызова:

void RequestAddinShmemSpace(int size)

из вашей функции _PG_init.

LWLocks резервируются путем вызова:

void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)

из _PG_init. Это обеспечит доступность массива num_lwlocks LWLocks под именем tranche_name. Используйте GetNamedLWLockTranche, чтобы получить указатель на этот массив.

Чтобы избежать возможных условий гонки, каждый бэкэнд должен использовать LWLock AddinShmemInitLock при подключении и инициализации распределения общей памяти, как показано здесь:

static mystruct *ptr = NULL;

if (!ptr)
{
        bool    found;

        LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
        ptr = ShmemInitStruct("my struct name", size, &found);
        if (!found)
        {
                initialize contents of shmem area;
                acquire any requested LWLocks using:
                ptr->locks = GetNamedLWLockTranche("my tranche name");
        }
        LWLockRelease(AddinShmemInitLock);
}

Использование C ++ для расширяемости

Хотя серверная часть QHB написана на C/Rust, можно написать расширения на C++, если следовать этим рекомендациям:

  • Все функции, к которым обращается бэкэнд, должны представлять интерфейс C для бэкэнда; эти функции C могут затем вызывать функции C++. Например, внешняя связь C необходима для функций, к которым обращаются к серверу. Это также необходимо для любых функций, которые передаются в виде указателей между серверной частью и кодом C++.

  • Освободите память, используя соответствующий метод освобождения. Например, большая часть внутренней памяти выделяется с помощью palloc(), поэтому используйте pfree() для ее освобождения. Использование C++ удалить в таких случаях не удастся.

  • Не допускайте распространения исключений в коде C (используйте блок catch-all на верхнем уровне всех внешних функций C). Это необходимо, даже если код C++ явно не выбрасывает какие-либо исключения, потому что такие события, как нехватка памяти, могут по-прежнему генерировать исключения. Любые исключения должны быть перехвачены и соответствующие ошибки переданы обратно в интерфейс C. Если возможно, скомпилируйте C++ с -fno-exception, чтобы полностью исключить исключения; в таких случаях вы должны проверять ошибки в вашем C++ коде, например, проверять NULL, возвращаемый new().

  • При вызове внутренних функций из кода C++ убедитесь, что стек вызовов C++ содержит только простые старые структуры данных (POD). Это необходимо, потому что ошибки бэкэнда генерируют удаленный longjmp (), который неправильно разворачивает стек вызовов C++ с объектами не POD.

Таким образом, лучше всего поместить код C++ за стеной внешних функций C/RUST, которые взаимодействуют с бэкэндом и избегают утечек исключительных ситуаций, памяти и стека вызовов.

Информация по оптимизации функций

По умолчанию функция — это «черный ящик», система очень мало знает о её поведении. Из этого следует, что запросы, использующие функцию, могут выполняться менее эффективно, чем могли бы. Можно предоставить дополнительную информацию, которая поможет планировщику оптимизировать вызовы функций.

Некоторые основные факты могут быть предоставлены декларативными аннотациями в команде CREATE FUNCTION. Наиболее важным из них является категория волатильности функции (IMMUTABLE, STABLE или VOLATILE); всегда нужно быть осторожным, чтобы правильно указать это при определении функции. Свойство безопасности параллельного исполнения (PARALLEL UNSAFE, PARALLEL RESTRICTED или PARALLEL SAFE) также должно быть указано, если вы надеетесь на использование этой функции в параллельных запросах. Также может быть полезно указать оценочную стоимость выполнения функции и/или количество строк, которые она должна вернуть. Однако декларативный способ указания этих двух фактов позволяет указывать только константное значение, а это будет неадекватно.

Также можно прикрепить вспомогательную функцию для планировщика к любой функции, которую можно вызвать из SQL, для вспомогательной функции та функция, к которой она прикреплена, называется её целевой функцией. Вспомогателная функция предоставляет информацию планировщику о своей целевой функции. Вспомогаетльная функция — нативная (в то время как целевая функция — на любом языке), это делает её написание сложным, и они применяются редко.

Вспомогательная функция для планировщика должна иметь следующую SQL-сигнатуру:

supportfn(internal) returns internal

Чтобы подсоединить её к целевой функции, нужно при создании последней использовать указание SUPPORT.

Далее описано, какие "запросы" может делать планировщик ко вспомогательной функции, и какие оптимизации это позволяет. В будущих версиях могут появиться и другие запросы.

Если целевая функция возвращают boolean, то часто полезно оценить процент строк, для которых она вернёт истину. Вы можете реализовать SupportRequestSelectivity и вернуть оптимизатору примерный процент.

Если время выполнения целевой функции сильно зависит от ее входных данных, может быть полезно предоставить оценку непостоянных затрат для нее. Это можно сделать, реализовав тип запроса SupportRequestCost в вашей вспомогательной функции.

Для целевых функций, которые возвращают наборы, часто бывает полезно предоставить непостоянную оценку числа строк, которые будут возвращены. Это можно сделать с помощью вспомогательной функции, которая реализует тип запроса SupportRequestRows.

Самый сложный тип запроса SupportRequestSimplify позволяет переписать непосредственно дерево разбора, заменив вызов целевой функции на что-то ещё. Например, int4mul(n, 1) можно упростить до просто n, этого не знает планировщик, но знает вспомогательная функция, тесно связанная с int4mul. SupportRequestSimplify будет вызвано для каждого вхождения целевой функции в запрос, однако не гарантируется, что не смотря на советы вспомогательной функции, планировщик всё-таки не вызовет оригинальную целевую функцию как есть.

Когда целевая функций, возвращающая boolean, используются для фильтрации (например, в секции WHERE), из неё можно выделить другой, "грубый" фильтр, который позволит индексный поиск. Это делает вспомогательная функция по запросу SupportRequestIndexCondition; фильтр, который она вернёт, может быть в точности эквивалентным вызову целевой функции или быть более слабым, в последне случае для каждой строки будет перепроверяться оригинальный фильтр вызовом целевой функции. Планировщик может решить, что индексный поиск всё равно невозможен или невыгоден, — в этом случае фильтр, созданный вспомогательной функцией, не будет использоваться.

Пользовательские агрегаты

Агрегатные функции в QHB определяются в терминах состояний и функций перехода состояний. То есть агрегат работает с некоторым состоянием, которое обновляется при обработке каждой последующей входной строки. Чтобы создать новую агрегатную функцию, надо выбрать тип данных для состояния, его начальное значение и функцию перехода (свёртки). Функция перехода состояния принимает предыдущее значение состояния и агрегируемое значение(я) текущей строки и возвращает новое значение состояния. Если тип состояния агрегата отличен от желаемого типа результата агрегации, то нужно указать также функцию финализации, которая в конце преобразует одно в другое. В принципе, функции перехода и финализации — это просто обычные функции, которые могут использоваться и вне контекста агрегата. Но на практике, даже если можно использовать в качестве функции перехода существующую функцию, вы, возможно, заходите написать её специальную версию, оптимизированную именно для агрегации.

Таким образом, помимо типов данных аргумента и результата, видимых пользователю агрегата, существует внутренний тип данных состояния, который может отличаться как от типа аргумента, так и от типа результата.

В качестве примера определим агрегат суммы для комплексных чисел:

CREATE AGGREGATE sum (complex)
(
    sfunc = complex_add,
    stype = complex,
    initcond = '(0,0)'
);

Здесь мы задали функцию перехода состояния, тип состояния и начальное состояние. Функцию финализации не задали, и поэтому результатом агрегации будет считаться конечное значение состояния. Как использовать этот агрегат:

SELECT sum(a) FROM test_complex;

   sum
-----------
 (34,53.9)

Обратите внимание, что мы полагаемся на перегрузку функций: существует более одной агрегатной функции с именем sum, но QHB может определить, какую из них применять к столбцу типа complex.

Приведенное выше определение sum вернет ноль (начальное состояние), если нет входных строк, или все они имеют значение NULL. А по стандарту SQL в таком случае положено вернуть NULL. Чтобы добиться этого, можно задать начальное состояние initcond = NULL или, что тоже самое, просто опустить initcond. Однако в этом случае функция перехода sfunc должна уметь обрабатывать текущее состояние NULL. Или можно объявить функцию перехода строгой (STRICT), в этом случае, если текущее состояние NULL, когда встретится строка со значнием NOT NULL, QHB поместит это первое NOT NULL значение в состояние агрегата (разумеется, типы входного значения и состояния должны быть одинаковыми). Для многих агрегатов, например, sum, min, max такое поведение — то, что нужно.

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

avg (среднее арифметическое) — пример более сложного агрегата. Рабочее состояние должно состоять из суммы значений и их количества. Окончательный результат получается делением этих величин. Встроенная реализация avg хранит промежуточное состояние в виде массива, а не в виде кортежа. Например, определение avg(float8) выглядит так:

CREATE AGGREGATE avg (float8)
(
    sfunc = float8_accum,
    stype = float8[],
    finalfunc = float8_avg,
    initcond = '{0,0,0}'
);

Замечание
Для float8_accum требуется массив из трех элементов, а не из только двух, потому что она накапливает количество, сумму и сумму квадратов. Это сделано для того, чтобы ту же самую функцию float8_accum можно было использовать и для других агрегатов помимо avg.

Вызовы агрегатных функций в SQL позволяют использовать опции DISTINCT и ORDER BY, которые определяют, какие строки передаются в агрегирующую функцию и в каком порядке. Это происходит за кулисами, и агрегатная функция не может настраивать это поведение.

Для получения дополнительной информации см. описание команды CREATE AGGREGATE.

Режим движущегося агрегата

Агрегатные функции могут дополнительно поддерживать режим движущегося(скользящего) агрегата, который позволяет гораздо быстрее считать агрегат по окну с движущимся началом. (См. разделы Руководство по оконным функциям и Вызовы оконных функций для получения информации об использовании агрегатных функций в качестве оконных функций). Основная идея заключается в том, что в дополнение к обычной «прямой» функции перехода агрегат предоставляет обратную функцию перехода, которая позволяет удалять строки из текущего состояния агрегата, когда они выпадают из окна. Например, агрегат sum, который использует сложение в качестве функции прямого перехода, будет использовать вычитание в качестве функции обратного перехода. Без функции обратного перехода придётся пересчитывать агрегат с нуля при каждом перемещении рамки окна, в результате чего время выполнения будет пропорционально числу входных строк, умноженному на среднюю длину окна. При использовании функции обратного перехода время выполнения пропорционально просто количеству входных строк.

В функцию обратного перехода передаётся текущее состояние и значение выпадающей строки. Это обязательно первая из строк, добавленных в агрегат (и ещё не выпавших). Формально функция должна восстановить состояние агрегата, как если бы выпадающая строка никогда и не была добавлена в агрегат, а агрегацию начали со второй строки. Чтобы это реализовать, может потребоваться хранить в текущем состоянии агрегата больше информации, чем для агрегирования "только вперёд". По этой причине режим движущегося агрегата имеет независимый набор из типа данных, функций перехода прямой и обратной, и функции финализации, если нужна. А в простом случае этот набор будет совпадать с набором для "обыкновенного" режима агрегации.

В качестве примера опять определение агрегата sum для комплексных чисел, теперь поддерживается скользящий режим:

CREATE AGGREGATE sum (complex)
(
    sfunc = complex_add,
    stype = complex,
    initcond = '(0,0)',
    msfunc = complex_add,
    minvfunc = complex_sub,
    mstype = complex,
    minitcond = '(0,0)'
);

Параметры, имена которых начинаются с m относятся к реализации движущегося агрегата. Все они (кроме функции обратного перехода minvfunc) имеют соотвествующий параметр без m, который относится к реализации "обычного " агрегата.

Функция прямого перехода для режима движущегося агрегата не может возвращать значение NULL в качестве нового значения состояния. Функция обратного перехода может вернуть NULL, но это будет воспринято как то, что она не может "убрать" данное значение — в этом случае агрегат пересчитают с нуля, начиная от нового начала окна. Это соглашение позволяет использовать функции движущегося агрегата, если иногда возникают ситуации, когда нельзя удалить значение из текущего состояния; в этих редких ситуациях функция обратного перехода "сдаётся", и агрегирование будет работать медленнее, но в большинстве ситуаций — быстро. Пример — агрегат, работающий с числами с плавающей запятой; функция обратного перехода будет вынуждена "сдаться", когда её попросят удалить значение NaN из текущего состояния агрегата.

При написании вспомогательных функций движущегося агрегата важно убедиться, что функция обратного перехода может точно восстановить правильное состояние. В противном случае могут быть заметные для пользователя различия в результатах в зависимости от того, используется ли режим скользящего агрегата. Примером агрегата, для которого добавление функции обратного перехода на первый взгляд кажется простым, а на самом деле имеет много подводных камней, является sum по float4 или float8. Наивное объявление sum(float8) может быть таким:

CREATE AGGREGATE unsafe_sum (float8)
(
    stype = float8,
    sfunc = float8pl,
    mstype = float8,
    msfunc = float8pl,
    minvfunc = float8mi
);

Этот агрегат, однако, может дать неожиданные результаты при скользящем агрегировании:

SELECT
  unsafe_sum(x) OVER (ORDER BY n ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING)
FROM (VALUES (1, 1.0e20::float8),
             (2, 1.0::float8)) AS v (n,x);

Вторая строчка результата будет 0, а не 1. Причиной является ограниченная точность значений с плавающей запятой: добавление 1 к 1e20 даёт снова 1e20, и потом вычитание 1e20 из этого дает 0, а не 1. Обратите внимание, что это проблемы арифметики с плавающей запятой в целом, а не QHB.

Полиморфные агрегаты

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

CREATE AGGREGATE array_accum (anyelement)
(
    sfunc = array_append,
    stype = anyarray,
    initcond = '{}'
);

Здесь типом состояния для любого конкретного вызова агрегата является массив, элементы которого имеют тип, равный типу фактических входных данных. Поведение агрегата заключается в добавлении всех входных данных в массив. (Примечание: встроенный агрегат array_agg предоставляет аналогичную функциональность с лучшей производительностью, чем этот пример).

Протестируем этот агрегат на двух разных типах данных:

SELECT attrelid::regclass, array_accum(attname)
    FROM pg_attribute
    WHERE attnum > 0 AND attrelid = 'pg_tablespace'::regclass
    GROUP BY attrelid;

   attrelid    |              array_accum
---------------+---------------------------------------
 pg_tablespace | {spcname,spcowner,spcacl,spcoptions}
(1 row)

SELECT attrelid::regclass, array_accum(atttypid::regtype)
    FROM pg_attribute
    WHERE attnum > 0 AND attrelid = 'pg_tablespace'::regclass
    GROUP BY attrelid;

   attrelid    |        array_accum
---------------+---------------------------
 pg_tablespace | {name,oid,aclitem[],text[]}
(1 row)

Обычно у агрегатной функции с полиморфным типом результата тоже будет полиморфным, как в приведенном выше примере. Это необходимо, потому что иначе функция финализации не может быть разумно объявлена: у нее должен полиморфный тип результата (если, конечно, агрегирование разных типов не приводит к одинаковому типу результата), но нет полиморфного типа аргумента. Такую функцию CREATE FUNCTION не даст создать. Но иногда неудобно использовать тип полиморфного состояния. Наиболее распространенный случай, когда вспомогательные функции должны быть написаны на нативном языке, и тип состояния должен быть объявлен как internal поскольку для него нет эквивалента на уровне SQL. Для решения этой проблемы разшено добавить к функции финализации фиктивный аргумент полиморфного типа. В конце агрегации в функцию финализации будет передано состояние агрегата и NULL для всех аргументов кроме первого. Фактический тип фиктивного аргумента считается равным типу агрегируемого столбца, и из этого выводится тип результата агрегата. Вот почти настоящее определение встроенного агрегата array_agg:

CREATE FUNCTION array_agg_transfn(internal, anynonarray)
  RETURNS internal ...;
CREATE FUNCTION array_agg_finalfn(internal, anynonarray)
  RETURNS anyarray ...;

CREATE AGGREGATE array_agg (anynonarray)
(
    sfunc = array_agg_transfn,
    stype = internal,
    finalfunc = array_agg_finalfn,
    finalfunc_extra
);

Здесь опция finalfunc_extra указывает, что функция финализации получает в дополнение к значению состояния фиктивные аргументы. Дополнительный аргумент функции array_agg_finalfn делает её валидной и позволяет вывести итоговый тип агрегата (не из типа состояния, а непосредственно из входного типа).

Агрегатная функция может принимать различное количество аргументов, для этого надо объявить её последний аргумент как VARIADIC-массив, так же, как для обычных функций (см. раздел Функции SQL с переменным числом аргументов). Второй агрумент функции перехода должен быть таким же массивом (обычно тоже VARIADIC, но это не обязательно).

Замечание
VARIADIC-агрегаты подвержены ошибкам при использовании одновременно с опцией ORDER BY (см. раздел Агрегатные выражения), т.к. не жалуются на неправильное количество аргументов. Помните, что все справа от ORDER BY — ключи сортировки, а не значения для агрегирования. Например,
SELECT myaggregate(a ORDER BY a, b, c) FROM ...
— это агрегация 1 колонке и 3 ключа сортировки. А пользователь, возможно, имел в виду
SELECT myaggregate(a, b, c ORDER BY a) FROM ...
Если myaggregate является VARIADIC, то оба этих вызова совершенно корректны.

По той же причине стоит дважды подумать, прежде чем создавать агрегатные функции с одинаковыми именами, но разным количеством обычных аргументов.

Сортирующие агрегатные функции

Агрегаты, которые мы описывали до сих пор, являются «нормальными» агрегатами. QHB также поддерживает т.н. сортирующие агрегаты. Результат нормального агрегата может зависеть от порядка строк (например, string_agg) или не зависеть (наприме, min); в любом случае при его вызове в агрегатном выражении можно указать ORDER BY по любым столбцам, не обязательно тем, что передаются в агрегатную функцию, и сортировку осуществляет исполнитель запросов перед подачей в агрегатную функцию. Сортирующие агрегаты осуществляют сортировку сами, именно по тем столбцам, которые они обрабатывают, поэтому набор данных для передачи в них "совмещён" с ORDER BY (WITHIN GROUP(ORDER BY col)), а в агрегатную функцию передаются дополнительные "прямые" параметры. Предполагается, что сортирующий агрегат будет сортировать и накапливать данные внутри себя, и так делают все встроенные сортирующие агрегаты, но это не обязательно. Возможно, вам нужен сортирующий агрегат, если:

  • требуется передать в функцию агрегирования дополнительный параметр,
  • и результат агрегации не зависит от порядка строк.

Типичные примеры — подсчёт ранга или процентиля. Например, встроенное определение percentile_disc эквивалентно следующему:

CREATE FUNCTION ordered_set_transition(internal, anyelement)
  RETURNS internal ...;
CREATE FUNCTION percentile_disc_final(internal, float8, anyelement)
  RETURNS anyelement ...;

CREATE AGGREGATE percentile_disc (float8 ORDER BY anyelement)
(
    sfunc = ordered_set_transition,
    stype = internal,
    finalfunc = percentile_disc_final,
    finalfunc_extra
);

Этот агрегат принимает прямой аргумент float8 (процентная отсечка) и входные данные для агрегации, которые могут иметь любой сортируемый тип данных. Например, получение медианного семейного дохода:

SELECT percentile_disc(0.5) WITHIN GROUP (ORDER BY income) FROM households;
 percentile_disc
-----------------
           50489

Здесь 0.5 - прямой аргумент, т.к. процентная отсечка — это свойство всего агрегата, а не отдельных значений.

При реализации сортирующего агрегата вам придётся самостоятельно выполнять сортировку (если она вам нужна). Обычно это делают, храня ссылку на объект tuplesort внутри состояния агрегата, помещая туда все входные значения, и завершая сортировки и считывая данных в функции финализации. Такой дизайн позволяет в функции финализации добавлять «гипотетические» строки в данные в порядке сортировки. Хотя обычные агрегаты часто могут быть реализованы с помощью вспомогательных функций, написанных на PL/pgSQL или другом процедурном языке, сортирующие агрегаты должны записываться на нативном языке, хотя бы потому, что их состояние — сложный объект, который на SQL нельзя определить иначе как internal. Далее, поскольку функция финализации выполняет сортировку, после неё невозможно (или по крайней мере неэффективно) продолжить добавление входных строк. Поэтому функция финализации должна быть зарегистрирована в CREATE AGGREGATE не как READ_ONLY, а как READ_WRITE (или как SHAREABLE, если для дополнительных вызовов функции финализации возможно использование уже отсортированного состояния).

Функция перехода состояния для сортирующего агрегата получает текущее значение состояния, агрегириреумые входные значения каждой строки, и возвращает обновленное значение состояния. В принципе то же самое, что и для обычных агрегатов, но обратите внимание, что прямые аргументы (если таковые имеются) не передаются в функцию перехода; они передаются только в функцию финализации. Как и в случае с обычными агрегатами, опция finalfunc_extra может использоваться для определения полиморфоной агрегатной функции.

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

Частичная агрегация

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

Для поддержки частичного агрегирования определение агрегата должно предоставлять функцию объединения, которая принимает два значения типа состояния агрегата (представляющих результаты агрегирования по двум подмножествам входных строк) и создает новое значение типа состояния, представляющее состояние было бы после агрегирования по комбинации этих наборов строк. Каким был бы относительный порядок строк из двух наборов — неизвестно. По этой причине для агрегатов, зависящих от порядка строк такую функцию нельзя определить.

В качестве простых примеров агрегаты MAX и MIN могут поддерживать частичного агрегирования, указав в качестве функции объединения максимум/минимум из двух, то же саамое, что и в функции перехода состояния.

Функция объединения используется также, как функция перехода, только входные аргументы имеют тип состояния, а не исходных данных; в частности такие же правила обработки NULL'ов в случае строгой/не сторгой функции. Кроме того, если у агрегата указана initcond, отличная от NULL, имейте в виду, что initcond будет стартовым значением для каждого частичного запуска агрегации и потом для функции объединения тоже.

Если тип состояния агрегата объявлен как internal, то функция объединения несет ответственность за то, чтобы ее результат был аллокирован в правильном контексте памяти; в частности, это означает, что, когда один из аргументов NULL, нельзя просто вернуть второй аргумент, поскольку это значение в неправильном контексте и имеет слишком короткий срок жизни.

Также, если тип состояния internal, нужно реализовать функции сериализации и десериализации, чтобы состояние можно было передать в другой процесс. Функция сериализации должна принимать один аргумент типа internal и возвращать результат типа bytea, который представляет собой упакованное значение состояния. И наоборот, функция десериализации выполняет это преобразование в обратном направлении. Она должна принимать два аргумента типов bytea и internal и возвращать результат типа internal. (Второй аргумент не используется и всегда равен нулю, но он необходим по соображениям безопасности типов). Результат функции десериализации должен быть просто размещен в текущем контексте памяти, поскольку в отличие от результата функции объединения, он короткоживущий.

Стоит также отметить, что для того, чтобы агрегат выполнялся параллельно, сам агрегат должен быть помечен как PARALLEL SAFE. А для его вспомогательных функциях такая пометка не обязательна.

Написание вспомогательных функций агрегатов

Функция, написанная на нативном языке, может обнаружить, что она вызывается как агрегатная вспомогательная функция, вызвав AggCheckCallContext, например:

if (AggCheckCallContext(fcinfo, NULL))

Представьте себе функцию, которая может вызываться и как функция перехода агрегата, так и как прост офункция. Если AggCheckCallContext вернуло true, то первый аргумент является временным значением, можно его исправить на месте и вернуть указатель на него же (а в общем случае надо аллокировать результат).

(В то время как агрегатным функциям перехода всегда разрешено изменять значение перехода на месте, агрегатным конечным функциям обычно не рекомендуется делать это; если они это делают, поведение должно быть объявлено при создании агрегата. См. CREATE AGGREGATE для более подробной информации).

Второй аргумент AggCheckCallContext может использоваться для получения контекста памяти, в котором хранятся значения агрегатного состояния. Это полезно для функций перехода, которые хотят использовать «расширенные» объекты (см. Раздел Использование TOAST) в качестве значений состояния. При первом вызове функция перехода должна вернуть расширенный объект, чей контекст памяти является дочерним по отношению к контексту совокупного состояния, и затем продолжать возвращать тот же расширенный объект при последующих вызовах. Смотрите array_append() для примера. (array_append() не является функцией перехода какого-либо встроенного агрегата, но написана так, чтобы вести себя эффективно при использовании в качестве функции перехода пользовательского агрегата).

Другая подпрограмма поддержки, доступная для агрегатных функций, написанных на нативном языке программирования, — это AggGetAggref, который возвращает узел разбора Aggref который определяет агрегированный вызов. Это в основном полезно для агрегатов с упорядоченным набором, которые могут проверить подструктуру узла Aggref чтобы выяснить, какой порядок сортировки они должны реализовать.

Пользовательские типы

Как описано в разделе Система типов QHB, QHB может быть расширен для поддержки новых типов данных. В этом разделе описывается, как определить новые базовые типы, определенные ниже уровня языка SQL. Создание нового базового типа требует реализации функций для работы с типом на нативном языке программирования.

Пользовательский тип должен всегда иметь функции ввода и вывода. Эти функции определяют, как тип отображается в строках (для ввода пользователем и вывода для пользователя) и как тип организован в памяти. Функция ввода принимает в качестве аргумента символьную строку с нулевым символом в конце и возвращает внутреннее (в памяти) представление типа. Функция вывода принимает внутреннее представление типа в качестве аргумента и возвращает символьную строку с нулевым символом в конце. Если мы хотим сделать что-то большее с типом, чем просто сохранить его, мы должны предоставить дополнительные функции для реализации любых операций, которые мы хотели бы иметь для этого типа.

Предположим, мы хотим определить тип complex, который представляет комплексные числа. Естественным способом представления комплексного числа в памяти будет следующая структура C:

typedef struct Complex {
    double      x;
    double      y;
} Complex;

Нам нужно будет сделать этот тип передаваемым по ссылке, поскольку он слишком велик, чтобы поместиться в одно значение Datum.

В качестве внешнего строкового представления типа мы выберем строку вида (x,y).

Функции ввода и вывода обычно не сложно написать, особенно функцию вывода. Но при определении внешнего строкового представления типа помните, что в конечном итоге вы должны написать полный и надежный синтаксический анализатор для этого представления в качестве входной функции. Например:

PG_FUNCTION_INFO_V1(complex_in);

Datum
complex_in(PG_FUNCTION_ARGS)
{
    char       *str = PG_GETARG_CSTRING(0);
    double      x,
                y;
    Complex    *result;

    if (sscanf(str, " ( %lf, %lf )", &x, &y) != 2)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                 errmsg("invalid input syntax for type %s: \"%s\"",
                        "complex", str)));

    result = (Complex *) palloc(sizeof(Complex));
    result->x = x;
    result->y = y;
    PG_RETURN_POINTER(result);
}

Функция вывода может быть просто

PG_FUNCTION_INFO_V1(complex_out);

Datum
complex_out(PG_FUNCTION_ARGS)
{
    Complex    *complex = (Complex *) PG_GETARG_POINTER(0);
    char       *result;

    result = psprintf("(%g,%g)", complex->x, complex->y);
    PG_RETURN_CSTRING(result);
}

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

Опционально, пользовательский тип может предоставлять процедуры ввода и вывода в бинарный формат. Двоичный ввод-вывод обычно быстрее, но менее переносим, чем текстовый ввод-вывод. Как и в случае с текстовым вводом/выводом, вы должны точно определить, что такое внешнее двоичное представление. Большинство встроенных типов данных пытаются обеспечить машинно-независимое двоичное представление. Для complex мы воспользуемся преобразователям в бинарное представление ввода/вывода типа float8:

PG_FUNCTION_INFO_V1(complex_recv);

Datum
complex_recv(PG_FUNCTION_ARGS)
{
    StringInfo  buf = (StringInfo) PG_GETARG_POINTER(0);
    Complex    *result;

    result = (Complex *) palloc(sizeof(Complex));
    result->x = pq_getmsgfloat8(buf);
    result->y = pq_getmsgfloat8(buf);
    PG_RETURN_POINTER(result);
}

PG_FUNCTION_INFO_V1(complex_send);

Datum
complex_send(PG_FUNCTION_ARGS)
{
    Complex    *complex = (Complex *) PG_GETARG_POINTER(0);
    StringInfoData buf;

    pq_begintypsend(&buf);
    pq_sendfloat8(&buf, complex->x);
    pq_sendfloat8(&buf, complex->y);
    PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
}

После того, как мы написали функции ввода-вывода и скомпилировали их в разделяемую библиотеку, мы можем определить тип complex в SQL. Сначала мы объявим "оболочку" типа:

CREATE TYPE complex;

Она служит заполнителем, который позволяет нам ссылаться на тип при определении его функций ввода/вывода. Теперь мы можем определить функции ввода/вывода:

CREATE FUNCTION complex_in(cstring)
    RETURNS complex
    AS 'filename'
    LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_out(complex)
    RETURNS cstring
    AS 'filename'
    LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_recv(internal)
   RETURNS complex
   AS 'filename'
   LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_send(complex)
   RETURNS bytea
   AS 'filename'
   LANGUAGE C IMMUTABLE STRICT;

Наконец, мы можем предоставить полное определение типа данных:

CREATE TYPE complex (
   internallength = 16,
   input = complex_in,
   output = complex_out,
   receive = complex_recv,
   send = complex_send,
   alignment = double
);

Когда вы определяете новый базовый тип, QHB автоматически обеспечивает поддержку массивов этого типа. Тип массива обычно имеет то же имя, что и базовый тип, с добавлением символа подчеркивания (_) в начале.

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

Если внутреннее представление типа данных имеет переменную длину, то его первые 4 байта должы хранить длину (на C это выглядит как поле char vl_len_[4]); к этому полю вы не должны никогда обращаться напрямую, но использовать специальные макросы (VARSIZE() и SET_VARSIZE()), эти макросы существуют, потому что поле длины может быть закодировано в зависимости от платформы. В SET_VARSIZE надо передавать общий размер элемента в байтах, включая 4 байта vl_len_.

Замечание
В более старом коде можно встретить объявления vl_len_ как int32 вместо char[4]. Это нормально, особенно если в структуре есть хотя бы одно другое поле с выравнивание 4+ байт. Если нет, то QHB может хранить структуру невыровено, для этого и char[4]

Для получения дополнительной информации см. Описание команды CREATE TYPE.

Использование TOAST

Если значения вашего типа данных сильно различаются по размеру (во внутреннем представлении), обычно желательно сделать тип данных сохраняющимся в TOAST (см. раздел TOAST). Стоит это сделать даже в том случае, когда значения всегда слишком малы для сжатия или внешнего хранения, поскольку TOAST может сэкономить пространство и для небольших данных, уменьшая издержки на заголовок.

Чтобы поддерживать сохранение в TOAST, функции C, работающие с типом данных, должны распаковать любые TOAST-значения, которые им передают с помощью PG_DETOAST_DATUM. (Эта деталь обычно скрывается путем определения макроса GETARG_DATATYPE_P для конкретного типа). Затем при запуске команды CREATE TYPE укажите внутреннюю длину как variable и выберите какой-либо подходящий вариант хранения, отличный от plain.

Если выравнивание данных неважно (для конкретной функции, либо потому, что тип данных имеет произвольное выравнивание (до 1 байта)), то можно избежать некоторых издержек PG_DETOAST_DATUM и вызывать вместо неё PG_DETOAST_DATUM_PACKED (обычно скрывается путем определения макроса GETARG_DATATYPE_PP ) и использовать макросы VARSIZE_ANY_EXHDR и VARDATA_ANY для доступа к потенциально упакованным данным. Опять же, данные, возвращаемые этими макросами, не выравниваются, даже если определение типа данных требует выравнивания. Если выравнивание важно, вы должны пройти через обычный интерфейс PG_DETOAST_DATUM.

Другая функция, включаемая поддержкой TOAST, - это возможность иметь расширенное представление данных в памяти, с которым удобнее работать, чем с форматом, хранящимся на диске. Обычный или «плоский» формат хранения varlena - это, в конечном счете, просто blob of bytes; например, он не может содержать указатели, так как он может быть скопирован в другие места в памяти. Для сложных типов данных плоский формат может быть довольно дорогим для работы, поэтому QHB предоставляет способ «развернуть» плоский формат в представление, более подходящее для вычислений, а затем передать этот формат в памяти между функциями тип данных.

Чтобы использовать расширенное хранилище, тип данных должен определять расширенный формат, который следует правилам, приведенным в src/include/utils/expandeddatum.h, и предоставлять функции для «раскрытия» значения плоской переменной varlena в расширенный формат и «выравнивания» расширенного формата для возвращения к обычному представлению varlena. Затем убедитесь, что все функции C для типа данных могут принимать любое представление, возможно, путем преобразования одной в другую сразу после получения. Это не требует одновременного исправления всех существующих функций для типа данных, поскольку стандартный макрос PG_DETOAST_DATUM определен для преобразования расширенных входных данных в обычный плоский формат. Следовательно, существующие функции, работающие с форматом плоской varlena, будут продолжать работать, хотя и неэффективно, с расширенными входными данными; их не нужно преобразовывать до тех пор, пока не будет важна лучшая производительность.

Функции на C/RUST, которые знают, как работать с расширенным представлением, обычно делятся на две категории: те, которые могут обрабатывать только расширенный формат, и те, которые могут обрабатывать либо расширенные, либо плоские входные данные varlena. Первые легче написать, но могут быть менее эффективными в целом, потому что преобразование плоского ввода в расширенную форму для использования одной функцией может стоить больше, чем экономится при работе в расширенном формате. Когда требуется обрабатывать только расширенный формат, преобразование плоских входных данных в расширенную форму может быть скрыто внутри макроса выборки аргументов, так что функция выглядит не более сложной, чем та, которая работает с традиционным вводом varlena. Чтобы обработать оба типа ввода, напишите функцию выборки аргумента, которая будет сбрасывать внешние, короткие и сжатые входные данные, но не расширенные. Такая функция может быть определена как возвращение указателя на объединение плоского формата varlena и расширенного формата. Вызывающие могут использовать VARATT_IS_EXPANDED_HEADER() чтобы определить, какой формат они получили.

Инфраструктура TOAST не только позволяет отличать обычные значения varlena от расширенных значений, но также различает указатели «чтение-запись» и «только для чтения» на расширенные значения. Функции C/RUST, которые должны проверять только расширенное значение или будут изменять его только безопасным и не семантически видимым образом, не должны заботиться о том, какой тип указателя они получают. Функции на C/RUST, которые создают измененную версию входного значения, могут изменять расширенное входное значение на месте, если они получают указатель чтения-записи, но не должны изменять ввод, если они получают указатель только для чтения; в этом случае они должны сначала скопировать значение, создав новое значение для изменения. Функция C/RUST, которая создала новое расширенное значение, всегда должна возвращать указатель на чтение-запись. Кроме того, функция C/RUST, которая изменяет расширенное значение для чтения-записи на месте, должна позаботиться о том, чтобы оставить значение в нормальном состоянии, если оно не будет выполнено частично.

Примеры работы с расширенными значениями см. В стандартной инфраструктуре массива, в частности в src/backend/utils/adt/array_expanded.c.

Пользовательские операторы

Каждый оператор является «синтаксическим сахаром» для вызова базовой функции, которая выполняет реальную работу; поэтому вы должны сначала создать базовую функцию, прежде чем сможете создать оператор. Тем не менее, оператор не просто синтаксический сахар, потому что он несет дополнительную информацию, которая помогает планировщику запросов оптимизировать запросы, использующие оператор. Следующий раздел будет посвящен объяснению этой дополнительной информации.

QHB поддерживает левый унарный, правый унарный и бинарный операторы. Операторы могут быть перегружены; то есть одно и то же имя оператора может использоваться для разных операторов, которые имеют разное количество и типы операндов. Когда запрос выполняется, система определяет оператор для вызова по числу и типам предоставленных аргументов.

Вот пример создания оператора для сложения двух комплексных чисел. Мы предполагаем, что уже создали определение типа complex (см. раздел Пользовательские типы. Сначала нам нужна функция, которая будет выполнять работу, а затем мы сможем определить оператор:

CREATE FUNCTION complex_add(complex, complex)
    RETURNS complex
    AS 'filename', 'complex_add'
    LANGUAGE C IMMUTABLE STRICT;

CREATE OPERATOR + (
    leftarg = complex,
    rightarg = complex,
    function = complex_add,
    commutator = +
);

Теперь мы можем выполнить запрос следующим образом:

SELECT (a + b) AS c FROM test_complex;

        c
-----------------
 (5.2,6.05)
 (133.42,144.95)

Это мы показали, как создать бинарный оператор. Чтобы создать унарный оператор, просто опустите один из leftarg (для левого унарного) или rightarg (для правого унарного). function и leftarg/rightarg являются единственными обязательными элементами в CREATE OPERATOR. Указание commutator, показанное в примере, является дополнительной подсказкой оптимизатору запросов. Дальнейшие подробности о commutator и других указаниях оптимизации приведены в следующем разделе.

Информация по оптимизации оператора

Определение оператора QHB может включать несколько необязательных указаний, которые сообщают системе полезные сведения о поведении оператора. Эти пункты должны предоставляться всякий раз, когда это уместно, поскольку они могут значительно ускорить выполнение запросов, использующих оператор. Но если вы их предоставите, вы должны быть уверены, что они действительно имеют место быть! Неправильное использование предложения по оптимизации может привести к медленным запросам, неправильному выводу или другим плохим вещам. Вы всегда можете опустить указание оптимизации, если вы не уверены в нём; единственное последствие — запросы могут выполняться медленнее, чем нужно.

Дополнительные пункты оптимизации могут быть добавлены в будущих версиях QHB. Описанные здесь — это те, которые есть в релизе 1.3.1.

Также можно прикрепить вспомогательную функцию для планировщика к функции, которая лежит в основе оператора — это другой способ рассказать системе о поведении оператора. См. раздел Информация по оптимизации функций для получения дополнительной информации.

COMMUTATOR

Указание COMMUTATOR, если оно есть, задаёт имя оператора, который является коммутатором данного. Мы говорим, что оператор A является коммутатором оператора B, если (x A y) равно (y B x) для всех возможных x, y. Обратите внимание, что B также является коммутатором A. Например, операторы < и > для определенного типа данных обычно являются коммутаторами друг друга, а оператор + обычно коммутирует сам с собой, т.е. является коммутативным. А вот оператор - не коммутирует ни с каким другим (понятно, что коммутатор - существует, но его не принято делать оператором).

Тип левого операнда коммутируемого оператора такой же, как тип правого операнда его коммутатора, и наоборот. Таким образом, имя оператора коммутатора - это все, что нужно дать QHB для поиска коммутатора, и это все, что необходимо указать в указании COMMUTATOR.

Очень важно предоставлять информацию о коммутаторах для операторов, которые будут использоваться в индексах и объединениях, поскольку это позволяет оптимизатору запросов «перевернуть» условие, чтобы привести его к форме, необходимой для различных типов планов. В частности, рассмотрим запрос с предложением WHERE, например tab1.x = tab2.y, где tab1.x и tab2.y имеют пользовательский тип, и предположим, что по tab2.y есть индекс. Чтобы сгенерировать сканирование индекса, оптимизатор должен привести условие к виду tab2.y = tab1.x, поскольку механизм сканирования индекса ожидает увидеть индексированный столбец слева от оператора, который ему дан. QHB не будет по умолчанию считать, что = коммутативно — создатель оператора = должен это указать, пометив оператор информацией коммутатора.

Когда вы определяете коммутативный оператор, вы просто делаете это. Когда вы определяете пару коммутативных операторов, все становится немного сложнее: как первый определяемый может ссылаться на другой, который вы еще не определили? Есть два решения этой проблемы:

  • Один из способов - опустить предложение COMMUTATOR в первом определяемом вами операторе, а затем указать его в определении второго оператора. Поскольку QHB знает, что коммутативные операторы идут парами, когда он увидит второе определение, он автоматически вернется и заполнит пропущенное предложение COMMUTATOR в первом определении.

  • Другой, более простой способ - просто включить предложения COMMUTATOR в оба определения. Когда QHB обрабатывает первое определение и понимает, что COMMUTATOR ссылается на несуществующий оператор, система сделает фиктивную запись для этого оператора в системном каталоге. Эта фиктивная запись будет содержать действительные данные только для имени оператора, левого и правого типов операндов и типа результата, поскольку это все, что QHB может вывести на данный момент. Первая запись в каталоге оператора будет связана с этой фиктивной записью. Позже, когда вы определяете второй оператор, система обновляет фиктивную запись дополнительной информацией из второго определения. Если вы попытаетесь использовать фиктивный оператор до его заполнения, вы просто получите сообщение об ошибке.

NEGATOR

Это понятие есть только для булевых операторов. Указание NEGATOR, если оно есть, задаёт оператор, который является отрицанием данного. Мы говорим, что оператор A является отрицанием оператора B, если оба возвращают булевы результаты и (x A y) равно NOT (x B y) для всех возможных x, y. Обратите внимание, что B также является отрицанием A. Например, < и >= являются парой отрицателей для большинства типов данных. Оператор никогда не может быть собственным отрицателем.

В отличие от коммутаторов, пара унарных операторов может быть действительно отмечена как отрицатели друг друга; это означало бы (A x) равно NOT (B x) для всех x или эквивалент для правых унарных операторов.

Отрицатель оператора должен иметь те же типы левого и/или правого операнда, что и определяемый оператор, поэтому, как и в случае с COMMUTATOR предложении NEGATOR должно быть указано только имя оператора.

Предоставление отрицателя очень полезно для оптимизатора запросов, поскольку оно позволяет упростить выражения типа NOT (x >= y) в x < y. Это происходит чаще, чем вы думаете, потому что операции NOT могут быть появляться вследствие других преобразований.

Пары взаимно противоположных операторов могут быть определены с использованием тех же методов, которые описаны выше для пар коммутаторов.

RESTRICT

Это понятие есть только для булевых операторов. Указание RESTRICT задаёт функцию оценки селективности ограничения для оператора. (Обратите внимание, что это имя функции, а не оператора). Идея этой оценки состоит в том, чтобы угадать, какая доля строк в таблице будет удовлетворять условию WHERE column OP constant для данного оператора и постоянного значения в правой части. Это помогает оптимизатору, давая ему некоторое представление о том, сколько строк будет отфильтровано таким WHERE. (А что, если константа будет слева, спросите вы? Ну, это одна из вещей, для которых предназначен COMMUTATOR...)

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

Вспомогательная функцияОператор
eqsel=
neqsel<>
scalarltsel<
scalarlesel<=
scalargtsel>
scalargesel>=

Например, вы можете обойтись использованием eqsel / neqsel для операторов, которые имеют очень высокую / очень низкую селективность, даже если они на самом деле не имеют никакого отношения к равенсту/неравенству. Например, геометрические операторы приближенного равенства используют eqsel в предположении, что им обычно соответствует лишь небольшая часть записей в таблице.

Вы можете использовать scalarltsel, scalarlesel, scalargtsel и scalargesel для сравнения типов данных, которые имеют некоторые разумные средства преобразования в скалярные величины для сравнения диапазонов. В этом случае желательно добавить поддержку типа данных в функции convert_to_scalar().

Если вы этого не сделаете, все будет работать, но оценки оптимизатора не будет так хорошо, как они могли бы быть.

JOIN

Это понятие есть только для булевых операторов. Указание JOIN задаёт функцию оценки избирательности соединения для оператора. (Обратите внимание, что это имя функции, а не имя оператора). Идея оценки избирательности соединения состоит в том, чтобы угадать, какая доля строк в паре таблиц будет удовлетворять условию вида ON table1.column1 OP table2.column2 для данного оператора. Как и в случае с RESTRICT, это существенно помогает оптимизатору, позволяя ему выяснить, какая из нескольких возможных последовательностей объединения может потребовать меньше всего работы.

И опять здесь не рассказывается, как писать свой функции оценки селективности, а просто предлагается использовать одну из стандартных функций оценки, если она похожа на то, что нужно:

Вспомогательная функцияОператор
eqjoinsel=
neqjoinsel<>
scalarltjoinsel<
scalarlejoinsel<=
scalargtjoinsel>
scalargejoinsel>=
areajoinsel2D area-based comparisons
positionjoinsel2D position-based comparisons
contjoinsel2D containment-based comparisons

HASHES

Предложение HASHES, если оно присутствует, сообщает системе, что для соединения, основанного на этом операторе, можно использовать метод hash join. HASHES имеет смысл только для двоичного оператора, который возвращает boolean, и на практике оператор должен представлять отношение эквивалентности, согласованное с хеширующей функцией.

Предположение, лежащее в основе хеш-соединения, заключается в том, что оператор соединения может возвращать true только для пары значений, которые хешируют один и тот же хеш-код. Если два значения помещаются в разные хэш-блоки, объединение никогда не сравнит их вообще, неявно предполагая, что результат оператора соединения должен быть ложным. Поэтому нельзя указывать HASHES для операторов, которые не представляют некоторую форму равенства. В большинстве случаев практично поддерживать хеширование для операторов, которые принимают одинаковый тип данных с обеих сторон. Однако иногда возможно разработать совместимые хеш-функции для двух или более типов данных, то есть функции, которые будут генерировать одинаковые хеш-коды для «равных» значений, даже если значения имеют разные представления. Например, довольно просто обеспечить это свойство при хешировании целых чисел различной ширины.

Чтобы иметь быть HASHES, оператор соединения должен принадлежать семейству операторов хеш-индекса. Это не проверяется при создании оператора, поскольку, конечно, в этот момент семейство ссылочных операторов еще не может существовать. Но попытки использовать оператор в хеш-соединениях потерпят неудачу во время выполнения, если такого семейства операторов не существует. Обращение к семейству операторов нужно, чтобы найти специфические для типа данных хеш-функции. Разумеется, нужно создать подходящие хеш-функции, прежде чем вы сможете создать семейство операторов.

При создании хэш-функции следует соблюдать осторожность, поскольку существуют машинно-зависимые обстоятельства, провоцирующие ошибки. которыми она может не справиться с задачей. Например, если ваш тип данных — это структура, в которой есть неинформативные байты (исклютельно для выравнивания), в них может быть случайный мусор, и вы не можете просто передать всю структуру hash_any. (Чтобы этого избежать, следует писать все функции так, чтобы неинформативные байты заполнялись нулями.) Другой пример — на машинах, использующих числа с плавающей запятой в соотвествии со стандартом IEEE отрицательный ноль и положительный ноль - это разные значения (разные битовые комбинации), но они должны считаться равными. Если значение с плавающей запятой может содержать отрицательный ноль, то нужен дополнительный шаг перед хешированием.

Оператор присоединения к хешу должен иметь коммутатор (сам, если два типа данных операндов одинаковы, или связанный оператор равенства, если они различны), принадлежащий тому же семейству операторов. Если коммутатор не пренадлежит семейству, могут вознуть ошибки планировщика. Кроме того, если семейство хеш-операторов поддерживает несколько типов данных, будет хорошей идеей иметь операторы равенства для всевозможных комбинаций типов, это позволит лучшую оптимизацию.

Замечание
Функция, лежащая в основе HASHES-оператора, должна быть помечена как IMMUTABLE или STABLE. Если он VOLATILE, система никогда не будет пытаться использовать оператор для хеш-соединения.

Замечание
И если эта функция строгая (STRICT), она должна быть всюдуопределенной, т.е. должна возвращать true или false для любых NOT NULL входных значений. Если это правило не соблюдается, хэш-оптимизация условий IN может привести к неверным результатам. (В частности, IN может вернуть false, когда правильный ответ в соответствии со стандартом NULL; или может выдать ошибку, сообщающую, что NULL — недопустимый результат).

MERGES

Предложение MERGES, если оно присутствует, сообщает системе, что для соединения, основанного на этом операторе, можно использовать метод merge-join. MERGES имеет смысл только для двоичного оператора, который возвращает boolean, и на практике оператор должен представлять равенство для некоторого типа данных или пары типов данных.

Объединение слиянием основано на идее упорядочения левой и правой таблиц по порядку и последующего их параллельного сканирования. Таким образом, оба типа данных должны быть полностью упорядочены, и оператор соединения должен иметь какой тип, который может быть успешным только для пар значений, попадающих в «одно и то же место» в порядке сортировки. На практике это означает, что оператор соединения должен вести себя как равенство. Но возможно объединить два разных типа данных, если они логически совместимы. Например, оператор равенства smallint -versus-integer является присоединяемым слиянием. Нам нужны только операторы сортировки, которые приведут оба типа данных в логически совместимую последовательность.

Чтобы пометить MERGES, оператор соединения должен появляться как член равенства семейства операторов индекса btree. Это не применяется при создании оператора, поскольку, конечно, семейство ссылочных операторов еще не могло существовать. Но оператор фактически не будет использоваться для объединений слиянием, если не будет найдено соответствующее семейство операторов. Флаг MERGES таким образом, служит подсказкой для планировщика, что стоит искать подходящее семейство операторов.

Оператор с объединением слиянием должен иметь коммутатор (сам, если два типа данных операндов одинаковы, или связанный оператор равенства, если они различны), который появляется в одном и том же семействе операторов. Если это не так, ошибки планировщика могут возникнуть при использовании оператора. Кроме того, это хорошая идея (но не обязательно) для семейства операторов btree которое поддерживает несколько типов данных, чтобы обеспечить операторы равенства для каждой комбинации типов данных; это позволяет лучше оптимизировать.

Заметка
Функция, лежащая в основе оператора объединения слиянием, должна быть помечена как неизменяемая или стабильная. Если он изменчив, система никогда не будет пытаться использовать оператор для объединения слиянием.

Интерфейсные расширения для индексов

Описанные выше процедуры позволяют вам определять новые типы, новые функции и новые операторы. Однако мы еще не можем определить индекс для столбца нового типа данных. Для этого мы должны определить класс операторов для нового типа данных. Позже в этом разделе мы проиллюстрируем эту концепцию на примере: новый класс операторов для метода индексирования B-дерева, который сохраняет и сортирует комплексные числа в порядке возрастания абсолютных значений.

Классы операторов могут быть сгруппированы в семейства операторов, чтобы показать взаимосвязи между семантически совместимыми классами. Когда задействован только один тип данных, достаточно класса операторов, поэтому мы сначала сосредоточимся на этом случае, а затем вернемся к семействам операторов.

Индексные методы и операторные классы

Таблица pg_am содержит одну строку для каждого индексного метода (внутренне известного как метод доступа). Поддержка регулярного доступа к таблицам встроена в QHB, но все индексные методы описаны в pg_am. Можно добавить новый метод доступа к индексу, написав необходимый код и затем создав запись в pg_am - но это выходит за рамки этой главы (см. Определение интерфейса метода доступа к индексу ).

Подпрограммы для индексного метода напрямую ничего не знают о типах данных, с которыми тот будет работать. Вместо этого операторский класс определяет набор операций, которые индексный метод должен использовать для работы с конкретным типом данных. Классы операторов называются так, потому что они указывают только одну вещь - набор операторов WHERE -предложения, которые можно использовать с индексом (то есть можно преобразовать в квалификацию сканирования индекса). Класс оператора также может указывать некоторую вспомогательную функцию, которая необходима внутренним операциям метода index, но не соответствует напрямую ни одному оператору WHERE-предложения, который может использоваться с индексом.

Можно определить несколько классов операторов для одного и того же типа данных и индексного метода. Благодаря этому для одного типа данных можно определить несколько наборов семантики индексирования. Например, для B-дерева требуется определить порядок сортировки для каждого типа данных, с которым он работает. Для типа данных с комплексным числом может быть полезно иметь один класс операторов B-дерева, который сортирует данные по комплексному абсолютному значению, другой, который сортирует по вещественной части, и так далее. Как правило, один из классов операторов считается наиболее полезным и помечается как класс операторов по умолчанию для этого типа данных и индексного метода.

Одно и то же имя класса оператора можно использовать для нескольких различных индексных методов (например, методы B-дерева и хеш-индекса имеют классы операторов с именем int4_ops), но каждый такой класс является независимой сущностью и должен определяться отдельно.

Стратегии индексного метода

Операторы, связанные с классом операторов, идентифицируются «номерами стратегий», которые служат для идентификации семантики каждого оператора в контексте его класса операторов. Например, B-деревья накладывают строгий порядок на ключи, от меньшего к большему, и поэтому операторы типа «меньше чем» и «больше или равно» интересны в отношении B-дерева. Поскольку QHB позволяет пользователю определять операторы, QHB не может посмотреть на имя оператора (например, < или >= ) и сказать, какое это сравнение. Вместо этого индексный метод определяет набор «стратегий», которые можно рассматривать как обобщенные операторы. Каждый класс операторов определяет, какой фактический оператор соответствует каждой стратегии для конкретного типа данных и интерпретации семантики индекса.

Метод индексирования B-дерева определяет пять стратегий, показанных в таблице 2.

Таблица 2. B-Tree Стратегии

ОперацияНомер стратегии
меньше, чем1
меньше или равно2
равный3
больше или равно4
больше чем5

Хеш-индексы поддерживают только сравнения на равенство, поэтому они используют только одну стратегию, показанную в таблице 3.

Таблица 3. Хэш-стратегии

ОперацияНомер стратегии
равный1

Индексы GiST более гибкие: у них нет фиксированного набора стратегий вообще. Вместо этого подпрограмма поддержки «согласованности» каждого конкретного класса операторов GiST интерпретирует числа стратегий так, как им нравится. Например, некоторые из встроенных классов операторов индекса GiST индексируют двумерные геометрические объекты, предоставляя стратегии «R-дерева», показанные в таблице 4. Четыре из них являются настоящими двумерными тестами (перекрывается, то же самое, содержит, содержится в); четыре из них рассматривают только направление X; и другие четыре обеспечивают те же самые тесты в направлении Y.

Таблица 4. GiST двумерные стратегии R-дерева

ОперацияНомер стратегии
строго слева от1
не распространяется на право2
перекрывается3
не распространяется на лево4
строго справа от5
одно и то же6
содержит7
содержится в8
не распространяется выше9
строго ниже10
строго выше11
не распространяется ниже12

По гибкости индексы SP-GiST аналогичны индексам GiST: у них нет фиксированного набора стратегий. Вместо этого подпрограммы поддержки каждого класса операторов интерпретируют номера стратегий в соответствии с определением класса операторов. Например, номера стратегий, используемые встроенными классами операторов для точек, показаны в таблице 5 .

Таблица 5. SP-GiST Point Strategies

ОперацияНомер стратегии
строго слева от1
строго справа от5
одно и то же6
содержится в8
строго ниже10
строго выше11

Индексы GIN аналогичны индексам GiST и SP-GiST тем, что у них также нет фиксированного набора стратегий. Вместо этого подпрограммы поддержки каждого класса операторов интерпретируют номера стратегий в соответствии с определением класса операторов. Например, номера стратегий, используемые встроенным классом операторов для массивов, показаны в таблице 6.

Таблица 6. Стратегии GIN Array

ОперацияНомер стратегии
перекрытие1
содержит2
содержится в3
равный4

Индексы BRIN аналогичны индексам GiST, SP-GiST и GIN тем, что они также не имеют фиксированного набора стратегий. Вместо этого подпрограммы поддержки каждого класса операторов интерпретируют номера стратегий в соответствии с определением класса операторов. В качестве примера, номера стратегий, используемые встроенными классами операторов Minmax, показаны в таблице 7.

Таблица 7. BRIN Minmax Стратегии

ОперацияНомер стратегии
меньше, чем1
меньше или равно2
равный3
больше или равно4
больше чем5

Обратите внимание, что все перечисленные выше операторы возвращают логические значения. На практике все операторы, определенные как операторы поиска индексного метода, должны возвращать тип boolean, так как они должны появляться на верхнем уровне WHERE для использования с индексом. (Некоторые методы доступа к индексу также поддерживают операторы упорядочения, которые обычно не возвращают логические значения; эта функция обсуждается в разделе Интерфейсные расширения для индексов).

Процедуры поддержки индексного метода

Стратегии обычно не достаточно информации для системы, чтобы понять, как использовать индекс. На практике индексные методы требуют дополнительных подпрограмм поддержки для работы. Например, метод индексирования B-дерева должен уметь сравнивать два ключа и определять, больше ли один, равен или меньше другого. Точно так же метод хэш-индекса должен иметь возможность вычислять хеш-коды для ключевых значений. Эти операции не соответствуют операторам, используемым в квалификациях в командах SQL; они являются административными процедурами, используемыми внутренними индексными методами.

Как и в случае стратегий, класс операторов определяет, какие конкретные функции должны играть каждую из этих ролей для данного типа данных и семантической интерпретации. Индексный метод определяет набор функций, которые ему нужны, а класс операторов определяет правильные функции для использования, назначая их «номерам функций поддержки», указанным в индексном методе.

B-деревья требуют функции поддержки сравнения и позволяют предоставлять две дополнительные функции поддержки по усмотрению автора класса оператора, как показано в таблице 8. Требования к этим функциям поддержки объясняются далее в разделе Вспомогательные функции B-дерева.

Таблица 8. Функции поддержки B-Tree

ФункцияНомер поддержки
Сравните два ключа и верните целое число меньше нуля, нуля или больше нуля, указывая, является ли первый ключ меньше, равен или больше второго1
Возвратите адреса C-вызываемой функции поддержки сортировки (необязательно)2
Сравните значение теста с базовым значением плюс / минус смещение и верните true или false в соответствии с результатом сравнения (необязательно)3

Для хеш-индексов требуется одна вспомогательная функция, и она может предоставляться по второму усмотрению по усмотрению автора класса оператора, как показано в таблице 9.

Таблица 9. Функции поддержки хэша

ФункцияНомер поддержки
Вычислить 32-битное хеш-значение для ключа1
Вычислить 64-битное хеш-значение для ключа с учетом 64-битной соли; если соль равна 0, младшие 32 бита результата должны соответствовать значению, которое было бы вычислено функцией 1 (необязательно)2

Индексы GiST имеют девять вспомогательных функций, две из которых являются необязательными, как показано в таблице 10.

Таблица 10. GiST Поддержка Функции

ФункцияОписаниеНомер поддержки
consistentопределить, удовлетворяет ли ключ квалификатору запроса1
unionвычислить объединение набора ключей2
compressвычислить сжатое представление ключа или значения для индексации3
decompressвычислить распакованное представление сжатого ключа4
penaltyвычислить штраф за вставку нового ключа в поддерево с заданным ключом поддерева5
picksplitопределить, какие записи страницы должны быть перемещены на новую страницу, и вычислить ключи объединения для получающихся страниц6
equalсравнить два ключа и вернуть true, если они равны7
distanceопределить расстояние от ключа до значения запроса (необязательно)8
fetchвычислять исходное представление сжатого ключа для сканирования только по индексу (необязательно)9

Для индексов SP-GiST требуется пять вспомогательных функций, как показано в таблице 11.

Таблица 11. Функции поддержки SP-GiST

ФункцияОписаниеНомер поддержки
configпредоставить основную информацию о классе оператора1
chooseопределить, как вставить новое значение во внутренний кортеж2
picksplitопределить, как разбить набор значений3
inner_consistentопределить, какие подразделы нужно искать для запроса4
leaf_consistentопределить, удовлетворяет ли ключ квалификатору запроса5

Индексы GIN имеют шесть вспомогательных функций, три из которых являются необязательными, как показано в таблице 12.

Таблица 12. Функции поддержки GIN

ФункцияОписаниеНомер поддержки
compareсравнить два ключа и вернуть целое число меньше нуля, нуля или больше нуля, указывающее, является ли первый ключ меньше, равен или больше второго1
extractValueизвлекать ключи из значения для индексации2
extractQueryизвлекать ключи из условия запроса3
consistentопределить, соответствует ли значение условию запроса (логический вариант) (необязательно, если присутствует функция поддержки 6)4
comparePartialсравнивать частичный ключ из запроса и ключ из индекса и возвращать целое число меньше нуля, нуля или больше нуля, указывающее, должен ли GIN игнорировать эту запись индекса, рассматривать запись как совпадение или остановить сканирование индекса (необязательно)5
triConsistentопределить, соответствует ли значение условию запроса (троичный вариант) (необязательно, если присутствует функция поддержки 4)6

Индексы BRIN имеют четыре основные вспомогательные функции, как показано в таблице 13 ; эти основные функции могут потребовать предоставления дополнительных функций поддержки.

Таблица 13. Функции поддержки BRIN

ФункцияОписаниеНомер поддержки
opcInfoвернуть внутреннюю информацию, описывающую сводные данные индексированных столбцов1
add_valueдобавить новое значение в существующий кортеж итогового индекса2
consistentопределить, соответствует ли значение условию запроса3
unionвычислить объединение двух сводных кортежей4

В отличие от операторов поиска, функции поддержки возвращают тот тип данных, который ожидает конкретный индексный метод, например в случае функции сравнения для B-деревьев целое число со знаком. Количество и типы аргументов каждой вспомогательной функции также зависят от индексного метода. Для B-дерева и хеша функции поддержки сравнения и хеширования принимают те же типы входных данных, что и операторы, включенные в класс операторов, но это не относится к большинству функций поддержки GiST, SP-GiST, GIN и BRIN.

Пример

Теперь, когда мы увидели идеи, вот обещанный пример создания нового класса операторов. (Вы можете найти рабочую копию этого примера в src/tutorial/complex.c и src/tutorial/complex.sql в исходном выпуске). Класс операторов инкапсулирует операторы, которые сортируют комплексные числа в порядке абсолютных значений, поэтому мы выбираем имя complex_abs_ops. Во-первых, нам нужен набор операторов. Процедура определения операторов обсуждалась в разделе Пользовательские операторы. Для класса операторов на B-деревьях нам нужны следующие операторы:

  • абсолютное значение меньше чем (стратегия 1)

  • абсолютное значение меньше или равно (стратегия 2)

  • абсолютное значение равно (стратегия 3)

  • абсолютное значение больше или равно (стратегия 4)

  • абсолютное значение больше чем (стратегия 5)

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

#define Mag(c)  ((c)->x*(c)->x + (c)->y*(c)->y)

static int
complex_abs_cmp_internal(Complex *a, Complex *b)
{
    double      amag = Mag(a),
                bmag = Mag(b);

    if (amag < bmag)
        return -1;
    if (amag > bmag)
        return 1;
    return 0;
}

Теперь функция меньше чем выглядит так:

PG_FUNCTION_INFO_V1(complex_abs_lt);

Datum
complex_abs_lt(PG_FUNCTION_ARGS)
{
    Complex    *a = (Complex *) PG_GETARG_POINTER(0);
    Complex    *b = (Complex *) PG_GETARG_POINTER(1);

    PG_RETURN_BOOL(complex_abs_cmp_internal(a, b) < 0);
}

Другие четыре функции отличаются только тем, как они сравнивают результат внутренней функции с нулем.

Далее мы объявляем функции и операторы на основе этих функций в SQL:

CREATE FUNCTION complex_abs_lt(complex, complex) RETURNS bool
    AS 'filename', 'complex_abs_lt'
    LANGUAGE C IMMUTABLE STRICT;

CREATE OPERATOR < (
   leftarg = complex, rightarg = complex, procedure = complex_abs_lt,
   commutator = >, negator = >=,
   restrict = scalarltsel, join = scalarltjoinsel
);

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

Другие вещи, которые стоит отметить, происходят здесь:

  • Может быть только один оператор с именем, скажем, = и принимающий complex типы для обоих операндов. В этом случае у нас нет никакого другого оператора = для complex, но если бы мы строили практический тип данных, мы бы, вероятно, хотели бы иметь = как обычную операцию равенства для комплексных чисел (а не равенства абсолютных значений). В этом случае нам нужно будет использовать другое имя оператора для complex_abs_eq.

  • Хотя QHB может справляться с функциями, имеющими одинаковое имя SQL, при условии, что они имеют разные типы данных аргументов, C может справиться только с одной глобальной функцией, имеющей данное имя. Поэтому мы не должны называть функцию C чем-то простым, например, abs_eq. Обычно рекомендуется включать имя типа данных в имя функции C, чтобы не конфликтовать с функциями других типов данных.

  • Мы могли бы сделать имя SQL для функции abs_eq, полагаясь на QHB, чтобы отличать его по типу данных аргумента от любой другой функции SQL с тем же именем. Чтобы упростить пример, мы заставляем функцию иметь одинаковые имена на уровне C и уровне SQL.

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

CREATE FUNCTION complex_abs_cmp(complex, complex)
    RETURNS integer
    AS 'filename'
    LANGUAGE C IMMUTABLE STRICT;

Теперь, когда у нас есть необходимые операторы и подпрограмма поддержки, мы наконец можем создать класс операторов:

CREATE OPERATOR CLASS complex_abs_ops
    DEFAULT FOR TYPE complex USING btree AS
        OPERATOR        1       <,
        OPERATOR        2       <=,
        OPERATOR        3       =,
        OPERATOR        4       >=,
        OPERATOR        5       >,
        FUNCTION        1       complex_abs_cmp(complex, complex);

И мы сделали! Теперь должна быть возможность создавать и использовать B-деревья для complex столбцов.

Мы могли бы написать записи оператора более подробно, как в:

OPERATOR 1 < (complex, complex),

но нет необходимости делать это, когда операторы принимают тот же тип данных, для которого мы определяем класс операторов.

В приведенном выше примере предполагается, что вы хотите сделать этот новый класс операторов классом операторов B-дерева по умолчанию для complex типа данных. Если вы этого не сделаете, просто пропустите слово DEFAULT.

Классы операторов и семейства операторов

До сих пор мы неявно предполагали, что класс операторов имеет дело только с одним типом данных. Хотя в определенном столбце индекса, безусловно, может быть только один тип данных, часто полезно индексировать операции, которые сравнивают индексированный столбец со значением другого типа данных. Кроме того, если используется оператор кросс-типа данных в связи с классом операторов, часто бывает так, что другой тип данных имеет собственный связанный класс операторов. Полезно сделать связи между связанными классами явными, потому что это может помочь планировщику в оптимизации запросов SQL (особенно для классов операторов B-дерева, поскольку планировщик содержит много знаний о том, как работать с ними).

Для удовлетворения этих потребностей QHB использует концепцию семейства операторов. Семейство операторов содержит один или несколько классов операторов, а также может содержать индексируемые операторы и соответствующие вспомогательные функции, которые принадлежат семейству в целом, но не относятся к какому-либо одному классу в семействе. Мы говорим, что такие операторы и функции «свободны» в семье, а не связаны с определенным классом. Как правило, каждый класс операторов содержит операторы с одним типом данных, в то время как операторы с несколькими типами данных в семействе отсутствуют.

Все операторы и функции в семействе операторов должны иметь совместимую семантику, где требования совместимости устанавливаются индексным методом. Поэтому вы можете спросить, зачем выделять отдельные подмножества семейства как операторные классы; и действительно, для многих целей классовые разделения не имеют значения, и семья является единственной интересной группировкой. Причиной определения классов операторов является то, что они указывают, сколько семейства необходимо для поддержки какого-либо конкретного индекса. Если существует индекс, использующий класс операторов, то этот класс операторов нельзя удалить без удаления индекса, но другие части семейства операторов, а именно другие классы операторов и свободные операторы, могут быть удалены. Таким образом, класс операторов должен быть указан, чтобы содержать минимальный набор операторов и функций, которые разумно необходимы для работы с индексом для определенного типа данных, и затем связанные, но несущественные операторы могут быть добавлены как свободные члены семейства операторов.

В качестве примера, QHB имеет встроенное семейство операторов B-дерева integer_ops, которое включает классы операторов int8_ops, int4_ops и int2_ops для индексов bigint (int8), integer (int4) и smallint (int2) соответственно. Семейство также содержит операторы сравнения между типами данных, позволяющие сравнивать любые два из этих типов, чтобы можно было искать индекс по одному из этих типов, используя значение сравнения другого типа. Семья может быть продублирована следующими определениями:

CREATE OPERATOR FAMILY integer_ops USING btree;

CREATE OPERATOR CLASS int8_ops
DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
  -- standard int8 comparisons
  OPERATOR 1 <,
  OPERATOR 2 <=,
  OPERATOR 3 =,
  OPERATOR 4 >=,
  OPERATOR 5 >,
  FUNCTION 1 btint8cmp(int8, int8),
  FUNCTION 2 btint8sortsupport(internal),
  FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ;

CREATE OPERATOR CLASS int4_ops
DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
  -- standard int4 comparisons
  OPERATOR 1 <,
  OPERATOR 2 <=,
  OPERATOR 3 =,
  OPERATOR 4 >=,
  OPERATOR 5 >,
  FUNCTION 1 btint4cmp(int4, int4),
  FUNCTION 2 btint4sortsupport(internal),
  FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ;

CREATE OPERATOR CLASS int2_ops
DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
  -- standard int2 comparisons
  OPERATOR 1 <,
  OPERATOR 2 <=,
  OPERATOR 3 =,
  OPERATOR 4 >=,
  OPERATOR 5 >,
  FUNCTION 1 btint2cmp(int2, int2),
  FUNCTION 2 btint2sortsupport(internal),
  FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ;

ALTER OPERATOR FAMILY integer_ops USING btree ADD
  -- cross-type comparisons int8 vs int2
  OPERATOR 1 < (int8, int2),
  OPERATOR 2 <= (int8, int2),
  OPERATOR 3 = (int8, int2),
  OPERATOR 4 >= (int8, int2),
  OPERATOR 5 > (int8, int2),
  FUNCTION 1 btint82cmp(int8, int2),

  -- cross-type comparisons int8 vs int4
  OPERATOR 1 < (int8, int4),
  OPERATOR 2 <= (int8, int4),
  OPERATOR 3 = (int8, int4),
  OPERATOR 4 >= (int8, int4),
  OPERATOR 5 > (int8, int4),
  FUNCTION 1 btint84cmp(int8, int4),

  -- cross-type comparisons int4 vs int2
  OPERATOR 1 < (int4, int2),
  OPERATOR 2 <= (int4, int2),
  OPERATOR 3 = (int4, int2),
  OPERATOR 4 >= (int4, int2),
  OPERATOR 5 > (int4, int2),
  FUNCTION 1 btint42cmp(int4, int2),

  -- cross-type comparisons int4 vs int8
  OPERATOR 1 < (int4, int8),
  OPERATOR 2 <= (int4, int8),
  OPERATOR 3 = (int4, int8),
  OPERATOR 4 >= (int4, int8),
  OPERATOR 5 > (int4, int8),
  FUNCTION 1 btint48cmp(int4, int8),

  -- cross-type comparisons int2 vs int8
  OPERATOR 1 < (int2, int8),
  OPERATOR 2 <= (int2, int8),
  OPERATOR 3 = (int2, int8),
  OPERATOR 4 >= (int2, int8),
  OPERATOR 5 > (int2, int8),
  FUNCTION 1 btint28cmp(int2, int8),

  -- cross-type comparisons int2 vs int4
  OPERATOR 1 < (int2, int4),
  OPERATOR 2 <= (int2, int4),
  OPERATOR 3 = (int2, int4),
  OPERATOR 4 >= (int2, int4),
  OPERATOR 5 > (int2, int4),
  FUNCTION 1 btint24cmp(int2, int4),

  -- cross-type in_range functions
  FUNCTION 3 in_range(int4, int4, int8, boolean, boolean),
  FUNCTION 3 in_range(int4, int4, int2, boolean, boolean),
  FUNCTION 3 in_range(int2, int2, int8, boolean, boolean),
  FUNCTION 3 in_range(int2, int2, int4, boolean, boolean) ;

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

В семействе операторов B-дерева все операторы в семействе должны сортироваться совместимо, как подробно описано в разделе Поведение классов операторов B-дерева. Для каждого оператора в семействе должна быть функция поддержки, имеющая те же два типа входных данных, что и оператор. Рекомендуется, чтобы семейство было полным, т.е. для каждой комбинации типов данных включены все операторы. Каждый класс операторов должен включать в себя только операторы не перекрестного типа и функцию поддержки для своего типа данных.

Чтобы создать семейство хеш-операторов с несколькими типами данных, необходимо создать совместимые функции поддержки хеш-функций для каждого типа данных, поддерживаемого этим семейством. Здесь совместимость означает, что функции гарантированно возвращают один и тот же хэш-код для любых двух значений, которые считаются равными операторами равенства семейства, даже когда значения имеют разные типы. Обычно это трудно сделать, когда типы имеют разные физические представления, но это может быть сделано в некоторых случаях. Кроме того, приведение значения из одного типа данных, представленного в семействе операторов, к другому типу данных, также представленному в семействе операторов через неявное или двоичное приведение приведения, не должно изменять вычисленное значение хеш-функции. Обратите внимание, что существует только одна вспомогательная функция для каждого типа данных, а не одна для оператора равенства. Рекомендуется, чтобы семейство было полным, т.е. предоставляло оператор равенства для каждой комбинации типов данных. Каждый класс операторов должен включать только оператор равенства без перекрестного типа и функцию поддержки для своего типа данных.

Индексы GiST, SP-GiST и GIN не имеют явного представления об операциях с кросс-типом данных. Набор поддерживаемых операторов - это то, что могут обрабатывать основные вспомогательные функции для данного класса операторов.

В BRIN требования зависят от структуры, предоставляющей классы операторов. Для классов операторов, основанных на minmax, требуемое поведение такое же, как и для семейств операторов B-дерева: все операторы в семействе должны выполнять совместимую сортировку, и приведение не должно изменять связанный порядок сортировки.

Системные зависимости от классов операторов

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

В частности, существуют функции SQL, такие как ORDER BY и DISTINCT которые требуют сравнения и сортировки значений. Чтобы реализовать эти функции для определенного пользователем типа данных, QHB ищет класс оператора B-дерева по умолчанию для типа данных. Член «равно» этого класса операторов определяет представление системы о равенстве значений для GROUP BY и DISTINCT, а порядок сортировки, налагаемый классом операторов, определяет порядок ORDER BY по умолчанию.

Если для типа данных не существует класса операторов B-дерева по умолчанию, система будет искать класс операторов хеша по умолчанию. Но поскольку этот класс операторов обеспечивает только равенство, он может поддерживать только группирование, а не сортировку.

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

Сортировка по классу операторов B-дерева, отличному от заданного по умолчанию, возможна, если указать, например, оператор класса меньше в опции USING, например:

SELECT * FROM mytable ORDER BY somecol USING ~<~;

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

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

Еще одна особенность SQL, которая требует еще больших знаний о типе данных, - это опция кадрирования RANGE offset PRECEDING/FOLLOWING для оконных функций (см. раздел Вызовы оконных функций). Для запроса, такого как

SELECT sum(x) OVER (ORDER BY x RANGE BETWEEN 5 PRECEDING AND 10 FOLLOWING)
  FROM mytable;

недостаточно знать, как упорядочить по x; база данных также должна понимать, как «вычесть 5» или «добавить 10» к значению x текущей строки, чтобы идентифицировать границы текущего фрейма окна. Сравнение результирующих границ со значениями x других строк возможно с помощью операторов сравнения, предоставляемых классом операторов B-дерева, который определяет порядок ORDER BY, но операторы сложения и вычитания не являются частью класса операторов, поэтому какие из них следует использовать? Такой выбор был бы нежелателен, поскольку разные порядки сортировки (разные классы операторов B-дерева) могут нуждаться в разном поведении. Поэтому класс операторов B-дерева может указывать вспомогательную функцию in_range, которая инкапсулирует поведения сложения и вычитания, которые имеют смысл для порядка сортировки. Он может даже обеспечить более одной функции поддержки in_range, если имеется более одного типа данных, который имеет смысл использовать в качестве смещения в предложениях RANGE. Если класс оператора B-дерева, связанный с предложением окна ORDER BY, не имеет соответствующей функции поддержки in_range, опция RANGE offset PRECEDING/FOLLOWING не поддерживается.

Другим важным моментом является то, что оператор равенства, который появляется в семействе хеш-операторов, является кандидатом на хеш-объединения, агрегирование хеш-функций и связанные с ними оптимизации. Семейство хеш-операторов здесь важно, так как оно идентифицирует хеш-функцию (и) для использования.

Операторы сортировки

Некоторые методы доступа к индексу (в настоящее время только GiST и SP-GiST) поддерживают концепцию операторов упорядочения. До сих пор мы обсуждали поисковые операторы. Оператор поиска - это оператор, для которого можно выполнить поиск по индексу, чтобы найти все строки, удовлетворяющие WHERE indexed_column operator constant. Обратите внимание, что ничего не обещано о порядке, в котором будут возвращены соответствующие строки. Напротив, оператор упорядочения не ограничивает набор строк, которые могут быть возвращены, а вместо этого определяет их порядок. Оператор упорядочения - это оператор, для которого индекс может быть отсканирован для получения строк в порядке, представленном ORDER BY indexed_column operator constant. Причиной определения операторов упорядочения таким образом является то, что он поддерживает поиск ближайшего соседа, если оператор измеряет расстояние. Например, запрос типа

SELECT * FROM places ORDER BY location <-> point '(101,456)' LIMIT 10;

находит десять мест, ближайших к заданной целевой точке. Индекс GiST для столбца местоположения может сделать это эффективно, потому что <-> является оператором упорядочения.

В то время как операторы поиска должны возвращать логические результаты, операторы упорядочения обычно возвращают некоторый другой тип, например, с плавающей или числовой для расстояний. Этот тип обычно не совпадает с индексируемым типом данных. Чтобы избежать жестких предположений о поведении различных типов данных, определение оператора упорядочения необходимо для именования семейства операторов B-дерева, которое определяет порядок сортировки результирующего типа данных. Как было сказано в предыдущем разделе, семейства операторов B-дерева определяют понятие упорядочения в QHB, так что это естественное представление. Поскольку оператор точки <-> возвращает float8, его можно указать в команде создания класса оператора, например:

OPERATOR 15 <-> (point, point) FOR ORDER BY float_ops

где float_ops - это встроенное семейство операторов, которое включает операции на float8. В этом объявлении говорится, что индекс может возвращать строки в порядке возрастания значений оператора <->.

Особенности операторских классов

Есть две особенности классов операторов, которые мы еще не обсуждали, в основном потому, что они бесполезны с наиболее часто используемыми индексными методами.

Как правило, объявление оператора в качестве члена класса (или семейства) оператора означает, что индексный метод может извлечь именно набор строк, которые удовлетворяют условию WHERE, используя оператор. Например:

SELECT * FROM table WHERE integer_column < 4;

может быть точно удовлетворен B-деревом по целочисленному столбцу. Но бывают случаи, когда индекс полезен как неточное руководство для соответствующих строк. Например, если индекс GiST хранит только ограничивающие рамки для геометрических объектов, он не может точно удовлетворить условие WHERE, которое проверяет перекрытие между непрямоугольными объектами, такими как многоугольники. Тем не менее, мы могли бы использовать индекс для поиска объектов, ограничивающий прямоугольник которых перекрывает ограничивающий прямоугольник целевого объекта, а затем выполнить точный тест перекрытия только для объектов, найденных индексом. Если этот сценарий применим, индекс считается «потерянным» для оператора. Поиск по индексу с потерями реализуется с помощью индексного метода, возвращающего флаг перепроверки, когда строка может или не может действительно удовлетворять условию запроса. Базовая система затем проверит исходное условие запроса в извлеченной строке, чтобы увидеть, следует ли возвращать его в качестве действительного соответствия. Этот подход работает, если индекс гарантированно возвращает все необходимые строки плюс, возможно, некоторые дополнительные строки, которые можно устранить, выполнив исходный вызов оператора. Индексные методы, которые поддерживают поиск с потерями (в настоящее время GiST, SP-GiST и GIN), позволяют функциям поддержки отдельных классов операторов устанавливать флаг повторной проверки, и, таким образом, это по сути функция класса операторов.

Рассмотрим снова ситуацию, когда мы храним в индексе только ограничивающую рамку сложного объекта, такого как многоугольник. В этом случае нет смысла хранить весь многоугольник в элементе индекса - мы могли бы также хранить просто более простой объект типа box. Эта ситуация выражается опцией STORAGE в CREATE OPERATOR CLASS: мы напишем что-то вроде:

CREATE OPERATOR CLASS polygon_ops
    DEFAULT FOR TYPE polygon USING gist AS
        ...
        STORAGE box;

В настоящее время только индексные методы GiST, GIN и BRIN поддерживают тип STORAGE, который отличается от типа данных столбца. Процедуры поддержки сжатия и распаковки GiST должны иметь дело с преобразованием типов данных при использовании STORAGE. В GIN тип STORAGE идентифицирует тип значений «ключа», который обычно отличается от типа индексированного столбца - например, класс оператора для столбцов целочисленного массива может иметь ключи, которые являются просто целыми числами. Функции извлечения GIN extractValue и extractQuery отвечают за извлечение ключей из индексированных значений. BRIN аналогичен GIN: тип STORAGE определяет тип хранимых итоговых значений, а процедуры поддержки классов операторов отвечают за правильную интерпретацию итоговых значений.

Упаковка связанных объектов в расширение

Полезное расширение QHB обычно включает несколько объектов SQL; например, новый тип данных потребует новых функций, новых операторов и, возможно, новых классов операторов индекса. Полезно собрать все эти объекты в один пакет, чтобы упростить управление базой данных. QHB называет такой пакет расширением. Чтобы определить расширение, вам нужен как минимум файл сценария, который содержит команды SQL для создания объектов расширения, и управляющий файл, который задает несколько основных свойств самого расширения. Если расширение включает в себя C/RUST-код, обычно также будет файл общей библиотеки, в который был встроен C/RUST-код. Когда у вас есть эти файлы, простая команда (см. CREATE EXTENSION загружает объекты в вашу базу данных.

Основное преимущество использования расширения вместо простого запуска сценария SQL для загрузки группы «незакрепленных» объектов в вашу базу данных заключается в том, что QHB поймет, что объекты расширения объединяются. Вы можете удалить все объекты с помощью одной команды DROP EXTENSION (нет необходимости поддерживать отдельный сценарий «удаления»). Еще более полезно, что qhb_dump знает, что он не должен создавать дамп отдельных объектов-членов расширения - вместо этого он будет просто включать команду CREATE EXTENSION в дампы. Это значительно упрощает миграцию на новую версию расширения, которая может содержать больше объектов или отличаться от старой версии. Однако обратите внимание, что при загрузке такого дампа в новую базу данных вы должны иметь доступ к элементу управления, сценарию и другим файлам расширения.

QHB не позволит вам удалить отдельный объект, содержащийся в расширении, за исключением удаления всего расширения. Кроме того, хотя вы можете изменить определение объекта-члена расширения (например, с помощью функции CREATE OR REPLACE FUNCTION для функции), имейте в виду, что измененное определение не будет выгружено qhb_dump. Такое изменение обычно имеет смысл только в том случае, если вы одновременно вносите такое же изменение в файл сценария расширения. (Но есть специальные положения для таблиц, содержащих данные конфигурации; см. Раздел Упаковка связанных объектов в расширение). В производственных ситуациях, как правило, лучше создавать сценарий обновления расширения для выполнения изменений в объектах-членах расширения.

Сценарий расширения может устанавливать привилегии для объектов, являющихся частью расширения, с помощью операторов GRANT и REVOKE. Окончательный набор привилегий для каждого объекта (если они установлены) будет сохранен в системном каталоге pg_init_privs. Когда используется qhb_dump, команда CREATE EXTENSION будет включена в дамп, за которым следует набор операторов GRANT и REVOKE необходимых для того, чтобы установить привилегии для объектов такими, какими они были на момент получения дампа.

QHB в настоящее время не поддерживает сценарии расширения, выдающие операторы CREATE POLICY или SECURITY LABEL. Ожидается, что они будут установлены после создания расширения. Все политики RLS и метки безопасности на объектах расширения будут включены в дампы, созданные qhb_dump.

Механизм расширения также содержит положения для упаковки сценариев модификации, которые корректируют определения объектов SQL, содержащихся в расширении. Например, если версия 1.1 расширения добавляет одну функцию и изменяет тело другой функции по сравнению с 1.0, автор расширения может предоставить скрипт обновления, который вносит только эти два изменения. Затем можно применить команду ALTER EXTENSION UPDATE чтобы применить эти изменения и отследить, какая версия расширения фактически установлена в данной базе данных.

Виды объектов SQL, которые могут быть членами расширения, показаны в описании ALTER EXTENSION. В частности, объекты, относящиеся к общему кластеру базы данных, такие как базы данных, роли и табличные пространства, не могут быть членами расширения, поскольку расширение известно только в одной базе данных. (Хотя сценарию расширения не запрещено создавать такие объекты, в этом случае они не будут отслеживаться как часть расширения). Также обратите внимание, что хотя таблица может быть членом расширения, ее вспомогательные объекты, такие как индексы, непосредственно не считаются членами расширения. Другим важным моментом является то, что схемы могут принадлежать расширениям, но не наоборот: расширение как таковое имеет неквалифицированное имя и не существует «внутри» какой-либо схемы. Объекты-члены расширения, тем не менее, будут принадлежать схемам, когда это уместно для их типов объектов. Расширение может или не может быть подходящим для того, чтобы расширение владело схемой (схемами), в которой находятся его элементы-члены.

Если сценарий расширения создает какие-либо временные объекты (например, временные таблицы), эти объекты обрабатываются как элементы расширения для оставшейся части текущего сеанса, но автоматически удаляются в конце сеанса, как и любой временный объект. Это исключение из правила, что объекты-члены расширения не могут быть удалены без удаления всего расширения.

Определение объектов расширения

Широко распространенные расширения должны предполагать немного о базе данных, которую они занимают. В частности, если вы не указали SET search_path = pg_temp, предположите, что каждое неквалифицированное имя может преобразовываться в объект, определенный злоумышленником. Остерегайтесь конструкций, которые неявно зависят от search_path: выражения IN и CASE expression WHEN всегда выбирают оператор, используя путь поиска. Вместо них используйте OPERATOR(schema.=) ANY и CASE WHEN expression.

Файлы расширений

Команда CREATE EXTENSION опирается на управляющий файл для каждого расширения, который должен называться так же, как расширение с суффиксом .control, и должен быть помещен в каталог SHAREDIR/extension. Также должен быть хотя бы один файл сценария SQL, который следует за расширению шаблона именования extension--version.sql (например, foo--1.0.sql для версии 1.0 расширения foo). По умолчанию файлы сценариев также размещаются в каталог SHAREDIR/extension; но файл управления может указывать другой каталог для файла(ов) сценария.

Формат файла для файла управления расширениями такой же, как и для файла qhb.conf, а именно список назначений parameter_name = value, по одному на строку. Пустые строки и комментарии, представленные #, разрешены. Не забудьте указать любое значение, которое не является ни одним словом или числом.

Управляющий файл может устанавливать следующие параметры:

directory (string)
Каталог, содержащий файл(ы) SQL- сценария расширения. Если не указан абсолютный путь, имя SHAREDIR каталога SHAREDIR установки. Поведение по умолчанию эквивалентно указанию directory = ’extension’.
default_version (string)
Версия расширения по умолчанию (та, которая будет установлена, если в CREATE EXTENSION не указана версия). Хотя это может быть опущено, это приведет к сбою CREATE EXTENSION если опция VERSION не появится, поэтому вы обычно не хотите этого делать.
comment (string)
Комментарий (любая строка) о расширении. Комментарий применяется при первоначальном создании расширения, но не при его обновлении (поскольку это может переопределить добавленные пользователем комментарии). Кроме того, комментарий расширения можно установить, написав команду COMMENT в файле сценария.
encoding (string)
Кодировка набора символов, используемая в файле(ах) скрипта. Это следует указывать, если файлы сценариев содержат символы, не относящиеся к ASCII. В противном случае предполагается, что файлы находятся в кодировке базы данных.
module_pathname (string)
Значение этого параметра будет заменено для каждого вхождения MODULE_PATHNAME в файлах скриптов. Если он не установлен, замена не производится. Как правило, это значение равно $libdir/shared_library_name а затем MODULE_PATHNAME используется в командах CREATE FUNCTION для функций языка C/RUST, поэтому файлам сценариев не нужно жестко связывать имя разделяемой библиотеки.
requires (string)
Список имен расширений, от которых зависит это расширение, например requires = ’foo, bar’. Эти расширения должны быть установлены до того, как можно будет установить это расширение.
superuser (boolean)
Если этот параметр имеет значение true (по умолчанию), только суперпользователи могут создать расширение или обновить его до новой версии. Если установлено значение false, требуются только те привилегии, которые необходимы для выполнения команд в сценарии установки или обновления.
relocatable (boolean)
Расширение можно перемещать, если возможно переместить содержащиеся в нем объекты в другую схему после первоначального создания расширения. По умолчанию установлено значение false, то есть расширение не может быть перемещено. См. Раздел Перемещаемость расширения для получения дополнительной информации.
schema (string)
Этот параметр может быть установлен только для не перемещаемых расширений. Это заставляет расширение загружаться в точно названную схему, а не в любую другую. Параметр schema используется только при первоначальном создании расширения, а не при его обновлении. См. Раздел Перемещаемость расширения для получения дополнительной информации.

В дополнение к основному управляющему файлу extension.control расширение может иметь вторичные управляющие файлы, названные в расширении стиля extension--version.control. Если они есть, они должны находиться в каталоге файлов сценариев. Вторичные управляющие файлы имеют тот же формат, что и основной управляющий файл. Любые параметры, установленные во вторичном управляющем файле, переопределяют первичный управляющий файл при установке или обновлении до этой версии расширения. Однако каталог параметров и default_version нельзя установить во вторичном управляющем файле.

Файлы сценариев SQL расширения могут содержать любые команды SQL, кроме команд управления транзакциями (BEGIN, COMMIT и т.д.) И команд, которые не могут быть выполнены внутри блока транзакции (например, VACUUM). Это связано с тем, что файлы сценариев неявно выполняются внутри блока транзакции.

Файлы сценариев SQL расширения также могут содержать строки, начинающиеся с \echo, которые будут игнорироваться (обрабатываться как комментарии) механизмом расширения. Это положение обычно используется для выдачи ошибки, если файл сценария подается в qsql, а не загружается через CREATE EXTENSION (см. Пример сценария в разделе Упаковка связанных объектов в расширение). Без этого пользователи могут случайно загрузить содержимое расширения как «незакрепленные» объекты, а не как расширение, - состояние дел, которое немного утомительно восстанавливать.

Хотя файлы сценариев могут содержать любые символы, разрешенные указанной кодировкой, управляющие файлы должны содержать только простой ASCII, поскольку QHB не может узнать, в какой кодировке находится управляющий файл. На практике это проблема, только если вы хотите используйте не-ASCII символы в комментарии расширения. В этом случае рекомендуется не использовать параметр comment к контрольному файлу, а вместо этого использовать COMMENT ON EXTENSION в файле сценария для установки комментария.

Перемещаемость расширения

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

  • Полностью перемещаемое расширение может быть перемещено в другую схему в любое время, даже после его загрузки в базу данных. Это делается с помощью команды ALTER EXTENSION SET SCHEMA, которая автоматически переименовывает все объекты-члены в новую схему. Обычно это возможно только в том случае, если расширение не содержит внутренних предположений о том, в какой схеме находится какой-либо из его объектов. Кроме того, все объекты расширения должны начинаться с одной схемы (игнорируя объекты, которые не принадлежат какой-либо схеме, например: процедурные языки). Отметьте полностью перемещаемое расширение, установив relocatable = true в его контрольный файл.

  • Расширение может перемещаться во время установки, но не после. Обычно это происходит, если файл сценария расширения должен явно ссылаться на целевую схему, например, при настройке свойств search_path для функций SQL. Для такого расширения установите relocatable = false в его управляющем файле и используйте @extschema@ чтобы обратиться к целевой схеме в файле сценария. Все вхождения этой строки будут заменены фактическим именем целевой схемы перед выполнением сценария. Пользователь может установить целевую схему, используя опцию SCHEMA команды CREATE EXTENSION.

  • Если расширение вообще не поддерживает перемещение, установите в его управляющем файле relocatable = false, а также задайте для schema имя предполагаемой целевой схемы. Это предотвратит использование опции SCHEMA CREATE EXTENSION, если только в ней не указана та же схема, что и в контрольном файле. Этот выбор обычно необходим, если расширение содержит внутренние предположения об именах схем, которые нельзя заменить использованием @extschema@. Механизм замещения @extschema@ доступен в этом случае, хотя он имеет ограниченное использование, поскольку имя схемы определяется управляющим файлом.

Во всех случаях файл сценария будет выполняться с параметром search_path, изначально установленным для указания на целевую схему; то есть CREATE EXTENSION делает эквивалент этого:

SET LOCAL search_path TO @extschema@;

Это позволяет объектам, созданным файлом сценария, перейти в целевую схему. Файл сценария может изменить search_path если пожелает, но это обычно нежелательно. search_path восстанавливается до прежнего значения после завершения CREATE EXTENSION.

Целевая схема определяется параметром schema в управляющем файле, если он указан, в противном случае - параметром SCHEMA в CREATE EXTENSION если он задан, в противном случае - текущей схемой создания объекта по умолчанию (первой в пути search_path вызывающего). Когда используется параметр schema управляющего файла, целевая схема будет создана, если она еще не существует, но в двух других случаях она уже должна существовать.

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

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

Таблицы конфигурации расширений

Некоторые расширения включают таблицы конфигурации, которые содержат данные, которые могут быть добавлены или изменены пользователем после установки расширения. Обычно, если таблица является частью расширения, ни определение таблицы, ни ее содержимое не будут выгружены qhb_dump. Но такое поведение нежелательно для таблицы конфигурации; любые изменения данных, сделанные пользователем, должны быть включены в дампы, иначе расширение будет вести себя по-другому после дампа и перезагрузки.

Чтобы решить эту проблему, файл сценария расширения может пометить созданную им таблицу или последовательность как отношение конфигурации, что заставит qhb_dump включить содержимое таблицы или последовательности (не ее определение) в дампы. Для этого вызовите функцию pg_extension_config_dump(regclass, text) после создания таблицы или последовательности, например

CREATE TABLE my_config (key text, value text);
CREATE SEQUENCE my_config_seq;

SELECT pg_catalog.pg_extension_config_dump('my_config', '');
SELECT pg_catalog.pg_extension_config_dump('my_config_seq', '');

Таким образом можно пометить любое количество таблиц или последовательностей. Последовательности, связанные с serial или bigserial столбцов, также могут быть отмечены.

Когда второй аргумент pg_extension_config_dump является пустой строкой, все содержимое таблицы выгружается с помощью qhb_dump. Обычно это верно только в том случае, если таблица изначально пуста, как это создано сценарием расширения. Если в таблице содержится смесь исходных данных и данных, предоставленных пользователем, второй аргумент pg_extension_config_dump предоставляет условие WHERE которое выбирает данные, которые должны быть выгружены. Например, вы можете сделать

CREATE TABLE my_config (key text, value text, standard_entry boolean);

SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entry');

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

Для последовательностей второй аргумент pg_extension_config_dump имеет никакого эффекта.

Более сложные ситуации, такие как изначально предоставленные строки, которые могут быть изменены пользователями, могут быть обработаны путем создания триггеров в таблице конфигурации, чтобы гарантировать, что измененные строки помечены правильно.

Вы можете изменить условие фильтра, связанное с таблицей конфигурации, снова вызвав pg_extension_config_dump. (Обычно это может быть полезно в скрипте обновления расширения). Единственный способ пометить таблицу как таблицу, которая больше не является таблицей конфигурации, - это отсоединить ее от расширения с помощью ALTER EXTENSION ... DROP TABLE.

Обратите внимание, что отношения внешнего ключа между этими таблицами будут определять порядок, в котором таблицы выгружаются qhb_dump. В частности, qhb_dump попытается вывести таблицу, на которую ссылаются, перед таблицей, на которую ссылаются. Поскольку отношения внешнего ключа устанавливаются во время CREATE EXTENSION (до загрузки данных в таблицы), циклические зависимости не поддерживаются. Когда существуют циклические зависимости, данные все равно будут выгружены, но дамп не сможет быть восстановлен напрямую, и потребуется вмешательство пользователя.

Последовательности, связанные с serial или bigserial столбцами, должны быть помечены напрямую, чтобы вывести их состояние. Маркировка их родительских отношений недостаточно для этой цели.

Обновления расширений

Одним из преимуществ механизма расширения является то, что он предоставляет удобные способы управления обновлениями команд SQL, которые определяют объекты расширения. Это делается путем привязки имени или номера версии к каждой выпущенной версии сценария установки расширения. Кроме того, если вы хотите, чтобы пользователи могли динамически обновлять свои базы данных с одной версии на другую, вы должны предоставить сценарии обновления, которые вносят необходимые изменения для перехода с одной версии на другую. Сценарии обновления имеют имена, следующего шаблона extension--old_version--target_version.sql (например, foo--1.0--1.1.sql содержит команды для изменения версии 1.0 расширения foo в версию 1.1 ).

При наличии подходящего сценария обновления команда ALTER EXTENSION UPDATE обновит установленное расширение до указанной новой версии. Сценарий обновления выполняется в той же среде, которую CREATE EXTENSION предоставляет для сценариев установки: в частности, search_path настраивается таким же образом, и любые новые объекты, созданные сценарием, автоматически добавляются в расширение. Кроме того, если сценарий выбирает удаление объектов-членов расширения, они автоматически отсоединяются от расширения.

Если расширение имеет вторичные контрольные файлы, параметры управления, используемые для сценария обновления, являются теми, которые связаны с целевой (новой) версией сценария.

Механизм обновления может использоваться для решения важного особого случая: преобразования «свободной» коллекции объектов в расширение. До того, как механизм расширения был добавлен в PostgreSQL (в 9.1), многие люди писали модули расширения, которые просто создавали разные неупакованные объекты. Учитывая существующую базу данных, содержащую такие объекты, как мы можем преобразовать объекты в правильно упакованное расширение? Удаление их, а затем выполнение простого CREATE EXTENSION - один из способов, но нежелательно, если у объектов есть зависимости (например, если существуют столбцы таблицы типа данных, созданные расширением). Чтобы исправить эту ситуацию, нужно создать пустое расширение, затем использовать ALTER EXTENSION ADD чтобы присоединить каждый существующий объект к расширению, а затем, наконец, создать любые новые объекты, которые находятся в текущей версии расширения, но отсутствуют в распакованном выпуске. CREATE EXTENSION поддерживает этот случай с опцией FROM old_version, которая заставляет его не запускать обычный скрипт установки для целевой версии, а вместо этого сценарий обновления с именем extension--old_version--target_version.sql. Выбор имени фиктивной версии для использования в качестве old_version зависит от автора расширения, хотя unpackaged является общим соглашением. Если у вас есть несколько предыдущих версий, вы должны иметь возможность обновиться до стиля расширения, используйте несколько фиктивных названий версий для их идентификации.

ALTER EXTENSION может выполнять последовательности файлов сценариев обновления для достижения запрошенного обновления. Например, если доступны только foo--1.0--1.1.sql и foo--1.1--2.0.sql, ALTER EXTENSION будет применять их последовательно, если будет запрошено обновление до версии 2.0, а в данный момент установлена версия 1.0.

QHB ничего не говорит о свойствах имен версий: например, он не знает, следует ли 1.1 за 1.0. Он просто сопоставляет доступные имена версий и следует по пути, который требует применения наименьшего количества сценариев обновления. (Имя версии может фактически быть любой строкой, которая не содержит -- или начало или конец -).

Иногда полезно предоставить сценарии «понижения», например, foo--1.1--1.0.sql чтобы позволить отменить изменения, связанные с версией 1.1. Если вы это сделаете, будьте осторожны с возможностью неожиданного применения скрипта понижения, поскольку он дает более короткий путь. Рискованный случай - это сценарий обновления «быстрого пути», который переходит вперед на несколько версий, а также сценарий перехода к начальной точке быстрого пути. Может потребоваться меньше шагов, чтобы применить понижение и затем быстрый путь, чем продвигаться вперед по одной версии за раз. Если скрипт понижения удаляет незаменимые объекты, это приведет к нежелательным результатам.

Чтобы проверить наличие неожиданных путей обновления, используйте эту команду:

SELECT * FROM pg_extension_update_paths('extension_name');

Это показывает каждую пару различных известных имен версий для указанного расширения вместе с последовательностью пути обновления, которая была бы принята, чтобы получить от исходной версии до целевой версии, или NULL если нет доступного пути обновления. Путь показан в текстовом виде с -- разделителями. Вы можете использовать regexp_split_to_array(path,'--') если вы предпочитаете формат массива.

Установка расширений с использованием скриптов обновления

Расширение, которое существует уже некоторое время, вероятно, будет существовать в нескольких версиях, для которых автору потребуется написать сценарии обновления. Например, если вы выпустили расширение foo в версиях 1.0, 1.1 и 1.2, должны быть сценарии обновления foo--1.0--1.1.sql и foo--1.1--1.2.sql. До PostgreSQL 10 необходимо было также создавать новые файлы сценариев foo--1.1.sql и foo--1.2.sql которые напрямую собирали более новые версии расширений, иначе более новые версии не могли быть установлены напрямую, только путем установки 1.0 и затем обновление. Это было утомительно и дублировало, но теперь это не нужно, потому что CREATE EXTENSION может автоматически следовать цепочкам обновлений. Например, если доступны только файлы сценариев foo--1.0.sql, foo--1.0--1.1.sql и foo--1.1--1.2.sql, то запрос на установку версии 1.2 выполняется с помощью запуска этих трех сценарии в последовательности. Обработка такая же, как если бы вы сначала установили 1.0 а затем обновили до 1.2. (Как и в случае с ALTER EXTENSION UPDATE, если доступно несколько путей, предпочтительнее использовать кратчайший путь). Размещение файлов сценариев расширения в этом стиле может уменьшить объем работ по обслуживанию, необходимых для создания небольших обновлений.

Если вы используете вторичные (зависящие от версии) контрольные файлы с расширением, поддерживаемым в этом стиле, имейте в виду, что каждой версии нужен контрольный файл, даже если у него нет отдельного сценария установки, поскольку этот контрольный файл будет определять, как неявное обновление на эту версию выполняется. Например, если foo--1.0.control указывает requires = 'bar' но другие управляющие файлы foo этого не делают, за