Отобразить отношение один ко многим в виде 2 столбцов - 1 уникальная строка (список, разделенный запятыми) - PullRequest
5 голосов
/ 03 апреля 2009

Мне нужно что-то похожее на эти 2 вопроса SO, но с использованием синтаксиса Informix SQL.

Мои данные выглядят так:

id     codes

63592  PELL
58640  SUBL
58640  USBL
73571  PELL
73571  USBL
73571  SUBL

Я хочу, чтобы это вернулось так:

id     codes 

63592  PELL
58640  SUBL, USBL
73571  PELL, USBL, SUBL

См. Также group_concat () в Informix .

Ответы [ 5 ]

18 голосов
/ 04 апреля 2009

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

CREATE FUNCTION gc_init(dummy VARCHAR(255)) RETURNING LVARCHAR;
    RETURN '';
END FUNCTION;

CREATE FUNCTION gc_iter(result LVARCHAR, value VARCHAR(255))
    RETURNING LVARCHAR;
    IF result = '' THEN
        RETURN TRIM(value);
    ELSE
        RETURN result || ',' || TRIM(value);
    END IF;
END FUNCTION;

CREATE FUNCTION gc_comb(partial1 LVARCHAR, partial2 LVARCHAR)
    RETURNING LVARCHAR;
    IF partial1 IS NULL OR partial1 = '' THEN
        RETURN partial2;
    ELIF partial2 IS NULL OR partial2 = '' THEN
        RETURN partial1;
    ELSE
        RETURN partial1 || ',' || partial2;
    END IF;
END FUNCTION;

CREATE FUNCTION gc_fini(final LVARCHAR) RETURNING LVARCHAR;
    RETURN final;
END FUNCTION;

CREATE AGGREGATE group_concat
    WITH (INIT = gc_init, ITER = gc_iter,
          COMBINE = gc_comb, FINAL = gc_fini);

При наличии таблицы элементов (называемых элементами) со столбцом с именем name, содержащим (как ни странно) имя элемента, и другим столбцом с именем atomic_number, этот запрос дает такой результат:

SELECT group_concat(name) FROM elements WHERE atomic_number < 10;

Hydrogen,Helium,Lithium,Beryllium,Boron,Carbon,Nitrogen,Oxygen,Fluorine

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

SELECT id, group_concat(codes)
    FROM anonymous_table
    GROUP BY id;

CREATE TEMP TABLE anonymous_table
(
    id      INTEGER NOT NULL,
    codes   CHAR(4) NOT NULL,
    PRIMARY KEY (id, codes)
);

INSERT INTO anonymous_table VALUES(63592, 'PELL');
INSERT INTO anonymous_table VALUES(58640, 'SUBL');
INSERT INTO anonymous_table VALUES(58640, 'USBL');
INSERT INTO anonymous_table VALUES(73571, 'PELL');
INSERT INTO anonymous_table VALUES(73571, 'USBL');
INSERT INTO anonymous_table VALUES(73571, 'SUBL');
INSERT INTO anonymous_table VALUES(73572, 'USBL');
INSERT INTO anonymous_table VALUES(73572, 'PELL');
INSERT INTO anonymous_table VALUES(73572, 'SUBL');

SELECT id, group_concat(codes)
    FROM anonymous_table
    GROUP BY id
    ORDER BY id;

Выход из этого:

58640 SUBL,USBL
63592 PELL
73571 PELL,SUBL,USBL
73572 PELL,SUBL,USBL

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


Примечания:

  1. Этот агрегат должен использоваться для любого типа, который может быть преобразован в VARCHAR (255), что означает любой числовой или временной тип. Длинные столбцы CHAR и типы BLOB-объектов (BYTE, TEXT, BLOB, CLOB) не обрабатываются.
  2. Простой LVARCHAR ограничивает совокупный размер до 2048 байтов. Если вы считаете, что вам нужны более длинные длины, укажите LVARCHAR(10240) (для 10 КиБ), например.
  3. Начиная с Informix 12.10.FC5, максимальная длина, которая работает, кажется 16380; что-то больше, кажется, вызывает SQL -528: Maximum output rowsize (32767) exceeded, что меня удивляет.
  4. Если вам нужно удалить агрегат, вы можете использовать:

    DROP AGGREGATE IF EXISTS group_concat;
    DROP FUNCTION IF EXISTS gc_fini;
    DROP FUNCTION IF EXISTS gc_init;
    DROP FUNCTION IF EXISTS gc_iter;
    DROP FUNCTION IF EXISTS gc_comb;
    
1 голос
/ 14 марта 2017

Опираясь на пример Джонатана Леффлера и комментарии RET о порядке упорядочения объединенных значений, используя Informix 12.10FC8DE, я придумал следующую совокупность пользователей:

CREATE FUNCTION mgc_init
(
    dummy VARCHAR(255)
)
RETURNING
    SET(LVARCHAR(2048) NOT NULL);

    RETURN SET{}::SET(LVARCHAR(2048) NOT NULL);

END FUNCTION;

CREATE FUNCTION mgc_iter
(
    p_result SET(LVARCHAR(2048) NOT NULL)
    , p_value VARCHAR(255)
)
RETURNING
    SET(LVARCHAR(2048) NOT NULL);

    IF p_value IS NOT NULL THEN
        INSERT INTO TABLE(p_result) VALUES (TRIM(p_value));
    END IF;

    RETURN p_result;

END FUNCTION;

CREATE FUNCTION mgc_comb
(
    p_partial1 SET(LVARCHAR(2048) NOT NULL)
    , p_partial2 SET(LVARCHAR(2048) NOT NULL)
)
RETURNING
    SET(LVARCHAR(2048) NOT NULL);

    INSERT INTO TABLE(p_partial1)
        SELECT vc1 FROM TABLE(p_partial2)(vc1);

    RETURN p_partial1;

END FUNCTION;

CREATE FUNCTION mgc_fini
(
    p_final SET(LVARCHAR(2048) NOT NULL)
)
RETURNING
    LVARCHAR;

    DEFINE l_str LVARCHAR(2048);
    DEFINE l_value LVARCHAR(2048);

    LET l_str = NULL;

    FOREACH SELECT vvalue1 INTO l_value FROM TABLE(p_final) AS vt1(vvalue1) ORDER BY vvalue1
        IF l_str IS NULL THEN
            LET l_str = l_value;
        ELSE
            LET l_str = l_str || ',' || l_value;
        END IF;
    END FOREACH;

    RETURN l_str;

END FUNCTION;
GRANT EXECUTE ON mgc_fini TO PUBLIC;

CREATE AGGREGATE m_group_concat
WITH
(
    INIT = mgc_init
    , ITER = mgc_iter
    , COMBINE = mgc_comb
    , FINAL = mgc_fini
);

Объединенные значения не будут иметь дубликатов и будут упорядочены.

Я использовал Informix collections, а именно SET, который не допускает дублирование значений, чтобы попытаться сделать код несколько простым.

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

Использование LVARCHAR для элементов SET связано с тем, что изначально я использовал VARCHAR, но потребление памяти было очень и очень высоким. В документации намекает, что внутренне Informix может приводить VARCHAR к CHAR. Я внес изменение, и оно фактически уменьшило потребление памяти (но оно все еще остается высоким).

Однако это совокупное потребление памяти примерно на 2 порядка выше, чем у Джонатана, и примерно в 2 раза медленнее в проведенных мною тестах (с использованием таблицы с 300 000 строк).

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

РЕДАКТИРОВАТЬ 1:

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

Итак, все еще пытаясь избежать необходимости кодировать агрегатную функцию в C, вот еще одна альтернатива, использующая встроенные функции Informix BSON, которая будет использовать намного меньше памяти и будет немного быстрее.

CREATE FUNCTION m2gc_init
(
    dummy VARCHAR(255)
)
RETURNING
    BSON;

    RETURN '{"terms":[]}'::JSON::BSON;

END FUNCTION;

CREATE FUNCTION m2gc_iter
(
    p_result BSON
    , p_value VARCHAR(255)
)
RETURNING
    BSON;

    DEFINE l_add_array_element LVARCHAR(2048);

    IF p_value IS NOT NULL THEN
        LET l_add_array_element = '{ $addToSet: { terms: "' || TRIM(p_value) || '" } }';
        LET p_result = BSON_UPDATE(p_result, l_add_array_element);
    END IF;

    RETURN p_result;

END FUNCTION;

CREATE FUNCTION m2gc_comb
(
    p_partial1 BSON
    , p_partial2 BSON
)
RETURNING
    BSON;

    DEFINE l_array_elements LVARCHAR(2048);
    DEFINE l_an_element LVARCHAR(2048);
    DEFINE l_guard INTEGER;

    LET l_array_elements = NULL;
    LET l_guard = BSON_SIZE(p_partial2, 'terms.0');

    IF l_guard > 0 THEN
        WHILE l_guard > 0
            LET l_an_element = BSON_VALUE_LVARCHAR(p_partial2, 'terms.0');
            IF l_array_elements IS NULL THEN
                LET l_array_elements = '"' || l_an_element || '"';
            ELSE
                LET l_array_elements = l_array_elements || ', "' || l_an_element || '"';
            END IF;
            LET p_partial2 = BSON_UPDATE(p_partial2, '{ $pop: { terms: -1 } }');
            LET l_guard = BSON_SIZE(p_partial2, 'terms.0');
        END WHILE;
        LET l_array_elements = '{ $addToSet: { terms: { $each: [ ' || l_array_elements || ' ] } } }';        
        LET p_partial1 = BSON_UPDATE(p_partial1, l_array_elements);
    END IF;

    RETURN p_partial1;

END FUNCTION;


CREATE FUNCTION m2gc_fini
(
    p_final BSON
)
RETURNING
    LVARCHAR;

    DEFINE l_str_agg LVARCHAR(2048);
    DEFINE l_an_element LVARCHAR(2048);
    DEFINE l_iter_int INTEGER;
    DEFINE l_guard INTEGER;

    LET l_str_agg = NULL;
    LET l_guard = BSON_SIZE(p_final, 'terms.0');

    IF l_guard > 0 THEN
        LET p_final = BSON_UPDATE(p_final, '{ $push: { terms: { $each: [], $sort: 1 } } }');    
        LET l_iter_int = 0;
        WHILE l_guard > 0
            LET l_an_element = BSON_VALUE_LVARCHAR(p_final, 'terms.' || l_iter_int);
            IF l_str_agg IS NULL THEN
                LET l_str_agg = TRIM(l_an_element);
            ELSE
                LET l_str_agg = l_str_agg || ',' || TRIM(l_an_element);
            END IF;
            LET l_iter_int = l_iter_int + 1;
            LET l_guard = BSON_SIZE(p_final, 'terms.' || l_iter_int);
        END WHILE;
    END IF;
    RETURN l_str_agg;

END FUNCTION;

CREATE AGGREGATE m2_group_concat
WITH
(
    INIT = m2gc_init
    , ITER = m2gc_iter
    , COMBINE = m2gc_comb
    , FINAL = m2gc_fini
)
;

Агрегированное возвращаемое значение будет упорядочено без дубликатов.

Опять же, это не было должным образом проверено. Это просто POC.

Одна из проблем заключается в том, что он не очищает входные значения. Некоторые из BSON манипулирующих функций получают параметры, которые строятся путем объединения строк, и не экранированные символы могут нарушать эти параметры. Например, строковое значение с кавычками: 'I"BrokeIt') может вызвать ряд ошибок (включая ошибки подтверждения).

И я уверен, что есть и другие проблемы.

Тем не менее, потребление памяти этой реализацией имеет тот же порядок величины, что и в примере с Джонатаном, и примерно на 60% медленнее (опять же, было проведено только очень элементарное тестирование).

1 голос
/ 04 апреля 2009

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

1 голос
/ 04 апреля 2009

Я не уверен насчет inforix sql, но в MSSQL или Oracle вы могли бы сделать это с помощью

DECODE или CASE ключевые слова, объединяя их вместе. Тем не менее, это потребует от вас знать все потенциальные значения заранее, что является хрупким.

Я предполагаю, что вам не нравится ключевое слово STUFF, потому что informix не поддерживает его?

Oracle также поддерживает ключевые слова CONNECT BY, которые будут работать, но, опять же, может не поддерживаться informix.

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

0 голосов
/ 04 апреля 2009

Я бы хотел указать вам этот ответ на другой похожий вопрос о переполнении стека. Вы ищете что-то вроде group_concat() функции MySQL.

...