CALCULATE 庖丁解牛系列- CALCULATE专解(1)

CALCULATE之庖丁解牛系列 -- CALCULATE专解(1)

                                ---- 学习DAX的门票

公众号:零售数据BI-laoliu  QQ: 2889374742

    前言:

      特别申明:本系列文档为方便自己学习而整理(因为非官方,参考了一些网络上官方发布的文档知识内容及简体笔记。在此一一致谢)。
      Power非官方DAX虽然分为多个系列,但实际是即关联又独立。大部分内容并不是按顺序进行。这不是DAX的启蒙文,但尽量从基础部分开始。
        这个CALCULATE系列其实酝酿了好几次。因为是DAX最基础的、最全面的部分,总觉得整理起来很难。也由于自己在该领域的非专业性、非科班性的局限……。
        当然,既然已经开始,那便开始!CALCULATE系列可能会持续很久(初步设计为108式(对应CALCUALTE的知识点,可能会在不同的合适位置提出来,但是可能会在其他业务场景中详细说明)。取108式,可能是因为这容易让人关联性想起梁山108条好汉。其他的系列都是这个系列的辅助并与之相关)……。

        注:所有Power非官方系列内容,属于“Power BI非官方”独家发布,引用及转载请注明出处或联系本人。
     
这是一个浮躁的年代。我本来也想使用快餐式的一句话、一个图、一个GIF的启蒙类写法。然而,经过仔细的、漫长的、艰苦的思考斟酌,还是不要为难自己为好……。让我们开始: 

    第1式:CALCULATE的隐式与显式

        CALCULATE是DAX语言中最基本、最重要、最实用、最复杂、最难懂、最能代表DAX计算的函数。一句话,毫不夸张的说,CALCULATE代表了DAX的全部。     
        类似的一个函数是CALCULATETABLE(它与CALCULATE的特性、行为几乎相同,不同的是,CALCULATETABLE函数:计算并返回一个或多个列表而不是标量值),为了简单,仅以CALCULATE来分析。

      本系列需要具备一定的DAX知识。因为CALCULATE的重要性涵盖了DAX的所有概念,所以,知识点可能比较跳跃(比如直接引用某个DAX案例公式或者某个概念)。但我们尽量从最基础的部分开始。
      实际上,CALCULATE函数本身很简单,简单到可理解为一句话:定义一个DAX计算。

      然而,实际上我们需要更全面的理解它,理解CALCULATE可能涉及的多个重要概念、规则、行为等。例如,它回答:CALCULATE在DAX是什么以及如何运用的诸多问题,我们略举一些:

1、任何DAX都需要一个CALCULATE(隐式或显式);
2、CALCULATE的计算执行顺序;
3、CALCULATE的行(列值)、列表筛选;
4、CALCULATE的第一个参数特点;
5、CALCULATE的其他参数(第一个参数以外)特点;
6、CALCULATE或CALCULATETABLE调整计算筛选;
7、CALCULATE 中ALL系列函数的行为;
8、CALCULATE中VALUES的作用;
9、CALCULATE的筛选转换作用;
10、CALCULATE的列表关系的自动传递作用; 
11、CALCULATE的筛选与扩展表概念。
……
      等一等,再列举下去(我们说有差不多108式!),估计要晕。在继续之前,我们先要聊两个似乎与本系列主题无关,但又不得不阐明的观点。
        第一个观点,是学习DAX过程中需要记住的:很多时候,我们可以某种方式或某个自己能把握的概念来达到理解DAX的目的(官方也推出过很多通俗易懂的概念,本人也想搞个关于DAX的三部曲,哈哈)……。但DAX最终的内部核心却一直是一个专业的领域。
        也就是说,你可以用这样或那样的方式、方法(思维)去理解DAX,但它内部不一定是这样!很多人学DAX,悟到一点东西,就上钢上线,大喜过望,以为掌握了,可是、可是、可是……。真的是这样嘛?只能很遗憾的告知你:不一定!
        所以,列表+关系、扩展表、列值、列表以及值列表、列表筛选等等,这些概念都是出于正确理解和使用DAX的需要……。用一句有点拗口的话来说:学习了DAX的这些概念,你不一定就“很DAX”,但“很DAX”的一定是先要理解了这些概念(使用DAX时不必要再纠结这些概念,所谓的“置于死地而后生”)。

      第二个观点。基于第一个观点,让我想起《天龙八部》的一句话:每当研习一门绝世武功之前,必须有相应的一种心法来容纳它,否则容易走火入魔……。
      DAX学习,目前似乎都有些乱: 先是“上下文”搞晕一大堆人;再是用反反复复的DAX把式,忽悠一大堆人;然后把人带入 “学会几句DAX口语”就能行走天下的误区,耽误一大堆人;用超炫的几个DAX场景图表羡慕死一大批人……。一句话:易得DAX浮躁病。

      其实,所谓万变不离其中。首先,DAX是一门函数语言工具。必须先学习这门语言的基本字母、发音规律,之后才能掌握它。哪怕一开始慢一些,困难一些。你想想,你在那些”学几句口语就走天下”的影响下,效仿炫酷得有些妖艳的场景引诱下所花的时间还少吗?
      准官方SQLBI提到的2018版DAX学习之路,很多人拿来就用,发现水土不服……。陷入莫名的DAX知识旋涡里出来不了。学习完一个知识点、搞定一个公式、解决一个业务场景……。之后呢?总会遇到一个又一个问题以及又回到瓶颈之处……。

    有没有想过,所有这些,问题出在哪里?

      也有人总问起一些接近这个问题的问题,一直都没办法回复,怕自己水平有限,说不清楚,如是总是说:这需要一个系列来回答。其实,想想《天龙八部》里的一句话就明白了:无论使用何种方式学习某个绝世武功(DAX也应该算),必须先要修炼出一个强大的内功心法来容纳它,否则注定会走火入魔!前面,我们将理解CALCULATE需要的知识点罗列了11项(当然不止这些,我们计划着108式)。基于CALCUALTE的了解,就是DAX的心法。

    第2式:CALCULATE的元度量

当你开始学习DAX时,第一个真正的DAX公式不是:
[元度量_隐式]:= SUM(Sales[sale])
而应该是包含  CALCULATE的DAX公式:
[元度量_显式]:= CALCULATE(SUM(Sales[sale]))

        也就是说:只有使用了CALCULATE才算是:定义了一个DAX计算。因为,任何DAX都需要一个CALCULATE(隐式或显式)。因此,第二个公式似乎更像DAX一些,前者不过是省略(隐式)了CALCULATE()而已。
      我们把这种不带任何筛选条件、CALCULATE只有第一参数的度量称为元度量。
     
“任何DAX都需要一个CALCULATE(隐式或显式)”,这是前面罗列的理解CALCULATE的第一个问题,这也许是你一开始学习DAX,就需要面对的第一个DAX问题。这需要我们厘清两个公式不同的行为(有点绕口的部分): 
    (1)两个公式对应于CALCULATE的隐式与显式,即前者公式隐式了CALCUALTE(),后者公式显式使用CALCULATE()。
    (2)很显然,接下来的问题就是:为什么有隐式或显式的CALCULATE的区别?即关于CALCULATE的第一个概念:隐式与显式。
      所谓隐式,就是内部引擎能自动完成,不需要定义的行为;
      所谓显式,就是必须由计算式(比如引用列表或函数定义,包括CALCULATE本身)定义的行为。
      也就是说,两个元度量公式能使用的前提,应该是:

        (1)定义计算式;(2)加上引擎能自动完成。即在两个前提(显式或隐式条件)下使用。例如,我们先在数据模型计算区域里定义一个元度量:

      销售:=SUM('订单'[销售额])

      这时候,并没有使用CALCULATE显式定义它,但公式也能计算出正确的结果值(这里是针对度量所在表的计算),唯一的解释(或条件)就是DAX内部引擎能自动判断并执行了该计算。如图:

      第3式:CALCULATE需要行、列表筛选共存

    (3)如果你已经知道DAX的行、列筛选:任何一个DAX公式都需要同时具备行筛选和列表筛选(无论隐式还是显式)。
      注:应该说,是每个DAX计算单元都需要行、列筛选(无论隐式还是显式),一个较长的DAX公式里可能包含一个或多个计算单元。

      所以,这里对应的就是隐式行筛选与显式列筛选的定义及不同的行为区别。

        注意:官方称这两种筛选为行上下文和筛选上下文。未做特别说明,所有本人的DAX系列文章全部采用行筛选与列表筛选的称呼。

        接下来的问题是:什么情况下是隐式还是显式的筛选?

      上图中,销售:=SUM('订单'[销售额]),这个被放置在模型表的度量能计算出正确的结果。那么,它就必须同时存在行、列表筛选。该公式告诉DAX,它针对数据模型里订单表的[销售额]这个列计算(对该列执SUM()求和)。   
        很显然,DAX并不知道我们要计算的列表是哪一个、以及是针对该列的整列还是部分,这里的[销售额]列是需要我们定义的计算列表(方式为SUM函数求和功能),因为DAX可以被认为是一种函数语言,因此,通俗点说:

    你首先需要告诉DAX,计算的列表是什么?

      上图中,销售:=SUM('订单'[销售额]):通过我们的定义,使它具有了显式的列表筛选(先不讨论列表关系传递、以及扩展表概念等概念叠加,后面部分会讨论这些)。
      一旦定义了计算列表,而另一个看不见却又存在的行筛选,即由引擎内部自动完成而无需再定义的隐式行筛选。这就是为什么明明只定义了一个显式列表('订单'[销售额]列),并没有指示它应该按何种行方式计算,DAX却能计算出正确的值的原因(后面将进一步介绍这种“行的行为”)。

      既然是自行完成,也就是说,不需要管行筛选是如何进行的,你只要指定一个显式列表给DAX,它就能正确计算!
      例如,我们通常将该度量值放置在透视表里,因为这时它天然自带行筛选,你只需要定义它为显式列表即可。然后你改变透视表的行、列、切片器等,这其实是一组显式列表筛选集,它们共同定义出计算列表(比如计算列表的范围)。
      这也就是我们通常所说的:当所有这些筛选器放入一个逻辑条件里(透视表或DAX定义)共同作用于显式列表)时,则形成当前列表筛选—DAX计算所在计算列表环境(本概念后面章节中会更新它)。它改变:销售:=SUM('tb订单'[销售额]) 定义的显式列表筛选,随之,与其对应的隐式行筛选将不用定义、并由引擎自行完成行筛选。

        注意:透视表的这种查询筛选其实并不是真正的DAX查询,而是MDX。这里从略,后面将有介绍。

      第4式:CALCULATE定义显式列表

        而且,由于DAX内部引擎是一个列式数据库,引擎始终认为列表是可见的、存在的。换句话说,它始终执行显式列表。
        因此,很多时候为了让DAX能正确计算,需要将某些行行为作列表显式化处理,以便内部引擎能识别出来。而唯一有这种能力的只有CALCULATE与CALCULATETABLE函数。这一点是前面提到的第9点。所以,我们有了关于第一个元度量的注释:

        销售:=SUM('订单'[销售额]) –-包含显式列表筛选与隐式行筛选。

      推而广之,任何一个隐式(不带)CALCULATE的元度量(比如由SUM、COUNT、MIN等函数直接定义的某个列表的计算),都包含一个显式的列表筛选以及一个隐式的行筛选。也就是说,任何元度量(未加任何筛选条件的)都具有隐式的行筛选。

      还是看看这些行为的运用场景:

    (1)前面的公式:销售:=SUM('订单'[销售额]) 。它针对数据模型计算(即针对整个表:这里是对应的[销售额]整列),因此,结果为整列的聚合值:=1758423,如图:

    (2)因为引擎自动执行行筛选,所以,只要改变显式列表(比如不是整列—列表的部分或其他筛选定义)。刚才的公式计算针对的是整个数据模型表的计算,现在,我们在数据模型里改变它,比如针对时期列筛选出一段时期(这同时会筛选到[销售额]列),结果将随着改变:=62686(不是1758423)。

      同一个公式,计算结果发生改变的原因只能是:显式列表的改变(列表范围的改变),因为这时候的行的行为是相同的(隐式行筛选行为是相同的—逐行扫描,即遍历,这也是暂时的理解)。

    (3)再看看针对计算列的CALCULATE隐式与显式:

      我们新建一个[销售列]的列。定义相同的公式:= SUM('订单'[销售额]) ,即针对[销售额]计算。

      结果并不是我们期待的,而是一个每行结果相同的值。这其中一定是与前面的度量行为发生了不一样的变化。这里要分清:行、列筛选中,哪个是定义的,哪个是不需要定义的,或者再啰嗦点说,这有几种可能:
      1)不需要定义的、引擎能自动执行的隐式行筛选,被显式定义了;
      2)需要定义的显式列表筛选,被隐式了;
      3)或者隐式、显式的行为完全都被颠倒了(该隐式的显式了,该显式的隐式了);
      4)要不缺少列表筛选,要不缺少行筛选,或者两者都没有(很少见)。
    在今后的DAX公式运用中,经常会出现这几种情况,这里暂时略过。

      要说清这个问题有点超前(涉及行列筛选概念),先做个预热了解。下一个部分会详说。

      本例中,涉及到计算列。因为任何计算列都具有物理列表属性(其实际就是一个物理表,需要占有内存来存储),请记住,在数据模型里,任何一个列本身并不具有单个行值(数据列表不具有行的概念,所以,你可以理解为:DAX里所有关于行的行为都是隐式的)。
      当然,列表的每一行都可以有一个不同的值。因此,如果想要定义单个列值,则需要定义出要使用的行。而指定要使用的行的唯一方法就是行筛选(下一部分将介绍的值列表筛选)。
      我们其实已经知道结果,对于计算为同一个值的这种行为,有DAX基础的应该都知道,这大都是因为缺少行筛选的原因!本例中计算列的计算,并没有行筛选,因而公式是错误的,DAX拒绝计算。

      问题是,我们不是定义了:= SUM('订单'[销售额]),告诉DAX要聚合计算'[销售额]列的值吗?,而且,我们前面刚刚说过:计算中使用的SUM和MAX等聚合函数定义的公式:具有显式的列表筛选,以及隐式的行筛选(即忽略行筛选)。
      虽然我们用一句话:“计算列里缺少行筛选”就解释清楚了,但研究这种行为很有用。后面还会继续。

      也就是说,我们真的是使用了迭代器以编程方式创建了行筛选,与前面的度量行为相同,这里的计算列都使用了同一DAX表达式,那么,区别应该在于计算的内容!前面已提问:
      你首先需要告诉DAX计算的列表是什么?
       
问题是,仅仅告诉DAX计算的列表是什么还不够,还需要DAX引擎能够听得懂你的语言,并执行你的定义。所以我们还得加上一句提问:
        其次, DAX能识别和执行你定义的列表计算吗?

      我们已经明白,度量是在透视表或DAX查询的当前行、列(都是列表筛选)子集中计算的,而计算列是在它所属的表的行级别上计算的。
      所以,当你使用SUM('订单'[销售额]) 时,你定义的是所有这些条件下行的总和。本例中的DAX计算列,只有列表筛选,因此只能使用列表筛选来确定列值(整列的列值):

      一方面,虽然计算列不存在行筛选,但还是可以通过行的行为来了解该问题。   
      我们假设它存在行筛选,则它为第一行创建一个行筛选,然后调用公式计算,一直到遍历整个表的所有行。公式计算了当前筛选中的所有销售额的总和。所以,现在真正的问题是:当前的筛选是什么?        你当然可能会回答:计算所在的数据模型表呀。但是,因为这里并没有当前的活动筛选存在,DAX引擎会将该计算列作为定义的一部分(即针对定义的显式列表:[销售额]列)计算。

      另一方面,即使有行筛选,SUM也会忽略它。它能使用的只有列表筛选(同一个不变的显式列表筛选),而当前筛选现在是完整的数据模型表(计算列所在的表,这意味着每次行筛选对应的都将是同一个显式列表)。因此,你会得到一个相同的销售总额(所有行的总值)。结果如前面的图所示。

你可以理解为,这时的行筛选与列表筛选是同一个筛选(都对应于整刻)。

      现在,我们换一种方式理解。根据隐式、显式筛选的定义来理解:定义一个计算列,对于DAX引擎来说该计算列属于显式列表。而事实上,计算列是不需要定义的,DAX引擎自然知道你针对的总是该列(没有度量方式的显式列表变化)并能执行计算。
      也就是说,主观上,我们似乎想给DAX定义显式的列表筛选(事实也确实如此:这是你写SUM('订单'[销售额]) 这个计算列的初衷),但客观上,相对于DAX引擎来说,这时候其实是隐式的列表筛选(无须显式定义)。
      使用SUM(),则是为了添加计算需要的隐式行筛选,以便运用DAX引擎的隐式行筛选,计算出每一行的正确值。
      可是,这时候无论这个隐式的行筛选是否存在,实际上,它面对的都是一个隐式的列表筛选,似乎行、列筛选都是隐式的行为(这种引擎的行为,相当于完全需要引擎自动执行计算而变得毫无意义,这种行为与我们后面要说的ALL的绝对值结果行为还不一样),这就给我们一个提示或者说是结论:

        通常,DAX计算(含计算单元)需要的都是一个显式的列表筛选以及一个隐式的行筛选。

    第5式:CALCULATE显式列表总伴随隐式行筛选

    (4)通过前面的介绍,我们来看看正确的计算列公式与度量公式的几种行为方式以及区别(关于计算列与度量的详细区别容后再续):

      1)我们可以直接定义一个显式列表的计算列,这种行为相当于显式列表后,伴随而来的是该列表筛选同等效果的隐式行筛选。更好的、可能有些拗口的理解是:你可以认为这是:一个显式列表伴随对应的一个隐式行筛选。
      是的,你没有看错,无论列表还是行筛选都只有一个!虽然我们还没有具体了解DAX引擎内部是如何运行的,但肯定不是前面讨论的行行为:一行一行的遍历!如果这样,那DAX的效率实在是太低了!也没有任何优势与存在的必要了。
      这时候,你只要知道遍历只发生一次就行。或者记住:计算列里只要显式定义列表后,就不必考虑隐式行筛选(前面的研究只是为了更好的理解),这是DAX中难于理解的一个行为。如图,直接定义 =[销售额],结果相当于复制了某个列表。

        2)接着,只要是针对某个表格的计算列操作,该表格里所有的列表都可以被定义为显式列表参与计算。比如,我们可以直接定义两个或多个标量值列表之间的计算。如下图的两个时期列表的差异计算(相减)。

      3)如前所说,上图中的公式运用在度量中,则会得到一个“错误号”提示。原因当然是已经提示的那样:缺少隐式的行筛选。

      4)既然是缺少隐式的行筛选,我们使用聚合函数的行行为特性,分别对两个列加上聚合计算(所有迭代函数都具有隐式行筛选行为,这将针对两列都创建了各自的行筛选)。如图:结果正确。

      5)在公式 = SUM('订单'[销售额]), 前面加上CALCULATE(显式定义列表筛选):
= CALCULATE ( SUM('订单'[销售额])),这时候,只要显式定义了列表筛选,那么,引擎会自动创建该显式列表筛选对应的隐式行筛选(这是CALCULATE的行为之一,前面已罗列,后面会论述)。如图所示,计算列的公式能计算出正确的值。

因此,也就有了CALCUALTE的第6式:

    第6式:CALCULATE将隐式行筛选显式为列表

      CALCULATE第6式,就是前面提到的CALCULATE的一个很重要的行为:它将隐式行筛选转变为(定义为)与之等效的显式列表筛选。这一点与前面的第5式是相通的,将隐式行筛选转变为(定义为)与之等效的显式列表筛选后,并不是行筛选消失了或不需要了,而是可能同时具备了隐式的行筛选或与其他行筛选组合成新的当前计算筛选,这也是使用CALCUALTE这样做的目的。
    (5)今后在学习DAX的过程中,将不可避免的接触到显式与隐式以及对应的筛选问题。而且由于所在的计算列表集的不同、以及所定义的逻辑条件不同而更加复杂。

      我们提前举例一个显式列表筛选的情况:我们知道,LASTNONBLANK函数是一个迭代函数(遍历所有行值),所以,与所有迭代器一样, 它有一个行筛选, 但可能没有列表筛选。

因此,以下公式将计算错误:

最近余额: =
CALCULATE (SUM ( 数据表[销售额] ) ,
LASTNONBLANK ( 日历表[时期] ,
SUM ( 数据表[销售额] ) ) )  -- 隐式行筛选,没有显式列表筛选。

正确的公式,应该使用显式的CALCULATE():

最近余额: =
CALCULATE (SUM ( 数据表[余额] ),
LASTNONBLANK ( 日历表[时期],
CALCULATE ( SUM ( 数据表[余额] ) ) )) --隐式行筛选,被转换为显式列表

    用图标示出隐式与显式的关系:

或者标示为如下图:

      到现在为止,我们都在围绕着一个主题在讨论,那就是DAX两种形态的元度量,并由此展开的有关CALCULATE的行为方式。这些行为方式的理解,是今后熟悉更复杂、逻辑条件更多的DAX的基础。我们把这两个元度量再次罗列出来:

[元度量_隐式]:= SUM(Sales[sale])
[元度量_显式]:= CALCULATE(SUM(Sales[sale]))

      通过前面的啰嗦,第二个度量公式:CALCULATE(SUM(Sales[sale])你应该不会像一开始那样觉得它很奇怪,至少稍微理解了一点。当然,我们接下来要讲到CALCULATE的参数语法、规则、行为方式等。

未完待续

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

推荐阅读更多精彩内容