PostgreSQL 源碼解讀(18)- 查詢語句#3(SQL Parse)

本文簡單介紹了PG執(zhí)行SQL的流程,重點介紹了查詢語句的解析(Parse)過程臀突。

一肆饶、SQL執(zhí)行流程

PG執(zhí)行SQL的過程有以下幾個步驟:
第一步秃嗜,根據(jù)輸入的SQL語句執(zhí)行SQL Parse骆捧,進行詞法和語法分析等澎羞,最終生成解析樹;
第二步敛苇,根據(jù)解析樹妆绞,執(zhí)行查詢邏輯/物理優(yōu)化、查詢重寫枫攀,最終生成查詢樹括饶;
第三步,根據(jù)查詢樹来涨,生成執(zhí)行計劃图焰;
第四步,執(zhí)行器根據(jù)執(zhí)行計劃蹦掐,執(zhí)行SQL技羔。

二、SQL解析

如前所述笤闯,PG的SQL Parse(解析)過程由函數(shù)pg_parse_query實現(xiàn)堕阔,在exec_simple_query函數(shù)中調(diào)用。
代碼如下:

 /*
  * Do raw parsing (only).
  *
  * A list of parsetrees (RawStmt nodes) is returned, since there might be
  * multiple commands in the given string.
  *
  * NOTE: for interactive queries, it is important to keep this routine
  * separate from the analysis & rewrite stages.  Analysis and rewriting
  * cannot be done in an aborted transaction, since they require access to
  * database tables.  So, we rely on the raw parser to determine whether
  * we've seen a COMMIT or ABORT command; when we are in abort state, other
  * commands are not processed any further than the raw parse stage.
  */
 List *
 pg_parse_query(const char *query_string)
 {
     List       *raw_parsetree_list;
 
     TRACE_POSTGRESQL_QUERY_PARSE_START(query_string);
 
     if (log_parser_stats)
         ResetUsage();
 
     raw_parsetree_list = raw_parser(query_string);
 
     if (log_parser_stats)
         ShowUsage("PARSER STATISTICS");
 
 #ifdef COPY_PARSE_PLAN_TREES
     /* Optional debugging check: pass raw parsetrees through copyObject() */
     {
         List       *new_list = copyObject(raw_parsetree_list);
 
         /* This checks both copyObject() and the equal() routines... */
         if (!equal(new_list, raw_parsetree_list))
             elog(WARNING, "copyObject() failed to produce an equal raw parse tree");
         else
             raw_parsetree_list = new_list;
     }
 #endif
 
     TRACE_POSTGRESQL_QUERY_PARSE_DONE(query_string);
 
     return raw_parsetree_list;
 }
 
 /*
  * raw_parser
  *      Given a query in string form, do lexical and grammatical analysis.
  *
  * Returns a list of raw (un-analyzed) parse trees.  The immediate elements
  * of the list are always RawStmt nodes.
  */
 List *
 raw_parser(const char *str)
 {
     core_yyscan_t yyscanner;
     base_yy_extra_type yyextra;
     int         yyresult;
 
     /* initialize the flex scanner */
     yyscanner = scanner_init(str, &yyextra.core_yy_extra,
                              ScanKeywords, NumScanKeywords);
 
     /* base_yylex() only needs this much initialization */
     yyextra.have_lookahead = false;
 
     /* initialize the bison parser */
     parser_init(&yyextra);
 
     /* Parse! */
     yyresult = base_yyparse(yyscanner);
 
     /* Clean up (release memory) */
     scanner_finish(yyscanner);
 
     if (yyresult)               /* error */
         return NIL;
 
     return yyextra.parsetree;
 }
 

重要的數(shù)據(jù)結(jié)構(gòu):SelectStmt結(jié)構(gòu)體

/* ----------------------
  *      Select Statement
  *
  * A "simple" SELECT is represented in the output of gram.y by a single
  * SelectStmt node; so is a VALUES construct.  A query containing set
  * operators (UNION, INTERSECT, EXCEPT) is represented by a tree of SelectStmt
  * nodes, in which the leaf nodes are component SELECTs and the internal nodes
  * represent UNION, INTERSECT, or EXCEPT operators.  Using the same node
  * type for both leaf and internal nodes allows gram.y to stick ORDER BY,
  * LIMIT, etc, clause values into a SELECT statement without worrying
  * whether it is a simple or compound SELECT.
  * ----------------------
  */
 typedef enum SetOperation
 {
     SETOP_NONE = 0,
     SETOP_UNION,
     SETOP_INTERSECT,
     SETOP_EXCEPT
 } SetOperation;
 
 typedef struct SelectStmt
 {
     NodeTag     type;
 
     /*
      * These fields are used only in "leaf" SelectStmts.
      */
     List       *distinctClause; /* NULL, list of DISTINCT ON exprs, or
                                  * lcons(NIL,NIL) for all (SELECT DISTINCT) */
     IntoClause *intoClause;     /* target for SELECT INTO */
     List       *targetList;     /* the target list (of ResTarget) */
     List       *fromClause;     /* the FROM clause */
     Node       *whereClause;    /* WHERE qualification */
     List       *groupClause;    /* GROUP BY clauses */
     Node       *havingClause;   /* HAVING conditional-expression */
     List       *windowClause;   /* WINDOW window_name AS (...), ... */
 
     /*
      * In a "leaf" node representing a VALUES list, the above fields are all
      * null, and instead this field is set.  Note that the elements of the
      * sublists are just expressions, without ResTarget decoration. Also note
      * that a list element can be DEFAULT (represented as a SetToDefault
      * node), regardless of the context of the VALUES list. It's up to parse
      * analysis to reject that where not valid.
      */
     List       *valuesLists;    /* untransformed list of expression lists */
 
     /*
      * These fields are used in both "leaf" SelectStmts and upper-level
      * SelectStmts.
      */
     List       *sortClause;     /* sort clause (a list of SortBy's) */
     Node       *limitOffset;    /* # of result tuples to skip */
     Node       *limitCount;     /* # of result tuples to return */
     List       *lockingClause;  /* FOR UPDATE (list of LockingClause's) */
     WithClause *withClause;     /* WITH clause */
 
     /*
      * These fields are used only in upper-level SelectStmts.
      */
     SetOperation op;            /* type of set op */
     bool        all;            /* ALL specified? */
     struct SelectStmt *larg;    /* left child */
     struct SelectStmt *rarg;    /* right child */
     /* Eventually add fields for CORRESPONDING spec here */
 } SelectStmt;

重要的結(jié)構(gòu)體:Value

 /*----------------------
  *      Value node
  *
  * The same Value struct is used for five node types: T_Integer,
  * T_Float, T_String, T_BitString, T_Null.
  *
  * Integral values are actually represented by a machine integer,
  * but both floats and strings are represented as strings.
  * Using T_Float as the node type simply indicates that
  * the contents of the string look like a valid numeric literal.
  *
  * (Before Postgres 7.0, we used a double to represent T_Float,
  * but that creates loss-of-precision problems when the value is
  * ultimately destined to be converted to NUMERIC.  Since Value nodes
  * are only used in the parsing process, not for runtime data, it's
  * better to use the more general representation.)
  *
  * Note that an integer-looking string will get lexed as T_Float if
  * the value is too large to fit in an 'int'.
  *
  * Nulls, of course, don't need the value part at all.
  *----------------------
  */
 typedef struct Value
 {
     NodeTag     type;           /* tag appropriately (eg. T_String) */
     union ValUnion
     {
         int         ival;       /* machine integer */
         char       *str;        /* string */
     }           val;
 } Value;
 
 #define intVal(v)       (((Value *)(v))->val.ival)
 #define floatVal(v)     atof(((Value *)(v))->val.str)
 #define strVal(v)       (((Value *)(v))->val.str)
 

實現(xiàn)過程本節(jié)暫時擱置颗味,先看過程執(zhí)行的結(jié)果超陆,函數(shù)pg_parse_query返回的結(jié)果是鏈表List,其中的元素是RawStmt浦马,具體的結(jié)構(gòu)需根據(jù)NodeTag確定(這樣的做法類似于Java/C++的多態(tài))时呀。
測試數(shù)據(jù)

testdb=# -- 單位信息
testdb=# drop table if exists t_dwxx;
ues('Y有限公司','1002','北京市海淀區(qū)');
insert into t_dwxx(dwmc,dwbh,dwdz) values('Z有限公司','1003','廣西南寧市五象區(qū)');
NOTICE:  table "t_dwxx" does not exist, skipping
DROP TABLE
testdb=# create table t_dwxx(dwmc varchar(100),dwbh varchar(10),dwdz varchar(100));
CREATE TABLE
testdb=# 
testdb=# insert into t_dwxx(dwmc,dwbh,dwdz) values('X有限公司','1001','廣東省廣州市荔灣區(qū)');
INSERT 0 1
testdb=# insert into t_dwxx(dwmc,dwbh,dwdz) values('Y有限公司','1002','北京市海淀區(qū)');
INSERT 0 1
testdb=# insert into t_dwxx(dwmc,dwbh,dwdz) values('Z有限公司','1003','廣西南寧市五象區(qū)');
INSERT 0 1
testdb=# -- 個人信息
testdb=# drop table if exists t_grxx;
NOTICE:  table "t_grxx" does not exist, skipping
DROP TABLE
testdb=# create table t_grxx(dwbh varchar(10),grbh varchar(10),xm varchar(20),nl int);
CREATE TABLE
insert into t_grxx(dwbh,grbh,xm,nl) values('1002','903','王五',43);
testdb=# 
testdb=# insert into t_grxx(dwbh,grbh,xm,nl) values('1001','901','張三',23);
INSERT 0 1
testdb=# insert into t_grxx(dwbh,grbh,xm,nl) values('1002','902','李四',33);
INSERT 0 1
testdb=# insert into t_grxx(dwbh,grbh,xm,nl) values('1002','903','王五',43);
INSERT 0 1
testdb=# -- 個人繳費信息
testdb=# drop table if exists t_jfxx;
NOTICE:  table "t_jfxx" does not exist, skipping
DROP TABLE
testdb=# create table t_jfxx(grbh varchar(10),ny varchar(10),je float);
CREATE TABLE
testdb=# 
testdb=# insert into t_jfxx(grbh,ny,je) values('901','201801',401.30);
insert into t_jfxx(grbh,ny,je) values('901','201802',401.30);
insert into t_jfxx(grbh,ny,je) values('901','201803',401.30);
insert into t_jfxx(grbh,ny,je) values('902','201801',513.30);
insert into t_jfxx(grbh,ny,je) values('902','201802',513.30);
insert into t_jfxx(grbh,ny,je) values('902','201804',513.30);
insert into t_jfxx(grbh,ny,je) values('903','201801',372.22);
insert into t_jfxx(grbh,ny,je) values('903','201804',372.22);
testdb=# insert into t_jfxx(grbh,ny,je) values('901','201801',401.30);
INSERT 0 1
testdb=# insert into t_jfxx(grbh,ny,je) values('901','201802',401.30);
INSERT 0 1
testdb=# insert into t_jfxx(grbh,ny,je) values('901','201803',401.30);
INSERT 0 1
testdb=# insert into t_jfxx(grbh,ny,je) values('902','201801',513.10);
INSERT 0 1
testdb=# insert into t_jfxx(grbh,ny,je) values('902','201802',513.30);
INSERT 0 1
testdb=# insert into t_jfxx(grbh,ny,je) values('902','201804',513.30);
INSERT 0 1
testdb=# insert into t_jfxx(grbh,ny,je) values('903','201801',372.22);
INSERT 0 1
testdb=# insert into t_jfxx(grbh,ny,je) values('903','201804',372.22);
INSERT 0 1
testdb=# -- 獲取pid
testdb=# select pg_backend_pid();
 pg_backend_pid 
----------------
           1560
(1 row)
-- 用于測試的查詢語句
testdb=# select t_dwxx.dwmc,t_grxx.grbh,t_grxx.xm,t_jfxx.ny,t_jfxx.je
testdb-# from t_dwxx,t_grxx,t_jfxx
testdb-# where t_dwxx.dwbh = t_grxx.dwbh 
testdb-# and t_grxx.grbh = t_jfxx.grbh
testdb-# and t_dwxx.dwbh IN ('1001','1002')
testdb-# order by t_grxx.grbh
testdb-# limit 8;
   dwmc    | grbh |  xm  |   ny   |   je   
-----------+------+------+--------+--------
 X有限公司 | 901  | 張三 | 201801 |  401.3
 X有限公司 | 901  | 張三 | 201802 |  401.3
 X有限公司 | 901  | 張三 | 201803 |  401.3
 Y有限公司 | 902  | 李四 | 201801 |  513.1
 Y有限公司 | 902  | 李四 | 201802 |  513.3
 Y有限公司 | 902  | 李四 | 201804 |  513.3
 Y有限公司 | 903  | 王五 | 201801 | 372.22
 Y有限公司 | 903  | 王五 | 201804 | 372.22
(8 rows)

結(jié)果分析

[xdb@localhost ~]$ gdb -p 1560
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-100.el7
Copyright (C) 2013 Free Software Foundation, Inc.
...
(gdb) b pg_parse_query
Breakpoint 1 at 0x84c6c9: file postgres.c, line 615.
(gdb) c
Continuing.

Breakpoint 1, pg_parse_query (
    query_string=0x1a46ef0 "select t_dwxx.dwmc,t_grxx.grbh,t_grxx.xm,t_jfxx.ny,t_jfxx.je\nfrom t_dwxx inner join t_grxx on t_dwxx.dwbh = t_grxx.dwbh\ninner join t_jfxx on t_grxx.grbh = t_jfxx.grbh\nwhere t_dwxx.dwbh IN ('1001','100"...) at postgres.c:615
615     if (log_parser_stats)
(gdb) n
618     raw_parsetree_list = raw_parser(query_string);
(gdb) 
620     if (log_parser_stats)
(gdb) 
638     return raw_parsetree_list;
(gdb) p *(RawStmt *)(raw_parsetree_list->head.data->ptr_value)
$7 = {type = T_RawStmt, stmt = 0x1a48c00, stmt_location = 0, stmt_len = 232}
(gdb) p *((RawStmt *)(raw_parsetree_list->head.data->ptr_value))->stmt
$8 = {type = T_SelectStmt}
#轉(zhuǎn)換為實際類型SelectStmt 
(gdb)  p *(SelectStmt *)((RawStmt *)(raw_parsetree_list->head.data->ptr_value))->stmt
$16 = {type = T_SelectStmt, distinctClause = 0x0, intoClause = 0x0, targetList = 0x1a47b18, 
  fromClause = 0x1a48900, whereClause = 0x1a48b40, groupClause = 0x0, havingClause = 0x0, windowClause = 0x0, 
  valuesLists = 0x0, sortClause = 0x1afd858, limitOffset = 0x0, limitCount = 0x1afd888, lockingClause = 0x0, 
  withClause = 0x0, op = SETOP_NONE, all = false, larg = 0x0, rarg = 0x0}
#設(shè)置臨時變量
(gdb) set $stmt=(SelectStmt *)((RawStmt *)(raw_parsetree_list->head.data->ptr_value))->stmt
#查看結(jié)構(gòu)體中的各個變量
#------------------->targetList 
(gdb) p *($stmt->targetList)
$28 = {type = T_List, length = 5, head = 0x1a47af8, tail = 0x1a48128}
#targetList有5個元素,分別對應(yīng)t_dwxx.dwmc,t_grxx.grbh,t_grxx.xm,t_jfxx.ny,t_jfxx.je
#先看第1個元素
(gdb) set $restarget=(ResTarget *)($stmt->targetList->head.data->ptr_value)
(gdb) p *$restarget->val
$25 = {type = T_ColumnRef}
(gdb) p *(ColumnRef *)$restarget->val
$26 = {type = T_ColumnRef, fields = 0x1a47a08, location = 7}
(gdb) p *((ColumnRef *)$restarget->val)->fields
$27 = {type = T_List, length = 2, head = 0x1a47a88, tail = 0x1a479e8}
(gdb) p *(Node *)(((ColumnRef *)$restarget->val)->fields)->head.data->ptr_value
$32 = {type = T_String}
#fields鏈表的第1個元素是數(shù)據(jù)表,第2個元素是數(shù)據(jù)列
(gdb) p *(Value *)(((ColumnRef *)$restarget->val)->fields)->head.data->ptr_value
$37 = {type = T_String, val = {ival = 27556248, str = 0x1a47998 "t_dwxx"}}
(gdb) p *(Value *)(((ColumnRef *)$restarget->val)->fields)->tail.data->ptr_value
$38 = {type = T_String, val = {ival = 27556272, str = 0x1a479b0 "dwmc"}}
#其他類似

#------------------->fromClause 
(gdb) p *(Node *)($stmt->fromClause->head.data->ptr_value)
$41 = {type = T_JoinExpr}
(gdb) set $fromclause=(JoinExpr *)($stmt->fromClause->head.data->ptr_value)
(gdb) p *$fromclause
$42 = {type = T_JoinExpr, jointype = JOIN_INNER, isNatural = false, larg = 0x1a484f8, rarg = 0x1a48560, 
  usingClause = 0x0, quals = 0x1a487d0, alias = 0x0, rtindex = 0}

#------------------->whereClause 
(gdb)  p *(Node *)($stmt->whereClause)
$44 = {type = T_A_Expr}
(gdb)  p *(FromExpr *)($stmt->whereClause)
$46 = {type = T_A_Expr, fromlist = 0x1a48bd0, quals = 0x1a489d0}

#------------------->sortClause 
(gdb)  p *(Node *)($stmt->sortClause->head.data->ptr_value)
$48 = {type = T_SortBy}
(gdb)  p *(SortBy *)($stmt->sortClause->head.data->ptr_value)
$49 = {type = T_SortBy, node = 0x1a48db0, sortby_dir = SORTBY_DEFAULT, sortby_nulls = SORTBY_NULLS_DEFAULT, 
  useOp = 0x0, location = -1}

#------------------->limitCount 
(gdb)  p *(Node *)($stmt->limitCount)
$50 = {type = T_A_Const}
(gdb)  p *(Const *)($stmt->limitCount)
$51 = {xpr = {type = T_A_Const}, consttype = 0, consttypmod = 216, constcollid = 0, constlen = 8, 
  constvalue = 231, constisnull = 16, constbyval = false, location = 0}

以上為簡單的數(shù)據(jù)結(jié)構(gòu)介紹,下一節(jié)將詳細(xì)解析parseTree的Tree結(jié)構(gòu)晶默。

三谨娜、小結(jié)

1、SQL執(zhí)行流程:簡單介紹了SQL的執(zhí)行流程磺陡,簡單分為解析趴梢、查詢優(yōu)化、執(zhí)行計劃生成和執(zhí)行這四步币他;
2坞靶、SQL解析:解析執(zhí)行后,結(jié)果存儲在解析樹中蝴悉,分為distinctClause彰阴、intoClause、targetList等多個部分拍冠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末尿这,一起剝皮案震驚了整個濱河市簇抵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌射众,老刑警劉巖碟摆,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異责球,居然都是意外死亡焦履,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門雏逾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人郑临,你說我怎么就攤上這事栖博。” “怎么了厢洞?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵仇让,是天一觀的道長。 經(jīng)常有香客問我躺翻,道長丧叽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任公你,我火速辦了婚禮踊淳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘陕靠。我一直安慰自己迂尝,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布剪芥。 她就那樣靜靜地躺著垄开,像睡著了一般。 火紅的嫁衣襯著肌膚如雪税肪。 梳的紋絲不亂的頭發(fā)上溉躲,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音益兄,去河邊找鬼锻梳。 笑死,一個胖子當(dāng)著我的面吹牛偏塞,可吹牛的內(nèi)容都是我干的唱蒸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼灸叼,長吁一口氣:“原來是場噩夢啊……” “哼神汹!你這毒婦竟也來了庆捺?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤屁魏,失蹤者是張志新(化名)和其女友劉穎滔以,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氓拼,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡你画,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了桃漾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坏匪。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖撬统,靈堂內(nèi)的尸體忽然破棺而出适滓,到底是詐尸還是另有隱情,我是刑警寧澤恋追,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布凭迹,位于F島的核電站,受9級特大地震影響苦囱,放射性物質(zhì)發(fā)生泄漏嗅绸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一撕彤、第九天 我趴在偏房一處隱蔽的房頂上張望鱼鸠。 院中可真熱鬧,春花似錦喉刘、人聲如沸瞧柔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽造锅。三九已至,卻和暖如春廉邑,著一層夾襖步出監(jiān)牢的瞬間哥蔚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工蛛蒙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留糙箍,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓牵祟,卻偏偏與公主長得像深夯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 31,930評論 2 89
  • 一咕晋、背景 為了分析postgresql代碼雹拄,了解其執(zhí)行查詢語句的過程,我采用eclipse + gdb集成調(diào)試環(huán)境...
    hemny閱讀 12,196評論 2 6
  • 一掌呜、正則表達(dá)式的作用: 給定的字符串是否符合正則表達(dá)式的過濾邏輯(稱作“匹配”) 可以通過正則表達(dá)式滓玖,從字符串中獲...
    伯恩的遺產(chǎn)閱讀 4,641評論 18 81
  • 早上起床,我們就開始了抱怨的一天质蕉。我們抱怨衛(wèi)生間總是被隔壁住戶占用著势篡,我們抱怨沒有漂亮衣服可以好好裝扮自己, 我們...
    北斗夜行閱讀 193評論 0 0
  • 一周過去了 可是什么都沒干 感覺自己像一個保鏢一樣 每天跟著就好 唉 不知道做什么 什么都不會 要學(xué)的還很多 時間...
    洛維閱讀 240評論 0 0