MySQL必知必会系列二

读完MySQL必知必会系列一,我们可以进一步了解MySQL中的数据处理函数,分组查询,联结表,以及组合查询的相关内容,方便我们进一步查询数据,处理成我们想要的结果

一 数据处理函数

函数

SQL 支持利用函数来处理数据。函数一般是在数据上执行的,它给数据的转换和处理提供了方便

如果你决定使用函数,应该保证做好代码注释,以便以后你(或其他人)能确切地知道所编写 SQL 代码的含义。

  1. 用于处理文本串(如删除或填充值,转换值为大写或小写)的文本函数
  2. 用于在数值数据上进行算术操作(如返回绝对值,进行代数运算)的数值函数
  3. 用于处理日期和时间值并从这些值中提取特定成分(例如,返回两个日期之差,检查日期有效性等)的日期和时间函数
  4. 返回 DBMS 正使用的特殊信息(如返回用户登录信息,检查版本细节)的系统函数

文本处理函数

image.png

image.png
//Upper() 将文本转换为大写
select vend_name,UPPER(vend_name) AS vend_name_upcase from vendors ORDER BY vend_name
+--------------+------------------+
| vend_name    | vend_name_upcase |
+--------------+------------------+
| ACME         | ACME             |
| Anvils R Us  | ANVILS R US      |
| Furball Inc. | FURBALL INC.     |
| LT Supplies  | LT SUPPLIES      |
+--------------+------------------+
4 rows in set (0.00 sec)

日期和时间处理函数

日期和时间采用相应的数据类型和特殊的格式存储,以便能快速和有效地排序或过滤,并且节省物理存储空间

image.png

MySQL 使用的日期格式。无论你什么时候指定一个日期,不管是插入或更新表值还是用 WHERE 子句进行过滤,日期必须为格式 yyyy-mm-dd


select * from orders WHERE order_date='2005-09-01'

order_date 的数据类型为 datetime 。这种类型存储日期及时间值, 比如,存储的 order_date 值为2005-09-01 11:30:05 ,则WHERE order_date = '2005-09-01' 匹配失败

解决办法是指示 MySQL 仅将给出的日期与列中的日期部分进行比较,而不是将给出的日期与整个列值进行比较如果要的是日期,请使用 Date().使用 Date() 是一个良好的习惯

select * from orders WHERE DATE(order_date)='2005-09-01'

+-----------+---------------------+---------+
| order_num | order_date          | cust_id |
+-----------+---------------------+---------+
|     20005 | 2005-09-01 00:00:00 |   10001 |
+-----------+---------------------+---------+
1 row in set (0.00 sec)

日期比较

如果你想检索出 2005 年 9 月下的所有订单

  • 方案一:
    BETWEEN 操作符用来把 2005-09-01 和 2005-09-30 定义为一个要匹配的日期范围
select * from orders WHERE DATE(order_date) BETWEEN '2005-09-01' AND '2005-09-30';
  • 方案二:
    Year() 是一个从日期(或日期时间)中返回年份的函数。类似,Month() 从日期中返回月份
select * from orders WHERE YEAR(order_date) = 2005 and MONTH(order_date)=9;

数值处理函数

数值处理函数仅处理数值数据。这些函数一般主要用于代数、三角或几何运算

image.png

二 汇总数据

我们经常需要汇总数据而不用把它们实际检索出来,MySQL 提供了专门的函数。使用这些函数,MySQL 查询可用于检索数据,以便分析和报表生成

聚集函数

image.png

AVG()

通过对表中行数计数并计算特定列值之和,求得该列的平均值

SELECT AVG(prod_price) as avg_price from products

SELECT AVG(prod_price) as avg_price from products WHERE vend_id=1003

COUNT()

利用 COUNT() 确定表中行的数目或符合特定条件的行的数目

  1. 使用 COUNT(*) 对表中行的数目进行计数,不管表列中包含的是空值( NULL )还是非空值
select count(*) from products
  1. 使用 COUNT(column) 对特定列中具有值的行进行计数,忽略 NULL
select count(cust_email) from customers

MAX()

返回指定列中的最大值。 MAX() 要求指定列名

MIN()

返回指定列的最小值。与 MAX() 一样, MIN() 要求指定列名

SUM()

用来返回指定列值的和(总计)。
举一个例子, orderitems 表 包含订单中实际的物品,每个物品有相应的数量( quantity )。可如下检索所订购物品的总数(所有 quantity 值之和)

select SUM(quantity) as items_ordered from orderitems where order_num=20005

//总销售量
select SUM(quantity*item_price) as total_price from orderitems where order_num=20005

聚集不同值

以上 5 个聚集函数都可以如下使用:

  1. ALL 为默认值, 对所有的行执行计算,指定 ALL 参数或不给参数;
  2. 只包含不同的值,指定 DISTINCT 参数
// 指定 DISTINCT 参 因此平均值只考虑各个不同的价格

 SELECT avg(DISTINCT prod_price) as avg_price from products

组合聚集函数

select count(*) as num_items,MIN(prod_price),MAX(prod_price),AVG(prod_price) from products

三 分组数据

创建分组 GROUP BY

分组是在 SELECT 语句的 GROUP BY 子句中建立的

select vend_id,count(*) as num_prods from products GROUP BY vend_id

使用 WITH ROLLUP 关键字,可以得到每个分组以及每个分组汇总级别(针对每个分组)的值

select vend_id,count(*) as num_prods from products GROUP BY vend_id  WITH ROLLUP

过滤分组 HAVING

HAVING 子句。 HAVING 非常类似于 WHERE,唯一的差别是 WHERE 过滤行,而 HAVING 过滤分组

//过滤 COUNT(*) >=2 (两个以上的订单)的那些分组
select cust_id,count(*) as ordersTotal from orders GROUP BY cust_id HAVING COUNT(*) >=2

HAVING 和 WHERE 的差别 这里有另一种理解方法, WHERE 在数据分组前进行过滤, HAVING 在数据分组后进行过滤。这是一个重要的区别,WHERE 排除的行不包括在分组中。这可能会改变计算值,从而影响 HAVING 子句中基于这些值过滤掉的分组。

分组和排序

一般在使用 GROUP BY 子句时,应该也给出 ORDER BY 子句。这是保证数据正确排序的唯一方法。千万不要仅依赖 GROUP BY 排序数据。

SELECT 子句顺序

子句 说明 是否必须使用
select 要返回的列或表达式
FROM 从中检索数据的表 仅在从表选择数据时使用
WHERE 行级过滤
GROUP BY 分组说明 仅在按组计算聚集时使用
HAVING 组级过滤
ORDER BY 输出排序顺序
LIMIT 要检索的行数

四 使用子查询

查询(query) 任何 SQL 语句都是查询。但此术语一般指 SELECT 语句,子查询(subquery),即嵌套在其他查询中的查询

products 表包含产品目录,每行一个产品
customers 表存储所有顾客的信息
orders 表存储顾客订单 ,orders 表不存储客户信息。它只存储客户的 ID
orderitems 表存储每个订单中的实际物品

//prod_id 为 TNT2 的所有订单物品
select order_num from orderitems WHERE prod_id="TNT2";

//下一步,查询具有订单 20005 和 20007 的客户ID
select cust_id from orders where order_num in(20005,20007)

//得到了订购物品 TNT2 的所有客户的ID。下一步是检索这些客户ID的客户信息
select cust_name,cust_contact from customers where cust_id in (10001,10004)

现在组合成一条语句,MySQL 实际上必须执行 3 条 SELECT 语句,可见,在 WHERE 子句中使用子查询能够编写出功能很强并且很灵活的 SQL 语句,不过在实际使用时由于性能的限制,不能嵌套太多的子查询

SELECT
    cust_name,
    cust_contact
FROM
    customers
WHERE
    cust_id IN (
        SELECT
            cust_id
        FROM
            orders
        WHERE
            order_num IN (
                SELECT
                    order_num
                FROM
                    orderitems
                WHERE
                    prod_id = "TNT2"
            )
    )

作为计算字段使用子查询

假如需要显示 customers 表中每个客户的订单总数


SELECT
    cust_id,
    cust_name,
    (
        SELECT
            COUNT(*)
        FROM
            orders
        WHERE
            orders.cust_id = customers.cust_id
    ) AS total
FROM
    customers
ORDER BY
    total

相关子查询语法:任何时候只要列名可能有多义性,就必须使用这种语法(表名和列名由一个句点分隔)

五 联结表(重点)

SQL 最强大的功能之一就是能在数据检索查询的执行中联结,表联结是利用 SQL 的 SELECT 能执行的最重要的操作,在能够有效地使用联结前,必须了解关系表以及关系数据库设计的一些基础知识面的介绍,并不是这个内容的全部知识,但作为入门已经足够了

关系表

假如有一个包含产品目录的数据库表,其中每种类别的物品占一行。对于每种物品要存储的信息包括产品描述和价格,以及生产该产品的供应商信息

现在,假如有由同一供应商生产的多种物品,那么在何处存储供应商信息(如,供应商名、地址、联系方法等)呢? 将这些数据与产品信息分开存储的理由如下:

1. 因为同一供应商生产的每个产品的供应商信息都是相同的,
   对每个产品重复此信息既浪费时间又浪费存储空间

2. 如果供应商信息改变(例如,供应商搬家或电话号码变动),只需改动一次即可

3. 如果有重复数据(即每种产品都存储供应商信息),
   很难保证每次输入该数据的方式都相同。不一致的数据在报表中很难利用

关系表的设计就是要保证把信息分解成多个表,一类数据一个表。各表通过某些常用的值(即关系设计中的关系(relational))互相关联,在这个例子中,可建立两个表,一个存储供应商信息,另一个存储产品信息。 vendors 表包含所有供应商信息,每个供应商占一行,每个供应商具有唯一的标识。此标识称为主键(primary key),products 表只存储产品信息,它除了存储供应商 ID( vendors 表的主键)外不存储其他供应商信息。 vendors 表的主键又叫作 products 的外键,它将 vendors 表与 products 表关联,利用供应商 ID 能从 vendors 表中找出相应供应商的详细信息。

外键

外键(foreign key) 外键为某个表中的一列,它包含另一个表的主键值,定义了两个表之间的关系,总之,关系数据可以有效地存储和方便地处理。因此,关系数据库的可伸缩性远比非关系数据库要好

可伸缩性(scale)

能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序称之为可伸缩性好(scale well)

为什么使用联结?

分解数据为多个表能更有效地存储,更方便地处理,并且具有更大的可伸缩性。但这些好处是有代价的,如果数据存储在多个表中,怎样用单条 SELECT 语句检索出数据?

答案是使用联结。简单地说,联结是一种机制,用来在一条 SELECT
语句中关联表,因此称之为联结。使用特殊的语法,可以联结多个表返
回一组输出,联结在运行时关联表中正确的行。

笛卡儿积

笛卡儿积(cartesian product):由没有联结条件的表关系返回的结果,就称为为笛卡儿积。检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。

select * from vendors,products
where vendors.vend_id=products.vend_id
order by vend_name,prod_name
笛卡尔乘积是指在数学中,两个集合X和Y的笛卡尔积(Cartesian product),又称直积,
表示为X×Y,第一个对象是X的成员而第二个对象是Y的所有可能有序对的其中一个成员 [3]  。

假设集合A={a, b},集合B={0, 1, 2},
则两个集合的笛卡尔积为{(a, 0), (a, 1), (a, 2), (b, 0), (b, 1), (b, 2)}。

类似的例子有,如果A表示某学校学生的集合,B表示该学校所有课程的集合,
则A与B的笛卡尔积表示所有可能的选课情况。
A表示所有声母的集合,B表示所有韵母的集合,
那么A和B的笛卡尔积就为所有可能的汉字全拼。

创建联结

SELECT
    vend_name,
    prod_name,
    prod_price
FROM
    vendors,
    products
WHERE
    vendors.vend_id = products.vend_id
ORDER BY
    vend_name,
    prod_name

内部联结

目前为止所用的联结称为等值联结(equijoin),它基于两个表之间的相等测试。这种联结也称为内部联结

对于这种联结可以使用稍微不同的语法来明确指定联结的类型以 INNER JOIN 指定。在使用这种语法时,联结条件用特定的 ON 子句而不是 WHERE 子句给出。传递给 ON 的实际条件与上面案例传递给 WHERE 的相同

SELECT
    vend_name,
    prod_name,
    prod_price
FROM
    vendors
INNER JOIN products ON vendors.vend_id = products.vend_id
ORDER BY
    vend_name,
    prod_name

联结多个表

SQL 对一条 SELECT 语句中可以联结的表的数目没有限制。创建联结的基本规则也相同。首先列出所有表,然后定义表之间的关系,然后用 where 定义联结关系

select order_name,vend_name,prod_name,prod_price
from table1,table2,table3
where table2.a=table3.a
and table1.b=table2.b
and order_num=2000

总结(重点)

  1. 性能考虑:MySQL 在运行时关联指定的每个表以处理联结。这种处理可能是非常耗费资源的,因此应该仔细,不要联结不必要的表。联结的表越多,性能下降越厉害
  1. 为执行任一给定的 SQL 操作,一般存在不止一种方法。很少有绝对正确或绝对错误的方法。性能可能会受操作类型、表中数据量、是否存在索引或键以及其他一些条件的影响。因此,有必要对不同的选择机制进行实验,以找出最适合具体情况的方法。

六 创建高级联结

使用表别名

表别名只在查询执行中使用。与列别名不一样,表别名不返回到客户机,可以帮助我们缩短 SQL 语句,允许在单条 SELECT 语句中多次使用相同的表

SELECT
    cust_name,
    cust_contact
FROM
    customers AS c,
    orders AS o,
    orderitems AS oi
WHERE
    c.cust_id = o.cust_id
AND oi.order_num = o.order_num
AND prod_id = 'TNT2'

自联结

假如你发现某物品(其 ID 为 DTNTR)存在问题,因此想知道生产该物品的供应商生产的其他物品是否也存在这些问题。此查询要求首先找到生产 ID 为 DTNTR 的物品的供应商,然后找出这个供应商生产的其他物品。

SELECT
    prod_id,
    prod_name
FROM
    products
WHERE
    vend_id = (
        SELECT
            vend_id
        FROM
            products
        WHERE
            prod_id = "DTNTR"
    )

现在来看使用联结的相同查询

WHERE(通过匹配 p1 中 的 vend_id 和 p2 中的 vend_id)首先联结两个表,然后按第二个表中的 prod_id 过滤数据,返回所需的数据

SELECT
    p1.prod_id,
    p1.prod_name
FROM
    products AS p1,
    products AS p2
WHERE
    p1.vend_id = p2.vend_id
AND p2.prod_id = "DTNTR"

自然联结

无论何时对表进行联结,应该至少有一个列出现在不止一个表中(被联结的列)。标准的联结(前一章中介绍的内部联结)返回所有数据,甚至相同的列多次出现。自然联结排除多次出现,使每个列只返回一次。

怎样完成这项工作呢?答案是,系统不完成这项工作,由你自己完成它。自然联结是这样一种联结,其中你只能选择那些唯一的列。这一般是通过对表使用通配符(SELECT *),对所有其他表的列使用明确的子集来完成的。

事实上,迄今为止我们建立的每个内部联结都是自然联结,很可能我们永远都不会用到不是自然联结的内部联结。

外部联结

许多联结将一个表中的行与另一个表中的行相关联。但有时候会需要包含没有关联行的那些行。例如,可能需要使用联结来完成以下工作:如:

  1. 对每个客户下了多少订单进行计数,包括那些至今尚未下订单的客户;
  2. 列出所有产品以及订购数量,包括没有人订购的产品;
  3. 计算平均销售规模,包括那些至今尚未下订单的客户。

在上述例子中,联结包含了那些在相关表中没有关联行的行。这种类型的联结称为外部联结

外部联结语法类似。为了检索所有客户,包括那些没有订单的客户,可如下进行

SELECT
    c.cust_id,
    o.order_num
FROM
    customers AS c
LEFT OUTER JOIN orders AS o ON c.cust_id = o.cust_id;

类似于上一章中所看到的内部联结,这条 SELECT 语句使用了关键字 OUTER JOIN 来指定联结的类型(而不是在 WHERE 子句中指定)。但是,与内部联结关联两个表中的行不同的是,外部联结还包括没有关联行的行。在使用 OUTER JOIN 语法时,必须使用 RIGHT 或 LEFT 关键字指定包括其所有行的表(RIGHT 指出的是 OUTER JOIN 右边的表,而 LEFT 指出的是 OUTER JOIN 左边的表)。上面的例子使用 LEFT OUTER JOIN 从 FROM 子句的左边表(customers 表)中选择所有行

带有聚合函数的联结

此 SELECT 语句使用 INNER JOIN 将 customers 和 orders 表互相关联。GROUP BY 子句按客户分组数据,因此函数调用 COUNT(orders.order_num) 对每个客户的订单计数

SELECT
    c.cust_id,
    c.cust_name,
    COUNT(o.order_num) AS order_total
FROM
    customers c
INNER JOIN orders o ON c.cust_id = o.cust_id
GROUP BY
    c.cust_id

七 组合查询

多数SQL查询都只包含从一个或多个表中返回数据的单条 SELECT 语句,MySQL允许执行多个查询(多条 SELECT 语句),并将结果作为单个查询结果集返回。这些组合查询通常称为并(union)或复合查询(compound query)

基本场景:

  1. 在单个查询中从不同的表返回类似结构的数据
  2. 对单个表执行多个查询,按单个查询返回数据。

创建组合查询

假如需要价格小于等于 5 的所有物品的一个列表,而且还想包括供应商 1001 和 1002 生产的所有物品(不考虑价格)。当然,可以利用 WHERE 子句来完成此工作,不过这次我们将使用 UNION

-- select * from products WHERE prod_price <=5;

-- select * from products WHERE vend_id in(1001,1002)

-- UNION 指示MySQL执行两条 SELECT 语句,并把输出组合成单个查询结果集
 select * from products WHERE prod_price <=5 
 UNION select * from products WHERE vend_id in(1001,1002)

在这个简单的例子中,使用 UNION 可能比使用 WHERE 子句更为复杂。
但对于更复杂的过滤条件,或者从多个表(而不是单个表)中检索数据
的情形,使用 UNION 可能会使处理更简单。

UNION规则

  1. UNION 必须由两条或两条以上的 SELECT 语句组成,语句之间用关键字 UNION 分隔(因此,如果组合4条 SELECT 语句,将要使用3个UNION 关键字)

  2. UNION 中的每个查询必须包含相同的列、表达式或聚集函数(不过各个列不需要以相同的次序列出)

SELECT
    prod_id,
    prod_name
FROM
    products
WHERE
    prod_price <= 5
UNION
    SELECT
        prod_id,
        prod_name
    FROM
        products
    WHERE
        vend_id IN (1001, 1002);
  1. 列数据类型必须兼容:类型不必完全相同,但必须是DBMS可以
    隐含地转换的类型(例如,不同的数值类型或不同的日期类型)。

包含或取消重复的行

UNION 从查询结果集中自动去除了重复的行(换句话说,它的行为与单条 SELECT 语句中使用多个 WHERE 子句条件一样)。因为供应商 1002 生产的一种物品的价格也低于 5 ,所以两条 SELECT 语句都返回该行。在使用UNION 时,重复的行被自动取消

-- 第一条 SELECT 语句返回4行,第二条 SELECT 语句返回5行。
-- 但在用 UNION 组合两条 SELECT 语句后,只返回了8行而不是9行
`
-- selec`t * from products WHERE prod_price <=5;
-- select * from products WHERE vend_id in(1001,1002)

这是 UNION 的默认行为,但是如果需要,可以改变它。事实上,如果想返回所有匹配行,可使用 UNION ALL 而不是 UNION

UNION 几乎总是完成与多个WHERE 条件相同的工作。 UNION ALL 为 UNION 的一种形式,它完成WHERE 子句完成不了的工作。如果确实需要每个条件的匹配行全部出现(包括重复行),则必须使用 UNION ALL 而不是 WHERE

对组合查询结果排序

在用 UNION 组合查询时,只能使用一条 ORDER BY 子句,它必须出现在最后一条 SELECT 语句之后

对于结果集,不存在用一种方式排序一部分,而又用另一种方式排序另一部分的情况,因此不允许使用多条 ORDER BY 子句。

SELECT
    prod_id,
    prod_name,
    vend_id,
    prod_price
FROM
    products
WHERE
    prod_price <= 5
UNION
    SELECT
        prod_id,
        prod_name,
        vend_id,
        prod_price
    FROM
        products
    WHERE
        vend_id IN (1001, 1002)
    ORDER BY
        vend_id,
        prod_price

这条 UNION 在最后一条 SELECT 语句后使用了 ORDER BY 子句。虽然 ORDER BY 子句似乎只是最后一条 SELECT 语句的组成部分,但实际上MySQL将用它来排序所有 SELECT 语句返回的所有结果

总结

利用 UNION ,可把多条查询的结果作为一条组合查询返回,不管它们的结果中包含还是不包含重复。使用 UNION 可极大地简化复杂的 WHERE 子句,简化从多个表中检索数据的工作。

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

推荐阅读更多精彩内容