DB2 объединяет уникальные значения столбцов в одну строку, разделенные запятыми

Две таблицы:

Parts Table:
Part_Number   Load_Date   TQTY
m-123         19940102    32
1234Cf        20010809    3
wf9-2         20160421    14


Locations Table:
PartNo     Condition     Location   QTY
m-123      U             A02        2
1234Cf     S             A02        3
m-123      U             B01        1
wf9-2      S             A06        7
m-123      S             A18        29
wf9-2      U             F16        7

Result:
Part_Number   Load_Date  TQTY   U_LOC    UQTY    S_LOC   SQTY
m-123         19940102   32     A02,B01  3       A18     29
1234Cf        20010809   3                       A02     3
wf9-2         20160421   14     F16      7       A06     7 

У меня возникли проблемы с поиском решения этой проблемы с моей текущей версией DB2. Я не совсем уверен, как найти версию, но она работает в системе AS400, и кажется, что версия DB2 привязана к версии ОС. Что использует коробка: Операционная система: версия i5/OS: V5R4M0 (я попробовал несколько команд, чтобы получить версию DB2, используя эти предложения Здесь, но ни один из них не работал, как и большинство заявленных).

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

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


EDIT: Используя принятый ответ и объяснение, а также пример ЗДЕСЬ, чтобы получить общее представление о рекурсии на простом примере, и это был
ЗДЕСЬ с использованием операторов "SELECT rownumber() over(partition by category)", которые действительно помогли собрать все вместе. Когда-то я понял это утверждение, конечно.

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


person eaglei22    schedule 08.08.2017    source источник
comment
XMLAGG?? Вам нужна функция LISTAGG (ibmmainframeforum.com/db2/topic10923.html) XMLAGG предназначен для создания XML. не то, что вы хотите сделать   -  person Hogan    schedule 09.08.2017
comment
LISTAGG предназначен для V7R3, а функции XML, такие как xmlserialize, — для V7R1. В V5R4 вам нужно создать хранимую процедуру, чтобы сделать это в SQL.   -  person Dam    schedule 09.08.2017
comment
@Dam, можешь привести пример или что-то, что поможет мне начать? Мы переходим с AS400 на технологию ASP.NET MVC и в настоящее время используем DB2 для синхронизации старых и новых программ. Так что я не слишком разбираюсь в языке DB2, кроме универсальных операторов. Я могу прибегнуть к обработке этого с помощью структур данных в C#, но хочу избежать накладных расходов, если смогу.   -  person eaglei22    schedule 09.08.2017
comment
Я наткнулся на эту статью: vertabelo.com/blog/technical-articles/group-concat Но я пытаюсь понять, как с ней работать. Это немного выше моего уровня владения DB2   -  person eaglei22    schedule 09.08.2017


Ответы (2)


Это довольно сложно, поэтому покажу всю свою работу:

Определения таблиц

create table parts
  (part_number      Varchar(64),
   load_date        Date,
   total_qty        Dec(5,0));
create table locations
  (part_number      Varchar(64),
   condition        Char(1),
   location         Char(3),
   qty              Dec(5,0));
insert into parts
  values ('m-123',  '1994-01-02', 32),
         ('1234Cf', '2001-08-09',  3),
         ('wf9-2',  '2016-04-21', 14);
insert into locations
  values ('m-123',  'U', 'A02', 2),
         ('1234Cf', 'S', 'A02', 3),
         ('m-123',  'U', 'B01', 1),
         ('wf9-2',  'S', 'A06', 7),
         ('m-123',  'S', 'A18', 29),
         ('wf9-2',  'U', 'F16', 7);

Запрос:

with -- CTE's
  -- This collects locations into a comma seperated list
  tmp (part_number, condition, location, csv, level) as (
    select part_number, condition, min(location), 
           cast(min(location) as varchar(128)), 1
      from locations
      group by part_number, condition
    union all
    select a.part_number, a.condition, b.location, 
           a.csv || ',' || b.location, a.level + 1
      from tmp a
        join locations b using (part_number, condition)
      where a.csv not like '%' || b.location || '%'
        and b.location > a.location),
  -- This chooses the correct csv list, and adds quantity for the condition
  tmp2 (part_number, condition, csv, qty) as (    
    select t.part_number, t.condition, t.csv, 
           (select sum(qty) qty
              from locations 
              where part_number = t.part_number
                and condition = t.condition)
      from tmp t
      where level = (select max(level)
              from tmp 
              where part_number = t.part_number
                and condition = t.condition))
-- This is the final select that combines the parts file with
-- the second stage CTE and arranges things horizontally by condition
select p.part_number, p.load_date, 
       (select sum(qty) 
          from locations 
          where part_number = p.part_number) as total_qty, 
       coalesce(u.csv, '') as u_loc,
       coalesce(u.qty, 0) as uqty,
       coalesce(s.csv, '') as s_loc, 
       coalesce(s.qty, 0) as sqty
  from parts p
    left outer join tmp2 u
      on u.part_number = p.part_number and u.condition = 'U'
    left outer join tmp2 s
      on s.part_number = p.part_number and s.condition = 'S'
  order by p.load_date;



EDIT Мне пришлось добавить сюда несколько дополнительных битов, чтобы поддерживать более двух местоположений для части/условия, и я сделал имена столбцов в CTE более согласованными. Итак, позвольте мне немного объяснить, этот запрос состоит из 3 частей, 2 CTE и запроса, вы можете видеть, что эти три части разделены комментариями. Первый CTE является рекурсивным CTE. Его цель состоит в том, чтобы создать список местоположений, разделенных запятыми. Вы должны иметь возможность запускать select сам по себе, чтобы увидеть, что он делает. tmp — это имя таблицы, part_number, condition, csv и level — это имена столбцов. Рекурсивному CTE требуется SELECT для заполнения CTE и UNION ALL с SELECT, который заполняет следующие детали. В этом случае заполнение SELECT извлекает номер детали, условие и первое местоположение (в алфавитном порядке) для этой комбинации. уровень установлен на 1. Если вы запустите только предварительный выбор, вы получите:

part_number  condition  location  csv  level
-----------  ---------  --------  ---  -----
1234Cf          S       A01       A02    1
m-123           S       A18       A18    1
m-123           U       A02       A02    1
wf9-2           U       F16       F16    1
wf9-2           S       A06       A06    1

Обратите внимание на одну строку на деталь/состояние. Оставшаяся часть рекурсивного CTE заполнит оставшиеся места в csv, но фактически добавит дополнительные записи, поэтому нам нужно отфильтровать результаты здесь и позже. Таким образом, записи обрабатываются по мере их добавления. Первые строки, перечисленные выше, объединяются с файлом местоположения по номеру детали и условию. Обратите внимание, что в начальном выборе у меня есть приведение второго min(location) к varchar(128). Это оставляет место для расширения столбца CSV. Без этого он все равно будет расширяться, но не настолько, чтобы удерживать более 2-х локаций.

Второй выбор в рекурсивном CTE объединяет запятую и следующее местоположение в конце CSV. Конкретный бит, который делает это, — a.csv || ',' || b.location. Он также увеличивает столбец уровня. Это помогает нам отслеживать, где мы находимся в запросе. В конце концов, мы хотим использовать строку с наибольшим значением level. У нас также есть способ завершить рекурсию и несколько фильтров для уменьшения количества строк, добавляемых во временный результирующий набор. Если у нас есть 2 местоположения, A02 и B02, оставленных непроверенными, мы получим следующие строки: A02, A02,A02, A02,B02, A02,A02,A02, A02,B02,A02, A02,A02,B02, A02,B02,B02, ... до бесконечности. Антидупликационного фильтра where a.csv not like '%' || b.location || '%' достаточно для двух местоположений, чтобы завершить рекурсию и минимизировать строки, как указано выше, для местоположений A02 и B02, с антидупликационным фильтром мы получим строки A02 и A02,B02. Обратите внимание, что ни один из других результатов из первого примера с повторяющимися местоположениями не возвращается. Добавление третьего местоположения C02 даст с фильтром предотвращения дублирования следующие строки: A02, A02,B02, A02,C02, A02,B02,C02, A02,C02,B02. Здесь нет дубликатов, но у нас есть избыточные строки, и по мере добавления местоположений становится все хуже. Здесь нам нужен способ обнаружения этих избыточных строк. Поскольку мы начинаем с наименьшего номера местоположения, мы всегда можем убедиться, что местоположения, добавленные к CSV, больше, чем ранее добавленное местоположение. Для этого все, что нам нужно сделать, это включить в результирующий набор столбец, указывающий, какой столбец был добавлен (мы могли бы опросить CSV, но это сложнее). Вот почему нам нужен столбец location в tmp. Тогда мы можем написать фильтр b.location > a.location. В приведенном выше примере с тремя местоположениями этот фильтр не позволяет строке A02,C02,B02 оставить только одну строку со всеми тремя местоположениями. Добавление более трех местоположений в файл местоположений приведет к еще большему увеличению количества строк в TMP, но для каждой детали и условия будет только одна строка со всеми местоположениями, и она будет содержать все местоположения в порядке возрастания.

Второй CTE делает две вещи. Во-первых, он фильтрует TMP, чтобы удалить все строки, кроме строк, содержащих все местоположения для данной детали/условия. Во-вторых, он накапливает общее количество для каждой детали/состояния.

Бит, который выполняет фильтрацию, находится в предложении where:

where level = (select max(level)
        from tmp 
        where part_number = t.part_number
          and condition = t.condition))

Довольно прямолинейно. Бит, который накапливает общее количество для детали/состояния, также является простым для понимания подзапросом:

(select sum(qty) qty
   from locations 
   where part_number = t.part_number
     and condition = t.condition)

Последняя часть этого чудовищного запроса — основной выбор. Он объединяет файл частей с результатами второго CTE, чтобы сформировать окончательный набор результатов:

select p.part_number, p.load_date, 
       (select sum(qty) from locations where part_number = p.part_number) as total_qty,
       coalesce(u.csv, '') as u_loc, coalesce(u.qty, 0) as uqty,
       coalesce(s.csv, '') as s_loc, coalesce(s.qty, 0) as sqty
  from parts p
    left outer join tmp2 u
      on u.part_number = p.part_number and u.condition = 'U'
    left outer join tmp2 s
      on s.part_number = p.part_number and s.condition = 'S'
  order by p.load_date

Обратите внимание на подзапрос для получения общего количества из таблицы locations. Вы можете использовать поле tqty в parts, но это может не синхронизироваться с фактическими количествами в таблице locations. Кроме того, есть два левых внешних соединения с tmp2, одно для условия U, а другое для условия S. Они создают горизонтальный массив местоположения/количества в строке результата. И последнее — это функции coalesce. Они дают нулевые значения (когда результат внешнего соединения отсутствует) значение по умолчанию.

Конец ИЗМЕНЕНИЯ


Окончательный результат:

part_number  load_date   tqty  u_loc    uqty  s_loc  sqty
-----------  ----------  ----  -------  ----  -----  ----
m-123        1994-01-02   32   A02,B01    3   A18     29
1234Cf       2001-08-09    3              0   A02      3
wf9-2        2016-04-21   14   F16        7   A06      7

Примечание XMLAGG и XMLSERIALIZE стали доступны в DB2 для i v7.1, а LISTAGG стали доступны в DB2 для i v7.2. Самая последняя версия по состоянию на 09.08.2017 — v7.3. Поскольку вы используете v5r4, вероятно, вам потребуется не только программное обеспечение, но и аппаратное обновление, чтобы получить актуальную версию.

person jmarkmurphy    schedule 09.08.2017
comment
Спасибо за ваш подробный ответ. И для примечания. Коробку снимаем, после конвертации приложений, поэтому предполагаю, у нее еще старая версия. Что подразумевается под CSV и LEVEL в запросе? Я не уверен, с чем это связано. - person eaglei22; 09.08.2017
comment
Это просто имена полей, которые мне понадобятся позже в запросе. CSV — это разделенный запятыми список соединенных местоположений. Уровень — это просто количество локаций, которые были добавлены в список. Я использую уровень, чтобы выбрать, какой список использовать для данной детали/условия, поскольку будет список с 1 локацией, список с 2 локациями, 3 и 4 вплоть до тех пор, пока вы не получите все местоположения в списке. - person jmarkmurphy; 09.08.2017
comment
хм, я скопировал ваше утверждение и только добавил библиотеку к именам таблиц, и я получаю несколько ошибок после того, как ...union all select a.part_number,.. (здесь это останавливается) и бросает, SQL0255: функция не поддерживается для запроса. Код причины 5. Код 5 = Рекурсивное общее табличное выражение не поддерживается для этого запроса. - person eaglei22; 09.08.2017
comment
Хммм... рекурсивные CTE стали доступны в v5r4, но, возможно, имели некоторые ограничения. Вышеупомянутое работает в версии 7.1, я не могу протестировать версию 5.4, так как у меня нет системы для тестирования. - person jmarkmurphy; 09.08.2017
comment
Я написал небольшой рекурсивный CTE для примера, и он сработал. Так что это просто придирчивость к чему-то. Я раньше не использовал рекурсию в SQL. Поэтому я пытаюсь понять ваш пример и переписать его сам. Можете ли вы подробнее объяснить, как первый CTE вызывает себя для сохранения значений, разделенных запятыми, в столбце CSV (что происходит)? Кроме того, в чем разница между определением имен столбцов в CTE tmp и не в CTE tmp2? Пытаюсь вникнуть в это дело впервые. Спасибо. - person eaglei22; 09.08.2017
comment
Большое спасибо! Я зациклился на нескольких ключевых концепциях, которые вы прояснили, что теперь имеет смысл (причина подсчета), а также на том, как a.csv || ',' || б.локация работает. Мне было неясно, как он сохраняется в a.csv, но после того, как я просмотрел его достаточно раз, я понял, что (надеюсь, я прав) именно так объявляются параметры CTE, а a.csv | | ',' || b.location — это значение, которое сохраняется в нем. Я как-то думал, как эта строка сохранила += в a.csv. Думаю, теперь я достаточно хорошо разобрался в этом. - person eaglei22; 10.08.2017
comment
Что действительно помогло, так это пройтись по каждому запросу (вместе с вашими подробными объяснениями), вывести данные и просто попытаться просмотреть, что происходит визуально, а не только с точки зрения синтаксиса. Еще раз спасибо! - person eaglei22; 10.08.2017

Не знаю, каковы правила для UQTY, S_LOC, SQTY, но вот столбец, о котором вы спрашивали ---

SELECT 
  P.Part_Number,
  P.Load_Date,
  P.TQTY,
  LISTAGG(L.Location, ', ') WITHIN GROUP (ORDER BY L.Location) AS U_LOC    
FROM "Parts Table" AS P
LEFT JOIN "Locations Table" AS L ON P.Part_Number = L.Part_Number
GROUP BY P.Part_Number, P.Load_Date, P.TQTY
person Hogan    schedule 08.08.2017
comment
Источник ошибки: Сообщение об ошибке команды IBMDA400: SQL0199: Ключевое слово GROUP не ожидается. - person eaglei22; 09.08.2017
comment
Согласно комментарию Дама выше, похоже, что функция LISTAGG недоступна для меня в моей версии. - person eaglei22; 09.08.2017