SQL Server 高效能寫入的一些總結
1.1.1 摘要
在開發過程中,我們不時會遇到系統效能瓶頸問題,而引起這一問題原因可以很多,有可能是程式碼不夠高效、有可能是硬體或網路問題,也有可能是資料庫設計的問題。
本篇博文將針對一些常用的資料庫效能調休方法進行介紹,而且,為了編寫高效的SQL程式碼,我們需要掌握一些基本程式碼優化的技巧,所以,我們將從一些基本優化技巧進行介紹。
本文目錄
1.1.2 正文
假設,我們要設計一個部落格系統,其中包含一個使用者表(User),它用來儲存使用者的賬戶名、密碼、顯示名稱和註冊日期等資訊。
由於時間的關係,我們已經把User表設計好了,它包括賬戶名、密碼(注意:這裡沒有考慮隱私資訊的加密儲存)、顯示名稱和註冊日期等,具體設計如下:
-- ============================================= -- Author: JKhuang -- Create date: 7/8/2012 -- Description: A table stores the user information. -- ============================================= CREATE TABLE [dbo].[jk_users]( -- This is the reference to Users table, it is primary key. [ID] [bigint] IDENTITY(1,1) NOT NULL, [user_login] [varchar](60) NOT NULL, [user_pass] [varchar](64) NOT NULL, [user_nicename] [varchar](50) NOT NULL, [user_email] [varchar](100) NOT NULL, [user_url] [varchar](100) NOT NULL, -- This field get the default from function GETDATE(). [user_registered] [datetime] NOT NULL CONSTRAINT [DF_jk_users_user_registered] DEFAULT (getdate()), [user_activation_key] [varchar](60) NOT NULL, [user_status] [int] NOT NULL CONSTRAINT [DF_jk_users_user_status] DEFAULT ((0)), [display_name] [varchar](250) NOT NULL )
圖1 Users表設計
上面,我們定義了Users表,它包含賬戶名、密碼、顯示名稱和註冊日期等10個欄位,其中,ID是一個自增的主鍵,user_resistered用來記錄使用者的註冊時間,它設定了預設值GETDATE()。
接下來,我們將通過客戶端程式碼實現資料儲存到Users表中,具體的程式碼如下:
//// Creates a database connection. var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString()); conn.Open();//// This is a massive SQL injection vulnerability, //// don't ever write your own SQL statements with string formatting! string sql = String.Format( @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')", userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey); var cmd = new SqlCommand(sql, conn); cmd.ExecuteNonQuery(); //// Because this call to Close() is not wrapped in a try/catch/finally clause, //// it could be missed if an exception occurs above. Don't do this! conn.Close();
程式碼中的問題
上面,我們使用再普通不過的ADO.NET方式實現資料寫入功能,但大家是否發現程式碼存在問題或可以改進的地方呢?
首先,我們在客戶端程式碼中,建立一個資料庫連線,它需要佔用一定的系統資源,當操作完畢之後我們需要釋放佔用的系統資源,當然,我們可以手動釋放資源,具體實現如下:
//// Creates a database connection. var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString()); conn.Open();//// This is a massive SQL injection vulnerability, //// don't ever write your own SQL statements with string formatting! string sql = String.Format(@"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')", userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey); var cmd = new SqlCommand(sql, conn); cmd.ExecuteNonQuery(); //// If throws an exception on cmd dispose. cmd.Dispose();//// conn can't be disposed. conn.Close();conn.Dispose();
假如,在釋放SqlCommand資源時丟擲異常,那麼在它後面的資源SqlConnection將得不到釋放。我們仔細想想當發生異常時,可以通過try/catch捕獲異常,所以無論是否發生異常都可以使用finally檢查資源是否已經釋放了,具體實現如下:
SqlCommand cmd = null; SqlConnection conn = null; try { //// Creates a database connection. conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString()); conn.Open(); //// This is a massive SQL injection vulnerability, //// don't ever write your own SQL statements with string formatting! string sql = String.Format( @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')", userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey); cmd = new SqlCommand(sql, conn); cmd.ExecuteNonQuery(); } finally { //// Regardless of whether there is an exception, //// we will dispose the resource. if (cmd != null) cmd.Dispose(); if (conn != null) conn.Dispose(); }
通過上面的finally方式處理了異常情況是很普遍的,但為了更安全釋放資源,使得我們增加了finally和if語句,那麼是否有更簡潔的方法實現資源的安全釋放呢?
其實,我們可以使用using語句實現資源的釋放,具體實現如下:
using語句:定義一個範圍,將在此範圍之外釋放一個或多個物件。
string sql = String.Format( @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')", userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey); //// Creates a database connection. using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString())) using (var cmd = new SqlCommand(sql, conn)) { //// Your code here. }
上面的程式碼使用了using語句實現資源的釋放,那麼是否所有物件都可以使用using語句實現釋放呢?
只有型別實現了IDisposable介面並且重寫Dispose()方法可以使用using語句實現資源釋放,由於SqlConnection和SqlCommand實現了IDisposable介面,那麼我們可以使用using語句實現資源釋放和異常處理。
在客戶端程式碼中,我們使用拼接SQL語句方式實現資料寫入,由於SQL語句是動態執行的,所以惡意使用者可以通過拼接SQL的方式實施SQL隱碼攻擊。
對於SQL隱碼攻擊,我們可以通過以下方式防禦:
- 正則表達校驗使用者輸入
- 引數化儲存過程
- 引數化SQL語句
- 新增資料庫新架構
- LINQ to SQL
接下來,我們將通過引數化SQL語句防禦SQL隱碼攻擊,大家也可以使用其他的方法防禦SQL隱碼攻擊,具體實現程式碼如下:
//// Creates a database connection. using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString())) { conn.Open(); string sql = string.Format( @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key)"); using (var cmd = new SqlCommand(sql, conn)) { //// Parameterized SQL to defense injection attacks cmd.Parameters.Add("@user_login", userLogin); cmd.Parameters.Add("@user_pass", userPass); cmd.Parameters.Add("@user_nicename", userNicename); cmd.Parameters.Add("@user_email", userEmail); cmd.Parameters.Add("@user_status", userStatus); cmd.Parameters.Add("@display_name", displayName); cmd.Parameters.Add("@user_url", userUrl); cmd.Parameters.Add("@user_activation_key", userActivationKey); cmd.ExecuteNonQuery(); } }
上面通過引數化SQL語句和using語句對程式碼進行改進,現在程式碼的可讀性更強了,而且也避免了SQL隱碼攻擊和資源釋放等問題。
接下來,讓我們簡單的測試一下程式碼執行時間,首先我們在程式碼中新增方法Stopwatch.StartNew()和Stopwatch.Stop()來計算寫入程式碼的執行時間,具體程式碼如下:
//// calc insert 10000 records consume time. var sw = Stopwatch.StartNew(); //// Creates a database connection. using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString())) { conn.Open(); int cnt = 0; while (cnt++ < 10000) { string sql = string.Format(@"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key) VALUES (@user_login, @user_pass, @user_nicename, @user_email, @user_status, @display_name, @user_url, @user_activation_key)"); using (var cmd = new SqlCommand(sql, conn)) { //// Parameterized SQL to defense injection attacks cmd.Parameters.Add("@user_login", userLogin); cmd.Parameters.Add("@user_pass", userPass); cmd.Parameters.Add("@user_nicename", userNicename); cmd.Parameters.Add("@user_email", userEmail); cmd.Parameters.Add("@user_status", userStatus); cmd.Parameters.Add("@display_name", displayName); cmd.Parameters.Add("@user_url", userUrl); cmd.Parameters.Add("@user_activation_key", userActivationKey); cmd.ExecuteNonQuery(); } } } sw.Stop(); }
上面,我們往資料庫中寫入了10000條資料,執行時間為 7.136秒(我的機器很破了),這樣系統效能還是可以滿足許多公司的需求了。
假如,使用者請求量增大了,我們還能保證系統能滿足需求嗎?事實上,我們不應該滿足於現有的系統效能,因為我們知道程式碼的執行效率還有很大的提升空間。
接下來,將進一步介紹程式碼改善的方法。
圖2 資料寫入Users表
為了使資料庫獲得更快的寫入速度,我們必須瞭解資料庫在進行寫入操作時的主要耗時。
資料庫效能開銷
連線時間
當我們執行conn.Open()時,首先,必須建立物理通道(例如套接字或命名管道),必須與伺服器進行初次握手,必須分析連線字串資訊,必須由伺服器對連線進行身份驗證,必須執行檢查以便在當前事務中登記,等等
這一系列操作可能需要一兩秒鐘時間,如果我們每次執行conn.Open()都有進行這一系列操作是很耗費時間的,為了使開啟的連線成本最低,ADO.NET使用稱為連線池的優化方法。
連線池:減少新連線需要開啟的次數,只要使用者在連線上呼叫 Open()方法,池程式就會檢查池中是否有可用的連線,如果某個池連線可用,那麼將該連線返回給呼叫者,而不是建立新連線;應用程式在該連線上呼叫 Close()或Dispose() 時,池程式會將連線返回到活動連線池集中,而不是真正關閉連線,連線返回到池中之後,即可在下一個 Open 呼叫中重複使用。
解析器的開銷
當我們向SQL Server傳遞SQL語句INSERT INTO …時,它需要對SQL語句進行解析,由於SQL Server解析器執行速度很快,所以解析時間往往是可以忽略不計,但我們仍然可以通過使用儲存過程,而不是直SQL語句來減少解析器的開銷。
資料庫連線
為了提供ACID(事務的四個特性),SQL Server必須確保所有的資料庫更改是有序的。它是通過使用鎖來確保該資料庫插入、刪除或更新操作之間不會相互衝突(關於資料庫的鎖請參考這裡)。
由於,大多數資料庫都是面向多使用者的環境,當我們對User表進行插入操作時,也許有成千上百的使用者也在對User表進行操作,所以說,SQL Server必須確保這些操作是有序進行的。
那麼,當SQL Server正在做所有這些事情時,它會產生鎖,以確保使用者獲得有意義的結果。SQL Server保證每條語句執行時,資料庫是完全可預測的(例如:預測SQL執行方式)和管理鎖都需要耗費一定的時間。
約束處理
在插入資料時,每個約束(如:外來鍵、預設值、SQL CHECK等)需要額外的時間來檢測資料是否符合約束;由於SQL Server為了保證每個插入、更新或刪除的記錄都符合約束條件,所以,我們需要考慮是否應該在資料量大的表中增加約束條件。
Varchar
VARCHAR是資料庫常用的型別,但它也可能導致意想不到的效能開銷;每次我們儲存可變長度的列,那麼SQL Server必須做更多的記憶體管理;字串可以很容易地消耗數百位元組的記憶體的,如果我們在一個VARCHAR列中設定索引,那麼SQL Server執行B-樹搜尋時,就需要進行O(字串長度)次比較,然而,整數字段比較次數只受限於記憶體延遲和CPU頻率。
磁碟IO
SQL Server最終會將資料寫入到磁碟中,首先,SQL Server把資料寫入到事務日誌中,當執行備份時,事務日誌會合併到永久的資料庫檔案中;這一系列操作由後臺完成,它不會影響到資料查詢的速度,但每個事物都必須擁有屬於自己的磁碟空間,所以我們可以通過給事務日誌和主資料檔案分配獨立的磁碟空間減少IO開銷,當然,最好解決辦法是儘可能減少事務的數量。
正如大家所看到的,我們通過優化聯接時間、 解析器的開銷、 資料庫聯接、約束處理,、Varchar和磁碟IO等方法來優化資料庫,接下來,我們將對前面的例子進行進一步的優化。
使用儲存過程
前面例子中,我們把SQL程式碼直接Hardcode在客戶端程式碼中,那麼,資料庫就需要使用解析器解析客戶端中SQL語句,所以我們可以改用使用儲存過程,從而,減少解析器的時間開銷;更重要的一點是,由於SQL是動態執行的,所以我們修改儲存過程中的SQL語句也無需重新編譯和釋出程式。
User表中的欄位user_registered設定了預設值(GETDATE()),那麼我們通過消除表預設值約束來提高系統的效能,簡而言之,我們需要提供欄位user_registered的值。
接下來,讓我們省去User表中的預設值約束和增加儲存過程,具體程式碼如下:
-- ============================================= -- Author: JKhuang -- Create date: 08/16/2012 -- Description: Creates stored procedure to insert -- data into table jk_users. -- ============================================= ALTER PROCEDURE [dbo].[SP_Insert_jk_users] @user_login varchar(60), @user_pass varchar(64), @user_nicename varchar(50), @user_email varchar(100), @user_url varchar(100), @user_activation_key varchar(60), @user_status int, @display_name varchar(250) AS BEGIN SET NOCOUNT ON; -- The stored procedure allows SQL server to avoid virtually all parser work INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key, user_registered) VALUES (@user_login, @user_pass, @user_nicename, @user_email, @user_status, @display_name, @user_url, @user_activation_key, GETDATE()); END
上面我們定義了儲存過程SP_Insert_jk_users向表中插入資料,當我們重新執行程式碼時,發現資料插入的時間縮短為6.7401秒。
圖3資料寫入時間
使用資料庫事務
想想資料是否可以延長寫入到資料庫中,是否可以批量地寫入呢?如果允許延遲一段時間才寫入到資料庫中,那麼我們可以使用Transaction來延遲資料寫入。
資料庫事務是資料庫管理系統執行過程中的一個邏輯單位,由一個有限的資料庫操作序列構成。 SQL Server確保事務執行成功後,資料寫入到資料庫中,反之,事務將回滾。
如果我們對資料庫進行十次獨立的操作,那麼SQL Server就需要分配十次鎖開銷,但如果把這些操作都封裝在一個事務中,那麼SQL Server只需要分配一次鎖開銷。
//// calc insert 10000 records consume time. var sw = Stopwatch.StartNew(); //// Creates a database connection. using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString())) { conn.Open(); int cnt = 0; SqlTransaction trans = conn.BeginTransaction(); while (cnt++ < 10000) { using (var cmd = new SqlCommand("SP_Insert_jk_users", conn)) { //// Parameterized SQL to defense injection attacks cmd.CommandType = CommandType.StoredProcedure; //// Uses transcation to batch insert data. //// To avoid lock and connection overhead. cmd.Transaction = trans; cmd.Parameters.Add("@user_login", userLogin); cmd.Parameters.Add("@user_pass", userPass); cmd.Parameters.Add("@user_nicename", userNicename); cmd.Parameters.Add("@user_email", userEmail); cmd.Parameters.Add("@user_status", userStatus); cmd.Parameters.Add("@display_name", displayName); cmd.Parameters.Add("@user_url", userUrl); cmd.Parameters.Add("@user_activation_key", userActivationKey); cmd.ExecuteNonQuery(); } } //// If no exception, commit transcation. trans.Commit(); } sw.Stop(); }
圖4 資料寫入時間
使用SqlBulkCopy
通過使用事務封裝了寫入操作,當我們重新執行程式碼,發現資料寫入的速度大大提高了,只需4.5109秒,由於一個事務只需分配一次鎖資源,減少了分配鎖和資料庫聯接的耗時。
當然,我們可以也使用SqlBulkCopy實現大量資料的寫入操作,首先我們建立資料行,然後使用SqlBulkCopy的WriteToServer()方法將資料行批量寫入到表中,具體實現程式碼如下:
/// <summary> /// Gets the data rows. /// </summary> /// <returns></returns> DataRow[] GetDataRows(int rowCnt) { //// Creates a custom table. var dt = new DataTable("jk_users"); dt.Columns.Add(new DataColumn("user_login", typeof(System.String))); dt.Columns.Add(new DataColumn("user_pass", typeof(System.String))); dt.Columns.Add(new DataColumn("user_nicename", typeof(System.String))); dt.Columns.Add(new DataColumn("user_email", typeof(System.String))); dt.Columns.Add(new DataColumn("user_url", typeof(System.String))); dt.Columns.Add(new DataColumn("user_registered", typeof(System.DateTime))); dt.Columns.Add(new DataColumn("user_activation_key", typeof(System.String))); dt.Columns.Add(new DataColumn("user_status", typeof(System.Int32))); dt.Columns.Add(new DataColumn("display_name", typeof(System.String))); //// Initializes data row. var dr = dt.NewRow(); dr["user_login"] = "JK_RUSH"; dr["user_pass"] = "D*<1C2jK#-"; dr["user_nicename"] = "JK"; dr["user_email"] = "jkhuang@gamil.com"; dr["user_status"] = 1; dr["display_name"] = "JK_RUSH"; dr["user_url"] = "http://www.cnblogs.com/rush"; dr["user_activation_key"] = "347894102386"; dr["user_registered"] = DateTime.Now; //// Creates data row array. var dataRows = new DataRow[rowCnt]; for (int i = 0; i < rowCnt; i++) { dataRows[i] = dr; } return dataRows; }
前面,我們定義了GetDataRows()方法用來建立資料行,首先我們建立了一個自定義表,給該表新增相應的資料列,這裡我們把資料列都命名為對應於表中列名,當然,名字可以不一樣,這時我們就有一個疑問了,那麼資料庫如何把自定義資料列和表中資料列對應起來呢?其實,我們需要呼叫ColumnMappings.Add方法建立起自定義資料列和表中資料列的對應關係,接下來,我們呼叫SqlBulkCopy的WriteToServer()方法將資料行寫入表中。
//// Creates 10001 data rows. var dataRows = GetDataRows(10001); var sw = Stopwatch.StartNew(); //// Creates a database connection. using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString())) { conn.Open(); using (var bulkCopy = new SqlBulkCopy(conn)) { //// Maping the data columns. bulkCopy.ColumnMappings.Add("user_login", "user_login"); bulkCopy.ColumnMappings.Add("user_pass", "user_pass"); bulkCopy.ColumnMappings.Add("user_nicename", "user_nicename"); bulkCopy.ColumnMappings.Add("user_email", "user_email"); bulkCopy.ColumnMappings.Add("user_url", "user_url"); bulkCopy.ColumnMappings.Add("user_registered", "user_registered"); bulkCopy.ColumnMappings.Add("user_activation_key", "user_activation_key"); bulkCopy.ColumnMappings.Add("user_status", "user_status"); bulkCopy.ColumnMappings.Add("display_name", "display_name"); bulkCopy.DestinationTableName = "dbo.jk_users"; //// Insert data into datatable. bulkCopy.WriteToServer(dataRows); } sw.Stop(); }
圖5 資料寫入時間
上面,我們通過事務和SqlBulkCopy實現資料批量寫入資料庫中,但事實上,每次我們呼叫cmd.ExecuteNonQuery()方法都會產生一個往返訊息,從客戶端應用程式到資料庫中,所以我們想是否存在一種方法只傳送一次訊息就完成寫入的操作呢?
使用表引數
如果,大家使用SQL Server 2008,它提供一個新的功能表變數(Table Parameters)可以將整個表資料彙整合一個引數傳遞給儲存過程或SQL語句。它的注意效能開銷是將資料彙整合引數(O(資料量))。
現在,我們修改之前的程式碼,在SQL Server中定義我們的表變數,具體定義如下:
-- ============================================= -- Author: JKhuang -- Create date: 08/16/2012 -- Description: Declares a user table paramter. -- ============================================= CREATE TYPE jk_users_bulk_insert AS TABLE ( user_login varchar(60), user_pass varchar(64), user_nicename varchar(50), user_email varchar(100), user_url varchar(100), user_activation_key varchar(60), user_status int, display_name varchar(250) )
上面,我們定義了一個表引數jk_users_bulk_insert,接著我們定義一個儲存過程接受表引數jk_users_bulk_insert,具體定義如下:
-- ============================================= -- Author: JKhuang -- Create date: 08/16/2012 -- Description: Creates a stored procedure, receive -- a jk_users_bulk_insert argument. -- ============================================= CREATE PROCEDURE sp_insert_jk_users @usersTable jk_users_bulk_insert READONLY AS INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_url, user_activation_key, user_status, display_name, user_registered) SELECT user_login, user_pass, user_nicename, user_email, user_url, user_activation_key, user_status, display_name, GETDATE() FROM @usersTable
接下我們在客戶端程式碼中,呼叫儲存過程並且將表作為引數方式傳遞給儲存過程。
var sw = Stopwatch.StartNew(); using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString())) { conn.Open(); //// Invokes the stored procedure. using (var cmd = new SqlCommand("sp_insert_jk_users", conn)) { cmd.CommandType = CommandType.StoredProcedure; //// Adding a "structured" parameter allows you to insert tons of data with low overhead var param = new SqlParameter("@userTable", SqlDbType.Structured) { Value = dt }; cmd.Parameters.Add(param); cmd.ExecuteNonQuery(); } } sw.Stop();
現在,我們重新執行寫入操作發現寫入效率與SqlBulkCopy相當。
1.1.3總結
本文通過部落格系統使用者表設計的例子,介紹我們在設計過程中容易犯的錯誤和程式碼的缺陷,例如:SQL隱碼攻擊、資料庫資源釋放等問題;進而使用一些常用的程式碼優化技巧對程式碼進行優化,並且通過分析資料庫寫入的效能開銷(連線時間、解析器、資料庫連線、約束處理、VARCHAR和磁碟IO),我們使用儲存過程、資料庫事務、SqlBulkCopy和表引數等方式降低資料庫的開銷。
參考
[1] http://beginner-sql-tutorial.com/sql-query-tuning.htm
[2] http://www.dzone.com/links/r/sql_optimization_tipsquestions.html
[3] http://blackrabbitcoder.net/archive/2010/11/11/c.net-little-wonders---a-presentation.aspx
[4] http://www.altdevblogaday.com/2012/05/16/sql-server-high-performance-inserts/
相關文章
- SQL Server大小寫 總結 --未完待續SQLServer
- SQL Server Alwayson概念總結SQLServer
- SQL Server函式總結SQLServer函式
- Sql Server資料庫的一些知識點定義和總結SQLServer資料庫
- [zt] sql server 死鎖總結SQLServer
- SQL SERVER 跟蹤標記總結SQLServer
- MS Sql server 總結(命令恢復)SQLServer
- SQL Server自增列跳號總結SQLServer
- Sql Server使用者名稱和登入名的關係總結SQLServer
- SQL Azure與SQL Server的異同點總結和歸納SQLServer
- SQL Server 中的一些概念SQLServer
- 如何寫出高效能SQLSQL
- Sql Server關於indexed view索引檢視的總結SQLServerIndexView索引
- 寫java程式碼的一些個人感受和總結Java
- sql server 大小寫敏感SQLServer
- html input文字輸入框的一些總結HTML
- sql server 中的一些實用的sql語句SQLServer
- SQL Server 遷移至MySQL 關鍵步驟的梳理總結ServerMySql
- SQL Server資料庫查詢優化的方法總結SQLServer資料庫優化
- 關於SQL Server資料查詢基本方法的總結SQLServer
- SQL Server資料庫SA許可權總結SQLServer資料庫
- 個人總結的一些寫JS程式碼的基本規範JS
- Sql Server關於許可權、角色以及登入名、使用者名稱的總結SQLServer
- Mysql學習總結(50)——Oracle,mysql和SQL Server的區別MySqlOracleServer
- 如何寫出高效能SQL語句SQL
- 總結在SQL Server檢視管理中限制條件SQLServer
- 總結一些,書寫 CSS 的時候,經常犯的錯誤!CSS
- Redux的一些總結Redux
- mysql的一些總結MySql
- SQL Server連線SQL Server、SQL Server連線ORACLE 連結伺服器SQLServerOracle伺服器
- SQL Server 索引結構SQLServer索引
- MSSQL-SQL SERVER一些使用中的技巧SQLServer
- SQL server中的日期變數縮寫SQLServer變數
- Microsoft Sql Server 命令彙總ROSSQLServer
- 基於Sql server資料庫的四種分頁方式總結SQLServer資料庫
- SQL Server日誌檔案總結及日誌滿的處理SQLServer
- ----------------SQL Server2000中死鎖經驗總結 ---------------SQLServer
- SQL Server系統儲存過程和引數總結SQLServer儲存過程