对于带PHP的MySQL,最好的排序规则是什么?

What is the best collation to use for MySQL with PHP?

我想知道对于一个普通的网站来说,在MySQL中是否有一个"最佳"的排序选择,你不100%确定会输入什么?我理解所有的编码都应该是相同的,比如mysql、apache、html以及php内部的任何东西。

在过去,我将php设置为以"utf-8"输出,但在MySQL中,这与哪种排序规则匹配?我想它是UTF-8的一种,但我以前用过utf8_unicode_ciutf8_general_ciutf8_bin


主要区别在于排序的准确性(在比较语言中的字符时)和性能。唯一特殊的是utf8_bin,它用于比较二进制格式的字符。

utf8_general_ciutf8_unicode_ci稍快,但(用于排序)精度较低。特定语言utf8编码(如utf8_swedish_ci)包含额外的语言规则,这些规则使它们对于这些语言的排序最准确。大多数时候我使用utf8_unicode_ci(我更喜欢精确性而不是小的性能改进),除非我有充分的理由喜欢特定的语言。

您可以在MySQL手册上阅读有关特定Unicode字符集的更多信息-http://dev.mysql.com/doc/refman/5.0/en/charset-unicode-sets.html


非常,非常清楚使用utf8_general_ci时可能出现的问题。

如果使用utf8_general_ci排序规则,MySQL将不会区分select语句中的某些字符。这会导致非常严重的错误,尤其是涉及用户名的地方。根据使用数据库表的实现,此问题可能允许恶意用户创建与管理员帐户匹配的用户名。

这个问题至少在早期的5.x版本中会暴露出来——我不确定这种行为是否会在以后发生变化。

我不是DBA,但为了避免这个问题,我总是使用utf8-bin,而不是不区分大小写的。

下面的脚本通过示例描述了这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
-- first, create a sandbox to play in
CREATE DATABASE `sandbox`;
use `sandbox`;

-- next, make sure that your client connection is of the same
-- character/collate type as the one we're going to test next:
charset utf8 collate utf8_general_ci

-- now, create the table and fill it with values
CREATE TABLE `test` (`key` VARCHAR(16), `value` VARCHAR(16) )
    CHARACTER SET utf8 COLLATE utf8_general_ci;

INSERT INTO `test` VALUES ('
Key ONE', 'value'), ('Key TWO', 'valúe');

-- (verify)
SELECT * FROM `test`;

-- now, expose the problem/bug:
SELECT * FROM test WHERE `value` = '
value';

--
-- Note that we get BOTH keys here! MySQLs UTF8 collates that are
-- case insensitive (ending with _ci) do not distinguish between
-- both values!
--
-- collate '
utf8_bin' doesn't have this problem, as I'll show next:
--

-- first, reset the client connection charset/collate type
charset utf8 collate utf8_bin

-- next, convert the values that we'
ve previously inserted in the table
ALTER TABLE `test` CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin;

-- now, re-check for the bug
SELECT * FROM test WHERE `value` = 'value';

--
-- Note that we get just one key now, as you'd expect.
--
-- This problem appears to be specific to utf8. Next, I'
ll try to
-- do the same with the 'latin1' charset:
--

-- first, reset the client connection charset/collate type
charset latin1 collate latin1_general_ci

-- next, convert the values that we've previously inserted
-- in the table
ALTER TABLE `test` CONVERT TO CHARACTER SET latin1 COLLATE latin1_general_ci;

-- now, re-check for the bug
SELECT * FROM test WHERE `value` = '
value';

--
-- Again, only one key is returned (expected). This shows
-- that the problem with utf8/utf8_generic_ci isn'
t present
-- in latin1/latin1_general_ci
--
-- To complete the example, I'll check with the binary collate
-- of latin1 as well:

-- first, reset the client connection charset/collate type
charset latin1 collate latin1_bin

-- next, convert the values that we'
ve previously inserted in the table
ALTER TABLE `test` CONVERT TO CHARACTER SET latin1 COLLATE latin1_bin;

-- now, re-check for the bug
SELECT * FROM test WHERE `value` = 'value';

--
-- Again, only one key is returned (expected).
--
-- Finally, I'll re-introduce the problem in the exact same
-- way (for any sceptics out there):

-- first, reset the client connection charset/collate type
charset utf8 collate utf8_generic_ci

-- next, convert the values that we'
ve previously inserted in the table
ALTER TABLE `test` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;

-- now, re-check for the problem/bug
SELECT * FROM test WHERE `value` = 'value';

--
-- Two keys.
--

DROP DATABASE sandbox;


实际上,您可能想使用utf8_unicode_ciutf8_general_ci

  • utf8_general_ci通过去掉所有重音符号并像ASCII那样排序来进行排序。
  • utf8_unicode_ci使用unicode排序顺序,因此它可以在更多语言中正确排序。

但是,如果您只是使用它来存储英语文本,那么这些内容不应该有所不同。


最好使用带有排序规则utf8mb4_unicode_ci的字符集utf8mb4

字符集utf8只支持少量的utf-8代码点,大约6%的可能字符。utf8只支持基本多语言平面(BMP)。还有16架飞机。每个平面包含65536个字符。utf8mb4支持所有17个飞机。

MySQL将截断4字节的utf-8字符,导致数据损坏。

2010-03-24在MySQL5.5.3中引入了utf8mb4字符集。

使用新字符集所需的一些更改并不简单:

  • 可能需要在应用程序数据库适配器中进行更改。
  • 需要对my.cnf进行更改,包括设置字符集、排序和将innodb_file_格式切换为baracuda。
  • SQL create语句可能需要包括:ROW_FORMAT=DYNAMIC
    • varchar(192)及更大版本上的索引需要动态。

注:从Antelope切换到Barracuda可能需要多次重启mysql服务。在mysql服务重新启动到:innodb_file_format = barracuda之前,innodb_file_format_max不会改变。

MySQL使用旧的Antelopeinnodb文件格式。Barracuda支持动态行格式,如果您不想在切换到charset:utf8mb4后遇到创建索引和键的SQL错误,则需要使用动态行格式。

  • #1709-索引列大小太大。最大列大小为767字节。
  • #1071-指定的密钥太长;最大密钥长度为767字节

以下场景已经在MySQL5.6.17上进行了测试:默认情况下,mysql的配置如下:

1
2
3
4
SHOW VARIABLES;

innodb_large_prefix = OFF
innodb_file_format = Antelope

停止MySQL服务并将选项添加到现有的my.cnf:

1
2
3
4
5
6
7
8
9
10
11
12
13
[client]
default-character-set= utf8mb4

[mysqld]
explicit_defaults_for_timestamp = true
innodb_large_prefix = true
innodb_file_format = barracuda
innodb_file_format_max = barracuda
innodb_file_per_table = true

# Character collation
character_set_server=utf8mb4
collation_server=utf8mb4_unicode_ci

示例SQL create语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE Contacts (
 id INT AUTO_INCREMENT NOT NULL,
 ownerId INT DEFAULT NULL,
 created timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
 modified timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
 contact VARCHAR(640) NOT NULL,
 prefix VARCHAR(128) NOT NULL,
 first VARCHAR(128) NOT NULL,
 middle VARCHAR(128) NOT NULL,
 last VARCHAR(128) NOT NULL,
 suffix VARCHAR(128) NOT NULL,
 notes MEDIUMTEXT NOT NULL,
 INDEX IDX_CA367725E05EFD25 (ownerId),
 INDEX created (created),
 INDEX modified_idx (modified),
 INDEX contact_idx (contact),
 PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB ROW_FORMAT=DYNAMIC;
  • 如果从create语句中删除ROW_FORMAT=DYNAMIC,则可以看到为INDEX contact_idx (contact)生成的错误1709。

注:更改索引以限制在contact上的前128个字符,消除了在ROW_FORMAT=DYNAMIC上使用梭鱼的要求。

1
INDEX contact_idx (contact(128)),

还要注意:当它说字段的大小是VARCHAR(128)时,它不是128字节。您可以使用128、4字节字符或128、1字节字符。

INSERT语句应在2行中包含4字节的"poo"字符:

1
2
3
4
INSERT INTO `Contacts` (`id`, `ownerId`, `created`, `modified`, `contact`, `prefix`, `first`, `middle`, `last`, `suffix`, `notes`) VALUES
(1, NULL, '0000-00-00 00:00:00', '2014-08-25 03:00:36', '1234567890', '12345678901234567890', '1234567890123456789012345678901234567890', '1234567890123456789012345678901234567890', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678', '', ''),
(2, NULL, '0000-00-00 00:00:00', '2014-08-25 03:05:57', 'poo', '12345678901234567890', '????????????????????????????????????????????????????????????????????????????????', '????????????????????????????????????????????????????????????????????????????????', '????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????', '', ''),
(3, NULL, '0000-00-00 00:00:00', '2014-08-25 03:05:57', 'poo', '12345678901234567890', '????????????????????????????????????????????????????????????????????????????????', '????????????????????????????????????????????????????????????????????????????????', '123??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????', '', '');

您可以看到last列使用的空间量:

1
2
3
4
5
6
7
8
mysql> SELECT BIT_LENGTH(`last`), CHAR_LENGTH(`last`) FROM `Contacts`;
+--------------------+---------------------+
| BIT_LENGTH(`last`) | CHAR_LENGTH(`last`) |
+--------------------+---------------------+
|               1024 |                 128 | -- All characters are ASCII
|               4096 |                 128 | -- All characters are 4 bytes
|               4024 |                 128 | -- 3 characters are ASCII, 125 are 4 bytes
+--------------------+---------------------+

在数据库适配器中,您可能需要设置连接的字符集和排序规则:

1
SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'

在php中,这将设置为:\PDO::MYSQL_ATTR_INIT_COMMAND

参考文献:

  • MySQL5.6参考手册:InnoDB表的限制
  • 如何在MySQL数据库中支持完整的Unicode


排序规则会影响数据的排序方式以及字符串之间的比较方式。这意味着您应该使用大多数用户期望的排序规则。

文档中的示例:

utf8_general_ci also is satisfactory
for both German and French, except
that ‘?’ is equal to ‘s’, and not to
‘ss’. If this is acceptable for your
application, then you should use
utf8_general_ci because it is faster.
Otherwise, use utf8_unicode_ci because
it is more accurate.

所以-这取决于你期望的用户群和你需要多少正确的排序。对于英语用户群,utf8_general_ci应该足够了,对于其他语言,如瑞典语,已经创建了特殊的排序规则。


本质上,它取决于你如何看待一个字符串。

我总是使用utf8_-bin,因为guus强调了这个问题。在我看来,就数据库而言,字符串仍然只是一个字符串。字符串是一个UTF-8字符数。一个字符有一个二进制表示,那么为什么它需要知道您使用的语言呢?通常,人们将为具有多语言站点范围的系统构建数据库。这就是使用UTF-8作为字符集的关键所在。我是一个有点纯粹的人,但我认为这个bug的风险大大超过了你在索引上可能获得的微小优势。任何与语言相关的规则都应该在比DBMS更高的级别上执行。

在我的书中,"价值"不应该在一百万年内等于"价值"。

如果我想存储一个文本字段并进行不区分大小写的搜索,我将使用mysql字符串函数和php函数,如lower()和php函数strtolower()。


对于utf-8文本信息,应该使用utf8_general_ci,因为…

  • utf8_bin比较字符串中每个字符的二进制值弦

  • utf8_general_ci:比较字符串使用通用语言规则和使用不区分大小写的比较

A.K.A.它将使搜索和索引数据更快/更有效/更有用。


这个公认的答案相当明确地建议使用utf8-unicode-ci,而对于新项目来说,这是很好的,我想把我最近的相反经验联系起来,以防节省任何人一些时间。

因为utf8_-general_-ci是MySQL中unicode的默认排序规则,如果您想使用utf8_-unicode_-ci,那么最终必须在很多地方指定它。

例如,所有客户机连接不仅具有默认字符集(对我来说是有意义的),而且还具有默认排序规则(即,对于Unicode,排序规则始终默认为utf8_-general_-ci)。

很可能,如果对字段使用utf8 _unicode _ci,则需要更新连接到数据库的脚本,以明确说明所需的排序规则,否则,当连接使用默认排序规则时,使用文本字符串的查询可能会失败。

结果是,当将任何大小的现有系统转换为unicode/utf8时,由于mysql处理默认值的方式,您可能最终被迫使用utf8_-general_-ci。


对于guus突出显示的情况,我强烈建议使用utf8 _unicode _cs(区分大小写,严格匹配,大部分部分正确排序)而不是utf8 _bin(严格匹配,错误排序)。

如果要搜索字段,而不是匹配用户,则使用utf8_-general_-ci或utf8_-unicode_-ci。两者都不区分大小写,其中一个将失败匹配("?"'等于's',而不是's s')。还有一些特定于语言的版本,如utf8_-german_-ci,其中lose-matching更适合指定的语言。

[编辑-近6年后]

我不再推荐MySQL上的"utf8"字符集,而是推荐"utf8mb4"字符集。它们几乎完全匹配,但允许更多的Unicode字符。

实际上,MySQL应该已经更新了"utf8"字符集和相应的排序规则,以匹配"utf8"规范,但是,为了不影响已经使用了不完整的"utf8"字符集的用户的存储指定,应该更新一个单独的字符集和相应的排序规则。


我发现这些排序图表很有用。http://collation charts.org/mysql60/。不过,我不确定使用的是哪种utf8_-general_-ci。

例如,这里是utf8_Swedish_ci的图表。它显示了它解释为相同的字符。http://collation-charts.org/mysql60/mysql604.utf8_瑞典语_ci.html


在数据库上载文件中,在任何行之前添加以下行:

1
SET NAMES utf8;

你的问题应该解决。