将一对多关系显示为 2 列 - 1 个唯一行(ID 和逗号分隔列表)

Posted

技术标签:

【中文标题】将一对多关系显示为 2 列 - 1 个唯一行(ID 和逗号分隔列表)【英文标题】:Show a one to many relationship as 2 columns - 1 unique row (ID & comma separated list) 【发布时间】:2010-10-17 10:51:33 【问题描述】:

我需要类似于这 2 个 SO 问题的内容,但使用 Informix SQL 语法。

Concatenate several fields into one with SQL

SQL Help: Select statement Concatenate a One to Many relationship

我传入的数据如下所示:

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() in Informix。

【问题讨论】:

【参考方案1】:

我不确定informix sql,但在MSSQL 或Oracle 中,您可以使用

DECODE 或 CASE 关键字,通过将它们连接在一起。但是,这需要您提前了解所有潜在值,这很脆弱。

我假设您不喜欢 STUFF 关键字的原因是因为 informix 不支持它?

Oracle 还支持 CONNECT BY 关键字,它可以工作,但可能不被 informix 支持。

可能最好的答案是在查询之后在您的客户端/数据层中构建此输出。是否有特殊原因必须在查询中执行此操作?

【讨论】:

这就是我打算做的,但我只是好奇这样做【参考方案2】:

我想就 Stack Overflow 上的另一个类似问题向您指出 this answer。您正在寻找类似 mysqlgroup_concat() 函数的东西。

【讨论】:

【参考方案3】:

另外,如果 informix 允许您创建用户函数,您可以创建一个函数,该函数返回一个带有连接值的字符串。

【讨论】:

【参考方案4】:

我相信你需要的答案是一个用户定义的聚合,类似于这个:

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

添加了额外的数据集来测试插入序列是否影响结果;它似乎没有这样做(代码按排序顺序排列;我不确定是否有办法改变 - 反转 - 那个顺序)。


注意事项:

    此聚合应该可用于任何可以转换为 VARCHAR(255) 的类型,这意味着任何数字或时间类型。不处理长 CHAR 列和 blob 类型(BYTE、TEXT、BLOB、CLOB)。 普通 LVARCHAR 将聚合大小限制为 2048 字节。例如,如果您认为需要更长的长度,请指定 LVARCHAR(10240)(对于 10 KiB)。 从 Informix 12.10.FC5 开始,工作的最大长度似乎是 16380;任何更长的时间似乎都会触发SQL -528: Maximum output rowsize (32767) exceeded,这让我很惊讶。

    如果需要去除聚合,可以使用:

    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;
    

【讨论】:

有理由在现实世界中使用这个,发现连接值的顺序并不一致。换句话说,它不能可靠地用于构建组合的直方图。使用足够大的数据集,您将获得 PELL,USBL,SUBL 代码和 PELL,SUBL,USBL 代码的混合。 经过反思,这是 COMBINE 函数中的一个限制。您需要将字符串分解回数组,对其进行排序并将其重新组合在一起。这在 perl 中是微不足道的,但在 SPL 中却是相当黑暗的魔法...... @RET:感谢您的提醒。我同意 SPL 中的排序是一项重要的练习(这是礼貌的)。有趣的是,我看到的排序只是数据集的产物。我确实尝试了很多曲折 - 但听到订购仍然是巧合,我并不感到惊讶。 @nurettin:我不这么认为。如果你尝试SELECT id, group_concat(codes) FROM anonymous_table GROUP BY id ORDER BY id, codes,你会得到-19828: ORDER BY column or expression must be in SELECT list in this context。如果您将codes 添加到 GROUP BY,您将在每个组中获得一行。如果你尝试将codes 添加到选择列表中,你会得到-294: The column (codes) must be in the GROUP BY list 的提示,如果你修复了这个问题,你会再次得到错误的结果。所以,总的来说,答案是否定的。 @CarlosLacerda:我已经用您在评论中概述的代码的略微简化版本更新了答案。【参考方案5】:

基于 Jonathan Leffler 示例和关于串联值排序的 RET cmets,使用 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's 来保留中间结果(并消除重复),最后从最终SET 的有序值构建连接字符串。

LVARCHAR 用于SET 元素是因为最初我使用的是VARCHAR,但内存消耗非常非常高。文档提示 Informix 内部可能会将 VARCHAR 转换为 CHAR。我进行了更改,它确实降低了内存消耗(但仍然很高)。

但是,在我进行的测试(使用大约 300 000 行的表)中,这个总内存消耗比 Jonathan 高大约 2 个数量级,并且慢了大约 2 倍。

因此请谨慎使用。它消耗大量内存并且没有经过广泛测试(它可能在某处泄漏内存)。

编辑 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') 会引发各种错误(包括断言失败)。

而且我确信还有其他问题。

但是,此实现的内存消耗与 Jonathan 的示例中的数量级相同,并且慢了大约 60%(同样,只执行了非常基本的测试)。

【讨论】:

【参考方案6】:

此解决方案支持用户指定的分隔符:

示例:

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

这与 Jonathan Leffler 的解决方案相同,但支持 分隔符。如果未指定,默认分隔符是 ','

代码:

CREATE ROW TYPE group_concat_t
(
   result   LVARCHAR,
   separator VARCHAR(1)
);

CREATE FUNCTION gc_init(dummy VARCHAR(255), separator VARCHAR(1) default ',' ) RETURNING group_concat_t;
    RETURN ROW('',separator)::group_concat_t;
END FUNCTION; 

CREATE FUNCTION gc_iter(result group_concat_t, value VARCHAR(255))
    RETURNING group_concat_t;
    IF result.result = '' THEN
        RETURN ROW(TRIM(value),result.separator)::group_concat_t ;
    ELSE
        RETURN ROW(result.result || result.separator || TRIM(value),result.separator)::group_concat_t;
    END IF;
END FUNCTION;

CREATE FUNCTION gc_comb(partial1 group_concat_t, partial2 group_concat_t)
    RETURNING group_concat_t;
    IF partial1 IS NULL OR partial1.result = '' THEN
        RETURN partial2;
    ELIF partial2 IS NULL OR partial2.result = '' THEN
        RETURN partial1;
    ELSE
        RETURN ROW(partial1.result || partial1.separator || partial2.result, partial1.separator)::group_concat_t;
    END IF;
END FUNCTION;

CREATE FUNCTION gc_fini(final group_concat_t) RETURNING LVARCHAR;
    RETURN final.result;
END FUNCTION;  

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

【讨论】:

【参考方案7】:

Oracle 为此类需求提供列表聚合器功能。

SELECT id, LISTAGG(codes,',') as CODE_LIST FROM &lt;TABLE&gt; GROUP BY id

输出会是这样的

ID     CODE_LIST 
63592  PELL
58640  SUBL,USBL
73571  PELL,USBL,SUBL

【讨论】:

以上是关于将一对多关系显示为 2 列 - 1 个唯一行(ID 和逗号分隔列表)的主要内容,如果未能解决你的问题,请参考以下文章

HBase 与多列的一对多关系

查询或合并一对多关系而不复制结果条目

查找2列之间的一对一/一对多关系

Laravel 5.5中的一对多关系

多表关系一对一

数据库查询一对多关系,只列出一条数据