欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

仿orm自动生成分页SQL分享

程序员文章站 2024-02-16 23:09:28
先看看目前这4种数据库的分页写法: 复制代码 代码如下:-- oracleselect * from (     select rownum...

先看看目前这4种数据库的分页写法:

复制代码 代码如下:

-- oracle
select * from (
    select rownum rn,  pagetab.* from 
                (
                select * from user_tables order by id desc
                ) pagetab  where rownum <= 3010
            ) where rn>= 3001

-- sqlite   
select * from user_tables order by id desc limit 3001,10

-- sql2000
select top 100 percent  * from (
    select top 10 * from (
        select top 3010 * from user_tables  order by id desc ) pagetab order by id asc 
) pagetab2 order by id desc

-- sql2005+   
select pagetab.* from ( 
    select top 3010 row_number() over (order by id desc) rn , * from user_tables 
) pagetab where rn >= 3001

其中针对 oracle和sql2005+的分页写法做个说明。

oracle使用rownum要比row_number()要快。sql示例中均是查询 [3001,3010] 区间的数据,在sql语句中,尽可能在子查询中减少查询的结果集行数,然后针对排序过后的行号,在外层查询中做条件筛选。 如oracle写法中 子查询有rownum <= 3010 ,sql2005 中有 top 3010 * 。

当然今天要讨论的问题,不是分页语句的性能问题,如果你知道更好更快的写法,欢迎交流。

上面的分页写法,基于的查询sql语句是:

复制代码 代码如下:

select * from user_tables order by id desc

首先要从sql语句中分析出行为,我把该sql拆成了n部分,然后完成了以上拼接功能。按照模子往里面套数据,难度不大。

逆序分页

我们来描述另外一种场景,刚刚演示的sql是查询 满足条件下行数在[3001,3010]之间的数据,如果说总行数仅仅只有3500行,那么结果则是需要查询出3010行数据,并取出最后10条,而前面3000条数据,是没用的。

所以借鉴以前的经验,姑且叫它 逆序分页 。在知道总行数的前提下,我们可以进行分析,是否需要逆序分页,因为逆序分页得到分页sql语句,也是需要时间的,并非所有的情况都有必要这么做。之前有假设,数据仅仅有3500行,我们期望取出 按照id 倒叙排序后的[3001,3010]数据,换种方式理解,若按照id升序,我们期望取出的数据则是[491,500] 这个区间,然后将这个数据,再按照id倒叙排序,也就是我们需要的数据了。

理论知识差不多就说完了,需要了解更多的话,百度一下,你就知道。下面是代码,有点长,展开当心:

复制代码 代码如下:

public enum dbtype
    {
        sqlserver2000,
        sqlserver,
        oracle,
        sqlite
    }

    public class page
    {
        /// <summary>
        /// 数据库类别
        /// </summary>
        public dbtype dbtype = dbtype.oracle;
        /// <summary>
        /// 逆序分页行数,总行数大于maxrow,则会生成逆序分页sql
        /// </summary>
        public int maxrow = 1000;//临时测试,把值弄小点

        /// <summary>
        /// 匹配sql语句中select字段
        /// </summary>
        private regex rxcolumns = new regex(@"\a\s*select\s+((?:\((?>\((?<depth>)|\)(?<-depth>)|.?)*(?(depth)(?!))\)|.)*?)(?<!,\s+)\bfrom\b", regexoptions.ignorecase | regexoptions.multiline | regexoptions.singleline | regexoptions.compiled);
        /// <summary>
        /// 匹配sql语句中order by字段
        /// </summary>
        private regex rxorderby = new regex(@"\b(?<ordersql>order\s+by\s+(?:\((?>\((?<depth>)|\)(?<-depth>)|.?)*(?(depth)(?!))\)|[\w\(\)\.])+)(?:\s+(?<order>asc|desc))?(?:\s*,\s*(?:\((?>\((?<depth>)|\)(?<-depth>)|.?)*(?(depth)(?!))\)|[\w\(\)\.])+(?:\s+(?:asc|desc))?)*", regexoptions.ignorecase | regexoptions.multiline | regexoptions.singleline | regexoptions.compiled);
        /// <summary>
        /// 匹配sql语句中distinct
        /// </summary>
        private regex rxdistinct = new regex(@"\adistinct\s", regexoptions.ignorecase | regexoptions.multiline | regexoptions.singleline | regexoptions.compiled);
        private string[] splitsqlforpaging(string sql)
        {
            /*存储分析过的sql信息 依次为:
             * 0.countsql
             * 1.pagesql(保留位置此处不做分析)
             * 2.移除了select的sql
             * 3.order by 字段 desc
             * 4.order by 字段
             * 5.desc
             */
            var sqlinfo = new string[6];
            // extract the columns from "select <whatever> from"
            var m = rxcolumns.match(sql);
            if (!m.success)
                return null;

            // save column list and replace with count(*)
            group g = m.groups[1];
            sqlinfo[2] = sql.substring(g.index);

            if (rxdistinct.ismatch(sqlinfo[2]))
                sqlinfo[0] = sql.substring(0, g.index) + "count(" + m.groups[1].tostring().trim() + ") " + sql.substring(g.index + g.length);
            else
                sqlinfo[0] = sql.substring(0, g.index) + "count(*) " + sql.substring(g.index + g.length);


            // look for an "order by <whatever>" clause
            m = rxorderby.match(sqlinfo[0]);
            if (!m.success)
            {
                sqlinfo[3] = null;
            }
            else
            {
                g = m.groups[0];
                sqlinfo[3] = g.tostring();
                //统计的sql 移除order
                sqlinfo[0] = sqlinfo[0].substring(0, g.index) + sqlinfo[0].substring(g.index + g.length);
                //存储排序信息
                sqlinfo[4] = m.groups["ordersql"].value;//order by xxx
                sqlinfo[5] = m.groups["order"].value;//desc

                //select部分 移除order
                sqlinfo[2] = sqlinfo[2].replace(sqlinfo[3], string.empty);
            }

            return sqlinfo;
        }


        /// <summary>
        /// 生成逆序分页sql语句
        /// </summary>
        /// <param name="sql"></param>
        /// <param name="sqls"></param>
        /// <param name="start"></param>
        /// <param name="limit"></param>
        /// <param name="total"></param>
        public void createpagesqlreverse(string sql,ref string[] sqls, int start, int limit, int total = 0)
        {
            //如果总行数不多或分页的条数位于前半部分,没必要逆序分页
            if (total < 100 || start <= total / 2)
            {
                return;
            }

            //sql正则分析过后的数组有5个值,若未分析,此处分析
            if (sqls == null || sqls.length == 6)
            {
                sqls = splitsqlforpaging(sql);
                if (sqls == null)
                {
                    //无法解析的sql语句
                    throw new exception("can't parse sql to pagesql ,the sql is " + sql);
                }
            }

            //如果未定义排序规则,则无需做逆序分页计算
            if (string.isnullorempty(sqls[5]))
            {
                return;
            }

            //逆序分页检查
            string sqlorder = sqls[3];
            int end = start + limit;

            //获取逆序排序的sql
            string sqlorderchange = string.compare(sqls[5], "desc", true) == 0 ?
                string.format("{0} asc ", sqls[4]) :
                string.format("{0} desc ", sqls[4]);

            /*理论
             * total:10000 start:9980 limit:10
             * 则 end:9990 分页条件为 rn >= 9980+1 and rn <= 9990
             * 逆序调整后
             * start = total - start = 20
             * end = total - end = 10
             * 交换start和end,分页条件为 rn >= 10+1 and rn<= 20
             */
            //重新计算start和end
            start = total - start;
            end = total - end;
            //交换start end
            start = start + end;
            end = start - end;
            start = start - end;

            //定义分页sql
            var pagesql = new stringbuilder();

            if (dbtype == dbtype.sqlserver2000)
            {
                pagesql.appendformat("select top @pagelimit * from ( select top @pageend {0} {1} ) ", sqls[2], sqlorderchange);
            }
            else if (dbtype == dbtype.sqlserver)
            {
                //组织分页sql语句
                pagesql.appendformat("select pagetab.* from ( select top @pageend row_number() over ({0}) rn , {1}  ) pagetab ",
                    sqlorderchange,
                    sqls[2]);

                //如果查询不是第一页,则需要判断起始行号
                if (start > 1)
                {
                    pagesql.append("where rn >= :pagestart ");
                }
            }
            else if (dbtype == dbtype.oracle)
            {
                pagesql.appendformat("select rownum rn,  pagetab.* from  ( select {0} {1} ) pagetab  where rownum <= :pageend ", sqls[2], sqlorderchange);

                //如果查询不是第一页,则需要判断起始行号
                if (start > 1)
                {
                    pagesql.insert(0, "select * from ( ");
                    pagesql.append(" ) ");
                    pagesql.append(" where rn>= :pagestart ");
                }
            }
            else if (dbtype == dbtype.sqlite)
            {
                pagesql.appendformat("select * from ( select {0} {1} limit  @pagestart,@pagelimit ) pagetab ", sqls[2], sqlorderchange);
            }

            //恢复排序
            pagesql.append(sqlorder);

            //存储生成的分页sql语句 
            sqls[1] = pagesql.tostring();

            //临时测试
            sqls[1] = sqls[1].replace("@", "").replace(":", "").replace("pagestart", ++start + "").replace("pageend", end + "").replace("pagelimit", limit + "");

            console.writeline("【count】{0}", sqls[0]);
            console.writeline("【page】{0}", sqls[1]);
            console.writeline();
        }

        /// <summary>
        /// 生成常规sql语句
        /// </summary>
        /// <param name="sql"></param>
        /// <param name="sqls"></param>
        /// <param name="start"></param>
        /// <param name="limit"></param>
        /// <param name="createcount"></param>
        public void createpagesql(string sql, out string[] sqls, int start, int limit, bool createcount = false)
        {
            //需要输出的sql数组
            sqls = null;

            //生成count的sql语句 sqlserver生成分页,必须通过正则拆分
            if (createcount || dbtype == dbtype.sqlserver || dbtype == dbtype.sqlserver2000)
            {
                sqls = splitsqlforpaging(sql);
                if (sqls == null)
                {
                    //无法解析的sql语句
                    throw new exception("can't parse sql to pagesql ,the sql is " + sql);
                }
            }
            else
            {
                sqls = new string[2];
            }

            //组织分页sql语句
            var pagesql = new stringbuilder();

            var end = start + limit;
            if (dbtype == dbtype.sqlserver2000)
            {
                pagesql.appendformat("select top @pageend {0} {1}", sqls[2], sqls[3]);

                if (start > 1)
                {
                    var orderchange = string.isnullorempty(sqls[5]) ? null :
                        string.compare(sqls[5], "desc", true) == 0 ?
                        string.format("{0} asc ", sqls[4]) :
                        string.format("{0} desc ", sqls[4]);
                    pagesql.insert(0, "select top 100 percent  * from (select top @pagelimit * from ( ");
                    pagesql.appendformat(" ) pagetab {0} ) pagetab2 {1}", orderchange, sqls[3]);
                }
            }
            else if (dbtype == dbtype.sqlserver)
            {
                pagesql.appendformat(" select top @pageend row_number() over ({0}) rn , {1}",
                    string.isnullorempty(sqls[3]) ? "order by (select null)" : sqls[3],
                    sqls[2]);

                //如果查询不是第一页,则需要判断起始行号
                if (start > 1)
                {
                    pagesql.insert(0, "select pagetab.* from ( ");
                    pagesql.append(" ) pagetab where rn >= @pagestart");
                }
            }
            else if (dbtype == dbtype.oracle)
            {
                pagesql.append("select rownum rn,  pagetab.* from ");
                pagesql.appendformat(" ( {0} ) pagetab ", sql);
                pagesql.append(" where rownum <= :pageend ");

                //如果查询不是第一页,则需要判断起始行号
                if (start > 1)
                {
                    pagesql.insert(0, "select * from ( ");
                    pagesql.append(" ) where rn>= :pagestart ");
                }
            }
            else if (dbtype == dbtype.sqlite)
            {
                pagesql.appendformat("{0} limit @pagestart,@pagelimit", sql, start, limit);
            }

            //存储生成的分页sql语句 
            sqls[1] = pagesql.tostring();

            //临时测试
            sqls[1] = sqls[1].replace("@", "").replace(":", "").replace("pagestart", ++start + "").replace("pageend", end + "").replace("pagelimit", limit + "");

            console.writeline("【count】{0}", sqls[0]);
            console.writeline("【page】{0}", sqls[1]);
            console.writeline();
        }
    }

1.交换2个整数用了这样的算法。交换a和b,a=a+b;b=a-b;b=a-b;这是原来找工作的时候被考到的,如果在不使用第三方变量的情况下交换2个整数。

2.sql2000下由于是使用top进行分页,除非条件一条数据都查不到,否则在分页start和limit参数超过了总行数时,也会查询出数据。

3.拆分sql语句,参考了petapoco的部分源代码。

4.我的应用场景则是在dbhelp类,某个方法传递sql,start,limit参数即可对sql查询出来的结果进行分页。其中start:查询结果的起始行号(不包括它),limit:需要取出的行数。如 start:0,limit:15 则是取出前15条数据。