本文不是SQL教程,只是關(guān)于T-SQL的一點小結(jié)及吐槽。
對于SQL入門的學習,推薦一本32開大小的小巧書目《SQL Server編程必知必會》,對于每個知識點介紹得很簡單但很到位,如果你不想閱讀那些動輒四五百頁的大作的話,這本書完全夠用。另外一本進階的書目《The Art of SQL》很出名,那是我打算看的但還沒有看。
這里討論和所有例子都基于微軟對SQL的一個實現(xiàn)版本T-SQL。表是基于manage sutio自帶的Northwind數(shù)據(jù)庫。
對于初學者來說,只知道SQL不知道T-SQL,T-SQL是什么,感覺像方言。對頭,它就是個方言。
SQL的全稱是結(jié)構(gòu)化查詢語言Structured Query Language,專門設(shè)計來與數(shù)據(jù)進行通信,就好比HTML被設(shè)計來展示網(wǎng)頁。它不是一種專利語言,不屬于哪個公司,好比你無法說出HTML是微軟的,還是IBM的。有專門的標準委員會來制定SQL語言的標準及對它進行維護。而我們真正使用的,是各個數(shù)據(jù)庫管理軟件對它的一個實現(xiàn)。就是說SQL是標準,其他人在遵守這一標準的前提下給出自己的一個實現(xiàn)版本。微軟的數(shù)據(jù)庫產(chǎn)品所實現(xiàn)的版本就叫Transact-SQL,簡稱T-SQL,Oracle的是PL/SQL等。當然,各個DBMS版本間會新增自己的一些特性,這也是為什么Oracle下寫的SQL腳本可能在SQL Server上不兼容的原因,但大部分語句都是可以在各平臺移植的。
SQL中,大部分查詢都是通過SELECT語句返回,最熟悉的莫過于從一張表中進行查詢返回結(jié)果,但其實還有一些返回是可以通過啟用函數(shù)的,比如GETDATE()。
GETDATE()方法返回當前系統(tǒng)時間,準確說是你的數(shù)據(jù)庫服務(wù)器的時間。如果有人問程序員What’s the time? 那我想《程序員裝逼指南》(如果有這本書的話)會告訴你,程序員才不會帶表或者看手機呢,他會優(yōu)雅地輸入SELECT GETDATE();
只是在單獨使用諸如GETDATE(),RAND()等函數(shù)時表忘了加上SELECT .
上面提到RAND()方法,它默認生成的是0到1且后面拖了一長串精度的浮點數(shù)。同時它也可以接收一個種子值,如果指定了種子值,居然每次產(chǎn)生的隨機數(shù)都是一樣的!真的是坑爹的隨機函數(shù).
但其實很多時候我們需要讓這個產(chǎn)生的隨機數(shù)更符合我們的需要一點,比如只想要一個0到10之間的隨機數(shù),這時候可以聯(lián)想到CEILING()和FLOOR()這兩個函數(shù),前者返回大于等于給定參數(shù)的最小整數(shù),后者相反,返回小于等于給定參數(shù)的最大整數(shù)。有點繞口,比如CEILING(0.3),那么比0.3大的整數(shù)就是1,2,3….最小的是1,返回1. 而FLOOR(1.5)呢,對頭,返回1.
接著上面的思路,要返回0到10之間的一個隨機數(shù),我們需要做的就是把RAND()返回的數(shù)先乘以10,這樣返回的就是幾點幾的一個數(shù),再用CEILING()或者FLOOR()就可以只取到一個整數(shù)部分的數(shù)字。
用CEILING()還是FLOOR(), 抑或兩者沒有區(qū)別。必需有區(qū)別,用CEILING()你永遠得不到0這個數(shù),而且FLOOR()又永遠得不到10這個數(shù)。
還有種方法就是使用字符串處理函數(shù)LEFT()或者RIGHT(),將RAND()返回的數(shù)同樣乘10后用LEFT()取左邊一位就是我們想要的了,RIGHT()同理。
想到這里,甚至可以用SUBSTRING(),它接收三個參數(shù),第一個是字符串,所以首先我們需要把RAND()返回的數(shù)通過CONVERT()轉(zhuǎn)成字符型,第二參數(shù)是要取的子字符串的開始位置,第三個參數(shù)為子字符串長度。
除非你真的是閑的,否則沒有必需用這種方法來做,這里只是提供一種思路,說明科學的道路其實很寬闊的。
至于要返回1到100,10到100隨機數(shù)等的情況,相信通過各位碼農(nóng)的智慧也是完全可能的了,這里就不贅述鳥。
SQL主要功能不是返回數(shù)字,更多是返回表中的行。同樣,也可以實現(xiàn)從表中返回隨機抽取的數(shù)據(jù)行。
通過NEWID()這個方法用如下語句輕松實現(xiàn)。
NEWID()是個什么樣的函數(shù),可以實驗一把來看看:
返回一串字符類似于GUID,唯一的標識。每次運行返回的都是一個不同的值。在上面的例子中,從ORDERS表中取三條數(shù)據(jù),數(shù)據(jù)是通過NEWID()產(chǎn)生的值到表中取到的,當然就是隨機的了。
其實,SQL SERVER有個專門從大量數(shù)據(jù)中返回隨機樣本的函數(shù)TABLESAMPLE(),通過參數(shù)可以指定返回多少條隨機樣本數(shù)據(jù)或者百分之幾的數(shù)據(jù):
但詭異的是,居然提示語法錯誤。這有點讓人摸不著北。后來發(fā)現(xiàn),TABLESAMPLE() 是SQL SERVER 2005 新增的內(nèi)容,而Northwind數(shù)據(jù)庫又是2005版本以前自帶的一個樣本數(shù)據(jù)庫,所以它只能兼容級別為80,也就是說在這個數(shù)據(jù)庫上能執(zhí)行2000或更老的SQL SERVER所以支持的命令但無法執(zhí)行更新的命令。像SQL SERVER 2005之后的版本中都用Adventureworks數(shù)據(jù)庫代替了Northwind的。于是轉(zhuǎn)到Adventureworks數(shù)據(jù)庫進行嘗試。
果然是兼容性的問題,那么能不能在老版本SQL SERVER中創(chuàng)建的數(shù)據(jù)庫上執(zhí)行呢,其實還是可以的,只要把數(shù)據(jù)庫的兼容性改到更高即可:
需要說明的是TABLESAMPLE()每次返回的結(jié)果數(shù)目是不一樣的,在給定的參數(shù)上下跳動,所以如果表本身的數(shù)據(jù)量很小的話,比如只有兩三百條,使用此命令一行結(jié)果也沒有返回是很正常的。
GETDATE()可以方便地獲得時間日期,DATEPART()則會返回指定的日期的某部分,比如只想要一條完事日期信息中的月分,星期等:
但我們看到返回的星期其實不夠人性化,貼心的顯示應(yīng)該是星期幾而不是本周的第幾天,這時就可以用DATENAME()了,它返回的是日期的名字。
其實更人性化一點的顯示應(yīng)該是這樣的,如果在中國,應(yīng)該返回星期一,十月!
運行Sp_helplanguage可以看到SQL SERVER中保存了各種語言對日期時間的顯示設(shè)置。當然中文的習慣稱呼也可以在里面找到。
再看我們數(shù)據(jù)庫當前的語言環(huán)境:
當前語言環(huán)境是英語,所以返回的是英文的日期相關(guān)名稱,那思路應(yīng)該就是把默認語言環(huán)境改成中文。
可以看到,現(xiàn)在系統(tǒng)默認語言已經(jīng)設(shè)置成了簡體中文,NOW,就是見證奇跡的時刻。
繁體中文:
一個有意思的事情是,讓你看一下日語情況下的所有星期:
是不是很有意思,華夏博大精深的五行在日本那里成了星期。如果我不是這么晚研究SQL這么入神,我也不知道今天是火什么日。
對于上面所執(zhí)行的語句,我不得不多說幾句,首先行1設(shè)置系統(tǒng)默認語言為日本兒語,行2行3定義了兩個變量一個保存當前日期,另一個用作下面循環(huán)輸出星期時的計數(shù)器,在對這兩個變量進行初始化時有個技巧,一般是用SET來進行變量初始化,但一次只能初始化一個,而SELECT語句里可以初始化多個變量。以前我甚至不知道SELECT可以進行變量初始化!!行6開始循環(huán)函數(shù),每輸出一次后把日期加一天并且計數(shù)器加一,這里日期加一天又用了個默默無聞的函數(shù)DATEADD(),你可以指定一個數(shù)加到一個日期上的哪部分,這里是加一天。
還有個常見的問題就是,很多時候我們只希望返回日期中的時間,或者日期中的日期不帶時間。
實現(xiàn)的方法很多,但基本上都是先把日期轉(zhuǎn)換成字符后操作。對于字符你就可以用LEFT()或者RIGHT()抑或者SUBSTR()來取一個日期數(shù)據(jù)中的任何部分了。
對于實現(xiàn)這個功能,一個印度阿三給出了如下的解法:
注意,上面那樣需要SQL SERVER 2008及以上。
無可否認,印度阿三的實力就是那樣讓人肅然起敬。如果你很不幸的干上了程序員這一行,終有一天你會感嘆既生我,何生印度阿三。
其實對于一個日期中想取天,月等,有直接的函數(shù)可用:
其實這三個函數(shù)就是DATEPART()的簡化而以,因為你完全可以用DATEPART()來完成。
只是DATEPART()更靈活,能取到的部分更多。
如果你注意到了SQL SERVER里面有個collation屬性的話,這個功能實現(xiàn)起來不那么難。并且對于中文環(huán)境的開發(fā)者來說,這樣的功能可能會時不時的碰到。
右擊任何一個數(shù)據(jù)庫,查看其屬性,你會發(fā)現(xiàn)在選項里可以看到collation這個屬性并且可以進行設(shè)置。
Collation所規(guī)定的就是一種字符集的排序規(guī)則。這個東西通常是被很多開發(fā)人員忽略而不知的。在這里所以設(shè)定的值會應(yīng)用到所選數(shù)據(jù)庫的默認排序規(guī)則上。通常對于一種語言比如中文,不止一種規(guī)則,通過點開下拉列表你可以看到里面分得很細,包括簡體繁體,香港大陸臺灣新加坡等,其中帶’stroke’就是涉及筆劃的。
除了通過上面的屬性窗口可以了解到collation外,還可以通過SELECT * FROM Fn_helpcollations(); 查看數(shù)據(jù)庫服務(wù)器所支持的字符集。
如你看到的這個Chinese_PRC_CS_AI_WS,前面CHINESES_PRC(中國大陸)當然是指國家地區(qū)了,而后面跟的后綴,也有其基體的含義,可以從SQL Server的幫助文檔中了解到,這里不說開了。正是后面這些后綴對排序起了很大的作用。
如果你只是臨時想在檢索結(jié)果中對列以筆劃排序,可以在SELECT語句后通過COLLATE指定相應(yīng)的collation(為了演示臨時建了個表并插入了中文數(shù)據(jù)):
在我的映像中筆劃順序貌似是橫豎撇捺折,這里看來我碰到了點認知上的疑惑。
同樣,你在列表中可以看到帶‘pingyin’的,那就是以拼音排序。
以拼音排序的結(jié)果:
我也是在想系統(tǒng)地學習一下SQL然后看了本兒書后才發(fā)現(xiàn)原來還有這么個神奇的函數(shù)。
SOUNDEX將字符轉(zhuǎn)換成表示其發(fā)音的字母與數(shù)字的組合,所以用它進行字符串的匹配是基于字符串的讀音而不是字符本身,夠強大吧。其實SOUNDEX并不屬于SQL里的概念,但T-SQL提供了對它的支持。下面來做個實驗。
在Categories表中CategoryName最后列有個seafood,我在其后面添加了四列,按照英語音標習慣這些讀音都和seafood差不多,看是否能通過soundex把它們檢索出來。
執(zhí)行下面的名句后及結(jié)果如圖:
可以看到,檢索出了3行新增用來實驗的數(shù)據(jù),效果還是非常令人滿意的。
從這里似乎可以窺見到語音搜索的端倪了:當你點擊谷歌搜索框旁邊的話筒,它把你的聲音編碼成文本返回服務(wù)器然后通過上面類似的方法進行數(shù)據(jù)檢索。這就是語音搜索。
當然,這只是一種猜想,具體算法當然要復(fù)雜得多。只是從這里可以看出,任何你需要的一個小功能都可以從一些不起眼但已經(jīng)存在的函數(shù)上來做文章。還有前面提到的漢字排序問題,如果你不知道這些知識點,面對一些需要真的就無從下手。
基本上我們在SQL SERVER中碰到和操作的都是文本,數(shù)字等類型的數(shù)據(jù),但圖片也是經(jīng)常要面對的一種文件類型,比如我們需要保存用戶的頭像。
對于圖片,可以保存在數(shù)據(jù)庫,那就是保存它們的文本編碼形式,而不是文件本身。比如用戶上傳了一張圖片,其實從前端傳送到服務(wù)器的過程中也是以文本編碼的形式,SQL SERVER也確實提供了image這種格式來方便保存圖片到數(shù)據(jù)庫。
另外一種方案就是保存路徑。比如前端用戶上傳了圖片,服務(wù)器指定圖片保存到某個文件夾下,并且對圖片進行一個統(tǒng)一有序的命名,然后將這個圖片的路徑和重命名后的信息可在到數(shù)據(jù)庫表中,以后使用是通過查找相應(yīng)的表獲得路徑及文件名,然后去調(diào)用真正的文件。
不說說第二種方案是更加合理的,試想誰也不可能把mp3甚至視頻文件編碼保存到數(shù)據(jù)庫表中的一列,那是怎樣的一個表!
對檢索出的結(jié)果一般是時間日期金錢等需要進行一些格式化操作以符合閱讀習慣。這里不打算講開了。
一般,搜索出的id我們習慣要加上編號方便閱讀,比如XXXX號,No.XXX ,但你如果嘗試通過這樣的語句直接加上想要顯示的字符串時:
會出錯,OrderId不是字符串,一個不是字符串的元素想跟一個字符串連接當然會出錯,需要將不是字符串的OrderId轉(zhuǎn)換成字符串后方可進行上述操作。剛好有個STR()可以用來完成需要。
但STR()也不是一個長久之計,因為碰到日期時,它就無法轉(zhuǎn)換了。日期還需要轉(zhuǎn)換嘛,興許你會認為日期本來就是字符串了,那我們可以先看一下:
所以以日期不是字符串,日期是一種專門的格式??粗褡址浴_@就叫長得像也是錯。
對于日期我們可以使用比較萬能的CONVERT()來進行轉(zhuǎn)換。
或者你會問,在SQL里加字符串整理好我們需要的數(shù)據(jù)沒必需,完全可以在應(yīng)用程序中格式化,怎么加都行,但SQL是專門設(shè)計來處理數(shù)據(jù)的,它對這些操作的執(zhí)行效率非常高,如果你一開始就把數(shù)據(jù)整理好再送到應(yīng)用程序便可直接使用,將會使你的程序性能有較大提高(這也是SQL必知必會一書中的觀點).
你可以猜想一下 SELECT SUM(c1*c2) 的結(jié)果是什么?;蛘吣氵€沒有這樣的概念,SUM就是對一列的數(shù)據(jù)進行求和嘛。AVG()也是對于一列求平均。但其實聚合函數(shù)是可以用在多列上的。
就比如上面的SELECT SUM(c1*c2),先是把c1列和c2列對應(yīng)的每行相乘,最后再把每行的乘積相加。
請看下面這個測試用列:
對于自增列,我們知道一般用來當表的主鍵,并且它的值被指定為按給定的種子值自動遞增下去,在你向表中插入一行數(shù)據(jù)后,它自動增加,確定的說它會去找還沒有被使用的數(shù)中最小的數(shù)來做為自己的值。但是我們對表中數(shù)據(jù)進行了刪除某行的操作后,比如我們有5條數(shù)據(jù),有一列ID它的值是從1到5由SQL SERVER生成的,我們執(zhí)行delete將ID為5的行刪除,但我們再次插入數(shù)據(jù)時,ID是6而不會是5. 雖然5已經(jīng)被刪了但5已經(jīng)被使用過了,所以新插入的行不再取5而從5后面一個開始。
那我非要接著之前的序號開始插入行呢,答案是不行的。曾經(jīng)我也思考過這個問題,相信很多人開始也尋求過這個問題的答案,都無功而返。
后來想了一下把問題想通了,這樣的設(shè)計是很合理的。 接著上面的情境,假如我們刪除了ID為2的行呢,如果僅僅為了保持ID這個序號值的連續(xù)性,那2后面所有行都要自動將自己的ID減一,然后我們插入的數(shù)據(jù)呢再接著最后一行的ID(現(xiàn)在變?yōu)?)插入。那可以想象,這是個多么耗費資源而又沒有任何意義的事情。變更了一行就要變更所有行,如果數(shù)據(jù)量大,那我們僅僅是做一個插入操作就要等半天。更不用說在高訪問量的服務(wù)器上怎么辦了。
HAVING or WHERE:其實這兩個關(guān)鍵字的功能是一樣的,甚至語法也完全一樣,唯一不同的是前者對分組進行過濾。后者對行進行過濾,也是我們用得最多的,似乎忽略了HAVING的存在。何時用HAVING,當你需要在分組內(nèi)進行過濾時就需要它,因為WHERE其實是沒有分組的概念的。
究其根本,WHERE 是先過濾,再分組,HAVING先分組,再過濾。
UNION完成多條WHERE語句相同的功能,結(jié)果集都已經(jīng)自動去除了重復(fù)的行,如果需要返回全部的行,需要使用UNION ALLl 。
對于上面這句話,我們一點點來看。
比如對于Orders表,先看一下里面有什么:
現(xiàn)在我們想返回Orderid=10249的那條數(shù)據(jù)和所有OrderId小于 10252的數(shù)據(jù)。
很明顯可以用兩條帶WHERE的語句來完成。
通過UNION可以把上面兩條獨立的SELECT語句組合起來,但組合前請先注意一下,上面一個返回了1行數(shù)據(jù),另一個返回了4行數(shù)據(jù)。
組合后返回了相同的數(shù)據(jù),只是OrderId為10249那行因為重復(fù)被自動去除了。如果確實需要保留完整的查詢結(jié)果不希望去掉重復(fù)行,可以用UNION ALL, 此時為原汁原味的把兩個SELECT語句返回的結(jié)果相連接,真的是1加1等于二。
注意到我在后面一條SELECT語句使用了ORDER BY,看似它是屬于第二條SELECT 語句的,只對第二條語句檢索出來的結(jié)果進行排序,其實不是,它對連接后的整體進行排序。
如果你使用WHERE,則無法做到保留重復(fù)行。
用CREATE TALBE 創(chuàng)建一張表后,為了插入數(shù)據(jù),你不得不寫一大堆SQL語句。當然,除了這種手動寫INSERT 語句來填充表的情況外,還有更加便捷的方式,可以把另一張表的數(shù)據(jù)全部或者有選擇地返回然后填充到新表。當然,前提是兩張表的結(jié)構(gòu)要一樣,不然相應(yīng)字段對應(yīng)不上如何填充。
下面拿NorthWind中的Region表為列,我們新建一張結(jié)構(gòu)跟他一樣的表,并插入一些數(shù)據(jù)先。
但先看一下Region表是個什么情況先:
之所以選它就是因為簡單,除了一個自增的主鍵列,只包含一列數(shù)據(jù)格式為文本的列。
下面我們就用這張RegionNew表中的數(shù)據(jù)來填充原來的Region表,確切地說是往Region表中再插入一些數(shù)據(jù)。先看INSERT INTO語句。
但如你所見,出錯了。原因是原來的Region表中的RegionDescription列允許空值,而我們新建的表中有一行的對應(yīng)列為空值。這就是前面提到的,兩張表的結(jié)構(gòu)必需要一致才能進行這樣的導入操作,這里的一致不是說表名列名完全一樣,列名其實可以不一樣,只順序?qū)Φ木托?,一致是指兩張表列?shù)相同并且相應(yīng)列保存的數(shù)據(jù)類型要一致。
把RegionNew表中的NULL改后再執(zhí)行一下,如果如下 :
數(shù)據(jù)已經(jīng)插入了。在新建RegionNew這張表時,你也看到了我并沒有讓ID列自動從1開始,而是通過SET IDENTITY_INSERT語句來讓我可以手動編寫ID列的值。不那樣的話RegionNew表中的ID列就會是1,2,3,這將會在你進行導入到Region表時發(fā)生沖突,因為這些數(shù)字已經(jīng)在Region中使用過了。
INSERT SELECT是用一張表填充另一張表,派生出一個SELECT的變種語法SELECT INTO. 使用此語句時目標表可以不存在,INTO后只需指定一個表名,系統(tǒng)會自動創(chuàng)建,當然,也可以跟一張已經(jīng)存在的表,已經(jīng)存在的表同樣需要滿足相應(yīng)列的要求。
現(xiàn)在把更新后的Region表選擇出來導入到一張新表中,在進行這個操作前這張表并不存在。
執(zhí)行完上述語句后在數(shù)據(jù)庫中新建了一張名為Mew的喵星表,并且擁有了Region表的全部數(shù)據(jù)。同時你可以加上WHERE等過濾語句來選擇性地導入數(shù)據(jù):
再發(fā)散一下,你還可以在SELECT時指定多張其他表這樣就可以從多個不同來源把數(shù)據(jù)匯總到一張表上,這個功能確實是很贊的。在你需要一些臨時表備份表進行分析時非常有用。
到這里,你可以把SELECT INTO理解為導出,而INSERT SELECT 視為一種導出操作。
系統(tǒng)提供了一個存儲過程來實現(xiàn)對表重命名 EXEC SP_RENAME ‘oldtablename’,’newtablename’; 重命名還不簡單嘛,直接點擊表就可以了,但命令是用在編程當中的,所以在寫SQL腳本時會非常有用。