我可以用纯mysql解决这个问题吗? (加入“;”列中的分隔值)

Posted

技术标签:

【中文标题】我可以用纯mysql解决这个问题吗? (加入“;”列中的分隔值)【英文标题】:Can I resolve this with pure mysql? (joining on ';' separated values in a column) 【发布时间】:2015-11-18 14:32:36 【问题描述】:

长话短说:我有几个表格中的数据需要一起抓取,我已经简化了它们,以便不必绘制一个巨大的表格。

我需要在一个查询中执行此操作,并且我不能使用 php 或任何其他语言来处理结果。 (如果我可以简单地解决这个问题,我会使用 PHP)

如果我有一个将 t1 行连接到 t2 的链接表,这不会是一个问题,但不幸的是我没有也不能引入一个。

User table: (alias t1)
user(varchar 150),resources(varchar 250)
+-------+-------+
| user1 | 1;2;4 |
+-------+-------+
| user2 | 2     |
+-------+-------+
| user3 | 3;4   |
+-------+-------+

Resources table: (alias t2)
id(int 11 AI), data(text)
+---+-------+
| 1 | data1 |
+---+-------+
| 2 | data2 |
+---+-------+
| 3 | data3 |
+---+-------+
| 4 | data4 |
+---+-------+
| 5 | data5 |
+---+-------+

多个用户可以连接到相同的资源,用户可以访问一个或多个资源。

我想要一个接近的结果:

user,data
+-------+-------+
| user1 | data1 |
+-------+-------+
| user1 | data2 |
+-------+-------+
| user1 | data4 |
+-------+-------+
| user2 | data2 |
+-------+-------+

....等等等等。

我有基本的 mysql 知识,但这超出了我的知识范围。有什么办法可以内联 t2 吗?

在发这篇文章之前我读过的帖子: How to join two tables using a comma-separated-list in the join field

mysql join two table with comma separated ids

【问题讨论】:

如果您可以通过使用子查询将user table (t1) 转换为“正确的表”,那么您可以在一个查询中解决它吗?如果是这样,问题就变成了将user1 | 1;2;4 转换为“行”的问题。这在标准 SQL 中是否可行。是的,如果你用一个连续的“整数”表加入它。我调用'整数'。在我测试它的时候,我最初会把它放在一个视图中。我还没有尝试过,但它应该可以工作。 如果您设置一个带有测试数据的SQLFiddle 会有所帮助。我想快速测试查询而不是设置表和测试数据。我做了类似的事情here @RyanVincent ,今天晚些时候当我有一台可用的电脑时会这样做,目前我发表这篇文章 sqlfiddle 无法访问:( @RyanVincent sqlfiddle.com/#!9/52d9b 整理并添加了一个有效的SQLFiddle。 【参考方案1】:

如果user_resources (t1) 是一个“规范化表”,每个user => resource 组合对应一行,那么获取答案的查询将像joining 将表放在一起一样简单。

唉,denormalizedresources 列作为:“资源 ID 列表”,用“;”分隔字符。

如果我们可以将“资源”列转换为行,那么随着表连接变得简单,很多困难都会消失。

生成请求的输出的查询:

SELECT user_resource.user, 
       resource.data

FROM user_resource 
     JOIN integerseries AS isequence 
       ON isequence.id <= COUNT_IN_SET(user_resource.resources, ';') /* normalize */

     JOIN resource 
       ON resource.id = VALUE_IN_SET(user_resource.resources, ';', isequence.id)      
ORDER BY
       user_resource.user,  resource.data

输出:

user        data    
----------  --------
sampleuser  abcde   
sampleuser  azerty  
sampleuser  qwerty  
stacky      qwerty  
testuser    abcde   
testuser    azerty  

如何:

“诀窍”是有一个包含从 1 到某个限制的数字的表格。我称之为integerseries。它可以用来将“水平”的东西转换成';' delimited stringsrows

其工作原理是,当您使用integerseries“加入”时,您正在执行cross join,这是使用“内部联接”“自然”发生的情况。

每一行都与integerseries 表中的不同“序列号”重复,我们将其用作列表中我们要用于row 的“资源”的“索引”。

这个想法是:

计算列表中的项目数。 根据其在列表中的位置提取每个项目。 使用integerseries 将一行转换为一组行,从user.resources 中提取单个“资源ID”。

我决定使用两个函数:

给定“分隔字符串列表”和“索引”的函数将返回列表中位置的值。我称之为:VALUE_IN_SET。即给定 'A;B;C' 和 'index' 2 然后它返回 'B'。

给定“分隔字符串列表”的函数将返回列表中项目数的计数。我称之为:COUNT_IN_SET。即给定 'A;B;C' 将返回 3

事实证明,这两个函数和integerseries 应该为delimited items list in a column 提供通用解决方案。

有用吗?

';' delimited string in column 创建“规范化”表的查询。它显示了所有列,包括由于“cross_join”而生成的值(isequence.id as resources_index):

SELECT user_resource.user, 
       user_resource.resources,
       COUNT_IN_SET(user_resource.resources, ';')                AS resources_count, 
       isequence.id                                              AS resources_index,
       VALUE_IN_SET(user_resource.resources, ';', isequence.id)  AS resources_value
FROM 
     user_resource 
     JOIN  integerseries AS isequence 
       ON  isequence.id <= COUNT_IN_SET(user_resource.resources, ';')
ORDER BY
       user_resource.user, isequence.id

“标准化”表输出:

user        resources  resources_count  resources_index  resources_value  
----------  ---------  ---------------  ---------------  -----------------
sampleuser  1;2;3                    3                1  1                
sampleuser  1;2;3                    3                2  2                
sampleuser  1;2;3                    3                3  3                
stacky      2                        1                1  2                
testuser    1;3                      2                1  1                
testuser    1;3                      2                2  3                

使用上面的“标准化”user_resources 表,它是一个简单的连接来提供所需的输出:

需要的功能这些是通用功能,可以在任何地方使用

注意:这些函数的名称与mysqlFIND_IN_SET function相关。即他们在字符串列表方面做类似的事情?

COUNT_IN_SET函数:返回列中character delimited items的计数。

DELIMITER $$

DROP FUNCTION IF EXISTS `COUNT_IN_SET`$$

CREATE FUNCTION `COUNT_IN_SET`(haystack VARCHAR(1024), 
                               delim CHAR(1)
                               ) RETURNS INTEGER
BEGIN
      RETURN CHAR_LENGTH(haystack) - CHAR_LENGTH( REPLACE(haystack, delim, '')) + 1;
END$$

DELIMITER ;

VALUE_IN_SET 函数:将delimited list 视为one based array 并返回给定“索引”处的值。

DELIMITER $$

DROP FUNCTION IF EXISTS `VALUE_IN_SET`$$

CREATE FUNCTION `VALUE_IN_SET`(haystack VARCHAR(1024), 
                               delim CHAR(1), 
                               which INTEGER
                               ) RETURNS VARCHAR(255) CHARSET utf8 COLLATE utf8_unicode_ci
BEGIN
      RETURN  SUBSTRING_INDEX(SUBSTRING_INDEX(haystack, delim, which),
                     delim,
                     -1);
END$$

DELIMITER ;

相关信息:

终于搞定了如何让SQLFiddle - working code编译函数。

有一个版本适用于 SQLite 数据库以及 SQLite- Normalizing a concatenated field and joining with it?

表格(有数据):

CREATE TABLE `integerseries` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=500 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

/*Data for the table `integerseries` */

insert  into `integerseries`(`id`) values (1);
insert  into `integerseries`(`id`) values (2);
insert  into `integerseries`(`id`) values (3);
insert  into `integerseries`(`id`) values (4);
insert  into `integerseries`(`id`) values (5);
insert  into `integerseries`(`id`) values (6);
insert  into `integerseries`(`id`) values (7);
insert  into `integerseries`(`id`) values (8);
insert  into `integerseries`(`id`) values (9);
insert  into `integerseries`(`id`) values (10);

资源:

CREATE TABLE `resource` (
  `id` int(11) NOT NULL,
  `data` varchar(250) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

/*Data for the table `resource` */

insert  into `resource`(`id`,`data`) values (1,'abcde');
insert  into `resource`(`id`,`data`) values (2,'qwerty');
insert  into `resource`(`id`,`data`) values (3,'azerty');

用户资源:

CREATE TABLE `user_resource` (
  `user` varchar(50) COLLATE utf8_unicode_ci NOT NULL,
  `resources` varchar(250) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

/*Data for the table `user_resource` */

insert  into `user_resource`(`user`,`resources`) values ('sampleuser','1;2;3');
insert  into `user_resource`(`user`,`resources`) values ('stacky','3');
insert  into `user_resource`(`user`,`resources`) values ('testuser','1;3');

【讨论】:

我已经用函数重做了这个,它更整洁了。将很快发布更新。 感谢您在这里为帮助我付出的巨大努力 :) 视图解决方案足以让我现在完成工作,但我真的很期待您的其他解决方案,也用于学习目的。如果您将此作为单独的答案发布,我可以投票。【参考方案2】:

如果您将 ; 替换为 ,,您可以使用 FIND_IN_SET 函数来加入您的表:

select u.user, r.data
from User u
join Resources r
  on find_in_set(r.id, replace(u.resources, ';', ','))
order by u.user, r.id

结果:

|  user |  data |
|-------|-------|
| user1 | data1 |
| user1 | data2 |
| user1 | data4 |
| user2 | data2 |
| user3 | data3 |
| user3 | data4 |

http://sqlfiddle.com/#!9/a0792b/5

【讨论】:

以上是关于我可以用纯mysql解决这个问题吗? (加入“;”列中的分隔值)的主要内容,如果未能解决你的问题,请参考以下文章

mysql - 可以加入多个领域吗?

用纯虚拟覆盖虚拟......可以吗?

使用 MySQL JSON 字段加入表

你能用纯序言写 between/3 吗?

mysql加入查询喜欢吗?

MS Access/JET“不支持加入表达式”有啥方法可以修复这个查询吗?