关于MYSQL和达梦数据库分组合并重复数据问题
问题模型
为了方便说明问题,让我们首先建立一个问题模型;假设有表A和表B,结构如下所述:
表A结构
列名 | 列类型 | 列说明 |
---|---|---|
a_id | bigint(20) | 主键 |
a_name | varchar(32) | a名称 |
表B结构
列名 | 列类型 | 列说明 |
---|---|---|
b_id | bigint(20) | 主键 |
b_a | bigint(20) | 关联表A主键 |
b_name | varchar(32) | b名称 |
数据如下
表A数据
a_id | a_name |
---|---|
1 | 张三 |
2 | 李四 |
表B数据
b_id | b_a | b_name |
---|---|---|
1 | 1 | Java |
2 | 1 | C# |
3 | 1 | Java |
4 | 2 | Java |
5 | 2 | Java |
6 | 2 | Java |
我们要以表A为主表做关联表B,查出表A的名字和表B对应其记录的名字的合并列,查询结果要求如下
查询结果
a_id | a_name | b_name_all |
---|---|---|
1 | 张三 | Java,C# |
2 | 李四 | Java |
问题解决思路
可以看出查询结果要求我们按照表A主键进行分组,并将分组后的结果去重后合并成一个列,在mysql中这很简单就可以做到:
select
a.a_id,
a.a_name,
group_concat(distinct b.b_name) b_name_all
from 表A a
inner join 表B b on a.a_id = b.b_a
group by a.a_id
上面使用了mysql的group_concat函数,该函数的作用就是将group产生的同一个分组的指定值连接起来,返回一个字符串结果,其语法如下
group_concat( [distinct] 要连接的字段 [order by 排序字段 asc/desc ] [separator '分隔符'] )
可以看到,group_concat函数天生就支持去重和排序,那么当数据库迁移到达梦之后,这种类似的函数还存在吗?
在查阅了随达梦发布的文档之后(DM_SQL语言使用手册),发现其中有个LISTAGG函数,达梦文档中对这个函数的介绍如下
首先根据 sql 语句中的 group by 分组(如果没有指定分组则所有结果集为一组),然后在组内按照 WITHIN GROUP 中的ORDER BY进行排序,最后将表达式exp1用表达式exp2串接起来。LISTAGG 返回的是 VARCHAR 类型
它的语法是这样的
<LISTAGG>(<参数>[, <参数>]) WITHIN GROUP(<ORDER BY 项>)
我们惊喜的发现,这个函数支持group_concat的分组合并和组内排序功能,那么它是否也支持去重的功能呢?
在我们实验之后发现达梦并不支持这样的语法,在官方文档里面有这么一段话
分析函数有 distinct 的时候,不允许 order by 一起使用;
在经过实验和官方文档说明之后,我们失望的发现在达梦数据库中,并不存在一个功能完全与group_concat相同的函数,而LISTAGG函数虽然可以满足一般的分组合并要求,但是当分组的数据出现重复时,它并没有提供可以去重的解决方案,而且在查阅达梦数据库之后,也并未发现一个可以将”Java,Java”这样的字符串直接处理为”Java”的函数,这意味着,我们是时候亮出我们的底牌了:存储函数
存储函数的优缺点这里不再赘述,另如果对编写达梦数据库的存储函数有些无从下手或吃力的话,可以参考随达梦数据库一期分发的文档(DMSQL程序设计),里面列出了编写存储函数/过程需要的所有基础知识
现在让我们分析一下我们即将定义的这个存储函数所需要做的事情和如何完成这件事情的思路;其实在上面我们也已经提到过,LISTAGG函数已经为我们完成了分组合并的全部工作,也已经输出了一个字符串结果集,我们这个函数需要做的就是将这个字符串结果集根据分隔符去除掉重复的数据,比如说将”Java,Java”输出为”Java”,将”Java,Java,C#“输出为”Java,C#”
在确定了需要做的事情后让我们首先来确定该函数的输入输出参数,输出毋庸置疑,肯定是一个字符串结果,那么输入呢?我们发现输入出了要将我们处理的字符串传入外,还需要指定一个分隔符让程序知道,如何将字符串划分为词组,那么为什么我们不将分隔符写死为分号’,’呢?第一呢,考虑到函数的适用性,将分隔符传入是最好的选择,这里举的例子是将字符串按照,号合并,如果以后出现用其它分隔符合并的字符串呢?第二也是最重要的一点,我们在这里要处理的是LISTAGG处理之后的结果字符,而我们也可以看到,这个函数的分隔符是可以程序员手动指定的;到此为止,我们就可以确定该函数的作用和函数参数了,整理一下,参数说明如下:
参数名 | 参数数据类型 | 参数类型 |
---|---|---|
返回参数 | varchar(2000) | OUT |
value | varchar(2000) | IN |
separator | char(1) | IN |
终于,我们要跨向最后也是最重要的一步了,我们的函数应该怎么去完成这件事情呢?这里目的不是禁锢大家的思路,只是提供一个参考:首先将字符按照分隔符分隔为一个词组,然后逐个去比较,由于LISTAGG函数已经将词组排了序,所以这里我们可以偷一下懒,当当前词项是第一次出现时,将它拼接到结果词项后面,如果不是说明是重复词项,忽略不计然后再去检查下一个词项。
下面给出该思路的实现及代码说明:
CREATE OR REPLACE FUNCTION "model"."LISTAGG_DE_DUPLICATE" FOR CALCULATE ("value" IN VARCHAR(2000),"separator" IN CHAR(1))
RETURN VARCHAR(2000)
AUTHID DEFINER
AS
/*变量说明部分*/
VARNAME INT;
/*声明数组类型 */
TYPE valueArrayType IS ARRAY VARCHAR[];
/*声明数组存储按照分隔符分隔后的字符串 */
valueArray valueArrayType;
/*分隔符分隔后的词个数,用于声明数组长度 */
wordCount INT;
/*每次按照分隔符截取词时截取的开始位置 */
subStart INT;
/*当前分隔符所在的索引 */
separatorIndex INT;
/*每次按照分隔符截取词时截取的词长度 */
wordLength INT;
/*当前比较值,用于词去重处理 */
currentComparisonValue VARCHAR(2000);
/*结果字符 */
resultValue VARCHAR(2000);
BEGIN
/*执行体*/
/*如果传入的值为null则返回空*/
if value is null then
return null;
end if;
/*如果传入的值为空字符串则返回空字符串*/
if value = '' then
return '';
end if;
/*词个数,词个数为分隔符出现的次数加一*/
wordCount := REGEXP_COUNT(value,separator)+1;
/*如果词个数为1则表示并无分隔符,此时直接返回原字符*/
if wordCount =1 then
return value;
end if;
/*声明分隔词数组*/
valueArray := new VARCHAR[wordCount];
/*初始截取位置为0*/
subStart := 0;
for i in 1..wordCount loop
/*分隔符出现的索引位置*/
separatorIndex := LOCATE(separator,value,subStart+1);
/*如果分隔符出现的索引为0, 则表示已经处理到最后一个,分隔符的位置为当前字符长度加一*/
if separatorIndex = 0 then
separatorIndex := CHAR_LENGTH(value) + 1;
end if;
/*词的长度为分隔符位置减去截取开始的位置*/
wordLength := separatorIndex - subStart;
if subStart = 0 then
wordLength := wordLength - 1;
end if;
/*截取词*/
valueArray[i] := SUBSTRING(value FROM subStart FOR wordLength);
/*如果当前截取位置为0,则表示为第一个词项,此时初始化当前比较词和结果值*/
if subStart = 0 then
currentComparisonValue := valueArray[i];
resultValue := currentComparisonValue;
else
/*如果当前词项不等于当前比较词项,则代码新词串已开始,此时更新当前比较词,并将新词追加到结果词后面*/
if currentComparisonValue != valueArray[i] then
currentComparisonValue := valueArray[i];
resultValue = CONCAT(resultValue,separator,currentComparisonValue);
end if;
end if;
subStart := separatorIndex+1;
end loop;
return resultValue;
END;
以上函数中使用到得达梦函数均可在随达梦数据库发布的文档(DM_SQL语言使用手册)的第8章函数中找到
该函数已经经过测试,可以放心使用,但是需要注意以下几点
- 通过函数的名字LISTAGG_DE_DUPLICATE我们可以发现,该函数是针对LISTAGG函数处理的结果进行去重的
- 在运行上面语句创建函数时,需要注意第一行的”model”.”LISTAGG_DE_DUPLICATE”中的model,需要将其替换为真实数据库的模式名
最终我们可以在达梦数据库中使用以下sql语句解决我们上面提出的问题
select
a.a_id,
a.a_name,
model.LISTAGG_DE_DUPLICATE(LISTAGG(b.b_name,'-') within group (order by b.b_name),'-') b_name_all
from 表A a
inner join 表B b on a.a_id = b.b_a
group by a.a_id
原文章地址请点击此处链接