上周六,寫了第一篇博客《訂餐系統之權限設計》,在此感謝那些鼓勵、關注我的園友們,更要感謝那些提出寶貴建議的朋友們。看了你們的評論,才真切的感受到:朋友們的評論往往會讓文章更有看點。上篇文章中 鄭明、人生就是賭 等幾個園友的留言讓我對我們系統的權限優化有了方向。當然,這樣的優化肯定不是一天兩天的事,做技術的朋友應該都知道:一個難題經常啃啃,某天也許就有了好的方案了(近段時間啃掉了幾個2、3年前未處理好的的問題,才想起初中數學老師讓我們經常啃一些競賽題的良苦用心),今天的文章說的就是一個從2010年就想優化,但一直未優化好的功能,也從 幸福框架的評論中,看到了他的博客,更是從他博客的留言中,找到了處理:按日期+6位順序號生成訂單編號(主要處理並發的情況)的方案,之前有客戶要這樣生成訂單編號,我只能回復實現不了,因為當時只知道,每次獲取最大的訂單編號,處理不了並發的情況,慚愧。
關於這個標題,我還是交代下背景吧。這個問題從2010年第一次實現時,就覺得當時那種方案太差了,自己都看不去,只因沒有別的辦法,從那以后,每每得空,就拿出來琢磨下,現在這個方案也許還有不少問題,也希望各位指點下。我們是做訂餐系統的,主要實體就是商家(有坐標)和用戶(有坐標)。所以就有這么個需求,返回距離N公里內的商家,按距離從近到遠排序。先看下,數據庫設計吧,如圖(1):
圖(1)
下面我先介紹下這幾個表的關系吧:
ETogo :商家表,dataid表示商家編號,togoname表示商家名稱。
ETogoLocalInfo:商家定位表,togoid對應etogo.dataid,lat表示商家緯度,lng表示商家經度(經緯度在地圖上標注所得)
EAddress :用戶地址表,保存用戶的經緯度,lat表示用戶緯度,lng表示用戶經度(經緯度在地圖上標注所得)
生活的經驗告訴我們:一條成功的路,背后總有數不完的錯誤的路。下面我把那些曾經錯誤的路也寫下來,以作對比。
當時年少,對sql基本只會簡單的select,更多的東西依賴於應用程序,於是有了下面的代碼,由於對第二頁的處理不了,所以用了方法:getDistancetogoid獲取前N頁的id然后再處理,里面關於距離的語句每次同事用到都抱怨,如果再加的N公里內的,這個語句就麻煩了。

/// <summary> /// 獲取列表,返回距離排序(出現商家重復的現象目前不采用) /// </summary> /// <param name="pagesize">每頁大小</param> /// <param name="pageindex">當前頁數</param> /// <param name="strWhere">搜索條件</param> /// <param name="orderName">排序字段</param> /// <param name="orderType">排序類型</param> /// <param name="mylat">用戶緯度</param> /// <param name="mylng">用戶經度</param> /// <returns></returns> public IList<TogoInfo> GetDistanceList(int pagesize, int pageindex, string strWhere, string orderName, int orderType, string mylat, string mylng) { IList<TogoInfo> infos = new List<TogoInfo>(); string ids = ""; if (pageindex > 1) { ids = getDistancetogoid(pagesize, pageindex, strWhere, orderName, orderType, mylat, mylng); if (ids == "") { return infos; } } SqlParameter[] parameters = { new SqlParameter("@tblName", SqlDbType.VarChar,255), new SqlParameter("@strGetFields", SqlDbType.VarChar,2000), new SqlParameter("@primary", SqlDbType.VarChar,255), new SqlParameter("@orderName", SqlDbType.VarChar,255), new SqlParameter("@PageSize", SqlDbType.Int), new SqlParameter("@PageIndex", SqlDbType.Int), new SqlParameter("@OrderType", SqlDbType.Bit), new SqlParameter("@strWhere", SqlDbType.VarChar,4500), new SqlParameter("@ids", SqlDbType.VarChar,2000) }; string orderstr = orderType > 0 ? "desc" : "asc"; parameters[0].Value = "etogo"; string field = "*, (select Lat from ETogoLocalInfo where togoid = etogo.dataid) as lat ,(select lng from ETogoLocalInfo where togoid = etogo.dataid) as lng, (select lastlogindate from ETogoPrinter where ETogoPrinter.togoid = etogo.dataid ) lastlogindate,(select (Case when DateDiff(mi,LastLoginDate,getdate()) < 5 then 1 else 0 end) from etogoprinter where ETogoPrinter.togoid = etogo.dataid) as online"; field += ",(( 6371 * acos( cos( radians(" + mylat + ") ) * cos( radians( (select Lat from ETogoLocalInfo where togoid = etogo.dataid) )) * cos( radians( (select lng from ETogoLocalInfo where togoid = etogo.dataid) ) - radians(" + mylng + ") ) + sin( radians(" + mylat + ") ) * sin( radians( (select Lat from ETogoLocalInfo where togoid = etogo.dataid) ) ) ) )) as Distance "; field += " ,CASE WHEN( ( CONVERT(varchar(12) , Time1Start, 114 ) < CONVERT(varchar(12) , getdate(), 114 )"; field += "and CONVERT(varchar(12) , Time1End, 114 ) > CONVERT(varchar(12) , getdate(), 114 )"; field += ") or ( CONVERT(varchar(12) , Time2Start, 114 ) < CONVERT(varchar(12) , getdate(), 114 )"; field += "and CONVERT(varchar(12) , Time2End, 114 ) > CONVERT(varchar(12) , getdate(), 114 )"; field += ") )THEN 1 ELSE 0 END AS havenew "; parameters[1].Value = field; parameters[2].Value = "DataID"; parameters[3].Value = orderName; parameters[4].Value = pagesize; parameters[5].Value = pageindex; parameters[6].Value = orderType; parameters[7].Value = strWhere; parameters[8].Value = ids; using (SqlDataReader dr = SQLHelper.ExecuteReader(CommandType.StoredProcedure, "pageselectpri_togo", parameters)) { while (dr.Read()) { TogoInfo info = new TogoInfo(); info.DataID = HJConvert.ToInt32(dr["DataID"]); info.Picture = HJConvert.ToString(dr["Picture"]); info.TogoName = HJConvert.ToString(dr["TogoName"]); int isonline = HJConvert.ToInt32(dr["havenew"]); if (togostatus == 1 && isonline == 1) { info.Status = 1; } else { info.Status = 0; } string _distance = HJConvert.ToString(dr["Distance"]); if (mylat == "0" || _distance == "") { info.mydistance = "0"; } else { info.mydistance = Convert.ToString(Convert.ToInt32(Convert.ToDecimal(_distance) * 1000)); } info.Lat = HJConvert.ToString(dr["Lat"]); if (info.Lat == "") { info.Lat = "0"; } info.Lng = HJConvert.ToString(dr["Lng"]); if (info.Lng == "") { info.Lng = "0"; } infos.Add(info); } } return infos; } /// <summary> /// 獲取所有商家的名稱和對應的編號 /// </summary> /// <param name="pagesize">每頁大小</param> /// <param name="pageindex">當前頁數</param> /// <param name="strWhere">搜索條件</param> /// <param name="orderName">排序字段</param> /// <param name="orderType">排序類型</param> /// <param name="mylat">用戶緯度</param> /// <param name="mylng">用戶經度</param> public string getDistancetogoid(int pagesize, int pageindex, string sqlwhere, string sortname, int ordertype, string mylat, string mylng) { string ids = ""; string orderstr = ordertype > 0 ? "desc" : "asc"; int top = (pageindex - 1) * pagesize; string field = "select top " + top + " dataid"; field += ",(( 6371 * acos( cos( radians(" + mylat + ") ) * cos( radians( (select Lat from ETogoLocalInfo where togoid = etogo.dataid) )) * cos( radians( (select lng from ETogoLocalInfo where togoid = etogo.dataid) ) - radians(" + mylng + ") ) + sin( radians(" + mylat + ") ) * sin( radians( (select Lat from ETogoLocalInfo where togoid = etogo.dataid) ) ) ) )) as Distance "; field += " from etogo where " + sqlwhere + " order by " + sortname + " " + orderstr + " , dataid desc"; using (SqlDataReader dr = SQLHelper.ExecuteReader(CommandType.Text, field, null)) { while (dr.Read()) { ids += dr["dataid"] + ","; } } ids = System.Text.RegularExpressions.Regex.Replace(ids, @",$", ""); return ids; }
以下是代碼中用到的分頁存儲過程:pageselectpri_togo

CREATE PROCEDURE [dbo].[pageselectpri_togo] @tblName varchar(255), -- 表名 @strGetFields varchar(1000) = '*', -- 需要返回的列 @primary varchar(255)='', -- 主鍵的字段名 @orderName varchar(255)='', --要排序的字段名 @PageSize int = 10, -- 頁尺寸 @PageIndex int = 1, -- 頁碼 @OrderType bit = 0, -- 設置排序類型, 非 0 值則降序 @strWhere varchar(1500) = '', -- 查詢條件 (注意: 不要加 where) @ids varchar(2000) AS declare @strSQL varchar(5000) -- 主語句 declare @strTmp varchar(110) -- 臨時變量 declare @strOrder varchar(400) -- 排序類型 if @OrderType != 0 begin set @strTmp = ' not in (select ' set @strOrder = ' order by ' + @orderName +' desc' if @orderName <> @primary begin set @strOrder = @strOrder + ',[' + @primary +'] desc' end --如果@OrderType不是0,就執行降序,這句很重要! end else begin set @strTmp = ' not in (select ' set @strOrder = ' order by ' + @orderName +' asc' if @orderName <> @primary begin set @strOrder = @strOrder + ',[' + @primary +'] asc' end 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 --如果是第一頁就執行以上代碼,這樣會加快執行速度 end else--后面的頁數 begin --以下代碼賦予了@strSQL以真正執行的SQL代碼 set @strSQL = 'select top ' + str(@PageSize) +' '+@strGetFields+ ' from [' + @tblName + '] where [' + @primary + ']' + @strTmp + '['+ @primary + '] from (select top ' + str((@PageIndex-1)*@PageSize) + ' ['+ @primary + '] from [' + @tblName + ']) as tblTmp)'+ @strOrder if @strWhere != '' set @strSQL = 'select top ' + str(@PageSize) +' '+@strGetFields+ ' from [' + @tblName + '] where [' + @primary + '] not in ( '+@ids+' ) and ' + @strWhere + ' ' + @strOrder end exec (@strSQL)
此方案用時基本沒有問題,但是就是太麻煩了,一個不留神,sql語句就拼錯了。
從那時實現了之前的方案后,后面的項目都沿用,每每用起時,心中總會不痛快,於是開始經常關注這個,一次機會看到了下面的代碼(之前一篇博客中看到的,已太久了,記不起出處了,哪位看到別介意),這個方案中也是大量拼接sql,很容易就出錯了,另外,對最后一頁還要做特別的處理,只因可以用一個方法(2010年方案要用兩個方法,查兩次數據庫)實現,就放到項目中了,后來,經常有重復的商家,或者有些未顯示出來,於是部分項目又用了2010年的方案,那個是麻煩,但至少沒有錯誤。

/// <summary> /// 獲取列表,返回距離排序 /// </summary> /// <param name="pagesize">每頁大小</param> /// <param name="pageindex">當前頁數</param> /// <param name="strWhere">搜索條件</param> /// <param name="orderName">排序名稱</param> /// <param name="orderType">排序類型</param> /// <param name="mylat">用戶緯度</param> /// <param name="mylng">用戶經度</param> /// <returns></returns> public IList<TogoInfo> GetDistanceList(int pagesize, int pageindex, string strWhere, string orderName, int orderType, string mylat, string mylng) { IList<TogoInfo> infos = new List<TogoInfo>(); StringBuilder sb = new StringBuilder(); string distancepstr = "(( 6371 * acos( cos( radians(" + mylat + ") ) * cos( radians( (b.lat) )) * cos( radians( (b.lng) ) - radians(" + mylng + ") ) + sin( radians(" + mylat + ") ) * sin( radians( ( b.Lat) ) ) ) )) as Distance "; int endrow = pagesize * pageindex; string ordertype = orderType == 1 ? "desc" : "asc"; string _ordertype = orderType == 0 ? "desc" : "asc"; sb.Append("select a.* ,b.* ,"); sb.Append(distancepstr); string field = " CASE WHEN( ( CONVERT(varchar(12) , Time1Start, 114 ) < CONVERT(varchar(12) , getdate(), 114 )"; field += "and CONVERT(varchar(12) , Time1End, 114 ) > CONVERT(varchar(12) , getdate(), 114 )"; field += ") or ( CONVERT(varchar(12) , Time2Start, 114 ) < CONVERT(varchar(12) , getdate(), 114 )"; field += "and CONVERT(varchar(12) , Time2End, 114 ) > CONVERT(varchar(12) , getdate(), 114 )"; field += ") )THEN 1 ELSE 0 END AS havenew"; sb.Append(" , "+ field); sb.Append(" from etogo as a left join ETogoLocalInfo as b on a.dataid = b.togoid "); sb.Append(" where a.dataid in "); sb.Append(" ( select top " + pagesize + " dataid from "); sb.Append(" (select top " + endrow + " a.dataid , " + distancepstr + ",sortnum from etogo as a left join ETogoLocalInfo as b on a.dataid = b.togoid where " + strWhere); sb.Append(" order by " + orderName + " " + ordertype + "" + " ,a.dataid desc ) as mytepm "); sb.Append(" order by " + orderName + " " + _ordertype + "" + ", dataid asc )"); sb.Append(" order by " + orderName + " " + ordertype + "" + ", a.dataid desc "); //Hangjing.Common.HJlog.toLog(sb.ToString()); using (SqlDataReader dr = SQLHelper.ExecuteReader(CommandType.Text,sb.ToString(), null)) { while (dr.Read()) { TogoInfo info = new TogoInfo(); info.DataID = HJConvert.ToInt32(dr["DataID"]); info.Picture = HJConvert.ToString(dr["Picture"]); info.TogoName = HJConvert.ToString(dr["TogoName"]); int togostatus = HJConvert.ToInt32(dr["Status"]); int isonline = HJConvert.ToInt32(dr["havenew"]); if (togostatus == 1 && isonline == 1) { info.Status = 1; } else { info.Status = 0; } info.mydistance = HJConvert.ToString(dr["Distance"]); if (info.mydistance == "") { info.mydistance = "-1"; } info.Lat = HJConvert.ToString(dr["Lat"]); info.Lng = HJConvert.ToString(dr["Lng"]); infos.Add(info); } } return infos; }
終於有一次再也忍受不了了,於是下定決心要優化(當然,就我目前的水平,想到的更多還是方便書寫)了,當前就想了一點,不在程序中拼接距離的sql語句。經過多次修改,於是有了下面的代碼:

-- ============================================= -- Author: jijunjian -- Create date: 2013-5-7 -- Description: 獲取商家列表(含坐標,按距離排序) -- 調用 EXEC ETogo_GetShopListWithDistance 10,1,'distance','asc','1=1','30.313035','120.390998','distance<1' -- ============================================= CREATE PROCEDURE [dbo].[ETogo_GetShopListWithDistance] @pagesize int, --分頁大小 @pageindex int, --頁碼 @orderfield varchar(20),--排序字段名稱 @ordertype varchar(5), --排序類別 desc asc @where varchar(2000), --查詢條件 @lat VARCHAR(50),--用戶緯度 @lng VARCHAR(50),--用戶經度, @otherwhere VARCHAR(2000)--這個條件是用來判斷距離,及根據營業時間的狀態條件 AS DECLARE @startRow int, @endRow int, @sql varchar(4000), @ordername varchar(200)--排序字段 SET @ordername = '( 6371 * acos( cos( radians(' + @lat + ') ) * cos( radians( Lat )) * cos( radians( lng ) - radians(' + @lng + ') ) + sin( radians(' + @lat + ') ) * sin( radians( Lat ) ) ) )' IF @otherwhere = '' SET @otherwhere = '1=1' SET @startRow = (@pageindex - 1) * @pagesize + 1 SET @endRow = @startRow + @pagesize - 1 SET @sql = ' SELECT * FROM ( select *,row_number() over(order by '+@orderfield+' '+@ordertype+') as [rowid] from ( select '+@ordername+' as distance,dbo.ETogo.*,ETogoLocalInfo.Lat,ETogoLocalInfo.Lng, CASE WHEN( ( CONVERT(varchar(12) , Time1Start, 114 ) < CONVERT(varchar(12) , getdate(), 114 ) and CONVERT(varchar(12) , Time1End, 114 ) > CONVERT(varchar(12) , getdate(), 114 ) ) or ( CONVERT(varchar(12) , Time2Start, 114 ) < CONVERT(varchar(12) , getdate(), 114 ) and CONVERT(varchar(12) , Time2End, 114 ) > CONVERT(varchar(12) , getdate(), 114 ) ) )THEN 1 ELSE 0 END AS havenew from etogo LEFT JOIN ETogoLocalInfo ON etogo.dataid=ETogoLocalInfo.togoid WHERE '+ @where +' ) m where '+ @otherwhere +' )m2 WHERE ROWID BETWEEN '+convert(varchar(5), @startRow) +' AND '+ convert(varchar(5), @endRow)+ ' ' print @sql EXEC(@sql)
這個方案讓我們搜索N公里內的代碼變得簡單了 :@otherwhere 參數 設置成 :distance < n 即可了。按距離排序只用:@orderfield=‘distance’就可以了。程序中再也不用出現距離兩點距離的sql語句,另一個意外收獲就是讓我搜索營業中的商家也變得簡單了(我們營業根據商家設置的兩個時間段(如:8:00-12:00 16:00-20:00)及一個狀態值而定).
以上便是此問題這幾年的優化歷程,當然,可能很多人對我們行業不了解,也可能我只是抽出了代碼片段,很多人看了可能還是會不知所雲,不過我想真正想研究這個問題的人看了,就應該能明白了。2013年的方案,可能還有不少問題,或者可以再進一步優化。希望有部分人能用到的同時 ,也能對此方案提出更多更好的意見或建議。
成為一名優秀的程序員!