訂餐系統之按距離[根據經緯度]排序、搜索


  上周六,寫了第一篇博客《訂餐系統之權限設計》,在此感謝那些鼓勵、關注我的園友們,更要感謝那些提出寶貴建議的朋友們。看了你們的評論,才真切的感受到:朋友們的評論往往會讓文章更有看點。上篇文章中 鄭明人生就是賭  等幾個園友的留言讓我對我們系統的權限優化有了方向。當然,這樣的優化肯定不是一天兩天的事,做技術的朋友應該都知道:一個難題經常啃啃,某天也許就有了好的方案了(近段時間啃掉了幾個2、3年前未處理好的的問題,才想起初中數學老師讓我們經常啃一些競賽題的良苦用心),今天的文章說的就是一個從2010年就想優化,但一直未優化好的功能,也從 幸福框架的評論中,看到了他的博客,更是從他博客的留言中,找到了處理:按日期+6位順序號生成訂單編號(主要處理並發的情況)的方案,之前有客戶要這樣生成訂單編號,我只能回復實現不了,因為當時只知道,每次獲取最大的訂單編號,處理不了並發的情況,慚愧。

  關於這個標題,我還是交代下背景吧。這個問題從2010年第一次實現時,就覺得當時那種方案太差了,自己都看不去,只因沒有別的辦法,從那以后,每每得空,就拿出來琢磨下,現在這個方案也許還有不少問題,也希望各位指點下。我們是做訂餐系統的,主要實體就是商家(有坐標)和用戶(有坐標)。所以就有這么個需求,返回距離N公里內的商家,按距離從近到遠排序。先看下,數據庫設計吧,如圖(1):

                        

                                                                                              圖(1)

   下面我先介紹下這幾個表的關系吧:

    ETogo :商家表,dataid表示商家編號,togoname表示商家名稱。

    ETogoLocalInfo:商家定位表,togoid對應etogo.dataid,lat表示商家緯度,lng表示商家經度(經緯度在地圖上標注所得)

        EAddress :用戶地址表,保存用戶的經緯度,lat表示用戶緯度,lng表示用戶經度(經緯度在地圖上標注所得)

   生活的經驗告訴我們:一條成功的路,背后總有數不完的錯誤的路。下面我把那些曾經錯誤的路也寫下來,以作對比。

2010年方案

   當時年少,對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;
        }
dal層代碼

 以下是代碼中用到的分頁存儲過程: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語句就拼錯了。

2011年方案

   從那時實現了之前的方案后,后面的項目都沿用,每每用起時,心中總會不痛快,於是開始經常關注這個,一次機會看到了下面的代碼(之前一篇博客中看到的,已太久了,記不起出處了,哪位看到別介意),這個方案中也是大量拼接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;
        }
dal層代碼

 

2013年方案

       終於有一次再也忍受不了了,於是下定決心要優化(當然,就我目前的水平,想到的更多還是方便書寫)了,當前就想了一點,不在程序中拼接距離的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年的方案,可能還有不少問題,或者可以再進一步優化。希望有部分人能用到的同時 ,也能對此方案提出更多更好的意見或建議。

 

  成為一名優秀的程序員!

 

    

   


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM