ES6 中的箭头函数 以及React的this

本文转自:

https://segmentfault.com/a/1190000006032223

写在前面

JavaScript中的作用域scope 和上下文 context 是这门语言的独到之处,每个函数有不同的变量上下文和作用域。这些概念是JavaScript中一些强大的设计模式的后盾。在ES5规范里,我们可以遵循一个原则——每个function内的上下文this指向该function的调用方。比如:

var Module = {
    name: 'Jafeney',
    first: function() {
        console.log(this);   // this对象指向调用该方法的Module对象
        var second = (function() {
            console.log(this)  // 由于变量提升,this对象指向Window对象
        })()
    },
    init: function() {
        this.first()
    }
}

Module.init()

但是,在ES6规范中,出现了一个逆天的箭头操作符 => ,它可以替代原先ES5里function的作用,快速声明函数。那么,在没有了function关键字,箭头函数内部的上下文this是怎样一种情况呢?

ES6 中的箭头函数

在阮一峰老师的《ECMAScript 6 入门》 中,对箭头函数的做了如下介绍:

箭头函数的基本介绍

ES6 允许使用“箭头”=> 定义函数。

var f = v => v;
//上面的箭头函数等同于:
var f = function(v) {
  return v;
};
  • 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分
    var f = () => 5;
    // 等同于
    var f = function () { return 5 };
    var sum = (num1, num2) => num1 + num2;
    // 等同于
    var sum = function(num1, num2) {
      return num1 + num2;
    };
  • 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回(重要)
    var sum = (num1, num2) => { return num1 + num2; }
  • 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号(重要)
    var getTempItem = id => ({ id: id, name"Temp" });
  • 箭头函数可以与变量解构结合使用
    const full = ({ first, last }) => first + ' ' + last;
    // 等同于
    function full(person) {
      return person.first + ' ' + person.last;
    }
  • 箭头函数使得表达更加简洁
    const isEven = n => n % 2 == 0;
    const square = n => n * n;

    上面代码只用了两行,就定义了两个简单的工具函数。如果不用箭头函数,可能就要占用多行,而且还不如现在这样写醒目。

  • 箭头函数的一个用处是简化回调函数
    // 正常函数写法
    [1,2,3].map(function (x) {
      return x * x;
    });
    
    // 箭头函数写法
    [1,2,3].map(x => x * x);

箭头函数使用注意点

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
  4. 不可以使用yield命令,因此箭头函数不能用作Generator函数。

this 指向固定化

ES5规范中,this对象的指向是可变的,但是在ES6的箭头函数中,它却是固定的。

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });   // 输出 id: 42

注意:上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到100毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42。

箭头函数的原理

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。所以,箭头函数转成ES5的代码如下:

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

上面代码中,转换后的ES5版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this

两道经典的面试题

// 请问下面有几个this 

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // 输出 id: 1
var t2 = f().call({id: 3})(); // 输出 id: 1
var t3 = f()().call({id: 4}); // 输出 id: 1

上面代码之中,其实只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。另外,由于箭头函数没有自己的this,所以也不能用call()apply()bind()这些方法去改变this的指向

// 请问下面代码执行输出什么

(function() {
  return [
    (() => this.x).bind({ x: 'inner' })()
  ];
}).call({ x: 'outer' });

上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this。所以上面的代码最终输出 ['outer']

函数绑定 ::

箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(callapplybind)。但是,箭头函数并不适用于所有场合,所以ES7提出了“函数绑定”(function bind)运算符,用来取代callapplybind调用。虽然该语法还是ES7的一个提案,但是Babel转码器已经支持。

函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
  return obj::hasOwnProperty(key);
}

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;

let log = ::console.log;
// 等同于
var log = console.log.bind(console);

由于双冒号运算符返回的还是原对象,因此可以采用链式写法。

// 例一
import { map, takeWhile, forEach } from "iterlib";

getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));

// 例二
let { find, html } = jake;

document.querySelectorAll("div.myClass")
::find("p")
::html("hahaha");

React 中的各种 this

目前React的编写风格已经全面地启用了ES6和部分ES7规范,所以很多ES6的坑在React里一个个浮现了。本篇重点介绍 this,也是近期跌得最疼的一个。

Component 方法内部的 this

还是用具体的例子来解释吧,下面是我 Royal 项目里一个Table组件(Royal正在开发中,欢迎fork贡献代码 ^_^)

import React, { Component } from 'react'
import Checkbox from '../../FormControls/Checkbox/' 
import './style.less'

class Table extends Component {
    constructor(props) {
        super(props)
        this.state = {
            dataSource: props.dataSource || [],
            columns: props.columns || [],
            wrapClass: props.wrapClass || null,
            wrapStyle: props.wrapStyle || null,
            style: props.style || null,
            className: props.className || null,
        }
        this.renderRow = props.renderRow || null
    }

    onSelectAll() {
        for (let ref in this.refs) {
            if (ref!=='selectAll') {
                this.refs[ref].setState({checked:true})
            }
        }
    }

    offSelectAll() {
        for (let ref in this.refs) {
            if (ref!=='selectAll') {
                this.refs[ref].setState({checked:false})
            }
        }
    }

    _renderHead() {
        return this.state.columns.map((item,i) => {
            return [<th>{i===0?<Checkbox ref="selectAll" onConfirm={()=>this.onSelectAll()} onCancel={()=>this.offSelectAll()} />:''}{item.title}</th>]
        })
    }

    _renderBody() {
        let _renderRow = this.renderRow;
        return this.state.dataSource.map((item) => {
            return _renderRow && _renderRow(item)
        })
    }

    render() {
        let state = this.state;
        return (
            <div className={state.wrapClass} style={state.wrapStyle}>
                <table
                    border="0"
                    style={state.style}
                    className={"ry-table " + (state.className && state.className : "")}>
                    <thead>
                        <tr>{this._renderHead()}</tr>
                    </thead>
                    <tbody>
                        {this._renderBody()}
                    </tbody>
                </table>
            </div>
        )
    }
}

export default Table

ComponentReact内的一个基类,用于继承和创建React自定义组件。ES6规范下的面向对象实现起来非常精简,class关键字 可以快速创建一个类,而Component类内的所有属性和方法均可以通过this访问。换而言之,在Component内的任意方法内,可以通过this.xxx的方式调用该Component的其他属性和方法。

接着分析上面的代码,寥寥几行实现的是对一个Table组件的封装,借鉴了ReactNative组件的设计思路,通过外部传递dataSource(数据源)、columns(表格的表头项)、renderRow(当行渲染的模板函数)来完成一个Table的构建,支持全选和取消全选的功能、允许外部传递classNamestyle对象来修改样式。

从这个例子我们可以发现:只要不采用function定义函数,Component所有方法内部的this对象始终指向该类自身。

container 调用 component 时传递的 this

还是继续上面的例子,下面在一个做为Demo的container里调用之前 的Table

import Table from '../../components/Views/Table/'

接着编写renderRow函数并传递给Table组件

_renderRow(row) {
    // ------------ 注意:这里对callback函数的写法 -----------
    let onEdit = (x)=> {
        console.log(x+x)
    }, onDelete = (x)=> {
        console.log(x*x)
    }
    // ---------------------------------------------------
    return (
        <tr>
            <td><Checkbox ref={"item_" + row.key} />{row.key}</td>
            <td>{row.name}</td>
            <td>{row.age}</td>
            <td>{row.birthday}</td>
            <td>{row.job}</td>
            <td>{row.address}</td>
            <td>
                <Button type="primary" callback={()=>onEdit(row.key)} text="编辑" />
                <Button type="secondary" callback={()=>onDelete(row.key)} text="删除" />
            </td>
        </tr>
    )
}

//... 省略一大堆代码

render() {
    let dataSource = [{
        key: '1',
        name: '胡彦斌',
        age: 32,
        birthday: '2016-12-29',
        job: '前端工程师',
        address: '西湖区湖底公园1号'
        }, {
        key: '2',
        name: '胡彦祖',
        age: 42,
        birthday: '2016-12-29',
        job: '前端工程师',
        address: '西湖区湖底公园1号'
    }],columns = [{
        title: '编号',
        dataIndex: 'key',
        key: 'key',
        },{
        title: '姓名',
        dataIndex: 'name',
        key: 'name',
        }, {
        title: '年龄',
        dataIndex: 'age',
        key: 'age',
        }, {
        title: '生日',
        dataIndex: 'birthday',
        key: 'birthday',
        }, {
        title: '职务',
        dataIndex: 'job',
        key: 'job',
        },{
        title: '住址',
        dataIndex: 'address',
        key: 'address',
        }, {
        title: '操作',
        dataIndex: 'operate',
        key: 'operate',
    }];
    return (
        <div>
            <Table dataSource={dataSource} columns={columns} renderRow={this._renderRow}/>
        </div>
    );
}

显示效果如下:

分析上面的代码,有几处容易出错的地方:

  1. _renderRow 作为component的方法来定义,然后在对应的render函数内通过this来调用。很重要的一点,这里this._renderRow作为的是函数名方式传递。
  2. _renderRow 内部Button组件的callback是按钮点击后触发的回调,也是一个函数,但是这个函数没有像上面一样放在component的方法里定义,而是作为一个变量定义并通过匿名函数的方式传递给子组件:
    let onEdit = (x)=> {
        console.log(x+x)
    }
    
    // .....
    callback={()=>onEdit(row.key)}

    这样就避开了使用this时上下文变化的问题。这一点是很讲究的,如果沿用上面的写法很容易这样写:

    onEdit(x) {
       console.log(x+x)
    }
    
    // ... 
    callback={()=>this.onEdit(row.key)}

    但是很遗憾,这样写this传递到子组件后会变成undefined,从而报错。

  3. 父组件如要调用子组件的方法,有两种方式:
    • 第一种 通过匿名函数的方式
      callback = {()=>this.modalShow()}
    • 第二种 使用 bind
      callback = {this.modalShow.bind(this)}

注意:如果要绑定的函数需要传参数,可以这么写: xxx.bind(this,arg1,arg2...)

MyBatis之Mapper XML 文件详解(一)

MyBatis 的真正强大在于它的映射语句,也是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 就是针对 SQL 构建的,并且比普通的方法做的更好。
SQL 映射文件有很少的几个顶级元素(按照它们应该被定义的顺序):
cache – 给定命名空间的缓存配置。
cache-ref – 其他命名空间缓存配置的引用。
resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
parameterMap – 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
sql – 可被其他语句引用的可重用语句块。
insert – 映射插入语句
update – 映射更新语句
delete – 映射删除语句
select – 映射查询语句 

下一部分将从语句本身开始来描述每个元素的细节。
select
查询语句是 MyBatis 中最常用的元素之一,光能把数据存到数据库中价值并不大,如果还能重新取出来才有用,多数应用也都是查询比修改要频繁。对每个插入、更新或删除操作,通常对应多个查询操作。这是 MyBatis 的基本原则之一,也是将焦点和努力放到查询和结果映射的原因。简单查询的 select 元素是非常简单的。比如:

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>

这个语句被称作 selectPerson,接受一个 int(或 Integer)类型的参数,并返回一个 HashMap 类型的对象,其中的键是列名,值便是结果行中的对应值。
注意参数符号:

#{id}

这就告诉 MyBatis 创建一个预处理语句参数,通过 JDBC,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,就像这样:

// Similar JDBC code, NOT MyBatis…
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);

当然,这需要很多单独的 JDBC 的代码来提取结果并将它们映射到对象实例中,这就是 MyBatis 节省你时间的地方。我们需要深入了解参数和结果映射,细节部分我们下面来了解。
select 元素有很多属性允许你配置,来决定每条语句的作用细节。

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10000"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">

Select Attributes

id:在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType:将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
parameterMap :这是引用外部 parameterMap 的已经被废弃的方法。使用内联参数映射和 parameterType 属性。
resultType:从这条语句中返回的期望类型的类的完全限定名或别名。注意如果是集合情形,那应该是集合可以包含的类型,而不能是集合本身。使用 resultType 或 resultMap,但不能同时使用。
resultMap:外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,对其有一个很好的理解的话,许多复杂映射的情形都能迎刃而解。使用 resultMap 或 resultType,但不能同时使用。
flushCache:将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false。
useCache:将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true。
timeout:这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。
fetchSize:这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset(依赖驱动)。
statementType:STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
resultSetType:FORWARDONLY,SCROLLSENSITIVE 或 SCROLL_INSENSITIVE 中的一个,默认值为 unset (依赖驱动)。
databaseId:如果配置了 databaseIdProvider,MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。
resultOrdered:这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。
resultSets:这个设置仅对多结果集的情况适用,它将列出语句执行后返回的结果集并每个结果集给一个名称,名称是逗号分隔的。

insert, update 和 delete
数据变更语句 insert,update 和 delete 的实现非常接近:

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

Insert, Update, Delete ‘s Attributes

id:命名空间中的唯一标识符,可被用来代表这条语句。
parameterType:将要传入语句的参数的完全限定类名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
parameterMap :这是引用外部 parameterMap 的已经被废弃的方法。使用内联参数映射和 parameterType 属性。
flushCache:将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:true(对应插入、更新和删除语句)。
timeout:这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。
statementType:STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
useGeneratedKeys:(仅对 insert 和 update 有用)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false。
keyProperty:(仅对 insert 和 update 有用)唯一标记一个属性,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认:unset。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
keyColumn:(仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
databaseId:如果配置了 databaseIdProvider,MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。
下面就是 insert,update 和 delete 语句的示例:

<insert id="insertAuthor">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>

如前所述,插入语句的配置规则更加丰富,在插入语句里面有一些额外的属性和子元素用来处理主键的生成,而且有多种生成方式。
首先,如果你的数据库支持自动生成主键的字段(比如 MySQL 和 SQL Server),那么你可以设置 useGeneratedKeys=”true”,然后再把 keyProperty 设置到目标属性上就OK了。例如,如果上面的 Author 表已经对 id 使用了自动生成的列类型,那么语句可以修改为:

<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username,password,email,bio)
  values (#{username},#{password},#{email},#{bio})
</insert>

如果你的数据库还支持多行插入, 你也可以传入一个Authors数组或集合,并返回自动生成的主键。

<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username, password, email, bio) values
  <foreach item="item" collection="list" separator=",">
    (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
  </foreach>
</insert>

对于不支持自动生成类型的数据库或可能不支持自动生成主键 JDBC 驱动来说,MyBatis 有另外一种方法来生成主键。
这里有一个简单(甚至很傻)的示例,它可以生成一个随机 ID(你最好不要这么做,但这里展示了 MyBatis 处理问题的灵活性及其所关心的广度):

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

在上面的示例中,selectKey 元素将会首先运行,Author 的 id 会被设置,然后插入语句会被调用。这给你了一个和数据库中来处理自动生成的主键类似的行为,避免了使 Java 代码变得复杂。
selectKey 元素描述如下:

<selectKey
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">

selectKey Attributes

keyProperty:selectKey 语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
keyColumn:匹配属性的返回结果集中的列名称。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
resultType:结果的类型。MyBatis 通常可以推算出来,但是为了更加确定写上也不会有什么问题。MyBatis 允许任何简单类型用作主键的类型,包括字符串。如果希望作用于多个生成的列,则可以使用一个包含期望属性的 Object 或一个 Map。
order:这可以被设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它会首先选择主键,设置 keyProperty 然后执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 元素 – 这和像 Oracle 的数据库相似,在插入语句内部可能有嵌入索引调用。
statementType:与前面相同,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 语句的映射类型,分别代表 PreparedStatement 和 CallableStatement 类型。

 

转自 http://blog.51cto.com/13542372/2053114

Jdbc如何从PostgreSql读取海量数据?PostgreSql源代码分析纪录

前言:

最近做数据同步,需要从PostgreSql获取数据,发现一旦数据比较多,那么读取的速度非常慢,并且内存占用特别多&GC不掉。

代码样例:

为了方便讲解,下面写了事例代码,从b2c_order获取数据,这个数据表6G左右。

复制代码
package com.synchro;

import java.sql.*;

/**
 * Created by qiu.li on 2015/10/16.
 */
public class Test {

    public static void main(String[] args) {
        Connection conn = null;
        try {
            Class.forName("org.postgresql.Driver");
            conn = DriverManager.getConnection("jdbc:postgresql://***.qunar.com:5432/database", "username", "password");
            String sql = "select * from mirror.b2c_order";
            PreparedStatement ps = conn.prepareStatement(sql);
            ResultSet rs = ps.executeQuery();
            int i = 0;
            while (rs.next()) {
                i++;
                if (i % 100 == 0) {
                    System.out.println(i);
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
复制代码

现象:

在Idea执行代码,发现卡死,并且占用大量的内存

解决方案:

然后我决定开始逐步调试,跟踪代码:

第一步、我发现是在执行executeQuery方法的时候卡住的

第二步、是在执行AbstractJdbc2Statement.executeWithFlags方法卡住的

第三步、继续跟踪,并在网络上查看可能引起的原因是和设置fetchSize参数相关,所以我设置了fetchSize,奇葩的是没有生效

第四步、sendQuery,sendOneQuery方法,在这里发现了问题,好在代码不太多,我就都贴出来了:

复制代码
        boolean usePortal = (flags & 8) != 0 && !noResults && !noMeta && fetchSize > 0 && !describeOnly;
        boolean oneShot = (flags & 1) != 0 && !usePortal;
        int rows;
        if(noResults) {
            rows = 1;
        } else if(!usePortal) {
            rows = maxRows;
        } else if(maxRows != 0 && fetchSize > maxRows) {
            rows = maxRows;
        } else {
            rows = fetchSize;
        }
复制代码

可见是usePortal是true,那么fetchSize才会生效。

boolean usePortal = (flags & 8) != 0 && !noResults && !noMeta && fetchSize > 0 && !describeOnly;

那么咱们逐一看一下这些条件:

  • !noResults表示这个SQL不需要返回任何结果,这个肯定等于true,因为所有的select都会要求返回结果
  • !noMeta表示这个SQL不需要返回元数据,这个肯定等于true,因为select都要求返回元数据,供后续的resultSet.get使用
  • !fetchSize大于0,这个不说了,自然是true
  • !describeOnly,这个只有在desc table这样的语句的时候,才会是false,对于select,也是true

那么,试下的唯一的可能导致usePortal为true的原因就是 flags & 8这个值是true。。(我想说这种写法很别致,tmd,设置flags的时候肯定是flags=flag|8,后来发现新的驱动修改了这种写法)

继续往上翻,看看什么时候才会执行flags = flags | 8 这个代码了,因为只有这个代码被执行过,才会导致上面这个条件为true

        if(this.fetchSize > 0 && !this.wantsScrollableResultSet() && !this.connection.getAutoCommit() && !this.wantsHoldableResultSet()) {
            flags |= 8;
        }

其中:wantsHoldableResultSet()代码直接返回的false,所以,不考虑这个。

那么,wantsScrollableResultSet()返回false,并且connection.getAutoCommit()返回false,才会导致fetchSize生效。wantsScrollableResultSet()这个方法的代码为:

protected boolean wantsScrollableResultSet() {
        return resultsettype != 1003; //老代码,看到这里我真想死,1003是啥?好在偶然的机会看见了新的Postgresql驱动,使用ResultSet.TYPE_FORWARD_ONLY表示1003
}

至此,问题终于被定位:

1、如果connection不是自动提交事务的,那么,fetchSize将生效(非默认)

2、如果statement是TYPE_FORWARD_ONLY的,那么,fetchSize也将生效(默认)

结论

如果想fetchSize生效,必须保证connection是autocommit = false的,并且,statement为1003(forward_only)的:

conn.setAutoCommit(false);
final Statement statement = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.FETCH_FORWARD);

另外,不带参数的conn.createStatement(),其默认就是TYPE_FORWARD_ONLY。所以,一般情况下,如果想fetchsize生效,只须设置autocommit为flase,也就是需要手工去管理事务。默认的源代码如下:

    public Statement createStatement() throws SQLException {
        return this.createStatement(1003, 1007); //有兴趣的同学可以继续跟踪看看,1003就是resultsettype
    }

代码:

那么修改代码如下:

复制代码
package com.synchro;

import java.sql.*;

/**
 * Created by qiu.li on 2015/10/16.
 */
public class Test {

    public static void main(String[] args) {
        Connection conn = null;
        try {
            Class.forName("org.postgresql.Driver");
            conn = DriverManager.getConnection("jdbc:postgresql://***.qunar.com:5432/datasource", "username", "password");
            conn.setAutoCommit(false); //并不是所有数据库都适用,比如hive就不支持,orcle不需要
            String sql = "select * from mirror.b2c_order";
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setFetchSize(1000); //每次获取1万条记录
            //ps.setMaxRows(1000);
            ResultSet rs = ps.executeQuery();
            int i = 0;
            while (rs.next()) {
                i++;
                if (i % 100 == 0) {
                    System.out.println(i);
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
复制代码

这次再一次执行,发现根本不卡。

感悟:类似这种问题都的慢慢跟踪代码,更重要的是身边需要有同事可以相互讨论,形成氛围,因为这个过程十分乏味,自己很难坚持下来。

 

转自:https://www.cnblogs.com/liqiu/p/4886581.html

如何避免JDBC内存溢出问题

方法一:在连接mysql的URL后加两个参数:useCursorFetch=true&defaultFetchSize=100

完整形式如:jdbc:mysql://192.168.1.252:3306/lims?useUnicode=true&amp;charsetEncoding=utf8&useCursorFetch=true&defaultFetchSize=100

此方法只针对mysql有效。

方法二:采用分页查询的方式,在不使用连接池的情况下,可采用同一个Connection对象,每执行完一次操作就关闭PreparedStatement对象和ResultSet对象。然后在下一次操作的时候,重新打开PreparedStatement对象和ResultSet对象。(注意这里必须关闭这两个对象)

 

Sqlserver和Oracle:

方法:采用分页查询的方式,在不使用连接池的情况下,可采用同一个Connection对象,每执行完一次操作就关闭PreparedStatement对象和ResultSet对象。然后在下一次操作的时候,重新打开PreparedStatement对象和ResultSet对象。

 

内部原理与探讨:

采用java的JDBC操作数据库时,默认数据缓存的方式,即将从数据库查询得到的数据先缓存到本地内存中,然后从内存中读取数据(这也是JDBC的结果集支持上下移动的原因,但Oracle提供的JDBC实现默认是不支持这种上下移动的),所以如果从数据库中得到的数据被过多的缓存在本地内存中,如果超出了内存承受的范围,就会造成内存溢出的情况。

像Mysql的方法一,使用的是URL加参数的方式,这其实是采用了Mysql的游标,数据就不会缓存在本地内存中,而是ResultSet对象指向的是数据库中的记录,即使是上亿条记录,只要网络不断,也不会出现异常情况。

像Mysql的方法二,依然采用了数据缓存的方式,但这种情况下缓存的数据量不会太对,只要PreparedStatement对象和ResultSet对象关闭了,缓存的数据也就消失了,故也不会出现内存溢出的情况。

 

转自:http://blog.csdn.net/wlhelloworld/article/details/9237365

JDBC内存溢出 排查经历

 

转自:https://www.jianshu.com/p/2284b28e47df

 

一、好言

熬得住就出众,熬不住就出局,相信糟糕得日志熬过去了,剩下得就是好运气 。


二、背景

上周一晚上十点多,就开始MQ消息累积然后报警,运维早上找我看,那些消息累积了,然后运维重启服务后消息就消费了,然后过一会又累积了,到上午十一点多的时候,开始报内存过高,然后发现已经又两台服务已经挂了,看了日志,有错误,但是没有很多可用的错误。所以重启,吃完饭服务又挂了,jconsole看到结果如下图:

jconsole-1.png

然后下午的时候内存的爆发简直更加频繁了

jsonsole-2

三:解决

3.1、内容

出现内存溢出问题,所以肯定首要需要dump内存溢出文件,所以先让运维导出*.hprof 文件,然后我也查看日志文件,我们使用kibana进行日志统计了,所以查看其中错误日志,的确又很多错误的信息,redis错误,MQ错误,还有mysql错误。

org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is com.lambdaworks.redis.RedisCommandTimeoutException: Command timed out
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionC
onverter.convert(LettuceExceptionConverter.java:66)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionC
onverter.convert(LettuceExceptionConverter.java:41)
    at org.springframework.data.redis.PassThroughExceptionTranslationStr
ategy.translate(PassThroughExceptionTranslationStrategy.java:37)
[ActiveMQ Transport: tcp://mqtest.com/10.10.10.10:6161@123] org.apache.activemq.transport.failover.FailoverTransport - Transport (tcp://mqtest.com/10.10.10.10:6161) failed, attempting to automatically reconnect
java.io.IOException: Unexpected error occurred: java.lang.OutOfMemoryError: Java heap space
    at org.apache.activemq.transport.tcp.TcpTransport.run(TcpTransport.java:222)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.OutOfMemoryError: Java heap space

如上两个认为是内存溢出了,造成没有内存可使用没法开线程处理。
看看下面mysql错误

### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackExceptio
n: Lock wait timeout exceeded; try restarting transaction
; SQL []; Lock wait timeout exceeded; try restarting transaction; 
nested exception is 
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackExceptio
n: Lock wait timeout exceeded; try restarting transaction
    at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTransl
ator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:259)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTran
slator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfP
ossible(MyBatisExceptionTranslator.java:74)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke
(SqlSessionTemplate.java:421)
    at com.sun.proxy.$Proxy31.update(Unknown Source)
    at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.j
ava:270)
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:55)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:53)
    at com.sun.proxy.$Proxy55.updateByPrimaryKey(Unknown 
Source)
    at com.mouse.moon.app.service.Userervice.update(
UserService.java:26)
    at com.mouse.moon.app.service.Userervice$$FastClassBySpringCGLIB$$3eb1bfc8.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.
proceedWithInvocation(TransactionInterceptor.java:99)
    at org.springframework.transaction.interceptor.TransactionAspectSuppor
t.invokeWithinTransaction(TransactionAspectSupport.java:281)
    at org.springframework.transaction.interceptor.TransactionInterceptor.inv
oke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proc
eed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvised
Interceptor.intercept(CglibAopProxy.java:655)
    at com.mouse.moon.app.service.UserService$$EnhancerBySpringCGLIB$$31a8197.update(<generated>)
    at com.mouse.moon.app.service.userService.dealUser(MobileAppRegisterService.java:154)
    at com.mouse.moon.app.service.UserService.(userService.java:104)
    at com.mouse.moon.app.service.UserService$$FastClassBySpringCGLIB$$a99b8cf4.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.interceptor.AsyncExecutionInterceptor$1.call(AsyncExecutionInterceptor.java:115)
    at java.util.concurrent.FutureTask.run(FutureTask.java:262)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:745)
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:404)
    at com.mysql.jdbc.Util.getInstance(Util.java:387)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:946)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3878)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3814)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2478)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2625)
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2551)
    at com.mysql.jdbc.MultiHostMySQLConnection.execSQL(MultiHostMySQLConnection.java:157)
    at sun.reflect.GeneratedMethodAccessor306.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.mysql.jdbc.LoadBalancedConnectionProxy.invokeMore(LoadBalancedConnectionProxy.java:484)
    at com.mysql.jdbc.MultiHostConnectionProxy.invoke(MultiHostConnectionProxy.java:452)
    at com.sun.proxy.$Proxy63.execSQL(Unknown Source)
    at com.mysql.jdbc.MultiHostMySQLConnection.execSQL(MultiHostMySQLConnection.java:157)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1861)
    at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1192)
    at sun.reflect.GeneratedMethodAccessor314.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.mysql.jdbc.MultiHostConnectionProxy$JdbcInterfaceProxy.invoke(MultiHostConnectionProxy.java:103)
    at com.sun.proxy.$Proxy68.execute(Unknown Source)

这个地方的错误我在网上也搜过,其实基本是锁等待超时问题,这个一个同事之前遇到过,跟他交流了下,这个错误说明我的事物太大了,造成事物等待超时,并且有查处和更新相同数据造成的。这个现在想想这里代码的业务逻辑的确是存在这种问题,由于数据量大,如果按照现在的查询条件,地区会查询出相同数据,并且由于事物放在servicec层,在for循环中update数据的时候,会造成事物最后一起提交,所以造成锁等待超时问题。所以最后更新代码业务如下
1:对查询代码顺序做优化

List list = ...
if(符合条件){
   list = ...(select ...where)
}

由于数据库数据量有4500w左右,上述代码先查询,然后再判断条件,所以有可能会查询两次数据库,所以代码优化,处理代码顺序,先判断条件。

2:对加在service的事物移动到dao层,每次更新一次提交一次,把事物缩小。

3:对于一些处理可以做异步处理

4:做限流操作(使用hystrix做限流)

5:<a href =”http://www.jianshu.com/p/82c27c58e0b6″>对消费做消费速度限制</a>

3.2:dump文件分析
由于之前系统是假死状态,每次冲上去之后,过十几分钟或者半小时后,可能又会慢慢回收。所以每次爆的时候并没有dump文件,<a href=”http://www.jianshu.com/p/05e9f46e3f09″>脚本</a>
每次没dump,最后我们在内存上升期间,只用使用

#查看内存
jmap -heap pid 
jmap -dump:format=b,file=$dumpfile pid
图片.png

导出文件,查看如下

图1.png

上图是在没有处理完sql错误时倒出的hprof文件。
下图是处理完sql异常导出的文件

图2.png

图二我们可以看到一个很大的对象,就是我画红色的部分,点进去看,有三百多万个对象,占用内存三百多兆,并且我们看到老年代使用的量特别大,基本没回收样。怎么会有这么大的对象了,根据对象我们找到代码位置,确定是查询问题,造成大量对象产生,并且也是之前大量更新,并且数据有重复的概率,所以造成占用大量内存。其实这种发生也是要有数据量的情况下才会出现,发生溢出.下面几张是检测到的日志信息数据:

log-1.png
log-2.png
log-3.png

最后简单总结下处理的过程,首先需要排除所有可能造成该问题的问题,也就是先解决掉看到的错误,然后查看导出dump文件中的大对象,根据大对象查看代码,进一步分析问题。其实在这个中间,做了很多各种处理看效果,比如MQ限流消费处理等。但是现在看来,最终原因应该还是出在数据库层面。

四:感触

其实之前没怎么解决过内存溢出问题,这算是自己第一次全程经历此次内存溢出并解决,不过首先说,这代码并不是我写的代码,是我被按排接手的项目,所以还的感谢这之前写这代码的人,才使得我有这么一次经历,通过这次经历,也会让自己对于一个内存溢出问题的解决思路更佳清晰,思考问题更佳全面,处理问题更佳稳重,并且对于一些工具的使用,比如MAT,Jprofile有了接触。所以有些经历真的是不可求的。因此才有了经验之说。努力吧,骚年。

作者:吴世浩
链接:https://www.jianshu.com/p/2284b28e47df
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

PostgreSQL中COUNT的各条件下(1亿条数据)例子

转自:

https://www.cnblogs.com/jacker1979/p/4661125.html

插入一亿条数据

(示例数据库:9.3.5)
参考资料:http://www.oschina.net/question/96003_70381

1
2
3
4
test=# insert into tbl_time1 select generate_series(1,100000000),clock_timestamp(),now();
INSERT 0 100000000
Time: 525833.218 ms
约:8.7分钟

COUNT,没有索引,1亿条数据。

1
2
3
4
5
6
7
test=# select count(1) from tbl_time1;
   count
-----------
 100000000
(1 row)
Time: 3070658.058 ms
约:51.2分钟

添加主键索引耗时

1
2
3
4
test=# alter table tbl_time1 add primary key (id);
ALTER TABLE
Time: 981276.804 ms
约:16.4分钟

COUNT,有索引(主键),1亿条数据,注意 where id > 0 的条件

1
2
3
4
5
6
7
8
这个有 where id > 0
test=#  select count(id) from tbl_time1 where id > 0;
   count
-----------
 100000000
(1 row)
Time: 244243.112 ms
约:4.071分钟

COUNT,有索引(主键),1亿条数据,注意没有 where id > 0 的条件

1
2
3
4
5
6
7
8
这个无 where id > 0
test=#  select count(id) from tbl_time1;
   count
-----------
 100000000
(1 row)
Time: 548650.606 ms
约:9.144分钟

通过修改配置文件调优postgresql.conf

1
2
3
4
5
6
7
8
9
10
11
enable_bitmapscan = off
enable_hashagg = on
enable_hashjoin = on
enable_indexscan = on
enable_indexonlyscan = on
#enable_material = on
#enable_mergejoin = on
#enable_nestloop = on
enable_seqscan = off
#enable_sort = on
enable_tidscan = off
1
2
3
4
5
6
7
test=# select count(id) from tbl_time1 where id > 0;
   count
-----------
100000000
(1 row)
Time: 87501.151 ms
约:1.456分钟

论count使用不当的罪名 和 分页的优化

转自:http://blog.163.com/digoal@126/blog/static/1638770402016468349463

分页是一个非常常见的应用场景,然而恐怕没有多少人想过其优化方法。
确一味的责怪为什么数据库用count(*)计算分页数是如此的慢。
很多开发人员喜欢用count先算一下结果集的大小,然后就知道需要排多少页。
然后再从数据库取出对应的数据,并展示给用户。
问题1
count会扫一遍数据,然后取数据又扫一遍数据。重复劳动。
问题2,很多人喜欢用order by offset limit来展示分页。
其实也是一个非常大的问题,因为扫描的数据也放大了,即使在order by 的列上用到了索引也会放大扫描的数据量。
因为offset的row也是需要扫的。

问题1的优化
使用评估行数,方法如下
创建一个函数,从explain中抽取返回的记录数

CREATE FUNCTION count_estimate(query text) RETURNS INTEGER AS
$func$
DECLARE
    rec   record;
    ROWS  INTEGER;
BEGIN
    FOR rec IN EXECUTE 'EXPLAIN ' || query LOOP
        ROWS := SUBSTRING(rec."QUERY PLAN" FROM ' rows=([[:digit:]]+)');
        EXIT WHEN ROWS IS NOT NULL;
    END LOOP;

    RETURN ROWS;
END
$func$ LANGUAGE plpgsql;

评估的行数和实际的行数相差不大,精度和柱状图有关。
PostgreSQL autovacuum进程会根据表的数据量变化比例自动对表进行统计信息的更新。
而且可以配置表级别的统计信息更新频率以及是否开启更新。

postgres=# select count_estimate('select * from sbtest1 where id between 100 and 100000');
 count_estimate 
----------------
         102166
(1 row)

postgres=# explain select * from sbtest1 where id between 100 and 100000;
                                      QUERY PLAN                                       
---------------------------------------------------------------------------------------
 Index Scan using sbtest1_pkey on sbtest1  (cost=0.43..17398.14 rows=102166 width=190)
   Index Cond: ((id >= 100) AND (id <= 100000))
(2 rows)

postgres=# select count(*) from sbtest1 where id between 100 and 100000;
 count 
-------
 99901
(1 row)

也就是说,应用程序完全可以使用评估的记录数来评估分页数。
这样做就不需要扫描表了,性能提升尤为可观。

问题2的优化
问题2其实表现在数据可能被多次扫描,使用游标就能解决。
未优化的情况,取前面的记录很快。

postgres=# explain analyze select * from sbtest1 where id between 100 and 1000000 order by id offset 0 limit 100;
                                                                QUERY PLAN                                                                
------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.43..9.74 rows=100 width=190) (actual time=0.019..0.088 rows=100 loops=1)
   ->  Index Scan using sbtest1_pkey on sbtest1  (cost=0.43..93450.08 rows=1003938 width=190) (actual time=0.018..0.051 rows=100 loops=1)
         Index Cond: ((id >= 100) AND (id <= 1000000))
 Planning time: 0.152 ms
 Execution time: 0.125 ms
(5 rows)

取后面的记录,因为前面的记录也要扫描,所以明显变慢。

postgres=# explain analyze select * from sbtest1 where id between 100 and 1000000 order by id offset 900000 limit 100;
                                                                  QUERY PLAN                                                                   
-----------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=83775.21..83784.52 rows=100 width=190) (actual time=461.941..462.009 rows=100 loops=1)
   ->  Index Scan using sbtest1_pkey on sbtest1  (cost=0.43..93450.08 rows=1003938 width=190) (actual time=0.025..308.865 rows=900100 loops=1)
         Index Cond: ((id >= 100) AND (id <= 1000000))
 Planning time: 0.179 ms
 Execution time: 462.053 ms
(5 rows)

如果有很多个分页,效率下降可想而知。

优化手段

postgres=# begin;
BEGIN
Time: 0.152 ms
postgres=# declare cur1 cursor for select * from sbtest1 where id between 100 and 1000000 order by id;
DECLARE CURSOR
Time: 0.422 ms
postgres=# fetch 100 from cur1;
。。。

获取到数据末尾时,效率也是一样的不会变化。