作者:沃趣科技高級數(shù)據(jù)庫技術(shù)專家 魏興華
創(chuàng)新互聯(lián)專注于梁子湖企業(yè)網(wǎng)站建設(shè),成都響應(yīng)式網(wǎng)站建設(shè)公司,電子商務(wù)商城網(wǎng)站建設(shè)。梁子湖網(wǎng)站建設(shè)公司,為梁子湖等地區(qū)提供建站服務(wù)。全流程定制網(wǎng)站制作,專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,創(chuàng)新互聯(lián)專業(yè)和態(tài)度為您提供的服務(wù)
Oracle企業(yè)版有一項(xiàng)非常厲害的技術(shù):并行查詢,也就是說一個語句可以雇傭多個服務(wù)器進(jìn)程(parallel slaves也叫PX slaves)來完成這一個查詢所需要的結(jié)果。并行操作不僅僅能夠充分利用主機(jī)的CPU資源,也能夠充分利用系統(tǒng)的IO資源、內(nèi)存資源,這看起來是一個優(yōu)點(diǎn),但是也需要看情況,如果數(shù)據(jù)庫系統(tǒng)沒有太多的空閑CPU、空閑IO或空閑內(nèi)存資源,那么并行技術(shù)是否要使用非常值得考慮,甚至即使系統(tǒng)有著很多的CPU空閑資源,但是IO資源已經(jīng)遠(yuǎn)遠(yuǎn)不夠,那么同樣需要考慮是否要使用并行(并行往往產(chǎn)生大量的IO)。鑒于并行操作的工作方式,不能讓它在系統(tǒng)中被濫用,否則可能導(dǎo)致系統(tǒng)的資源很快的被耗盡。并行操作本身也是復(fù)雜的,它有著很多串行執(zhí)行所不具備的概念,例如table queue,數(shù)據(jù)分發(fā)方式等等,并且閱讀并行語句執(zhí)行計(jì)劃的方式也與串行可能會有所不同。
并行操作的目的是為了提升語句執(zhí)行的線性度,如果一個語句串行執(zhí)行的時間為4分鐘,那么通過指定4個并行來操作,可以加快查詢執(zhí)行時間為1分鐘,當(dāng)然這只是一種預(yù)期,現(xiàn)實(shí)的情況往往不能達(dá)到這種線性度。有一些消耗和事實(shí)需要了解:
雇傭并行進(jìn)程本身需要一些時間,這些時間往往比較短,如果進(jìn)程池中沒有可用的并行進(jìn)程,那么還需要操作系統(tǒng)去spawn出需要的并行進(jìn)程,這時數(shù)據(jù)庫可能會遭遇os thread startup等待。如果語句執(zhí)行時間只有數(shù)秒,你要考慮它是否適合使用并行。
QC進(jìn)程給PX slaves分配工作,這會消耗一些時間,這個時間一般也非常短。例如QC進(jìn)程需要給每個PX slave進(jìn)程分配掃描ROWID的范圍。
如果并行查詢要返回大量的數(shù)據(jù)給客戶端,那么僅有的一個QC進(jìn)程本身可能會成為瓶頸。
由于Oracle的并行執(zhí)行采用的是生產(chǎn)者消費(fèi)者模型,因此一般DOP為4的查詢,最終雇傭的PX slaves為8,再加上QC進(jìn)程本身,一共會占用9個系統(tǒng)進(jìn)程,你要認(rèn)識到付出的這些是否值得。
在Exadata下即使使用串行查詢,由于在IO層面默認(rèn)就是并行,因此Exadata下的語句并行效果沒有非Exadata下好。
為了讓并行能夠非常好的發(fā)揮作用,有一些要求需要被滿足:
非常有效率的執(zhí)行計(jì)劃,如果執(zhí)行計(jì)劃本身非常糟糕,使用并行可能并不能多大程度上改善語句的執(zhí)行效率。
數(shù)據(jù)庫系統(tǒng)有著充足的資源可用。這點(diǎn)已經(jīng)在文章的開頭提到過。
工作量的分配沒有明顯的傾斜,大家都熟悉短板理論,如果某一個PX slave干了絕大部分的活,那么最終的響應(yīng)時間最大的瓶頸就是它。
也許上面的很多概念和術(shù)語你還不清楚,沒關(guān)系,我們下面的內(nèi)容都會介紹到。使用并行首先應(yīng)該考慮的問題是如何分配工作量,在串行執(zhí)行的情況,這個問題不用考慮,只有一個進(jìn)程干活,所有的工作都是由它來完成,但是如果使用了并行操作,那就意味著有多個進(jìn)程在干同樣一件事,工作的分配就顯得非常的重要。
對于單表的并行操作,工作量的切分是比較簡單的,Oracle也沒有設(shè)計(jì)任何復(fù)雜的算法,它一般是按照
ROWID或者分區(qū)(假如它是分區(qū)表的話)來分配工作。例如下面的并行查詢:
上面的SQL及其執(zhí)行計(jì)劃顯示,對表test以并行度2進(jìn)行了記錄數(shù)的統(tǒng)計(jì),Id為5的行源Operation部分為:PX BLOCK ITERATOR,這是一個在并行操作中經(jīng)常能看到的一個操作,代表了QC進(jìn)程按照ROWID把表做了切分,每個PX slave掃描表的不同范圍,然后每個PX slave聚合出自己所掃描部分的記錄數(shù)(Id=4,SORT AGGREGATE ),最后把結(jié)果發(fā)送給QC,QC進(jìn)一步聚合這些PX slaves的結(jié)果形成一個記錄返回給客戶端。
通過SQL MONITORING可以看到的更為直觀(下圖),絕大部分的工作都是通過藍(lán)色的PX slaves來完成的,然后這些PX slaves把各自做過預(yù)聚集的結(jié)果發(fā)送給(行源ID為3)QC做最終的聚合。
不過我們隨著后續(xù)的學(xué)習(xí)會發(fā)現(xiàn),這里的這個例子只雇傭了一組PX slaves進(jìn)程,這在Oracle并行的世界中是一個特殊案例。按照Oracle的生產(chǎn)者、消費(fèi)者模型,一般會雇傭兩組PX slaves,一組作為生產(chǎn)者掃描數(shù)據(jù),另一組作為消費(fèi)者把從生產(chǎn)者接收過來的數(shù)據(jù)做各種加工。(不過這個例子可以把QC作為消費(fèi)者看待)。
本文大量使用了SQL MOMITORING工具,如果你對這個工具還不熟悉,請參閱我的另一篇文章:
http://www.jianshu.com/p/ce85dd0c05ab
我們對SQL進(jìn)行簡單的改造,增加ORDER BY部分,看看結(jié)果會怎么樣。
SQL>select /*+ parallel(a 2) */ * from hash_t1 a order by object_name;
同樣我們通過SQL MONITORING來進(jìn)行可視化解析,【操作】列出現(xiàn)了兩種不同顏色的PX slaves,紅色的PX slaves作為生產(chǎn)者正在掃描表HASH_T1,然后把掃描到的數(shù)據(jù)分發(fā)給藍(lán)色的PX slaves消費(fèi)者,PX slaves消費(fèi)者接收到這些數(shù)據(jù)后并做排序然后把結(jié)果集發(fā)送給QC。
這個例子雖小,但是五臟俱全,在Oracle并行執(zhí)行中,一個可以并行的操作單元(樹)稱為Data Flow Operator,一個QC代表了一個DFO單元,一個查詢可以有多個DFO單元(DFO tree),例如典型的像union all語句,就可以有多個DFO單元,不同的DFO單元之間也可以并行。
具備了Oracle并行執(zhí)行生產(chǎn)者和消費(fèi)者的概念,繼續(xù)看上圖中的【名稱】列,會發(fā)現(xiàn)有TQ10001,TQ10000的東西,這個是啥?
上面已經(jīng)提到Oracle并行操作有生產(chǎn)者和消費(fèi)者的概念,生產(chǎn)者和消費(fèi)者分別代表著一組進(jìn)程,他們之間需要傳遞消息和數(shù)據(jù),那么他們是靠什么來進(jìn)行傳遞消息和數(shù)據(jù)的呢?這就是table queue的作用。
繼續(xù)以上圖為例:
這里一共包含了兩組PX slaves,一組為紅色的生產(chǎn)者,一組為藍(lán)色的消費(fèi)者,生產(chǎn)者通過ID為6,7的行源掃描表HASH_T1,同時通過ID為5的行源把掃描結(jié)果寫入table queue TQ10000(PX SEND RANGE),消費(fèi)者從table queue TQ10000讀取數(shù)據(jù)然后做排序(PX RECEIVE),消費(fèi)者對于已經(jīng)完成排序的結(jié)果通過table queue TQ10001發(fā)送給QC進(jìn)程,QC進(jìn)程把接收到的結(jié)果聚合后發(fā)送給客戶端。
對于單表(無JOIN)的數(shù)據(jù)切分是非常簡單的,只需要按照ROWID做切分就可以保證結(jié)果的正確,因?yàn)槎鄠€并行slaves之間沒有數(shù)據(jù)的交叉,也就不會有數(shù)據(jù)的丟失,而且按照ROWID切分也非常容易保證每個PX slave的工作量均勻。但是如果是兩表的JOIN呢?你如何保證1/N的X表的記錄和相對應(yīng)的1/N的Y表的記錄在一個并行操作內(nèi)(也就是由一個并行進(jìn)程處理)?兩個表都按照ROWID來切分是不能保證的。
為了讓例子足夠的簡單,可以通過如下例子來進(jìn)行描述:
集合一:
【1,3,5,7,9,11】
集合二:
【1,9,3,6,7,8,5】
假如要求使用并行度2來判斷,【集合二】和【集合一】有多少數(shù)據(jù)有交集,該如何實(shí)現(xiàn)?
我們模擬通過ROWID來切分,把【集合一】按照順序切分為2部分:
set 1:1,3,5 =>進(jìn)程1
set 2:7,9,11 =>進(jìn)程2
我們再使用同樣的辦法,把【集合二】切分為2部分:
set 3:1,9,3 =>進(jìn)程1
set 4:6,7,8,5 =>進(jìn)程2
通過上面一系列的操作我們把2個集合都切分為了2份,然后我們通過進(jìn)程1對set 1與set 3做join,進(jìn)程2對set 2與set 4做join,OK?
顯然是不行的,因?yàn)樽罱K的結(jié)果集是不對的。
兩個集合做JOIN正確的結(jié)果是:3,5,7,9
但是按照上面的算法,set 1和set 3的結(jié)果集為3,set 2和set 4的結(jié)果集為7,最終的結(jié)果集為3,7,丟失了5,9兩個結(jié)果。
因此不能為了加快查詢的速度而不保證結(jié)果正確性對對數(shù)據(jù)進(jìn)行隨意切割。那么Oracle是如何做的?如何保證進(jìn)程讀取了X表的1/N的數(shù)據(jù)與Y表相對應(yīng)的1/N數(shù)據(jù)?
從這里看出了引入了數(shù)據(jù)分布算法的重要性,也解釋了為什么運(yùn)行并行度N需要2N個并行slave來完成工作,一組進(jìn)程用來掃描表X,然后把數(shù)據(jù)按照分布算法把數(shù)據(jù)分發(fā)給另一組進(jìn)程Y,這樣表X的數(shù)據(jù)分布完成后,Y的表記錄要根據(jù)X表的分布算法來決定自己的分布方式。你看到這里可能有些地方可能還看不明白,沒關(guān)系,后續(xù)有足夠的內(nèi)容讓你明白這些操作。
繼續(xù)前面的例子
【集合一】:
1,3,5,7,9,11
【集合二】:
1,9,3,6,7,8,5
broadcast的分發(fā)方式為(這里假設(shè)并行度為2):
Oracle首先需要產(chǎn)生2組PX slaves,一組為生產(chǎn)者包含2個PX slave進(jìn)程,一組為消費(fèi)者,同樣包含2個PX slave進(jìn)程,(注意生產(chǎn)者和消費(fèi)者角色是可能互換的)。
每個生產(chǎn)者PX slave按照ROWID切分,掃描1/2的【集合一】,然后廣播給每一個消費(fèi)者的PX slave,最終每一個消費(fèi)者的PX slave都有一份全量的【集合一】。
然后每個消費(fèi)者的PX slave進(jìn)程按照ROWID切分,掃描【集合二】,然后與【集合一】做關(guān)聯(lián)判斷,最終得出結(jié)果集。
這里的關(guān)鍵是,每個消費(fèi)者的PX slave都持有了全量的【集合一】,因此不需要再對集合二有任何的分發(fā)需要,只需要按照ROWID掃描然后再進(jìn)行JOIN操作就能夠保證結(jié)果的正確性。
集合一:
【1,3,5,7,9,11】
分發(fā)后為:
set 1: 1,3,5,7,9,11 =>進(jìn)程1
set 2:1,3,5,7,9,11 =>進(jìn)程2
集合二:
【1,9,3,6,7,8,5】
分發(fā)后為:
set 3: 1,9,3,6 =>進(jìn)程1
set 4: 7,8,5 =>進(jìn)程2
set 1,set 3的結(jié)果集為1,3,9,set 2和set 4的結(jié)果集為5,最終的結(jié)果集為1,3,5,9,這樣不但把工作量做了比較均勻的切分,而且保證了結(jié)果的正確性。
這里我們通過一個具體的查詢例子再來看一下整個過程:
表T1的數(shù)據(jù)量為70,表T4的數(shù)據(jù)量為343000。
SQL>select /*+parallel(2) pq_distribute(t1 none broadcast) full(t1) full(t4) monitor*/ count(*) from t1,t4 where t1.id = t4.id1;
我們通過添加hint pq_distribute(t1 none broadcast)強(qiáng)制讓hash join左邊的表進(jìn)行了廣播分發(fā),根據(jù)SQL MONITORING的輸出,我們做如下分析:
(行ID 9,8,7),生產(chǎn)者紅色進(jìn)程(【操作】列)按照ROWID做切分掃描表T1,然后把掃描的結(jié)果寫入table queue,以廣播方式做分發(fā),ID為7的行源PX SEND BROADCAST操作代表了廣播的分發(fā)方式。
行ID6,5,藍(lán)色的消費(fèi)進(jìn)程(【操作】列)接收到紅色PX slave廣播的數(shù)據(jù),然后構(gòu)建HASH TABLE。每一個藍(lán)色的消費(fèi)PX slave都接收到了全量的T1表 的數(shù)據(jù),根據(jù)【實(shí)際行數(shù)】列可以顯示這一點(diǎn),表T1總共70行的數(shù)據(jù)經(jīng)過廣播分發(fā)后,實(shí)際產(chǎn)生了70*2(并行度)=140行的記錄。
行ID 11,10,藍(lán)色的消費(fèi)進(jìn)程按照ROWID切割掃描T4表并與前面構(gòu)建的HASH TABLE做JOIN。
這里并沒有對T4進(jìn)行任何的分發(fā),認(rèn)識到這一點(diǎn)很重要,藍(lán)色的消費(fèi)進(jìn)程只需要按照ROWID范圍掃描即可,因?yàn)門1表的數(shù)據(jù)在每個消費(fèi)者的PX slave都保持著全量。
這里我們做一個階段性的總結(jié):
對于broadcast分發(fā)方式來說:
HASH JOIN右邊的表不用分發(fā)。
BROADCAST方式, 沒有結(jié)果不對的風(fēng)險,因?yàn)橄M(fèi)者的每個PX slave持有了全部的HASH JOIN左邊表的數(shù)據(jù),每個消費(fèi)者進(jìn)程都持有一個完整的HASH TABLE。
HASH JOIN左邊表 如果小的話,分發(fā)代價不大。但是隨著并行度DOP的提高或者左邊表數(shù)據(jù)量的增大,分發(fā)的代價會越來越大。
如果左邊表小的話,BROADCAST的執(zhí)行計(jì)劃具有非常好的擴(kuò)展性。
第一組PX進(jìn)程掃描HASH JOIN左邊表廣播給第二組PX slave,CPU,內(nèi)存,競爭都會有消耗,競爭的消耗來自于第一個組的每一個進(jìn)程掃描的數(shù)據(jù)都要廣播給第二組的每一個進(jìn)程,如下圖:
replicate代表每個并行進(jìn)程都全量掃描hash join左邊的表,不按照rowid做卻分,由于數(shù)據(jù)是被每一個進(jìn)程全量掃描的,因此不需要再對數(shù)據(jù)做分發(fā),也就只需要一組PX slaves。
select /*+ parallel(2) */ count(*) from hash_t1 a,reptest b where a.id=b.id;
觀察操作列只有一組藍(lán)色的PX slaves進(jìn)程,這里沒有涉及到數(shù)據(jù)的分發(fā):
2個進(jìn)程全量掃描reptest表,然后構(gòu)建hash table(全量的hash table)
掃描完成后,2個進(jìn)程按照ROWID范圍掃描hash_t1表,由于2個進(jìn)程持有了全量的reptest表的hash table,因此對于hash_t1表不需要分發(fā)。掃描hash_t1表過程中探測hash table。
就像上面提到的,broadcast/replicate分發(fā)方式有一個問題是,因?yàn)橄M(fèi)者的每一個PX slaves要持有完整左邊表的記錄, 因此適合左邊表比較小的情況。如果對于兩個大表的HASH 連接,Oracle一般使用HASH的分發(fā)方式。例如還是上面的例子:
【集合一】:
1,3,5,7,9,11
【集合二】:
1,9,3,6,7,8,5
【集合一】和【集合2】按照同樣的HASH 函數(shù)分發(fā)后,總能保證有關(guān)聯(lián)的數(shù)據(jù)對在一起,這樣就能保證結(jié)果集的正確性。但是這樣的方式,多出了一個代價,那就是對于【集合二】也需要做HASH分發(fā),會多出一些CPU資源的消耗,相對于廣播的分發(fā)方式,只有【集合一】需要做分發(fā)。
我們來看一個具體的例子:
select /*+ parallel(3) pq_distribute(b hash hash)*/ count(*) from hash_t1 a ,hash_t2 b where a.id=b.id;
首先紅色的生產(chǎn)者PX slaves按照ROWID切分并行掃描表HASH_T1,然后依據(jù)HASH算法把記錄通過table queue TQ10000分發(fā)給特定的藍(lán)色的消費(fèi)者PX slave。
藍(lán)色的消費(fèi)者從table queue TQ10000接收到數(shù)據(jù)后構(gòu)建HASH TABLE。
上面2步操作完成后,紅色的生產(chǎn)者PX slaves繼續(xù)按照ROWID切分并行掃描表HASH_T2,然后按照HASH算法把記錄通過table queue TQ10001分發(fā)給特定的藍(lán)色消費(fèi)者PX slave,藍(lán)色消費(fèi)者PX slave從table queue TQ10001接收數(shù)據(jù)并與前面構(gòu)建的HASH TABLE做JOIN。最后每個藍(lán)色的消費(fèi)者PX slave把自己聚合的結(jié)果通過table queue TQ10002發(fā)送給QC。
注意
【實(shí)際行數(shù)】列,記錄按照HASH分發(fā)后并沒有增加。
對hash_t2掃描過程,由于數(shù)據(jù)需要分發(fā),因此會有同時2組PX slaves同時活躍。
HASH分發(fā)有著很好的擴(kuò)展性,每個進(jìn)程有部分的HASH 表,而不是完整的HASH表,每一行只會分發(fā)給一個特定的PX SLAVE。而不是像broadcast分發(fā)把每一行廣播給每一個SLAVE。
但是就像上面已經(jīng)指出過,待對HASH JOIN左邊表分發(fā)完畢后,同樣對于HASH JOIN右邊的表也需要進(jìn)行分發(fā),多了一次分發(fā)的代價,增加了一些CPU和內(nèi)存的成本。
錯誤的分發(fā)方式可能會對并行執(zhí)行帶來非常大的性能問題,Oracle 12C介紹自適應(yīng)的并行分發(fā)方法,hybrid hash,在真正執(zhí)行過程中,再決定該使用何種分發(fā)方式,Oracle 優(yōu)化器要做到這一點(diǎn),使用了statistics collector,它在語句運(yùn)行過中統(tǒng)計(jì)語句的一些運(yùn)行時信息,例如返回記錄的數(shù)量等等。需要注意使用了HYBRID-HASH后,每次語句執(zhí)行,都要通過statistics collector來動態(tài)決定使用的并行分發(fā)方式。
例如上面的執(zhí)行計(jì)劃,觀察行源ID 7,并行執(zhí)行過程中會統(tǒng)計(jì)結(jié)果集的返回值數(shù)量,如果返回的結(jié)果集數(shù)量小于并行度*2,那么會使用廣播方式來進(jìn)行數(shù)據(jù)分發(fā),反之則使用HASH的數(shù)據(jù)分發(fā)方式,作為回應(yīng),在行源ID 為6的分發(fā)方式確定后,行源ID 11再決定使用round-robin還是hash分發(fā)。
我剛做DBA那會,一些老DBA告訴我如何看并行執(zhí)行計(jì)劃,那就是把PX相關(guān)的操作都統(tǒng)統(tǒng)抹去,然后再看,例如:
真的等價嗎?我們把相關(guān)的PX等操作都全部去掉,最終和串行執(zhí)行的如下文本是"等價"的:
在串行執(zhí)行過程中,對于上面執(zhí)行計(jì)劃的執(zhí)行順序是這樣的:
掃描T3表,構(gòu)建hash table,掃描T2表,構(gòu)建hash table,掃描T1表構(gòu)建hash table,最后掃描T4表,掃描到的每一個記錄都要探測前面所產(chǎn)生的3個hash table。
但是并行執(zhí)行的執(zhí)行順序并不一定是按照上面描述的順序,對于并行執(zhí)行計(jì)劃的閱讀要跟隨table queue的創(chuàng)建順序,它代表著并行執(zhí)行中數(shù)據(jù)分發(fā)的順序。因此就上面的并行執(zhí)行,執(zhí)行順序?yàn)椋?/p>
掃描table T1,構(gòu)建hash table,之所以首先掃描T1,是因?yàn)閠able queue的編號TQ10000 是最小的。
根據(jù)table queue TQ10001的位置知道,然后掃描table T4并與上面的hash table做hash join。
根據(jù)table queue TQ10002的位置知道,接著掃描table T2,構(gòu)建hash table,然后,上面兩步產(chǎn)生的結(jié)果集與這個hash table做hash join。
根據(jù)table queue TQ10002的位置知道,最后掃描table T3,構(gòu)建hash table,然后上面三步產(chǎn)生的結(jié)果集與這個hash table做hash join。
v$pq_tqstat視圖是非常特別的,它的內(nèi)容只記錄在QC進(jìn)程的私有PGA中,而且只在 并行查詢結(jié)束后內(nèi)容才會被填充,因此如果并行執(zhí)行過程中,你取消了查詢,那么查詢這個視圖依然不會有任何結(jié)果,因?yàn)樗淮嬖谶M(jìn)程的PGA中,因此你不能通過另一個會話去查詢它。
可以通過它了解并行執(zhí)行過程中數(shù)據(jù)是如何通過table queue分發(fā)的。舉兩個例子:
例如我們對hash_t1 以并行度4進(jìn)行記錄統(tǒng)計(jì),執(zhí)行完成后,查看v$pq_tqstat視圖:
4個生產(chǎn)者把各自掃描到的記錄做了匯聚各自產(chǎn)生一個記錄并把它寫入table queue,QC通過table queue接收了這四個記錄。注意NUM_ROWS代表的是PX slaves通過table queue寫入、讀取的數(shù)據(jù)量,可以通過NUM_ROWS的值非常容易看出并行進(jìn)程的工作量是否均勻,是否有并行傾斜存在。
再看一個復(fù)雜點(diǎn)的例子:
觀察SQL的hint部分,強(qiáng)制讓hash join的左邊表使用了broadcast的分發(fā)方式,結(jié)合上面的輸出和下面的圖一起看,每個紅色的生產(chǎn)者先按照ROWID范圍掃描HASH_T2,然后把數(shù)據(jù)通過table queue TQ10000廣播給藍(lán)色的消費(fèi)者,由于采用了并行度4,因此其實(shí)每個生產(chǎn)者真正寫入table queue中的數(shù)據(jù)量是掃描數(shù)據(jù)量的4倍(9999999 是真實(shí)的記錄數(shù),經(jīng)過廣播分發(fā)產(chǎn)生了9999999 *4 的記錄數(shù)),消費(fèi)者從table queueTQ10000中接收數(shù)據(jù)后構(gòu)建hash table,每個消費(fèi)者PX slave都構(gòu)建了表hash_t2完整的的hash table,然后藍(lán)色消費(fèi)者開始掃描hash_t1,并與之前構(gòu)建的hash table做join,最后每個藍(lán)色消費(fèi)者把各自最終形成的預(yù)聚合結(jié)果發(fā)送給QC(這里其實(shí)已經(jīng)轉(zhuǎn)化了角色變?yōu)榱松a(chǎn)者),QC接收到4條記錄。