SQL优化

连接的优化

由于连接的成本较高,因此对于高并发的应用,应该尽量减少使用连接的查询,连接表的个数建议控制在四个以内.以对于数据库应用的设计,最好在早期就确定未来可能会影响性能的一些查询,进行反范式设计减少连接的表,或者考虑在应用层进行连接

优化连接的一些要点

  • 1)ON、USING子句中的列确认有索引。如果优化器选择了连接的顺序为B、A,那么我们只需要在A表的列上创建索引即可。例如,对于查询“SELECTB.*,A.*FROMBJOIN AON B.col1=A.col2;”语句MySQL会全表扫描B表,对B表的每一行记录探测A表的记录(利用A表col2列上的索引)。
  • 2)最好是转换成INNER JOIN,LEFT JOIN的成本比INNER JOIN高很多.
  • 3)使用EXPLAIN检查连接,留意EXPLAIN输出的rows列,如果rows列太高,比如几千,上万,那么就需要考虑是否索引不佳或连接表的顺序不当。
  • 4)反范式设计,这样可以减少连接表的个数,加快存取数据的速度。
  • 5)考虑在应用层实现连接。

对于一些复杂的连接查询,更值得推荐的做法是将它分解为几个简单的查询,可以先执行查询以获得一个较小的结果集,然后遍历此结果集,最后根据一定的条件去获取完整的数据,这样做往往是更加高效的,更加不容易发生变化,更加方便缓存数据,数据也可以按照设计的需求从缓存中进行获取.

示例:

SELECT a.* FROM a WHERE a.id IN (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17)

如果果id=1~15的记录已经被存储在缓存(如Memcached)中了,那么我们只需要到数据库查询“SELECT a.* FROM a WHEREa.id=16”和“SELECT a.* FROMa WHERE a.id=17”了。而且,把IN列表分解为等值查找,往往可以提高性能。

  • 6)一些应用可能需要访问不同的数据库实例,这种情况下,在应用层实现连接将是更好的选择

GROUP BY、DISTINCT、ORDER BY语句优化

GROUP BY、DISTINCT、ORDERBY这几类子句比较类似,GROUP BY默认也是要进行ORDERBY排序的.可以考虑的优化方式如下.

  • 尽量对较少的行进行排序.
  • 如果连接了多张表,ORDERBY的列应该属于连接顺序的第一张表。
  • 利用索引排序,如果不能利用索引排序,那么EXPLAIN查询语句将会看到有filesort。
  • GROUP BY、ORDERBY语句参考的列应该尽量在一个表中,如果不在同一个表中,那么可以考虑冗余一些列,或者合并表。
  • 需要保证索引列和ORDERBY的列相同,且各列均按相同的方向进行排序(有点类似与利用索引排序)。
  • 增加sort_buffer_size

sort_buffer_size是为每个排序线程分配的缓冲区的大小。增加该值可以加快ORDERBY或GROUP BY操作。但是,这是为每个客户端分配的缓冲区,因此不要将全局变量设置为较大的值,因为每个需要排序的连接都会分配sortbuffersize大小的内存

  • ·改变tmpdir变量指向基于内存的文件系统或其他更快的磁盘。

如果是MySQL服务器正作为复制从服务器被使用,那么不应将“--tmpdir”设置为指向基于内存的文件系统的目录,或者当服务器主机重启时将要被清空的目录。因为,对于复制从服务器,需要在机器重启时仍然保留一些临时文件,以便能够复制临时表或执行LOADDATAINFILE操作。如果在服务器重启时丢失了临时文件目录下的文件,那么复制将会失败。

  • 指定ORDERBYNULL。

默认情况下 MySQL将排序所有GROUP BY的查询,如果想要避免排序结果所产生的消耗,可以指定ORDER BY NULL。

SELECT count(*) cnt, cluster_id FROM stat GROUP BY cluster_id ORDER BY NULL LIMIT 10;
  • 优化GROUP BY WITH ROLLUP。

GROUP BY WITH ROLLUP可以方便地获得整体分组的聚合信息(superaggregation),但如果存在性能问题,可以考虑在应用层实现这个功能,这样往往会更高效,伸缩性也更佳。

  • 使用非GROUP BY的列来代替GROUP BY的列。

原来是“GROUP BYxx_name,yy_name”,如果GROUP BYxx_id可以得到一样的结果,那么使用GROUP BYxx_id也是可行的.(多个的组合排序转化为单个排序)


优化子查询

对于数据库来说,在绝大部分情况下,连接会比子查询更快。使用连接的方式,MySQL优化器一般可以生成更佳的执行计划,可以预先装载数据,更高效地处理查询。而子查询往往需要运行重复的查询,子查询生成的临时表上也没有索引,因此效率会更低。

一些商业数据库已经可以智能地识别子查询,转化子查询为连接查询,或者转化连接为子查询。这种情况下,编写子查询也许是更好的方式,毕竟更符合人的思考方式,也能避免因为重复记录的匹配导致连接结果集的异常。但MySQL对于子查询的优化一直不佳,就目前的研发实践来说,子查询应尽量改写成JOIN的写法。如果我们不能确定是否要使用连接的方式,那么可以使用EXPLAIN语法查看语句具体的执行计划

Example1 如下是一个带子查询的语句

SELECT DISTINCT column1 FROM t1 WHERE t1.column1 IN ( SELECT column1 FROM t2);

转化为连接的写法

SELECT DISTINCT t1.column1 FROM t1, t2 WHERE t1.column1 = t2.column1;

Example2 如下的两个查询是等价的 SELECT * FROM t1 WHERE id NOT IN (SELECT id FROM t2); SELECT * FROM t1 WHERE NOT EXISTS (SELECT id FROM t2 WHERE t1.id=t2.id); 改写为LEFT JOIN形式 SELECT table1.* FROM table1 LEFT JOIN table2 ON table1.id=table2.id WHERE table2.id IS NULL;Example3 把子句从子查询的外部转移到内部。 #感觉这句SQL有问题,如果s1不为空,效果和SELECT * FROM t1是一样的 SELECT * FROM t1 WHERE s1 IN (SELECT s1 FROM t1) OR s1 IN (SELECT s1 FROM t2); 转换后 SELECT * FROM t1 WHERE s1 IN (SELECT s1 FROM t1 UNION ALL SELECT s1 FROM t2);Example4 SELECT * FROM t1 WHERE EXISTS (SELECT * FROM t2 WHERE t2.column1=t1.column1 AND t2.column2=t1.column2); 转化成如下的写法,使用行子查询来代替关联子查询 SELECT * FROM t1 WHERE (column1,column2) IN (SELECT column1,column2 FROM t2); 对于只返回一行的无关联子查询,IN的速度慢于“=”。

Web 应用经常需要对查询的结果进行分页,分页算法经常需要用到 “LIMIT offset,row_count ORDER BY col_id” 之类的语句。一旦 offset 的值很大,效率就会很差,因为 MySQL 必须检索大量的记录( offset+row_count ),然后丢弃大部分记录。

可提供的优化方法有如下四种: 1) 限制页数,只显示前几页,超过了一定的页数后,直接显示 “ 更多( more ) ” ,一般来说,对于 N 页之后的结果,用户一般不会关心。

limit offset用法演示

语句1:select * from student limit 9,4
语句2:slect * from student limit 4 offset 9

语句1和2均返回表student的第10、11、12、13行
语句2中的4表示返回4行,9表示从表的第十行开始

通过limit和offset 或只通过limit可以实现分页功能。

  • 假设 numberperpage 表示每页要显示的条数,pagenumber表示页码,那么 返回第pagenumber页,每页条数为numberperpage的sql语句:

代码示例:

语句3:select * from studnet limit (pagenumber-1)*numberperpage,numberperpage
语句4:select * from student limit numberperpage offset (pagenumber-1)*numberperpage

2 )要避免设置 offset 值,也就是避免丢弃记录。 示例:

  • 按照 id 排序( id 列上有索引),通过增加一个定位的列 “id>990” ,可以避免设置 offset 的值。
SELECT id, name, address, phone
FROM customers
WHERE id > 990
ORDER BY id LIMIT 10;
  • 也可以使用条件限制要排序的结果集,如可以这样使用。
WHERE date_time BETWEEN ‘
2014-04-01 00:00:00 ’
AND  ‘
2014-04-02 00:00:00 ’
ORDER BY id
  • 对条件值可以进行估算,对于几百上千页的检索,往往不需要很精确。也可以专门增加冗余的列来定位记录,比如如下的查询,有一个 page 列,指定记录所在的页,代价是在修改数据的时候需要维护这个列的数据,如下面的查询。
SELECT id, name, address, phone
FROM customers
WHERE page = 100
ORDER BY name;

3 )使用 Sphinx 。

一个软件,用来代替MySQL全文检索

4 )使用 INNER JOIN 以下的例子中,先按照索引排序获取到 id 值,然后再使用 JOIN 补充其他列的数据。 customers 表的主键列是 id 列, name 列上有索引,由于 “SELECT id FROM customers…” 可以用到覆盖索引,所以效率尚可。

SELECT id, name, address, phone
FROM customers
INNER JOIN (
SELECT id
FROM customers
ORDER BY name
LIMIT 999,10)
AS my_results USING(id);

优化IN列表

  • 对于IN列表,MySQL会排序IN列表中的值,并使用二分查找 ( Binary Search )的方式去定位数据。
  • 把IN子句改写成OR的形式并不能提高性能。 IN 列表不宜过长,最好不要超过 200 。对于高并发的业 务,小于几十为佳。

如果能够将其转化为多个等于的查询,那么这种方式会更优。

SELECT * FROM table_a WHERE id IN (SELECT id FROM table_b);

们可以先查询 SELECT id FROM table_b ,然后把获取到的 id 值,逐个地和 “SELECT*FROM table_a” 进行拼接,转化为 “SELECT id FROM table_a WHERE id=?” 的形式。


优化 UNION

UNION 语句默认是移除重复记录的,需要用到排序操作,如果结果集很大,成本将会很高,所以,建议尽量使用 UNIONALL 语句。对于 UNION 多个分表的场景,应尽可能地在数据库分表的时候,就确定各个分表的数据是唯一的,这样就无须使用UNION 来去除重复的记录了。

另外,查询语句外层的 WHERE 条件,并不会应用到每个单独的 UNION 子句内,所以,应在每一个 UNION 子句中添加上WHERE 条件,从而尽可能地限制检索的记录数。


优化带有 BLOB 、 TEXT 类型字段的查询

由于Mysql的内存临时表不支持 BLOB 、 TEXT 类型,如果包含 BLOB 或 TEXT 类型列的查询需要用到临时表,就会使用基于磁盘的临时表,性能将会急剧降低。所以,编写查询语句时,如果没有必要包含 BLOB 、 TEXT 列,就不要写入查询条件。

规避 BLOB 、 TEXT 列的办法有如下两种。 1 )使用 SUBSTRING() 函数。 2 )设置 MySQL 变量 tmpdir ,把临时表存放在基于内存的文件系统中。如 Linux 下的 tmpfs 。可以设置多个临时表的路径(用分号分隔), MySQL 将使用轮询的方式。

**优化的方法有如下3种:

    1. 如果必须使用,可以考虑拆分表,把 BLOB 、 TEXT 字段分离到单独的表。
    1. 如果有许多的大字段,可以考虑合并这些字段到一个字段,存储一个大的200KB比存储20个10KB更高效
    1. 考虑使用 COMPRESS() ,或者在应用层进行压缩,再存储到 BLOB 字段中。

注意 如果 BLOB 列很大,可能需要增大 innodb_log_file_size ( MySQL 错误日志内可能会提示事务日志小了)。


filesort 的优化

有时我们使用 EXPLAIN 工具,可以看到查询计划的输出中的 Extra 列有 filesort 。 filesort 往往意味着你没有利用到索引进行排序。 filesort 的字面意思可能会导致混淆,它和文件排序没有任何关系,可以理解为不能利用索引实现排序。

排序一个带 JOIN (连接)的查询,如果 ORDER BY 子句参考的是 JOIN 顺序里的第一张表的列且不能利用索引进行排序,那么 MySQL 会对这个表进行文件排序( filesort ), EXPLAIN 输出中的 Extra 列就有 filesort 。如果排序的列来自于其他的表,且需要临时文件来帮助排序,那么 EXPLAIN 输出的 Extra 列就有 “Using temporary;Using filesort” 字样。对于 MySQL 5.1 ,如果有 LIMIT 子句,那么是在 filesort 之后执行 LIMIT 的,这样做效率可能会很差,因为需要排序过多的记录。

#### 两种 filesort 算法

  • ( 1 ) two-pass

这是旧的算法。列长度之和超过 max_length_for_sort_data 字节时就使用这个算法,其原理是:先按照 WHERE 筛选条件读取数据行,并存储每行的排序字段和行指针到排序缓冲( sort buffer )。如果排序缓冲大小不够,就在内存中运行一个快速排序( quick sort )操作,把排序结果存储到一个临时文件里,用一个指针指向这个已经排序好了的块。然后继续读取数据,直到所有行都读取完毕为止。这是第一次读取记录。

然后合并如上的临时文件,进行排序。

然后依据排序结果再去读取所需要的数据,读入行缓冲( row buffer ,由 read_rnd_buffer_size 参数设定其大小)。这是第二次读取记录。

以上第一次读取记录时,可以按照索引排序或表扫描,可以做到顺序读取。但第二次读取记录时,虽然排序字段是有序的,行缓冲里存储的行指针是有序的,但所指向的物理记录需要随机读,所以这个算法可能会带来很多随机读,从而导致效率不佳。

优点: 排序的数据量较小,一般在内存中即可完成。 缺点: 需要读取记录两次,第二次读取时,可能会产生许多随机 I/O ,成本可能会比较高。

  • ( 2 ) single-pass MySQL 一般使用这种算法。其原理是:按筛选条件,把 SQL 中涉及的字段全部读入排序缓冲中,然后依据排序字段进行排序,如果排序缓冲不够,则会将临时排序结果写入到一个临时文件中,最后合并临时排序文件,直接返回已经排序好的结果集。

优点: 不需要读取记录两次,相对于 two-pass ,可以减少 I/O 开销。 缺点: 由于要读入所有字段,排序缓冲可能不够,需要额外的临时文件协助进行排序,导致增加额外的 I/O成本。

相关参数的设置和优化

相关参数如下。 max_length_for_sort_data :如果各列长度之和(包括选择列、排序列)超过了 max_length_for_sort_data 字节,那么就使用 two-pass 算法。如果排序 BLOB 、 TEXT 字段,使用的也是 two-pass 算法,那么这个值设置得太高会导致系统 I/O 上升, CPU 下降,建议不要将 max_length_for_sort_data 设置得太高。

max_sort_length :如果排序 BLOB 、 TEXT 字段,则仅排序前 max_sort_length 个字节。

可以考虑的优化方向如下。

  • 加大 sort_buffer_size 。

一般情况下使用默认的 single-pass 算法即可。可以考虑加大 sort_buffer_size 以减少 I/O 。 需要留意的是字段长度之和不要超过 max_length_for_sort_data ,只查询所需要的列,注意列的类型、长度。 MySQL 目前读取和计算列的长度是按照定义的最大的度进行的,所以在设计表结构的时候,不要将VARCHAR 类型的字段设置得过大,虽然对于 VARCHAR 类型来说,在物理磁盘中的实际存储可以做到紧凑,但在排序的时候,是会分配最大定义的长度的,有时排序阶段所产生的临时文件甚至比原始表还要大

  • 对于 two-pass 算法,可以考虑增大 read_rnd_buffer_size ,但由于这个全局变量是对所有连接都生效的,因此建议只在会话级别进行设置,以加速一些特殊的大操作。
  • 在操作系统层面,优化临时文件的读写。

优化 SQL_CALC_FOUND_ROWS

建议不要使用 SQL_CALC_FOUND_ROWS 这个提示,虽然它可以让开发过程变得简单一些,但并没有减少数据库所做的事情。例如以下这个查询。

SELECT SQL_CALC_FOUND_ROWS col_name FROM table_name where ... LIMIT N

这样只要执行一次较耗时的复杂查询可以同时得到与不带limit同样的记录条数第二个 SELECT返回一个数字,指示了在没有LIMIT子句的情况下,第一个SELECT返回了多少行 (若上述的 SELECT语句不包括 SQL_CALC_FOUND_ROWS 选项,则使用LIMIT 和不使用时,FOUND_ROWS() 可能会返回不同的结果)。


优化临时表

如果不能利用索引排序,那么我们在MySQL中需要创建一个临时表用于排序。MySQL中的临时表分为“内存临时表”“磁盘临时表”用于排序。其中内存临时表使用Mysql MEMORY 存储引擎。磁盘临时表使用 MySQL 的 MyISAM 存储引擎;一般情况下, MySQL 会先创建内存临时表,但当内存临时表超过配置参数指定的值后, MySQL 会将内存临时表导出到磁盘临时表。

触发以下条件,会创建临时表。

  • ORDER BY 子句和 GROUP BY 子句引用的列不一样。
  • 在连接查询中, ORDER BY 或 GROUP BY 使用的列不是连接顺序中的第一个表。
  • ORDER BY 中使用了 DISTINCT 关键字。

通过EXPLAIN的Extra列可以查看是否使用到了临时表: “Using temporary” 表示使用了临时表。

如果查询创建了临时表(in-memory table)来排序或检索结果集,分配的内存大于 tmp_table_size 与 max_heap_table_size 参数之间的最小值,那么内存临时表就会转换为磁盘临时表( on-disk table ), MySQL 会在磁盘上创建磁盘临时表,这样会可能导致I/O 瓶颈,进而影响性能。

  • tmp_table_size :指定系统创建的内存临时表的最大大小。
  • max_heap_table_size :指定用户创建的内存表的最大大小。

SHOW FULL PROCESSLIST 命令输出的 state 列为 “Converting heap to MyISAM” 时表明临时表大于我们所设置的参数值,此时将会产生磁盘临时表,但是数据库执行查询往往很快, “Converting heap to MyISAM” 这个状态不一定能及时被看到,我们需要关注Created_tmp_tablesCreated_tmp_disk_tables 这两个变量的变化。由于 MySQL 慢查询日志里没有使用临时表的信息,这就给我们诊断性能问题带来了一些不便,第三方的版本如 Percona Server ,在慢查询里可以有更详细的信息,将会记录临时表使用的情况,从而有助于我们诊断和调优。

如下情况也可能会导致使用磁盘临时表:

  • 表中有BLOB 或 TEXT 字段。
  • 使用 UNION 或 UNION ALL 时, SELECT 子句中包含了大于 512 字节的列。

使用临时表一般意味着性能会比较底,特别是使用磁盘临时表的时,性能将会更慢,因此我们在实际应用当中应当尽可能的避免临时表的使用。

常见的避免临时表的方法有如下3点:

  • 创建索引:在ORDER BY或者GROUP BY的列上创建索引
  • 分拆长的列:一般情况下, TEXT 、 BLOB ,大于 512 字节的字符串,基本上都是为了显示信息,而不会用于查询条件,因此设计表的时候,可以考虑将这些列分离到另外一张表中。
  • 不需要用 DISTINCT 时就没必要用 DISTINCT ,能用 UNION ALL 就不要用 UNION

OLAP 业务优化

由于 MySQL 对于复杂 SQL 的优化不佳,所以对于一些 OLAP (Online Analytical Processing)的应用需要格外小心,在前期就做好一些针对性的设计,以尽量避免数据量剧增后碰到性能问题。

  • 使用冗余数据 冗余数据可以让查询的执行得更快。(大表之间的连接操作很耗时,增加冗余字段则可以有效地减少连接表的个数)
  • 计算复用,使用缓存表 可以使用存储一些结果,这里这里所说的 “ 缓存表 ” ,意思是这些值在逻辑上是冗余的,可以从原始表中获取到,但显然从原始表中获取数据更慢。
  • 预计算 预先对一些常用的大查询商城汇总表。当需要处理大量数据的时候,需要昂贵的计算成本。可以先把查询结果存储到单独的汇总表中,或者可以把相关联的表的一些字段放在一个独立的新表中,让这个新的汇总表做统计。

我们使用缓存表和汇总表时,我们要做出决定:是实时更新数据还是定期更新,这依赖于你的应用。

  • 统计框架的改善 报表研发人员可以和用户沟通,限制一些查询的使用,引导用户培养一些能够更快查询数据的习惯,让用户能够自己综合分析一些报表而不是完全借助计算机系统。报表的复杂度和生成报表所花的时间是成正比的。

小结

一般SQL的优化有两个方向,一个是让SQL执行得更快,一个是蓝SQL语句做更少的事。

MySQL 的查询优化器比较简单,没有商业数据库那么强大和智能,我们应该理解 MySQL 的优化器限制,按优化器能理解的方式编写 SQL 。对于大流量的业务,应该尽量保持 MySQL 查询的简单性,以保证尽可能地支持更高的并发。现实中,对于数据库流量很大的业务,数据库往往已经退化为一个存储数据的容器,只利用它最高效的核心的特性。

整理自书籍《MYSQLDBA修炼之道》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342