14

(注意:这个问题不是关于转义查询,而是关于转义结果)

我正在使用GROUP_CONCAT将多行组合成一个逗号分隔的列表。例如,假设我有两个(示例)表:

CREATE TABLE IF NOT EXISTS `Comment` (
`id` int(11) unsigned NOT NULL auto_increment,
`post_id` int(11) unsigned NOT NULL,
`name` varchar(255) collate utf8_unicode_ci NOT NULL,
`comment` varchar(255) collate utf8_unicode_ci NOT NULL,
PRIMARY KEY  (`id`),
KEY `post_id` (`post_id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=6 ;

INSERT INTO `Comment` (`id`, `post_id`, `name`, `comment`) VALUES
(1, 1, 'bill', 'some comment'),
(2, 1, 'john', 'another comment'),
(3, 2, 'bill', 'blah'),
(4, 3, 'john', 'asdf'),
(5, 4, 'x', 'asdf');


CREATE TABLE IF NOT EXISTS `Post` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(255) collate utf8_unicode_ci NOT NULL,
PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=7 ;

INSERT INTO `Post` (`id`, `title`) VALUES
(1, 'first post'),
(2, 'second post'),
(3, 'third post'),
(4, 'fourth post'),
(5, 'fifth post'),
(6, 'sixth post');

我想列出所有帖子以及对帖子发表评论的每个用户名的列表:

SELECT
Post.id as post_id, Post.title as title, GROUP_CONCAT(name) 
FROM Post 
LEFT JOIN Comment on Comment.post_id = Post.id
GROUP BY Post.id

给我:

id  title   GROUP_CONCAT( name )
1   first post  bill,john
2   second post     bill
3   third post  john
4   fourth post     x
5   fifth post  NULL
6   sixth post  NULL

这很好用,除了如果用户名包含逗号,它将破坏用户列表。MySQL 是否有一个函数可以让我转义这些字符?(请假设用户名可以包含任何字符,因为这只是一个示例模式)

4

10 回答 10

46

实际上,有ascii control characters专门设计用于分离数据库字段和记录:

0x1F (31): unit (fields) separator

0x1E (30): record separator

0x1D (29): group separator

阅读更多:关于 ascii 字符

您永远不会在用户名中使用它们,而且很可能永远不会在non-binary data数据库中的任何其他用户名中使用它们,因此可以安全地使用它们:

GROUP_CONCAT(foo SEPARATOR 0x1D)

CHAR(0x1D)然后以您想要的任何客户端语言拆分。

于 2012-03-07T11:51:09.503 回答
15

如果用户名中有其他非法字符,您可以使用鲜为人知的语法指定不同的分隔符:

...GROUP_CONCAT(name SEPARATOR '|')...

...您要允许管道吗?或任何字符?

转义分隔符,可能使用反斜杠,但在此之前转义反斜杠本身:

group_concat(replace(replace(name, '\\', '\\\\'), '|', '\\|') SEPARATOR '|')

这会:

  1. 用另一个反斜杠转义任何反斜杠
  2. 用反斜杠转义分隔符
  3. 将结果与分隔符连接起来

要获得未转义的结果,请按相反的顺序执行相同的操作:

  1. 在前面没有反斜杠的情况下,用分隔符分割结果。实际上,这有点棘手,您想将其拆分到前面没有奇数个黑斜线的地方。此正则表达式将匹配:
    (?<!\\)(?:\\\\)*\|
  2. 用文字替换所有转义的分隔符,即替换 \| 与 |
  3. 用单反斜杠替换所有双反斜杠,例如用 \ 替换 \\
于 2009-01-16T23:33:02.100 回答
4

REPLACE()

例子:

... GROUP_CONCAT(REPLACE(name, ',', '\\,')) 

请注意,您必须使用双反斜杠(如果您用反斜杠转义逗号),因为反斜杠本身很神奇,并且\,变得简单,

于 2009-01-16T23:14:05.757 回答
4

我建议使用 GROUP_CONCAT(name SEPARATOR '\n'),因为 \n 通常不会出现。这可能会更简单一些,因为您不需要逃避任何事情,但可能会导致意想不到的问题。nick 提出的编码/正则表达式解码当然也不错。

于 2009-06-18T20:43:28.530 回答
1

如果您要在应用程序中进行解码,也许只需使用hex

SELECT GROUP_CONCAT(HEX(foo)) ...

或者您也可以将长度放入其中:

SELECT GROUP_CONCAT(CONCAT(LENGTH(foo), ':', foo)) ...

也不是我测试过:-D

于 2009-01-17T01:26:00.687 回答
0

尼克说的是真的,通过增强 - 分隔符也可以是多个字符。

我经常用

GROUP_CONCAT(name SEPARATOR '"|"')

用户名包含“|”的可能性 我会说相当低。

于 2009-01-16T23:51:24.853 回答
0

您正在进入那个灰色区域,在 SQL 世界之外对它进行后处理可能会更好。

至少这就是我要做的:我只是 ORDER BY 而不是 GROUP BY,并循环遍历结果以将分组处理为以客户端语言完成的过滤器:

  1. 首先初始化last_id为 NULL
  2. 获取结果集的下一行(如果没有更多行,请转到步骤 6)
  3. 如果行的 id 不同于last_id开始一个新的输出行:

    一种。如果last_id不是 NULL 则输出分组的行

    湾。设置新的分组行 = 输入行,但将名称存储为单个元素数组

    C。设置last_id为当前 ID 的值

  4. 否则(id 与 相同last_id)将行名称附加到现有的分组行上。

  5. 返回第 2 步
  6. 否则你已经完成了;如果last_id不是 NULL,则输出现有的组行。

然后,您的输出最终会包括以数组形式组织的名称,然后您可以决定如何处理/转义/格式化它们。

您使用什么语言/系统?php?珀尔?爪哇?

于 2009-01-17T01:18:38.580 回答
0

Jason S:这正是我正在处理的问题。我正在使用 PHP MVC 框架,并且正在处理您所描述的结果(每个结果多行,并且代码将结果组合在一起)。但是,我一直在为我的模型实现两个功能。一个返回重新创建对象所需的所有必要字段的列表,另一个是一个函数,它给定一行包含第一个函数的字段,实例化一个新对象。这让我可以从数据库中请求一行并轻松地将其转回对象,而无需了解模型所需数据的内部结构。当多行代表一个对象时,这不会很好,所以我试图使用 GROUP_CONCAT 来解决这个问题。

于 2009-01-17T01:31:09.707 回答
0

现在我允许任何角色。我意识到管道不太可能出现,但我想允许它。

一个控制字符怎么样,无论如何你都应该从应用程序输入中去掉它?我怀疑你需要例如。名称字段中的制表符或换行符。

于 2009-01-18T01:30:55.793 回答
0

只是为了扩展一些答案,我在 PHP 中实现了@derobert 的第二个建议,并且效果很好。给定 MySQL,例如:

GROUP_CONCAT(CONCAT(LENGTH(field), ':', field) SEPARATOR '') AS fields

我使用以下函数对其进行拆分:

function concat_split( $str ) {
    // Need to guard against PHP's stupid multibyte string function overloading.
    static $mb_overload_string = null;
    if ( null === $mb_overload_string ) {
        $mb_overload_string = defined( 'MB_OVERLOAD_STRING' )
                && ( ini_get( 'mbstring.func_overload' ) & MB_OVERLOAD_STRING );
    }
    if ( $mb_overload_string ) {
        $mb_internal_encoding = mb_internal_encoding();
        mb_internal_encoding( '8bit' );
    }

    $ret = array();
    for ( $offset = 0; $colon = strpos( $str, ':', $offset ); $offset = $colon + 1 + $len ) {
        $len = intval( substr( $str, $offset, $colon ) );
        $ret[] = substr( $str, $colon + 1, $len );
    }

    if ( $mb_overload_string ) {
        mb_internal_encoding( $mb_internal_encoding );
    }

    return $ret;
}

我最初还使用@Lemon Juice 的分隔符之一实施了@ʞɔıu 的建议。它工作得很好,但除了它的复杂性之外它更慢,主要问题是 PCRE 只允许固定长度的lookbehind,因此使用建议的正则表达式进行拆分需要捕获分隔符,否则字符串末尾的双反斜杠将丢失。因此,给定 MySQL,例如(注意 4 PHP 反斜杠 => 2 MySQL 反斜杠 => 1 真正的反斜杠):

GROUP_CONCAT(REPLACE(REPLACE(field, '\\\\', '\\\\\\\\'),
    CHAR(31), CONCAT('\\\\', CHAR(31))) SEPARATOR 0x1f) AS fields

拆分功能是:

function concat_split( $str ) {
    $ret = array();
    // 4 PHP backslashes => 2 PCRE backslashes => 1 real backslash.
    $strs = preg_split( '/(?<!\\\\)((?:\\\\\\\\)*+\x1f)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE );
    // Need to add back any captured double backslashes.
    for ( $i = 0, $cnt = count( $strs ); $i < $cnt; $i += 2 ) {
        $ret[] = isset( $strs[ $i + 1 ] ) ? ( $strs[ $i ] . substr( $strs[ $i + 1 ], 0, -1 ) ) : $strs[ $i ];
    }
    return str_replace( array( "\\\x1f", "\\\\" ), array( "\x1f", "\\" ), $ret );
}
于 2016-05-31T02:47:23.563 回答