MSSQL парсинг XML

В MSSQL нет опыт работы с XML, поэтому непонятно как сделать.

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

  1. Как распарсить XML во временную табличку? для:

    a. читабельного отображения в определенном формате параметров дат ([beginDt1; endDt1], [beginDt2; endDt2])

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

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

Исходный код:

DECLARE @response XML = '<response>
    <sku artId = "5023">
        <p>
            <n>Наименование товара</n>
            <v>Кофе Tchibo сублимированный</v>
        </p>
        <p>
            <n>Категория</n>
            <v>Кофе</v>
        </p>
        <p>
            <n>Вес</n>
            <v>0.2</v>
        </p>
        <p>
            <n>Периоды акций</n>
            <v>
                <val1><type>Total</type><beginDt>2024-06-20</beginDt><endDt>2024-07-22</endDt></val1>
                <val2><type>Total</type><beginDt>2024-08-21</beginDt><endDt>2024-09-18</endDt></val2>
            </v>
        </p>
    </sku>
</response>
'
SELECT @response

DROP TABLE IF EXISTS #tbl;
CREATE TABLE #tbl(
     num INT NOT NULL PRIMARY KEY IDENTITY(1,1)
    ,artId INTEGER
    ,paramName NVARCHAR(255)
    ,val NVARCHAR(MAX)
    ,multipleVal NVARCHAR(MAX)
);


INSERT INTO #tbl(
     paramName
    ,val
    ,artId
    ,multipleVal
)
SELECT
     tTbl.paramName
    ,tTbl.val
    ,tTbl.artId
    ,tTbl.multipleVal
FROM(
    SELECT
         n.value('(n)[1]','NVARCHAR(255)') AS paramName
        ,n.value('../@artId','INT') AS artId
        ,CASE
            WHEN n.value('(n)[1]','NVARCHAR(255)') = 'Периоды акций'
            -- TODO :: вывести все периоды в строке в формате [beginDt1; endDt1], [beginDt2; endDt2]
            THEN '[beginDt1; endDt1], [beginDt2; endDt2]'
            ELSE n.value('(v)[1]','NVARCHAR(MAX)')
        END AS val
        ,CASE
            WHEN n.value('(n)[1]','NVARCHAR(255)') = 'Периоды акций'
            THEN CAST(n.query('.') AS VARCHAR(MAX))
            ELSE NULL
        END AS multipleVal
    FROM @response.nodes('/response/sku/p') col(n)
) AS tTbl


SELECT * FROM #tbl

DECLARE @todayDt DATE = GETDATE();

DROP TABLE IF EXISTS #tCatalogue;
CREATE TABLE #tCatalogue(
     id INTEGER NOT NULL PRIMARY KEY IDENTITY(1,1)
    ,artId INTEGER NOT NULL
    ,name VARCHAR(MAX)
    ,category VARCHAR(MAX)
    ,groupName VARCHAR(MAX)
    ,weight DECIMAL(6,2)
    ,unit VARCHAR(MAX)
);

INSERT INTO #tCatalogue(artId,name,category,groupName,weight,unit)
VALUES
     (5022, 'Хлеб белый нарезной', 'Хлеб', 'Мучные изделия','0.5','kg')
    ,(5023, 'Кофе Tchibo сублимированный', 'Кофе', 'Чай и кофе','0.2','kg')

SELECT
    tCatg.artId
    ,tCatg.name
    ,tParam.val as period

    -- найти попадает ли текущая дата (@todayDt) в один из периодов акций, если да - вывести 1, иначе - 0
FROM #tCatalogue AS tCatg
LEFT JOIN #tbl AS tParam
    ON tCatg.artId = tParam.artId
    AND tParam.paramName = 'Периоды акций'
artId name period isInPromotion
5022 Хлеб белый нарезной NULL NULL
5023 Кофе Tchibo сублимированный [beginDt1; endDt1], [beginDt2; endDt2] 1

Ответы (1 шт):

Автор решения: Yitzhak Khabinsky

Попробуйте следующее решение на основе SQL Server XML и XQuery.

SQL

DECLARE @response XML = 
N'<response>
    <sku artId="5023">
        <p>
            <n>Наименование товара</n>
            <v>Кофе Tchibo сублимированный</v>
        </p>
        <p>
            <n>Категория</n>
            <v>Кофе</v>
        </p>
        <p>
            <n>Вес</n>
            <v>0.2</v>
        </p>
        <p>
            <n>Периоды акций</n>
            <v>
                <val1>
                    <type>Total</type>
                    <beginDt>2024-06-20</beginDt>
                    <endDt>2024-07-22</endDt>
                </val1>
                <val2>
                    <type>Total</type>
                    <beginDt>2024-08-21</beginDt>
                    <endDt>2024-09-18</endDt>
                </val2>
            </v>
        </p>
    </sku>
</response>';

DECLARE @todayDt DATE = GETDATE()
    , @period NVARCHAR(30) = N'Периоды акций';

DECLARE @Catalogue TABLE (
     id INTEGER NOT NULL PRIMARY KEY IDENTITY(1,1)
    ,artId INTEGER NOT NULL
    ,name NVARCHAR(MAX)
    ,category NVARCHAR(MAX)
    ,groupName NVARCHAR(MAX)
    ,weight DECIMAL(6,2)
    ,unit VARCHAR(MAX)
);
INSERT INTO @Catalogue (artId, name, category, groupName, weight, unit) VALUES
(5022, N'Хлеб белый нарезной', N'Хлеб', N'Мучные изделия','0.5','kg')
,(5023, N'Кофе Tchibo сублимированный', N'Кофе', N'Чай и кофе','0.2','kg');

SELECT * FROM @Catalogue;

SELECT artId
    , name
    , IIF(artId != @response.value('(/response/sku/@artId)[1]', 'INT')
        , NULL
        , @response.query('for $x in /response/sku/p[n/text()=sql:variable("@period")]/v/*
        return concat("[", ($x/beginDt/text())[1], ";", ($x/endDt/text())[1], "]")
    ').value('.', 'VARCHAR(100)')) AS period
    , @todayDt AS todayDT
    -- найти попадает ли текущая дата (@todayDt) в один из периодов акций, если да - вывести 1, иначе - 0
    , @response.query('
        /response/sku[@artId eq sql:column("artId")] 
        and
        some $r in /response/sku/p[n/text()=sql:variable("@period")]/v/*
        satisfies (($r/beginDt/text())[1] ge sql:variable("@todayDt") 
            and sql:variable("@todayDt") le ($r/endDt/text())[1])
    ').value('.', 'BIT') AS isInPromotion
FROM @Catalogue;

Результат

artId name period todayDT isInPromotion
5022 Хлеб белый нарезной NULL 2024-07-18 0
5023 Кофе Tchibo сублимированный [2024-06-20;2024-07-22] [2024-08-21;2024-09-18] 2024-07-18 1
→ Ссылка