...

SQL 索引您了解多少

2022-02-09

)深入淺出理解索引結構

實際上,您可以把索引理解爲一種(zhǒng)特殊的目錄。微軟的SQL SERVER提供了兩(liǎng)種(zhǒng)索引:聚集索引(clustered index,也稱聚類索引、簇集索引)和非聚集索引(nonclustered index,也稱非聚類索引、非簇集索引)。下面(miàn),我們舉例來說(shuō)明一下聚集索引和非聚集索引的區别:

其實,我們的漢語字典的正文本身就(jiù)是一個聚集索引。比如,我們要查“安”字,就(jiù)會(huì)很自然地翻開(kāi)字典的前幾頁,因爲“安”的拼音是“an”,而按照拼音排序漢字的字典是以英文字母“a”開(kāi)頭并以“z”結尾的,那麼(me)“安”字就(jiù)自然地排在字典的前部。如果您翻完了所有以“a”開(kāi)頭的部分仍然找不到這(zhè)個字,那麼(me)就(jiù)說(shuō)明您的字典中沒(méi)有這(zhè)個字;同樣的,如果查“張”字,那您也會(huì)將(jiāng)您的字典翻到最後(hòu)部分,因爲“張”的拼音是“zhang”。也就(jiù)是說(shuō),字典的正文部分本身就(jiù)是一個目錄,您不需要再去查其他目錄來找到您需要找的内容。我們把這(zhè)種(zhǒng)正文内容本身就(jiù)是一種(zhǒng)按照一定規則排列的目錄稱爲“聚集索引”。

如果您認識某個字,您可以快速地從自動中查到這(zhè)個字。但您也可能(néng)會(huì)遇到您不認識的字,不知道(dào)它的發(fā)音,這(zhè)時(shí)候,您就(jiù)不能(néng)按照剛才的方法找到您要查的字,而需要去根據“偏旁部首”查到您要找的字,然後(hòu)根據這(zhè)個字後(hòu)的頁碼直接翻到某頁來找到您要找的字。但您結合“部首目錄”和“檢字表”而查到的字的排序并不是真正的正文的排序方法,比如您查“張”字,我們可以看到在查部首之後(hòu)的檢字表中“張”的頁碼是672頁,檢字表中“張”的上面(miàn)是“馳”字,但頁碼卻是63頁,“張”的下面(miàn)是“弩”字,頁面(miàn)是390頁。很顯然,這(zhè)些字并不是真正的分别位于“張”字的上下方,現在您看到的連續的“馳、張、弩”三字實際上就(jiù)是他們在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我們可以通過(guò)這(zhè)種(zhǒng)方式來找到您所需要的字,但它需要兩(liǎng)個過(guò)程,先找到目錄中的結果,然後(hòu)再翻到您所需要的頁碼。我們把這(zhè)種(zhǒng)目錄純粹是目錄,正文純粹是正文的排序方式稱爲“非聚集索引”。

通過(guò)以上例子,我們可以理解到什麼(me)是“聚集索引”和“非聚集索引”。進(jìn)一步引申一下,我們可以很容易的理解:每個表隻能(néng)有一個聚集索引,因爲目錄隻能(néng)按照一種(zhǒng)方法進(jìn)行排序。

二、何時(shí)使用聚集索引或非聚集索引

下面(miàn)的表總結了何時(shí)使用聚集索引或非聚集索引(很重要):

動作描述

使用聚集索引

使用非聚集索引

列經(jīng)常被分組排序

返回某範圍内的數據

不應

一個或極少不同值

不應

不應

小數目的不同值

不應

大數目的不同值

不應

頻繁更新的列

不應

外鍵列

主鍵列

頻繁修改索引列

不應

事(shì)實上,我們可以通過(guò)前面(miàn)聚集索引和非聚集索引的定義的例子來理解上表。如:返回某範圍内的數據一項。比如您的某個表有一個時(shí)間列,恰好(hǎo)您把聚合索引建立在了該列,這(zhè)時(shí)您查詢2004年1月1日至2004年10月1日之間的全部數據時(shí),這(zhè)個速度就(jiù)將(jiāng)是很快的,因爲您的這(zhè)本字典正文是按日期進(jìn)行排序的,聚類索引隻需要找到要檢索的所有數據中的開(kāi)頭和結尾數據即可;而不像非聚集索引,必須先查到目錄中查到每一項數據對(duì)應的頁碼,然後(hòu)再根據頁碼查到具體内容。

三、結合實際,談索引使用的誤區

理論的目的是應用。雖然我們剛才列出了何時(shí)應使用聚集索引或非聚集索引,但在實踐中以上規則卻很容易被忽視或不能(néng)根據實際情況進(jìn)行綜合分析。下面(miàn)我們將(jiāng)根據在實踐中遇到的實際問題來談一下索引使用的誤區,以便于大家掌握索引建立的方法。

1、主鍵就(jiù)是聚集索引

這(zhè)種(zhǒng)想法筆者認爲是極端錯誤的,是對(duì)聚集索引的一種(zhǒng)浪費。雖然SQL SERVER默認是在主鍵上建立聚集索引的。

通常,我們會(huì)在每個表中都(dōu)建立一個ID列,以區分每條數據,并且這(zhè)個ID列是自動增大的,步長(cháng)一般爲1。我們的這(zhè)個辦公自動化的實例中的列Gid就(jiù)是如此。此時(shí),如果我們將(jiāng)這(zhè)個列設爲主鍵,SQL SERVER會(huì)將(jiāng)此列默認爲聚集索引。這(zhè)樣做有好(hǎo)處,就(jiù)是可以讓您的數據在數據庫中按照ID進(jìn)行物理排序,但筆者認爲這(zhè)樣做意義不大。

顯而易見,聚集索引的優勢是很明顯的,而每個表中隻能(néng)有一個聚集索引的規則,這(zhè)使得聚集索引變得更加珍貴。

從我們前面(miàn)談到的聚集索引的定義我們可以看出,使用聚集索引的最大好(hǎo)處就(jiù)是能(néng)夠根據查詢要求,迅速縮小查詢範圍,避免全表掃描。在實際應用中,因爲ID号是自動生成(chéng)的,我們并不知道(dào)每條記錄的ID号,所以我們很難在實踐中用ID号來進(jìn)行查詢。這(zhè)就(jiù)使讓ID号這(zhè)個主鍵作爲聚集索引成(chéng)爲一種(zhǒng)資源浪費。其次,讓每個ID号都(dōu)不同的字段作爲聚集索引也不符合“大數目的不同值情況下不應建立聚合索引”規則;當然,這(zhè)種(zhǒng)情況隻是針對(duì)用戶經(jīng)常修改記錄内容,特别是索引項的時(shí)候會(huì)負作用,但對(duì)于查詢速度并沒(méi)有影響。

在辦公自動化系統中,無論是系統首頁顯示的需要用戶簽收的文件、會(huì)議還(hái)是用戶進(jìn)行文件查詢等任何情況下進(jìn)行數據查詢都(dōu)離不開(kāi)字段的是“日期”還(hái)有用戶本身的“用戶名”。

通常,辦公自動化的首頁會(huì)顯示每個用戶尚未簽收的文件或會(huì)議。雖然我們的where語句可以僅僅限制當前用戶尚未簽收的情況,但如果您的系統已建立了很長(cháng)時(shí)間,并且數據量很大,那麼(me),每次每個用戶打開(kāi)首頁的時(shí)候都(dōu)進(jìn)行一次全表掃描,這(zhè)樣做意義是不大的,絕大多數的用戶1個月前的文件都(dōu)已經(jīng)浏覽過(guò)了,這(zhè)樣做隻能(néng)徒增數據庫的開(kāi)銷而已。事(shì)實上,我們完全可以讓用戶打開(kāi)系統首頁時(shí),數據庫僅僅查詢這(zhè)個用戶近3個月來未閱覽的文件,通過(guò)“日期”這(zhè)個字段來限制表掃描,提高查詢速度。如果您的辦公自動化系統已經(jīng)建立的2年,那麼(me)您的首頁顯示速度理論上將(jiāng)是原來速度8倍,甚至更快。

在這(zhè)裡(lǐ)之所以提到“理論上”三字,是因爲如果您的聚集索引還(hái)是盲目地建在ID這(zhè)個主鍵上時(shí),您的查詢速度是沒(méi)有這(zhè)麼(me)高的,即使您在“日期”這(zhè)個字段上建立的索引(非聚合索引)。下面(miàn)我們就(jiù)來看一下在1000萬條數據量的情況下各種(zhǒng)查詢的速度表現(3個月内的數據爲25萬條):

(1)僅在主鍵上建立聚集索引,并且不劃分時(shí)間段:

Select gid,fariqi,neibuyonghu,title from tgongwen

用時(shí):128470毫秒(即:128秒)

(2)在主鍵上建立聚集索引,在fariq上建立非聚集索引:

select gid,fariqi,neibuyonghu,title from Tgongwen
where fariqi> dateadd(day,-90,getdate())

用時(shí):53763毫秒(54秒)

(3)將(jiāng)聚合索引建立在日期列(fariqi)上:

select gid,fariqi,neibuyonghu,title from Tgongwen
where fariqi> dateadd(day,-90,getdate())

用時(shí):2423毫秒(2秒)

雖然每條語句提取出來的都(dōu)是25萬條數據,各種(zhǒng)情況的差異卻是巨大的,特别是將(jiāng)聚集索引建立在日期列時(shí)的差異。事(shì)實上,如果您的數據庫真的有1000萬容量的話,把主鍵建立在ID列上,就(jiù)像以上的第1、2種(zhǒng)情況,在網頁上的表現就(jiù)是超時(shí),根本就(jiù)無法顯示。這(zhè)也是我摒棄ID列作爲聚集索引的一個最重要的因素。得出以上速度的方法是:在各個select語句前加:

declare @d datetime
set @d=getdate()

并在select語句後(hòu)加:

select [語句執行花費時(shí)間(毫秒)]=datediff(ms,@d,getdate())

2、隻要建立索引就(jiù)能(néng)顯著提高查詢速度

事(shì)實上,我們可以發(fā)現上面(miàn)的例子中,第2、3條語句完全相同,且建立索引的字段也相同;不同的僅是前者在fariqi字段上建立的是非聚合索引,後(hòu)者在此字段上建立的是聚合索引,但查詢速度卻有著(zhe)天壤之别。所以,并非是在任何字段上簡單地建立索引就(jiù)能(néng)提高查詢速度。

從建表的語句中,我們可以看到這(zhè)個有著(zhe)1000萬數據的表中fariqi字段有5003個不同記錄。在此字段上建立聚合索引是再合适不過(guò)了。在現實中,我們每天都(dōu)會(huì)發(fā)幾個文件,這(zhè)幾個文件的發(fā)文日期就(jiù)相同,這(zhè)完全符合建立聚集索引要求的:“既不能(néng)絕大多數都(dōu)相同,又不能(néng)隻有極少數相同”的規則。由此看來,我們建立“适當”的聚合索引對(duì)于我們提高查詢速度是非常重要的。

3、把所有需要提高查詢速度的字段都(dōu)加進(jìn)聚集索引,以提高查詢速度

上面(miàn)已經(jīng)談到:在進(jìn)行數據查詢時(shí)都(dōu)離不開(kāi)字段的是“日期”還(hái)有用戶本身的“用戶名”。既然這(zhè)兩(liǎng)個字段都(dōu)是如此的重要,我們可以把他們合并起(qǐ)來,建立一個複合索引(compound index)。

很多人認爲隻要把任何字段加進(jìn)聚集索引,就(jiù)能(néng)提高查詢速度,也有人感到迷惑:如果把複合的聚集索引字段分開(kāi)查詢,那麼(me)查詢速度會(huì)減慢嗎?帶著(zhe)這(zhè)個問題,我們來看一下以下的查詢速度(結果集都(dōu)是25萬條數據):(日期列fariqi首先排在複合聚集索引的起(qǐ)始列,用戶名neibuyonghu排在後(hòu)列):

select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>''2004-5-5''

查詢速度:2513毫秒

select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>'2004-5-5' and neibuyonghu='辦公室'

查詢速度:2516毫秒

select gid,fariqi,neibuyonghu,title from Tgongwen where neibuyonghu='辦公室'

查詢速度:60280毫秒

從以上試驗中,我們可以看到如果僅用聚集索引的起(qǐ)始列作爲查詢條件和同時(shí)用到複合聚集索引的全部列的查詢速度是幾乎一樣的,甚至比用上全部的複合索引列還(hái)要略快(在查詢結果集數目一樣的情況下);而如果僅用複合聚集索引的非起(qǐ)始列作爲查詢條件的話,這(zhè)個索引是不起(qǐ)任何作用的。當然,語句1、2的查詢速度一樣是因爲查詢的條目數一樣,如果複合索引的所有列都(dōu)用上,而且查詢結果少的話,這(zhè)樣就(jiù)會(huì)形成(chéng)“索引覆蓋”,因而性能(néng)可以達到最優。同時(shí),請記住:無論您是否經(jīng)常使用聚合索引的其他列,但其前導列一定要是使用最頻繁的列。

四、其他書上沒(méi)有的索引使用經(jīng)驗總結

1、用聚合索引比用不是聚合索引的主鍵速度快

下面(miàn)是實例語句:(都(dōu)是提取25萬條數據)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16'

使用時(shí)間:3326毫秒

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid<=250000

使用時(shí)間:4470毫秒

這(zhè)裡(lǐ),用聚合索引比用不是聚合索引的主鍵速度快了近1/4。

2、用聚合索引比用一般的主鍵作order by時(shí)速度快,特别是在小數據量情況下

select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by fariqi

用時(shí):12936

select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by gid

用時(shí):18843

這(zhè)裡(lǐ),用聚合索引比用一般的主鍵作order by時(shí),速度快了3/10。事(shì)實上,如果數據量很小的話,用聚集索引作爲排序列要比使用非聚集索引速度快得明顯的多;而數據量如果很大的話,如10萬以上,則二者的速度差别不明顯。

3、使用聚合索引内的時(shí)間段,搜索時(shí)間會(huì)按數據占整個數據表的百分比成(chéng)比例減少,而無論聚合索引使用了多少個:

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-1'

用時(shí):6343毫秒(提取100萬條)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-6-6'

用時(shí):3170毫秒(提取50萬條)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16'

用時(shí):3326毫秒(和上句的結果一模一樣。如果采集的數量一樣,那麼(me)用大于号和等于号是一樣的)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-1' and fariqi<'2004-6-6'

用時(shí):3280毫秒

4、日期列不會(huì)因爲有分秒的輸入而減慢查詢速度

下面(miàn)的例子中,共有100萬條數據,2004年1月1日以後(hòu)的數據有50萬條,但隻有兩(liǎng)個不同的日期,日期精确到日;之前有數據50萬條,有5000個不同的日期,日期精确到秒。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-1' order by fariqi

用時(shí):6390毫秒

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi<'2004-1-1' order by fariqi

用時(shí):6453毫秒

五、其他注意事(shì)項

“水可載舟,亦可覆舟”,索引也一樣。索引有助于提高檢索性能(néng),但過(guò)多或不當的索引也會(huì)導緻系統低效。因爲用戶在表中每加進(jìn)一個索引,數據庫就(jiù)要做更多的工作。過(guò)多的索引甚至會(huì)導緻索引碎片。

所以說(shuō),我們要建立一個“适當”的索引體系,特别是對(duì)聚合索引的創建,更應精益求精,以使您的數據庫能(néng)得到高性能(néng)的發(fā)揮。

當然,在實踐中,作爲一個盡職的數據庫管理員,您還(hái)要多測試一些方案,找出哪種(zhǒng)方案效率最高、最爲有效。

(二)改善SQL語句

很多人不知道(dào)SQL語句在SQL SERVER中是如何執行的,他們擔心自己所寫的SQL語句會(huì)被SQL SERVER誤解。比如:

select * from table1 where name='zhangsan' and tID > 10000

和執行

select * from table1 where tID > 10000 and name='zhangsan'

一些人不知道(dào)以上兩(liǎng)條語句的執行效率是否一樣,因爲如果簡單的從語句先後(hòu)上看,這(zhè)兩(liǎng)個語句的确是不一樣,如果tID是一個聚合索引,那麼(me)後(hòu)一句僅僅從表的10000條以後(hòu)的記錄中查找就(jiù)行了;而前一句則要先從全表中查找看有幾個name='zhangsan'的,而後(hòu)再根據限制條件條件tID>10000來提出查詢結果。

事(shì)實上,這(zhè)樣的擔心是不必要的。SQL SERVER中有一個“查詢分析優化器”,它可以計算出where子句中的搜索條件并确定哪個索引能(néng)縮小表掃描的搜索空間,也就(jiù)是說(shuō),它能(néng)實現自動優化。

雖然查詢優化器可以根據where子句自動的進(jìn)行查詢優化,但大家仍然有必要了解一下“查詢優化器”的工作原理,如非這(zhè)樣,有時(shí)查詢優化器就(jiù)會(huì)不按照您的本意進(jìn)行快速查詢。

在查詢分析階段,查詢優化器查看查詢的每個階段并決定限制需要掃描的數據量是否有用。如果一個階段可以被用作一個掃描參數(SARG),那麼(me)就(jiù)稱之爲可優化的,并且可以利用索引快速獲得所需數據。

SARG的定義:用于限制搜索的一個操作,因爲它通常是指一個特定的匹配,一個值得範圍内的匹配或者兩(liǎng)個以上條件的AND連接。形式如下:

列名 操作符 <常數>或<常數> 操作符列名

列名可以出現在操作符的一邊,而常數或變量出現在操作符的另一邊。如:

Name=’張三’

價格>5000

5000<價格

Name=’張三’ and 價格>5000

如果一個表達式不能(néng)滿足SARG的形式,那它就(jiù)無法限制搜索的範圍了,也就(jiù)是SQL SERVER必須對(duì)每一行都(dōu)判斷它是否滿足WHERE子句中的所有條件。所以一個索引對(duì)于不滿足SARG形式的表達式來說(shuō)是無用的。

介紹完SARG後(hòu),我們來總結一下使用SARG以及在實踐中遇到的和某些資料上結論不同的經(jīng)驗:

1、Like語句是否屬于SARG取決于所使用的通配符的類型

如:name like ‘張%’ ,這(zhè)就(jiù)屬于SARG

而:name like ‘%張’ ,就(jiù)不屬于SARG。

原因是通配符%在字符串的開(kāi)通使得索引無法使用。

2、or 會(huì)引起(qǐ)全表掃描

Name=’張三’ and 價格>5000 符号SARG,而:Name=’張三’ or 價格>5000 則不符合SARG。使用or會(huì)引起(qǐ)全表掃描。

3、非操作符、函數引起(qǐ)的不滿足SARG形式的語句

不滿足SARG形式的語句最典型的情況就(jiù)是包括非操作符的語句,如:NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等,另外還(hái)有函數。下面(miàn)就(jiù)是幾個不滿足SARG形式的例子:

ABS(價格)<5000

Name like ‘%三’

有些表達式,如:

WHERE 價格*2>5000

SQL SERVER也會(huì)認爲是SARG,SQL SERVER會(huì)將(jiāng)此式轉化爲:

WHERE 價格>2500/2

但我們不推薦這(zhè)樣使用,因爲有時(shí)SQL SERVER不能(néng)保證這(zhè)種(zhǒng)轉化與原始表達式是完全等價的。

4、IN 的作用相當與OR

語句:

Select * from table1 where tid in (2,3)和Select * from table1 where tid=2 or tid=3

是一樣的,都(dōu)會(huì)引起(qǐ)全表掃描,如果tid上有索引,其索引也會(huì)失效。

5、盡量少用NOT

6、exists 和 in 的執行效率是一樣的

很多資料上都(dōu)顯示說(shuō),exists要比in的執行效率要高,同時(shí)應盡可能(néng)的用not exists來代替not in。但事(shì)實上,我試驗了一下,發(fā)現二者無論是前面(miàn)帶不帶not,二者之間的執行效率都(dōu)是一樣的。因爲涉及子查詢,我們試驗這(zhè)次用SQL SERVER自帶的pubs數據庫。運行前我們可以把SQL SERVER的statistics I/O狀态打開(kāi):

select title,price from titles where title_id in (select title_id from sales where qty>30)

該句的執行結果爲:

表 ''sales''。掃描計數 18,邏輯讀 56 次,物理讀 0 次,預讀 0 次。

表 ''titles''。掃描計數 1,邏輯讀 2 次,物理讀 0 次,預讀 0 次。

select title,price from titles where exists (select * from sales where sales.title_id=titles.title_id and qty>30)

第二句的執行結果爲:

表 ''sales''。掃描計數 18,邏輯讀 56 次,物理讀 0 次,預讀 0 次。

表 ''titles''。掃描計數 1,邏輯讀 2 次,物理讀 0 次,預讀 0 次。

我們從此可以看到用exists和用in的執行效率是一樣的。

7、用函數charindex()和前面(miàn)加通配符%的LIKE執行效率一樣

前面(miàn),我們談到,如果在LIKE前面(miàn)加上通配符%,那麼(me)將(jiāng)會(huì)引起(qǐ)全表掃描,所以其執行效率是低下的。但有的資料介紹說(shuō),用函數charindex()來代替LIKE速度會(huì)有大的提升,經(jīng)我試驗,發(fā)現這(zhè)種(zhǒng)說(shuō)明也是錯誤的:

select gid,title,fariqi,reader from tgongwen where charindex('刑偵支隊',reader)>0 and fariqi>'2004-5-5'

用時(shí):7秒,另外:掃描計數 4,邏輯讀 7155 次,物理讀 0 次,預讀 0 次。

select gid,title,fariqi,reader from tgongwen where reader like '%' + '刑偵支隊' + '%' and fariqi>'2004-5-5'

用時(shí):7秒,另外:掃描計數 4,邏輯讀 7155 次,物理讀 0 次,預讀 0 次。

8、union并不絕對(duì)比or的執行效率高

我們前面(miàn)已經(jīng)談到了在where子句中使用or會(huì)引起(qǐ)全表掃描,一般的,我所見過(guò)的資料都(dōu)是推薦這(zhè)裡(lǐ)用union來代替or。事(shì)實證明,這(zhè)種(zhǒng)說(shuō)法對(duì)于大部分都(dōu)是适用的。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16' or gid>9990000

用時(shí):68秒。掃描計數 1,邏輯讀 404008 次,物理讀 283 次,預讀 392163 次。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi=''2004-9-16''
union
select gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid>9990000

用時(shí):9秒。掃描計數 8,邏輯讀 67489 次,物理讀 216 次,預讀 7499 次。

看來,用union在通常情況下比用or的效率要高的多。

但經(jīng)過(guò)試驗,筆者發(fā)現如果or兩(liǎng)邊的查詢列是一樣的話,那麼(me)用union則反倒和用or的執行速度差很多,雖然這(zhè)裡(lǐ)union掃描的是索引,而or掃描的是全表。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16' or fariqi='2004-2-5'

用時(shí):6423毫秒。掃描計數 2,邏輯讀 14726 次,物理讀 1 次,預讀 7176 次。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16'
union
select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-2-5'

用時(shí):11640毫秒。掃描計數 8,邏輯讀 14806 次,物理讀 108 次,預讀 1144 次。

9、字段提取要按照“需多少、提多少”的原則,避免“select *”

我們來做一個試驗:

select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc

用時(shí):4673毫秒

select top 10000 gid,fariqi,title from tgongwen order by gid desc

用時(shí):1376毫秒

select top 10000 gid,fariqi from tgongwen order by gid desc

用時(shí):80毫秒

由此看來,我們每少提取一個字段,數據的提取速度就(jiù)會(huì)有相應的提升。提升的速度還(hái)要看您舍棄的字段的大小來判斷。

10、count(*)不比count(字段)慢

某些資料上說(shuō):用*會(huì)統計所有列,顯然要比一個世界的列名效率低。這(zhè)種(zhǒng)說(shuō)法其實是沒(méi)有根據的。我們來看:

select count(*) from Tgongwen

用時(shí):1500毫秒

select count(gid) from Tgongwen

用時(shí):1483毫秒

select count(fariqi) from Tgongwen

用時(shí):3140毫秒

select count(title) from Tgongwen

用時(shí):52050毫秒

從以上可以看出,如果用count(*)和用count(主鍵)的速度是相當的,而count(*)卻比其他任何除主鍵以外的字段彙總速度要快,而且字段越長(cháng),彙總的速度就(jiù)越慢。我想,如果用count(*), SQL SERVER可能(néng)會(huì)自動查找最小字段來彙總的。當然,如果您直接寫count(主鍵)將(jiāng)會(huì)來的更直接些。

11、order by按聚集索引列排序效率最高

我們來看:(gid是主鍵,fariqi是聚合索引列):

select top 10000 gid,fariqi,reader,title from tgongwen

用時(shí):196 毫秒。 掃描計數 1,邏輯讀 289 次,物理讀 1 次,預讀 1527 次。

select top 10000 gid,fariqi,reader,title from tgongwen order by gid asc

用時(shí):4720毫秒。 掃描計數 1,邏輯讀 41956 次,物理讀 0 次,預讀 1287 次。

select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc

用時(shí):4736毫秒。 掃描計數 1,邏輯讀 55350 次,物理讀 10 次,預讀 775 次。

select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi asc

用時(shí):173毫秒。 掃描計數 1,邏輯讀 290 次,物理讀 0 次,預讀 0 次。

select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi desc

用時(shí):156毫秒。 掃描計數 1,邏輯讀 289 次,物理讀 0 次,預讀 0 次。

從以上我們可以看出,不排序的速度以及邏輯讀次數都(dōu)是和“order by 聚集索引列” 的速度是相當的,但這(zhè)些都(dōu)比“order by 非聚集索引列”的查詢速度是快得多的。

同時(shí),按照某個字段進(jìn)行排序的時(shí)候,無論是正序還(hái)是倒序,速度是基本相當的。

12、高效的TOP

事(shì)實上,在查詢和提取超大容量的數據集時(shí),影響數據庫響應時(shí)間的最大因素不是數據查找,而是物理的I/0操作。如:

select top 10 * from (
select top 10000 gid,fariqi,title from tgongwen
where neibuyonghu='辦公室'
order by gid desc) as a
order by gid asc

這(zhè)條語句,從理論上講,整條語句的執行時(shí)間應該比子句的執行時(shí)間長(cháng),但事(shì)實相反。因爲,子句執行後(hòu)返回的是10000條記錄,而整條語句僅返回10條語句,所以影響數據庫響應時(shí)間最大的因素是物理I/O操作。而限制物理I/O操作此處的最有效方法之一就(jiù)是使用TOP關鍵詞了。TOP關鍵詞是SQL SERVER中經(jīng)過(guò)系統優化過(guò)的一個用來提取前幾條或前幾個百分比數據的詞。經(jīng)筆者在實踐中的應用,發(fā)現TOP确實很好(hǎo)用,效率也很高。但這(zhè)個詞在另外一個大型數據庫ORACLE中卻沒(méi)有,這(zhè)不能(néng)說(shuō)不是一個遺憾,雖然在ORACLE中可以用其他方法(如:rownumber)來解決。在以後(hòu)的關于“實現千萬級數據的分頁顯示存儲過(guò)程”的讨論中,我們就(jiù)將(jiāng)用到TOP這(zhè)個關鍵詞。

到此爲止,我們上面(miàn)讨論了如何實現從大容量的數據庫中快速地查詢出您所需要的數據方法。當然,我們介紹的這(zhè)些方法都(dōu)是“軟”方法,在實踐中,我們還(hái)要考慮各種(zhǒng)“硬”因素,如:網絡性能(néng)、服務器的性能(néng)、操作系統的性能(néng),甚至網卡、交換機等。

)實現小數據量和海量數據的通用分頁顯示存儲過(guò)程

建立一個 Web 應用,分頁浏覽功能(néng)必不可少。這(zhè)個問題是數據庫處理中十分常見的問題。經(jīng)典的數據分頁方法是:ADO 紀錄集分頁法,也就(jiù)是利用ADO自帶的分頁功能(néng)(利用遊标)來實現分頁。但這(zhè)種(zhǒng)分頁方法僅适用于較小數據量的情形,因爲遊标本身有缺點:遊标是存放在内存中,很費内存。遊标一建立,就(jiù)將(jiāng)相關的記錄鎖住,直到取消遊标。遊标提供了對(duì)特定集合中逐行掃描的手段,一般使用遊标來逐行遍曆數據,根據取出數據條件的不同進(jìn)行不同的操作。而對(duì)于多表和大表中定義的遊标(大的數據集合)循環很容易使程序進(jìn)入一個漫長(cháng)的等待甚至死機。

更重要的是,對(duì)于非常大的數據模型而言,分頁檢索時(shí),如果按照傳統的每次都(dōu)加載整個數據源的方法是非常浪費資源的。現在流行的分頁方法一般是檢索頁面(miàn)大小的塊區的數據,而非檢索所有的數據,然後(hòu)單步執行當前行。

最早較好(hǎo)地實現這(zhè)種(zhǒng)根據頁面(miàn)大小和頁碼來提取數據的方法大概就(jiù)是“俄羅斯存儲過(guò)程”。這(zhè)個存儲過(guò)程用了遊标,由于遊标的局限性,所以這(zhè)個方法并沒(méi)有得到大家的普遍認可。

後(hòu)來,網上有人改造了此存儲過(guò)程,下面(miàn)的存儲過(guò)程就(jiù)是結合我們的辦公自動化實例寫的分頁存儲過(guò)程:

CREATE procedure pagination1

(@pagesize int, --頁面(miàn)大小,如每頁存儲20條記錄

@pageindex int --當前頁碼

)

as



set nocount on


begin

declare @indextable table(id int identity(1,1),nid int) --定義表變量

declare @PageLowerBound int --定義此頁的底碼

declare @PageUpperBound int --定義此頁的頂碼

set @PageLowerBound=(@pageindex-1)*@pagesize

set @PageUpperBound=@PageLowerBound+@pagesize

set rowcount @PageUpperBound

insert into @indextable(nid) select gid from TGongwen

      where fariqi >dateadd(day,-365,getdate()) order by fariqi desc

select O.gid,O.mid,O.title,O.fadanwei,O.fariqi from TGongwen O,@indextable t

where O.gid=t.nid and t.id>@PageLowerBound

and t.id<=@PageUpperBound order by t.id

end


set nocount off

以上存儲過(guò)程運用了SQL SERVER的最新技術――表變量。應該說(shuō)這(zhè)個存儲過(guò)程也是一個非常優秀的分頁存儲過(guò)程。當然,在這(zhè)個過(guò)程中,您也可以把其中的表變量寫成(chéng)臨時(shí)表:CREATE TABLE #Temp。但很明顯,在SQL SERVER中,用臨時(shí)表是沒(méi)有用表變量快的。所以筆者剛開(kāi)始使用這(zhè)個存儲過(guò)程時(shí),感覺非常的不錯,速度也比原來的ADO的好(hǎo)。但後(hòu)來,我又發(fā)現了比此方法更好(hǎo)的方法。

筆者曾在網上看到了一篇小短文《從數據表中取出第n條到第m條的記錄的方法》,全文如下:

--從publish 表中取出第 n 條到第 m 條的記錄:

SELECT TOP m-n+1 *

FROM publish

WHERE (id NOT IN

    (SELECT TOP n-1 id

     FROM publish))



--id 爲publish 表的關鍵字

我當時(shí)看到這(zhè)篇文章的時(shí)候,真的是精神爲之一振,覺得思路非常得好(hǎo)。等到後(hòu)來,我在作辦公自動化系統(ASP.NET+ C#+SQL SERVER)的時(shí)候,忽然想起(qǐ)了這(zhè)篇文章,我想如果把這(zhè)個語句改造一下,這(zhè)就(jiù)可能(néng)是一個非常好(hǎo)的分頁存儲過(guò)程。于是我就(jiù)滿網上找這(zhè)篇文章,沒(méi)想到,文章還(hái)沒(méi)找到,卻找到了一篇根據此語句寫的一個分頁存儲過(guò)程,這(zhè)個存儲過(guò)程也是目前較爲流行的一種(zhǒng)分頁存儲過(guò)程,我很後(hòu)悔沒(méi)有争先把這(zhè)段文字改造成(chéng)存儲過(guò)程:

CREATE PROCEDURE pagination2

(

@SQL nVARCHAR(4000), --不帶排序語句的SQL語句

@Page int, --頁碼

@RecsPerPage int, --每頁容納的記錄數

@ID VARCHAR(255), --需要排序的不重複的ID号

@Sort VARCHAR(255) --排序字段及規則

)

AS

 

DECLARE @Str nVARCHAR(4000)

 

SET @Str=''SELECT TOP ''+CAST(@RecsPerPage AS VARCHAR(20))+'' * FROM

(''+@SQL+'') T WHERE T.''+@ID+''NOT IN (SELECT TOP''+CAST((@RecsPerPage*(@Page-1))

AS VARCHAR(20))+'' ''+@ID+'' FROM (''+@SQL+'') T9 ORDER BY''+@Sort+'') ORDER BY ''+@Sort
 
PRINT @Str

 

EXEC sp_ExecuteSql @Str

GO

--其實,以上語句可以簡化爲:

SELECT TOP 頁大小 *

FROM Table1 WHERE (ID NOT IN (SELECT TOP 頁大小*頁數 id FROM 表 ORDER BY id))

ORDER BY ID

--但這(zhè)個存儲過(guò)程有一個緻命的缺點,就(jiù)是它含有NOT IN字樣。雖然我可以把它改造爲:

SELECT TOP 頁大小 *

FROM Table1 WHERE not exists

(select * from (select top (頁大小*頁數) * from table1 order by id) b where b.id=a.id )

order by id

--目前流行的一種(zhǒng)分頁存儲過(guò)程

即,用not exists來代替not in,但我們前面(miàn)已經(jīng)談過(guò)了,二者的執行效率實際上是沒(méi)有區别的。既便如此,用TOP 結合NOT IN的這(zhè)個方法還(hái)是比用遊标要來得快一些。

雖然用not exists并不能(néng)挽救上個存儲過(guò)程的效率,但使用SQL SERVER中的TOP關鍵字卻是一個非常明智的選擇。因爲分頁優化的最終目的就(jiù)是避免産生過(guò)大的記錄集,而我們在前面(miàn)也已經(jīng)提到了TOP的優勢,通過(guò)TOP 即可實現對(duì)數據量的控制。

在分頁算法中,影響我們查詢速度的關鍵因素有兩(liǎng)點:TOP和NOT IN。TOP可以提高我們的查詢速度,而NOT IN會(huì)減慢我們的查詢速度,所以要提高我們整個分頁算法的速度,就(jiù)要徹底改造NOT IN,同其他方法來替代它。

我們知道(dào),幾乎任何字段,我們都(dōu)可以通過(guò)max(字段)或min(字段)來提取某個字段中的最大或最小值,所以如果這(zhè)個字段不重複,那麼(me)就(jiù)可以利用這(zhè)些不重複的字段的max或min作爲分水嶺,使其成(chéng)爲分頁算法中分開(kāi)每頁的參照物。在這(zhè)裡(lǐ),我們可以用操作符“>”或“<”号來完成(chéng)這(zhè)個使命,使查詢語句符合SARG形式。如:

Select top 10 * from table1 where id>200

--于是就(jiù)有了如下分頁方案:

select top 頁大小 *

from table1

where id>

(select max (id) from

(select top ((頁碼-1)*頁大小) id from table1 order by id) as T

)

order by id

在選擇即不重複值,又容易分辨大小的列時(shí),我們通常會(huì)選擇主鍵。下表列出了筆者用有著(zhe)1000萬數據的辦公自動化系統中的表,在以GID(GID是主鍵,但并不是聚集索引。)爲排序列、提取gid,fariqi,title字段,分别以第1、10、100、500、1000、1萬、10萬、25萬、50萬頁爲例,測試以上三種(zhǒng)分頁方案的執行速度:(單位:毫秒)

頁碼

方案1

方案2

方案3

1

60

30

76

10

46

16

63

100

1076

720

130

500

540

12943

83

1000

17110

470

250

10000

24796

4500

140

100000

38326

42283

1553

250000

28140

128720

2330

500000

121686

127846

7168

從上表中,我們可以看出,三種(zhǒng)存儲過(guò)程在執行100頁以下的分頁命令時(shí),都(dōu)是可以信任的,速度都(dōu)很好(hǎo)。但第一種(zhǒng)方案在執行分頁1000頁以上後(hòu),速度就(jiù)降了下來。第二種(zhǒng)方案大約是在執行分頁1萬頁以上後(hòu)速度開(kāi)始降了下來。而第三種(zhǒng)方案卻始終沒(méi)有大的降勢,後(hòu)勁仍然很足。

在确定了第三種(zhǒng)分頁方案後(hòu),我們可以據此寫一個存儲過(guò)程。大家知道(dào)SQL SERVER的存儲過(guò)程是事(shì)先編譯好(hǎo)的SQL語句,它的執行效率要比通過(guò)WEB頁面(miàn)傳來的SQL語句的執行效率要高。下面(miàn)的存儲過(guò)程不僅含有分頁方案,還(hái)會(huì)根據頁面(miàn)傳來的參數來确定是否進(jìn)行數據總數統計。

--獲取指定頁的數據:

CREATE PROCEDURE pagination3

@tblName varchar(255), -- 表名

@strGetFields varchar(1000) = ''*'', -- 需要返回的列

@fldName varchar(255)='''', -- 排序的字段名

@PageSize int = 10, -- 頁尺寸

@PageIndex int = 1, -- 頁碼

@doCount bit = 0, -- 返回記錄總數, 非 0 值則返回

@OrderType bit = 0, -- 設置排序類型, 非 0 值則降序

@strWhere varchar(1500) = '''' -- 查詢條件 (注意: 不要加 where)

AS

 

declare @strSQL varchar(5000) -- 主語句

declare @strTmp varchar(110) -- 臨時(shí)變量

declare @strOrder varchar(400) -- 排序類型

 

if @doCount != 0

begin

if @strWhere !=''''

set @strSQL = "select count(*) as Total from [" + @tblName + "] where "+@strWhere

else

set @strSQL = "select count(*) as Total from [" + @tblName + "]"

end

--以上代碼的意思是如果@doCount傳遞過(guò)來的不是0,就(jiù)執行總數統計。以下的所有代碼都(dōu)是@doCount爲0的情況:

else

begin

if @OrderType != 0

begin

set @strTmp = "<(select min"

set @strOrder = " order by [" + @fldName +"] desc"

--如果@OrderType不是0,就(jiù)執行降序,這(zhè)句很重要!

end

else

begin

set @strTmp = ">(select max"

set @strOrder = " order by [" + @fldName +"] asc"

end

 

if @PageIndex = 1

begin

if @strWhere != ''''

 

set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ "

        from [" + @tblName + "] where " + @strWhere + " " + @strOrder

else

 

set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ "

        from ["+ @tblName + "] "+ @strOrder

--如果是第一頁就(jiù)執行以上代碼,這(zhè)樣會(huì)加快執行速度

end

else

begin

--以下代碼賦予了@strSQL以真正執行的SQL代碼 

set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ " from ["

+ @tblName + "] where [" + @fldName + "]" + @strTmp + "(["+ @fldName + "])

      from (select top " + str((@PageIndex-1)*@PageSize) + " ["+ @fldName + "]

      from [" + @tblName + "]" + @strOrder + ") as tblTmp)"+ @strOrder

 

if @strWhere != ''''

set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ " from ["

+ @tblName + "] where [" + @fldName + "]" + @strTmp + "(["

+ @fldName + "]) from (select top " + str((@PageIndex-1)*@PageSize) +" ["

+ @fldName + "] from [" + @tblName + "] where " + @strWhere + " "

+ @strOrder + ") as tblTmp) and " + @strWhere + " " + @strOrder

end

 

end

 

exec (@strSQL)

 

GO

上面(miàn)的這(zhè)個存儲過(guò)程是一個通用的存儲過(guò)程,其注釋已寫在其中了。在大數據量的情況下,特别是在查詢最後(hòu)幾頁的時(shí)候,查詢時(shí)間一般不會(huì)超過(guò)9秒;而用其他存儲過(guò)程,在實踐中就(jiù)會(huì)導緻超時(shí),所以這(zhè)個存儲過(guò)程非常适用于大容量數據庫的查詢。筆者希望能(néng)夠通過(guò)對(duì)以上存儲過(guò)程的解析,能(néng)給大家帶來一定的啓示,并給工作帶來一定的效率提升,同時(shí)希望同行提出更優秀的實時(shí)數據分頁算法。

)聚集索引的重要性和如何選擇聚集索引

在上一節的标題中,筆者寫的是:實現小數據量和海量數據的通用分頁顯示存儲過(guò)程。這(zhè)是因爲在將(jiāng)本存儲過(guò)程應用于“辦公自動化”系統的實踐中時(shí),筆者發(fā)現這(zhè)第三種(zhǒng)存儲過(guò)程在小數據量的情況下,有如下現象:

1、分頁速度一般維持在1秒和3秒之間。

2、在查詢最後(hòu)一頁時(shí),速度一般爲5秒至8秒,哪怕分頁總數隻有3頁或30萬頁。

雖然在超大容量情況下,這(zhè)個分頁的實現過(guò)程是很快的,但在分前幾頁時(shí),這(zhè)個1-3秒的速度比起(qǐ)第一種(zhǒng)甚至沒(méi)有經(jīng)過(guò)優化的分頁方法速度還(hái)要慢,借用戶的話說(shuō)就(jiù)是“還(hái)沒(méi)有ACCESS數據庫速度快”,這(zhè)個認識足以導緻用戶放棄使用您開(kāi)發(fā)的系統。

筆者就(jiù)此分析了一下,原來産生這(zhè)種(zhǒng)現象的症結是如此的簡單,但又如此的重要:排序的字段不是聚集索引!

本篇文章的題目是:“查詢優化及分頁算法方案”。筆者隻所以把“查詢優化”和“分頁算法”這(zhè)兩(liǎng)個聯系不是很大的論題放在一起(qǐ),就(jiù)是因爲二者都(dōu)需要一個非常重要的東西――聚集索引。

在前面(miàn)的讨論中我們已經(jīng)提到了,聚集索引有兩(liǎng)個最大的優勢:

1、以最快的速度縮小查詢範圍。

2、以最快的速度進(jìn)行字段排序。

1條多用在查詢優化時(shí),而第2條多用在進(jìn)行分頁時(shí)的數據排序。

而聚集索引在每個表内又隻能(néng)建立一個,這(zhè)使得聚集索引顯得更加的重要。聚集索引的挑選可以說(shuō)是實現“查詢優化”和“高效分頁”的最關鍵因素。

但要既使聚集索引列既符合查詢列的需要,又符合排序列的需要,這(zhè)通常是一個矛盾。筆者前面(miàn)“索引”的讨論中,將(jiāng)fariqi,即用戶發(fā)文日期作爲了聚集索引的起(qǐ)始列,日期的精确度爲“日”。這(zhè)種(zhǒng)作法的優點,前面(miàn)已經(jīng)提到了,在進(jìn)行劃時(shí)間段的快速查詢中,比用ID主鍵列有很大的優勢。

但在分頁時(shí),由于這(zhè)個聚集索引列存在著(zhe)重複記錄,所以無法使用max或min來最爲分頁的參照物,進(jìn)而無法實現更爲高效的排序。而如果將(jiāng)ID主鍵列作爲聚集索引,那麼(me)聚集索引除了用以排序之外,沒(méi)有任何用處,實際上是浪費了聚集索引這(zhè)個寶貴的資源。

爲解決這(zhè)個矛盾,筆者後(hòu)來又添加了一個日期列,其默認值爲getdate()。用戶在寫入記錄時(shí),這(zhè)個列自動寫入當時(shí)的時(shí)間,時(shí)間精确到毫秒。即使這(zhè)樣,爲了避免可能(néng)性很小的重合,還(hái)要在此列上創建UNIQUE約束。將(jiāng)此日期列作爲聚集索引列。

有了這(zhè)個時(shí)間型聚集索引列之後(hòu),用戶就(jiù)既可以用這(zhè)個列查找用戶在插入數據時(shí)的某個時(shí)間段的查詢,又可以作爲唯一列來實現max或min,成(chéng)爲分頁算法的參照物。

經(jīng)過(guò)這(zhè)樣的優化,筆者發(fā)現,無論是大數據量的情況下還(hái)是小數據量的情況下,分頁速度一般都(dōu)是幾十毫秒,甚至0毫秒。而用日期段縮小範圍的查詢速度比原來也沒(méi)有任何遲鈍。聚集索引是如此的重要和珍貴,所以筆者總結了一下,一定要將(jiāng)聚集索引建立在:

1、您最頻繁使用的、用以縮小查詢範圍的字段上;

2、您最頻繁使用的、需要排序的字段上。

結束語

本篇文章彙集了筆者近段在使用數據庫方面(miàn)的心得,是在做“辦公自動化”系統時(shí)實踐經(jīng)驗的積累。希望這(zhè)篇文章不僅能(néng)夠給大家的工作帶來一定的幫助,也希望能(néng)讓大家能(néng)夠體會(huì)到分析問題的方法;最重要的是,希望這(zhè)篇文章能(néng)夠抛磚引玉,掀起(qǐ)大家的學(xué)習和讨論的興趣,以共同促進(jìn),共同爲公安科技強警事(shì)業和金盾工程做出自己最大的努力。

最後(hòu)需要說(shuō)明的是,在試驗中,我發(fā)現用戶在進(jìn)行大數據量查詢的時(shí)候,對(duì)數據庫速度影響最大的不是内存大小,而是CPU。在我的P4 2.4機器上試驗的時(shí)候,查看“資源管理器”,CPU經(jīng)常出現持續到100%的現象,而内存用量卻并沒(méi)有改變或者說(shuō)沒(méi)有大的改變。即使在我們的HP ML 350 G3服務器上試驗時(shí),CPU峰值也能(néng)達到90%,一般持續在70%左右。

本文的試驗數據都(dōu)是來自我們的HP ML 350服務器。服務器配置:雙Inter Xeon 超線程 CPU 2.4G,内存1G,操作系統Windows Server 2003 Enterprise Edition,數據庫SQL Server 2000 SP3

(完)

有索引情況下,insert速度一定有影響,不過(guò):
1. 你不大可能(néng)一該不停地進(jìn)行insert, SQL Server能(néng)把你傳來的命令緩存起(qǐ)來,依次執行,不會(huì)漏掉任何一個insert。
2. 你也可以建立一個相同結構但不做索引的表,insert數據先插入到這(zhè)個表裡(lǐ),當這(zhè)個表中行數達到一定行數再用insert table1 select * from table2這(zhè)樣的命令整批插入到有索引的那個表裡(lǐ)。

 

注:文章來源與網絡,僅供讀者參考!

人是有思想的,這(zhè)是人與動物本質的區别。人的社會(huì)屬性要求我們在操守的規範下實現自我價值,越有這(zhè)越給予。因此,我們要實現自己的社會(huì)價值 。這(zhè)些都(dōu)離不開(kāi)堅定的信仰,有無信仰是一個在精神層面(miàn)狀态好(hǎo)壞的體現,不能(néng)覺得一切都(dōu)無所謂。生活是一面(miàn)鏡子,自己是什麼(me)樣子很快現行。 用知識武裝自己,用信仰升華自己,用愛好(hǎo)裝點自己,用個性标識自己。 我就(jiù)是我,不一樣的煙火;我就(jiù)是我,不一樣的水果;我就(jiù)是我,不一樣的花朵;我就(jiù)是我,不一樣的自我。 生活寄語:越努力,越幸運。 做最好(hǎo)的自己!


來源:cnblogs