Prepare SQL產生的緣由。首先從mysql服務器執行sql的過程開始講起,SQL執行過程包括如下階段 詞法分析->語法分析->語義分析->執行計劃優化->執行。詞法分析->語法分析這兩個階段咱們稱之爲硬解析。詞法分析識別sql中每一個詞,語法分析解析SQL語句是否符合sql語法,並獲得一棵語法樹(Lex)。對於只是參數不一樣,其餘均相同的sql,它們執行時間不一樣但硬解析的時間是相同的。而同一SQL隨着查詢數據的變化,屢次查詢執行時間可能不一樣,但硬解析的時間是不變的。對於sql執行時間較短,sql硬解析的時間佔總執行時間的比率越高。而對於淘寶應用的絕大多數事務型SQL,查詢都會走索引,執行時間都比較短。所以淘寶應用db sql硬解析佔的比重較大。 html
Prepare的出現就是爲了優化硬解析的問題。Prepare在服務器端的執行過程以下java
1) Prepare 接收客戶端帶」?」的sql, 硬解析獲得語法樹(stmt->Lex), 緩存在線程所在的preparestatement cache中。此cache是一個HASH MAP. Key爲stmt->id. 而後返回客戶端stmt->id等信息。mysql
2) Execute 接收客戶端stmt->id和參數等信息。注意這裏客戶端不須要再發sql過來。服務器根據stmt->id在preparestatement cache中查找獲得硬解析後的stmt, 並設置參數,就能夠繼續後面的優化和執行了。git
Prepare在execute階段能夠節省硬解析的時間。若是sql只執行一次,且以prepare的方式執行,那麼sql執行需兩次與服務器交互(Prepare和execute), 而以普通(非prepare)方式,只須要一次交互。這樣使用prepare帶來額外的網絡開銷,可能得不償失。咱們再來看同一sql執行屢次的狀況,好比以prepare方式執行10次,那麼只須要一次硬解析。這時候 額外的網絡開銷就顯得微乎其微了。所以prepare適用於頻繁執行的SQL。github
Prepare的另外一個做用是防止sql注入,不過這個是在客戶端jdbc經過轉義實現的,跟服務器沒有關係。 sql
壓測時經過perf 獲得的結果,硬解析相關的函數比重都比較靠前(MYSQLparse 4.93%, lex_one_token 1.79%, lex_start 1.12%)總共接近8%。所以,服務器使用prepare是能夠帶來較多的性能提高的。緩存
jdbc服務器端的參數:服務器
useServerPrepStmts:默認爲false. 是否使用服務器prepare開關網絡
jdbc客戶端參數:mybatis
cachePrepStmts:默認false.是否緩存prepareStatement對象。每一個鏈接都有一個緩存,是以sql爲惟一標識的LRU cache. 同一鏈接下,不一樣stmt能夠不用從新建立prepareStatement對象。
prepStmtCacheSize:LRU cache中prepareStatement對象的個數。通常設置爲最經常使用sql的個數。
prepStmtCacheSqlLimit:prepareStatement對象的大小。超出大小不緩存。
Jdbc對prepare的處理過程:
useServerPrepStmts=true時Jdbc對prepare的處理
1) 建立PreparedStatement對象,向服務器發送COM_PREPARE命令,並傳送帶問號的sql. 服務器返回jdbc stmt->id等信息
2) 向服務器發送COM_EXECUTE命令,並傳送參數信息。
useServerPrepStmts=false時Jdbc對prepare的處理
1) 建立PreparedStatement對象,此時不會和服務器交互。
2) 根據參數和PreparedStatement對象拼接完整的SQL,向服務器發送QUERY命令
咱們再看參數cachePrepStmts打開時在useServerPrepStmts爲true或false時,均緩存PreparedStatement對象。只不過useServerPrepStmts爲的true緩存PreparedStatement對象包含服務器的stmt->id等信息,也就是說若是重用了PreparedStatement對象,那麼就省去了和服務器通信(COM_PREPARE命令)的開銷。而useServerPrepStmts=false是,開啓cachePrepStmts緩存PreparedStatement對象只是簡單的sql解析信息,所以此時開啓cachePrepStmts意義不是太大。
咱們來開看一段java代碼
Connection con = null; PreparedStatement ps = null; String sql = "select * from user where id=?"; ps = con.prepareStatement(sql); ps.setInt(1, 1); ps.executeQuery(); ps.close(); ps = con.prepareStatement(sql); ps.setInt(1, 3); ps.executeQuery(); ps.close();
這段代碼在同一會話中兩次prepare執行同一語句,而且之間有ps.close();
useServerPrepStmts=false時,服務器會兩次硬解析同一SQL。
useServerPrepStmts=true, cachePrepStmts=false時服務器仍然會兩次硬解析同一SQL。
useServerPrepStmts=true, cachePrepStmts=true時服務器只會硬解析一次SQL。
若是兩次prepare之間沒有ps.close();那麼cachePrepStmts=true,cachePrepStmts=false也只需一次硬解析.
所以,客戶端對同一sql,頻繁分配和釋放PreparedStatement對象的狀況下,開啓cachePrepStmts參數是頗有必要的。
1)作了一個簡單的測試,主要測試prepare的效果和useServerPrepStmts參數的影響.
cnt = 5000; // no prepare
String sql = "select biz_order_id,out_order_id,seller_nick,buyer_nick,seller_id,buyer_id,auction_id,auction_title,auction_price,buy_amount,biz_type,sub_biz_type,fail_reason,pay_status,logistics_status,out_trade_status,snap_path,gmt_create,status,ifnull(buyer_rate_status, 4) buyer_rate_status from tc_biz_order_0030 where " +
"parent_id = 594314511722841 or parent_id =547667559932641;"; begin = new Date(); System.out.println("begin:" + df.format(begin)); stmt = con.createStatement(); for (int i = 0; i < cnt; i++) { stmt.executeQuery(sql); } end = new Date(); System.out.println("end:" + df.format(end)); long temp = end.getTime() - begin.getTime(); System.out.println("no perpare interval:" + temp); // test prepare
sql = "select biz_order_id,out_order_id,seller_nick,buyer_nick,seller_id,buyer_id,auction_id,auction_title,auction_price,buy_amount,biz_type,sub_biz_type,fail_reason,pay_status,logistics_status,out_trade_status,snap_path,gmt_create,status,ifnull(buyer_rate_status, 4) buyer_rate_status from tc_biz_order_0030 where " +
"parent_id = 594314511722841 or parent_id =?;"; ps = con.prepareStatement(sql); BigInteger param = new BigInteger("547667559932641"); begin = new Date(); System.out.println("begin:" + df.format(begin)); for (int i = 0; i < cnt; i++) { ps.setObject(1, param); ps.executeQuery(); } end = new Date(); System.out.println("end:" + df.format(end)); temp = end.getTime() - begin.getTime(); System.out.println("prepare interval:" + temp);
經屢次採樣測試結果以下
非prepare和prepare時間比 | |
useServerPrepStmts=true | 0.93 |
useServerPrepStmts=false | 1.01 |
結論:
useServerPrepStmts=true時,prepare提高7%;
useServerPrepStmts=false時,prepare與非prepare性能至關。
若是將語句簡化爲select * from tc_biz_order_0030 where parent_id =?。那麼測試的結論useServerPrepStmts=true時,prepare僅提高2%;sql越簡單硬解析的時間就越少,prepare的提高就越少。
注意:這個測試是在單個鏈接,單條sql的理想狀況下進行的,線上會出現多鏈接多sql,還有sql執行頻率,sql的複雜程度等不一樣,所以prepare的提高效果會隨具體環境而變化。
2)prepare 先後的perf top 對比
如下爲非prepare
6.46% mysqld mysqld [.] _Z10MYSQLparsePv 3.74% mysqld libc-2.12.so [.] __memcpy_ssse3 2.50% mysqld mysqld [.] my_hash_sort_utf8 2.15% mysqld mysqld [.] cmp_dtuple_rec_with_match 2.05% mysqld mysqld [.] _ZL13lex_one_tokenPvS_ 1.46% mysqld mysqld [.] buf_page_get_gen 1.34% mysqld mysqld [.] page_cur_search_with_match 1.31% mysqld mysqld [.] _ZL14build_templateP19row_prebuilt_structP3THDP5TABLEj 1.24% mysqld mysqld [.] rec_init_offsets 1.11% mysqld libjemalloc.so.1 [.] free 1.09% mysqld mysqld [.] rec_get_offsets_func 1.01% mysqld libjemalloc.so.1 [.] malloc 0.96% mysqld libc-2.12.so [.] __strlen_sse42 0.93% mysqld mysqld [.] _ZN4JOIN8optimizeEv 0.91% mysqld mysqld [.] _ZL15get_hash_symbolPKcjb 0.88% mysqld mysqld [.] row_search_for_mysql 0.86% mysqld [kernel.kallsyms] [k] tcp_recvmsg
如下爲perpare
3.46% mysqld libc-2.12.so [.] __memcpy_ssse3 2.32% mysqld mysqld [.] cmp_dtuple_rec_with_match 2.14% mysqld mysqld [.] _ZL14build_templateP19row_prebuilt_structP3THDP5TABLEj 1.96% mysqld mysqld [.] buf_page_get_gen 1.66% mysqld mysqld [.] page_cur_search_with_match 1.54% mysqld mysqld [.] row_search_for_mysql 1.44% mysqld mysqld [.] btr_cur_search_to_nth_level 1.41% mysqld libjemalloc.so.1 [.] free 1.35% mysqld mysqld [.] rec_init_offsets 1.32% mysqld [kernel.kallsyms] [k] kfree 1.14% mysqld libjemalloc.so.1 [.] malloc 1.08% mysqld [kernel.kallsyms] [k] fget_light 1.05% mysqld mysqld [.] rec_get_offsets_func 0.99% mysqld mysqld [.] _ZN8Protocol24send_result_set_metadataEP4ListI4ItemEj 0.90% mysqld mysqld [.] sync_array_print_long_waits 0.87% mysqld mysqld [.] page_rec_get_n_recs_before 0.81% mysqld mysqld [.] _ZN4JOIN8optimizeEv 0.81% mysqld libc-2.12.so [.] __strlen_sse42 0.78% mysqld mysqld [.] _ZL20make_join_statisticsP4JOINP10TABLE_LISTP4ItemP16st_dynamic_array 0.72% mysqld [kernel.kallsyms] [k] tcp_recvmsg 0.63% mysqld libpthread-2.12.so [.] __pthread_getspecific_internal 0.63% mysqld [kernel.kallsyms] [k] sk_run_filter 0.60% mysqld mysqld [.] _Z19find_field_in_tableP3THDP5TABLEPKcjbPj 0.60% mysqld mysqld [.] page_check_dir 0.57% mysqld mysqld [.] _Z16dispatch_command19enum_server_commandP3THDP
對比能夠發現 MYSQLparse lex_one_token在prepare時已優化掉了。
1 開啓cachePrepStmts的問題,前面談到每一個鏈接都有一個緩存,是以sql爲惟一標識的LRU cache. 在分表較多,大鏈接的狀況下,可能會個應用服務器帶來內存問題。這裏有個前提是ibatis是默認使用prepare的。 在mybatis中,標籤statementType能夠指定某個sql是不是使用prepare.
statementType Any one of STATEMENT, PREPARED or CALLABLE. This causes MyBatis to use Statement, PreparedStatement orCallableStatement respectively. Default: PREPARED.
這樣能夠精確控制只對頻率較高的sql使用prepare,從而控制使用prepare sql的個數,減小內存消耗。遺憾的是目前集團貌似大多使用的是ibatis 2.0版本,不支持statementType
標籤。
2 服務器端prepare cache是一個HASH MAP. Key爲stmt->id,同時也是每一個鏈接都維護一個。所以也有可能出現內存問題,待實際測試。若有必要需改形成Key爲sql的全局cache,這樣不一樣鏈接的相同prepare sql能夠共享。
3 oracle prepare與mysql prepare的區別:
mysql與oracle有一個重大區別是mysql沒有oracle那樣的執行計劃緩存。前面咱們講到SQL執行過程包括如下階段 詞法分析->語法分析->語義分析->執行計劃優化->執行。oracle的prepare實際上包括如下階段:詞法分析->語法分析->語義分析->執行計劃優化,也就是說oracle的prepare作了更多的事情,execute只須要執行便可。所以,oracle的prepare比mysql更高效。