Шпаргалка по работе с полями XML в Postgres
Документация по PostgreSQL на русском языке тут
С типами PostgreSQL можно ознакомиться по этой ссылке
С функциями массивов можно ознакомиться по этой ссылке
Вводные по используемым функциям
Нумерация элементов в XML документах начинается с 1
Функция xpath
Функция xpath возвращает массив значений XML, соответствующих выражению XPath. Если выражение XPath возвращает скалярное значение, xpath возвращает массив из одного элемента.
Запрос
SELECT  xpath('/collaborator/category_id', data) AS cats
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат
Функция xpath_exists
Функция xpath_exists в PostgreSQL определяет, существуют ли элементы, соответствующие выражению XPath.
Результат её выполнения — значение Boolean, определяющее, будет ли возвращено какое-либо значение, кроме пустого набора узлов.
Запрос
SELECT  xpath_exists('/collaborator/category_id', data) AS bcats
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат

Функция unnest
Функция unnest() в PostgreSQL принимает массив в качестве аргумента и расширяет данный массив в набор строк. Проще говоря, unnest преобразует массив в табличную структуру.
Запрос
SELECT  unnest(xpath('/collaborator/category_id', data)) AS bcats
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат

Приведение типов
Для приведения типов используется конструкция CAST (exp AS data_type)
Запрос
Результат

Чаще всего используется укороченный синтаксис приведения
Запрос
Результат

Сборник запросов
Получаем первый XML узел по его имени
Найдем узел lastname
Запрос
SELECT   xpath('/collaborator/lastname[1]', data) AS lastname
        ,xpath('/collaborator/lastname[2]', data) AS lastname
        ,(xpath('/collaborator/lastname', data))[1] AS lastname
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат

Обратите внимание
Первый вариант вернет массив с узлом lastname с индексом 1
Второй вариант вернет пустой массив, так как в документе не существует узел lastname с индексом 2
Третий вариант возвращает первый узел lastname из массива найденных узлов lastname
Получаем все XML узлы по имени
Найдем все узлы с именем category_id. Каждая категория, присвоенная сотруднику, находится в отдельном <category_id>...</category_id>
Запрос
SELECT  xpath('/collaborator/category_id', data) AS cats
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат
Обратите внимание
xpath всегда возвращает массив найденных узлов
Получаем XML узел со всем его содержимым
Найдем узлы кастомных полей с его родителем. Кастомные поля хранятся в узлах с именем custom_elem, корневой узел для которых custom_elems
Запрос
SELECT  xpath('/collaborator/custom_elems', data) AS custom_elems
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат

<custom_elems>
    <custom_elem>
        <name>f_nj0z</name>
    </custom_elem>
    <custom_elem>
        <name>field1</name>
        <value>0x61FD2936115CA629</value>
    </custom_elem>
    <custom_elem>
        <name>field2</name>
        <value>sdfsfsfsdfsf</value>
    </custom_elem>
    <custom_elem>
        <name>fld_doc</name>
        <value>0x61ADE372EBEAD381</value>
    </custom_elem>
    <custom_elem>
        <name>fld_relation</name>
        <value>0x61FD2D422FA94E42</value>
    </custom_elem>
    <custom_elem>
        <name>fld_city</name>
        <value>Киров</value>
    </custom_elem>
    <custom_elem>
        <name>fld_comment</name>
        <value>Что-то умеет делать</value>
    </custom_elem>
    <custom_elem>
        <name>fld_age</name>
        <value>36</value>
    </custom_elem>
    <custom_elem>
        <name>fld_weight</name>
        <value>74</value>
    </custom_elem>
    <custom_elem>
        <name>fld_birth_date</name>
        <value>1988-06-02T00:00:00</value>
    </custom_elem>
    <custom_elem>
        <name>fld_married</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>fld_sex</name>
        <value>М</value>
    </custom_elem>
    <custom_elem>
        <name>fld_skills</name>
        <value>Офис;Javascript</value>
    </custom_elem>
    <custom_elem>
        <name>skill_javascript</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>skill_PHP</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>skill_Docker</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>skill_Kerberos</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>skill_HTML</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>skill_WebTutor</name>
        <value>true</value>
    </custom_elem>
</custom_elems>
Получаем XML узел по его индексу
Найдем узел custom_elem по его индексу
Запрос
SELECT   xpath('/collaborator/custom_elems/custom_elem[6]', data) AS custom_elem
        ,(xpath('/collaborator/custom_elems/custom_elem', data))[6] AS custom_elem
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат
Обратите внимание
В первом случае возвращается массив xml, во втором узел xml
Получаем XML узел по значению его вложенного узла
Найдем узел custom_elem в котором содержится узел name со значением fld_city
Запрос
SELECT  xpath('/collaborator/custom_elems/custom_elem[name="fld_city"]', data) AS custom_elem
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат
Конструкция ...custom_elem[name="fld_city"]... ищет узел custom_elem в котором находится узел name со значением fld_city и возвращает массив подходящих узлов custom_elem
Получаем XML узел по значению соседнего XML узла
Найдем узел value, который вложен в узел custom_elem, где так же вложен узел name со значением fld_city
Запрос
SELECT  xpath('/collaborator/custom_elems/custom_elem[name="fld_city"]/value', data) AS custom_elem_city
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат

Получаем значение XML узла
Найдем значения узлов с основными данными сотрудника (ИД, фамилию, дату рождения и флаг уволен сотрудник или нет) и приведем их к примитивным типам
Запрос
SELECT   (xpath('/collaborator/id/text()', data))[1]::TEXT::BIGINT AS id
        ,(xpath('/collaborator/lastname/text()', data))[1]::TEXT AS lastname
        ,(xpath('/collaborator/birth_date/text()', data))[1]::TEXT::DATE AS birth_date
        ,(xpath('/collaborator/is_dismiss/text()', data))[1]::TEXT::BOOLEAN AS is_dismiss
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат

Обратите внимание
Если попытаться привести поле lastname к дате, то получим ошибку

Получаем значения кастомных полей
Найдем значения value кастомных полей с именами fld_city, fld_birth_date и fld_skills
Запрос
SELECT   (xpath('/collaborator/custom_elems/custom_elem[name="fld_city"]/value/text()', data))[1]::TEXT
        ,(xpath('/collaborator/custom_elems/custom_elem[name="fld_birth_date"]/value/text()', data))[1]::TEXT::TIMESTAMP
        ,(xpath('/collaborator/custom_elems/custom_elem[name="fld_skills"]/value/text()', data))[1]::TEXT
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат

Получаем значения дочерних узлов из массива родительских узлов
Найдем из списка состояний сотрудника history_states состояние history_state, поле state_id которого имеет значение vacation, и выведем значения узлов start_date и finish_date в формате даты
Запрос
SELECT   (xpath('/collaborator/history_states/history_state[state_id="vacation"]/start_date/text()', data))::TEXT[]::TIMESTAMP[]
        ,(xpath('/collaborator/history_states/history_state[state_id="vacation"]/finish_date/text()', data))::TEXT[]::TIMESTAMP[]
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат

Обратите внимание
В карточке сотрудника существует 2 состояния с кодом vacation, а как мы уже знаем, xpath возвращает массив xml.
Сответственно и привидение типов тоже должно выполняться для массивов.
Получаем последний дочерний XML узел
Когда количество узлов неизвестно, можно получить последний узел с помощью функции last(), предпоследний last()-1 и т.д.
Запрос
SELECT   (xpath('/collaborator/custom_elems/custom_elem[last()]/name/text()', data))[1]::TEXT
        ,(xpath('/collaborator/custom_elems/custom_elem[last()-1]/name/text()', data))[1]::TEXT
        ,(xpath('/collaborator/custom_elems/custom_elem[last()-2]/name/text()', data))[1]::TEXT
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат

Использование агрегации для значений XML узлов
У сотрудника есть 2 отпуска. В комментариях к состоянию хранится количество дней отпуска. Нам нужно получить количество отпусков и их сумму дней. Для этого применим 2 функции count() и sum()
XML состояний сотрудника
<history_states>
    <history_state>
        <id>y8gr8e</id>
        <state_id>vacation</state_id>
        <start_date>2024-10-01T00:00:00+00:00</start_date>
        <finish_date>2024-10-06T00:00:00+00:00</finish_date>
        <comment>6</comment>
    </history_state>
    <history_state>
        <id>7sgn58</id>
        <state_id>g5k8</state_id>
        <start_date>2024-09-01T00:00:00+00:00</start_date>
        <finish_date>2024-09-04T00:00:00+00:00</finish_date>
        <comment>4</comment>
    </history_state>
    <history_state>
        <id>mvchh6</id>
        <state_id>vacation</state_id>
        <start_date>2025-01-01T00:00:00+00:00</start_date>
        <finish_date>2025-01-31T00:00:00+00:00</finish_date>
        <comment>30</comment>
    </history_state>
</history_states>
Запрос
SELECT   (xpath('sum(/collaborator/history_states/history_state[state_id="vacation"]/comment/text())', data))[1]::TEXT::INTEGER AS sum
        ,(xpath('count(/collaborator/history_states/history_state[state_id="vacation"]/comment/text())', data))[1]::TEXT::INTEGER AS count
FROM    dbo.collaborator
WHERE   id = 6148914691236517121;
Результат

Проверяем существование XML узла
Проверим наличие узла password и отдельно наличие значения узла password
Запрос
SELECT   CLS.login
        ,xpath('/collaborator/password', CL.data) AS pwd
        ,xpath_exists('/collaborator/password', CL.data) AS pwd_exist
        ,xpath_exists('/collaborator/password/text()', CL.data) AS pwd_text_exist
FROM    dbo.collaborators CLS INNER JOIN dbo.collaborator CL ON CLS.id = Cl.id
ORDER BY xpath_exists('/collaborator/password/text()', CL.data) ASC
Результат

Обратите внимание
Наличие узла <password></password> и наличие значения узла <password>111</password> будут обработаны по разному

Проверяем равенство значения XML узла с нашим значением
Найдем все строки с узлом login со значением user1
Запрос
SELECT   (xpath('/collaborator/login/text()', data))[1]::TEXT AS login
        ,(xpath('/collaborator/login/text() = "user1"', data))[1]::TEXT::BOOLEAN AS login_xpath
        ,xpath_exists('/collaborator/login[text() = "user1"]', data) AS login_xpath_exist
FROM    dbo.collaborator
ORDER BY (xpath('/collaborator/login/text()', data))[1]::TEXT DESC
Результат

Проверяем, содержит ли значение XML узла подстроку
Найдем всех сотрудников для которых пароль (узел password) содержит в значении 1234
Запрос
SELECT   (xpath('/collaborator/login/text()', data))[1]::TEXT AS login
        ,(xpath('/collaborator/password/text()', data))[1]::TEXT AS password
        ,xpath_exists('(/collaborator/password/text()[contains(., "1234")])', data) AS password_1234
FROM    dbo.collaborator
ORDER BY xpath_exists('(/collaborator/password/text()[contains(., "1234")])', data) DESC
Результат

В примере используется дополнительная конструкция [contains(., "1234")]. Функция contains(str, substr) проверяет, содержит ли строка str подстроку substr. Оператор . - возвращает все найденное содержимое предшествующему запросу XPath - значение элемента password.
Проверяем значение кастомного поля в WebTutor
Найдем все записи, где узел custom_elem содержит узел name со значением fld_city и узел value со значением Киров
Запрос
SELECT   (xpath('/collaborator/login/text()', data))[1]::TEXT AS login
        ,(xpath('/collaborator/custom_elems/custom_elem[name = "fld_city"]/value/text()', data))[1]::TEXT
        ,xpath_exists('/collaborator/custom_elems/custom_elem[name = "fld_city" and value = "Киров"]', data)
FROM    dbo.collaborator
WHERE   xpath_exists('/collaborator/custom_elems/custom_elem[name = "fld_city"]/value/text()', data)
Результат

Разделяем по разным строкам каждый найденный XML узел
Для каждого имеющегося узла custom_elem в документе, выведем значения его дочерних элементов.
Запрос
SELECT   (xpath('/collaborator/lastname/text()', CL.data))[1]::TEXT
        ,(xpath('/custom_elem/name/text()', cstm))[1]::TEXT AS name
        ,(xpath('/custom_elem/value/text()', cstm))[1]::TEXT AS value
FROM    dbo.collaborator CL
        CROSS JOIN unnest(xpath('/collaborator/custom_elems/custom_elem', data)) cstm
WHERE   id = 6148914691236517121;
Результат

В данном примере функция unnest принимает на вход результат выражения XPath, который в свою очередь всегда возвращает массив XML, и возвращает табличное представление каждого отдельного XML, к которому мы снова можем обращаться через xpath.
Получаем порядковые номера XML узлов на одном уровне
Для функции unnest можно указать опцию WITH ORDINALITY, которая добавит к результату функции ещё один столбец с типом bigint. В этом столбце нумеруются строки результирующего набора, начиная с 1. Так же можно использовать оконные функции.
Запрос
SELECT   (xpath('/collaborator/lastname/text()', CL.data))[1]::TEXT
        ,(xpath('/custom_elem/name/text()', CLD.cstm))[1]::TEXT AS name
        ,(xpath('*/value/text()', CLD.cstm))[1]::TEXT AS value
        ,CLD.idx AS node_number
        ,ROW_NUMBER() OVER (PARTITION BY CL.id ORDER BY CL.id) AS node_number_wf
FROM    dbo.collaborator CL
        CROSS JOIN unnest(xpath('/collaborator/custom_elems/custom_elem', data)) with ordinality CLD(cstm, idx)
WHERE   id = 6148914691236517121;
Результат

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

Запрос:
Результат:

Получение полной иерархии подразделений
Документ:

Запрос:
WITH RECURSIVE tree (id, name, arr, level) AS 
(
    SELECT   id
            ,name
            ,ARRAY[name::TEXT]
            ,0 AS level
    FROM    dbo.subdivisions
    WHERE   parent_object_id IS NULL
    UNION ALL
    SELECT   S.id
            ,S.name
            ,array_append(arr, S.name::TEXT)
            ,level + 1
    FROM    tree T INNER JOIN dbo.subdivisions S ON T.id = S.parent_object_id
)
SELECT   id
        ,array_to_string(arr, ' >> ') AS path
        ,arr[1] AS top_level
        ,arr[array_length(arr, 1)] AS bottom_level -- "1" - число, указывающее на измерение, для которого нужно найти длину. Для одномерного массива это значение обычно равно 1
        ,level
FROM    tree
Результат:

Получаем список подразделений по иерархии вверх, включая само подразделение
Документ:

Запрос:
WITH RECURSIVE tree AS (
    SELECT  id, name, parent_object_id, 1 AS level
    FROM    dbo.subdivisions
    WHERE   id = 7060462602275304338
    UNION ALL
    SELECT  S.id, S.name, S.parent_object_id, level + 1
    FROM    dbo.subdivisions S INNER JOIN tree T ON S.id = T.parent_object_id
)
SELECT  *
        ,(string_to_array(reverse(array_to_string((SELECT array_agg(level) FROM tree), ',')), ','))[level]::INTEGER AS level_reverse
        ,(array_length((SELECT array_agg(level) FROM tree), 1) - level + 1) AS level_reverse
FROM    tree
Результат:

Получаем значения XML узлов и собираем в строку через разделитель
Документ
Кастомные поля в админке

<custom_elems>
    <custom_elem>
        <name>skill_javascript</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>skill_PHP</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>skill_Docker</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>skill_Kerberos</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>skill_HTML</name>
        <value>true</value>
    </custom_elem>
    <custom_elem>
        <name>skill_WebTutor</name>
        <value>true</value>
    </custom_elem>
</custom_elems>
Наша задача - собрать для каждого сотрудника указанные скиллы.
Запрос:
SELECT   CLS.fullname
        ,array_to_string(
            xpath('/collaborator/custom_elems/custom_elem[contains(.,"skill_") and value = true()]/name/text()', CL.data)::TEXT[],
            ', '
        )
FROM    dbo.collaborators CLS
        INNER JOIN dbo.collaborator CL ON CLS.id = CL.id
WHERE   xpath_exists('/collaborator/custom_elems/custom_elem[contains(.,"skill_") and value = true()]/name/text()', CL.data)
Результат

Если хотим получить не значения полей, а их названия
Запрос:
WITH custom_template AS (
    SELECT   (xpath('/field/name/text()', fld))[1]::TEXT AS name
            ,(xpath('/field/title/text()', fld))[1]::TEXT AS title
    from    dbo."(spxml_blobs)"
            CROSS JOIN unnest(xpath('/custom_templates/collaborator/fields/field', convert_from(data, 'UTF-8')::XML)) fld
    where   url = 'x-local://wt_data/lists/wtv_custom_templates.xml'
)
,skills AS (
    SELECT   CLS.fullname
            ,CLS.id
            ,unnest(
                xpath('/collaborator/custom_elems/custom_elem[contains(.,"skill_") and value = true()]/name/text()', CL.data)::TEXT[]
            ) AS skill
    FROM    dbo.collaborators CLS
            INNER JOIN dbo.collaborator CL ON CLS.id = CL.id
    WHERE   xpath_exists('/collaborator/custom_elems/custom_elem[contains(.,"skill_") and value = true()]/name/text()', CL.data)
)
SELECT   S.fullname
        ,string_agg(C.title, ', ')
FROM    skills S INNER JOIN custom_template C ON S.skill = C.name
GROUP BY S.id, S.fullname
Результат




