Continuations 对于程序设计的意义,就像达芬奇密码对人类历史的意义:即对人类最大秘密的惊人揭示。也许不是,但他在概念上的突破性至少和负数平方根的意义等同


看到CPS变换却不是在SICP学习的时候,而是在国庆前一天翻看王垠的GTF-Great Teacher Friedman时,当看到:

一个例子就是课程进入到没几个星期的时候,我们开始写解释器来执行简单的 Scheme 程序。然后我们把这个解释器进行 CPS 变换,引入全局变量作为”寄存器” (register),把 CPS 产生的 continuation 转换成数据结构(也就是堆栈)。最后我们得到的是一个抽象机 (abstract machine),而这在本质上相当于一个真实机器里的中央处理器(CPU)或者虚拟机(比如 JVM)。所以我们其实从无到有,“发明”了 CPU!从这里,我才真正的理解到寄存器,堆栈等的本质,以及我们为什么需要它们。我才真正的明白了,冯诺依曼体系构架为什么要设计成这个样子。

的时候,被兴趣吸引然后顺手搜索了一下CPS是个什么东西,结果便在这之上消磨了2天的时光,也是做一下总结罢.

What:什么是CPS?

在讨论CPS变换的时候我们在讨论什么,那就要从CPS开始讲起,既然(CPS)被称为一种style,那么可以认为它就是一种编码风格,而CPS变换也就是把我们的代码格式化成_Continuation-passing style_的过程.下面是一个关于CPS的解释:

CPS,是Continuation Passing Style的缩写,它是一种编码风格,函数执行完以后,并不通过返回值,而是调用它自己的Continuation来完成计算。

How:怎么做CPS(变换)

知道了定义之后,就可以开始做CPS变换了,虽然定义很抽象但是在看了几个例子之后还是可以理解这种书写代码的风格的.

下面是一个简单的例子(并不对特定的语言讨论,可以看成是伪代码):

1
2
3
4
5
6
7
// 一般的写法
func add(x, y) = x + y
print add(a,b)

// CPS风格的写法
func add(x, y, fun) = fun(x+y)
add(a, b, print)

对上面代码的解释:在一般的写法中我们定义了一个相加函数然后向控制台打印了a+b的结果.而CPS风格的写法是将一个print函数传递给函数add,在执行完add之后将结果传递给打印函数然后输出.也可以理解为add函数做完了自己的活之后将结果和控制权交到了print手里然后print输出了结果并返回了,但是print也可以继续传递这个结果给别人.

再举一个阶乘的例子:

1
2
3
4
5
// 一般写法
func f(x) = x == 1 ? 1 : x * f(x-1)f(4)

// CPS风格写法
func f(x, k) = x == 1 ? k(1) : f(x-1, lamdba(v):k(v*x))f(4, lamdba(v):v)

因为CPS约定:每个函数都需要有一个参数kont,kont是continuation的简写,表示对计算结果的后续处理,而对kont的约定是:kont为一个单参数输入,单参数输出的函数(假定print符合要求).

Why:为什么需要CPS变换

在例子中可以看到CPS的主要的功能就是:在一个函数执行结束之后,将返回值交给下一个函数,但是到现在为止,有一个很明显的问题是显而易见的,那就是我们可能会写出一个很长的函数调用串.也就是说这个看上去只是花哨了一点的代码风格在没有优势的情况下还带来了缺点,所以下面就整理一下我在浏览时看到的CPS的使用和优势(这个缺点其实不致命因为可以通过语法糖来解决).

执行顺序

来考虑这样一个问题,现在我们有2行代码:

1
2
print "enter something"
getInput()

很简单,在终端产生一个提示并且获取用户的下一个输入,但是问题是:我们不能保证代码执行的顺序,它们执行的顺序完全取决于编译器怎么安排.那么让我们来用CPS重写一下:
print ("enter something", getInput())

结果是很明显的,我们可以用这种方法来强制决定函数的执行顺序,这一点在函数式编程中是很重要的.当然在并发中,这也可能会有点用(没深究).

堆栈

来考虑一下阶乘函数的调用链:

1
2
3
4
5
6
7
8
9
// 一般写法
f(4) ==> 4*f(3) ==> 4*3*f(2) ==> 4*3*2*f(1) ==> 4*3*2*1

// CPS风格写法
f(4) = f(3, lamdba(v):(v*4))
= f(2, lamdba(v):(lamdba(v):(v*4)(v*3)))
= ...

改写之后可以化简为:v = 1 ==> 2*v ==> 3*v ==> 4*v ==> v

可以看到,第一种递归就是经典的递归模型,每次递归调用自己的时候都需要将当前的函数状态入栈然后进入到下一个函数中直到到递归终止情况然后逐一向上返回结果最后得出答案.但是在第二种CPS风格中,我们可以看到状态是顺序的,计算是从1开始向后乘到4然后返回,这是因为k承载了每次计算的结果并向后传递,也就是说我们不需要保存函数当前的状态而可以直接调用下一个函数,也就是说CPS变换将普通递归函数变成了尾递归.可以这么理解:递归中的保存状态的栈和kont函数是等价的,并且kont函数还有一个优势:因为kont函数可以被显示调用,也就是说我们可以在任意时刻任意情况下,调用函数的某一时刻的状态,只要它是CPS风格的!!

下面给出一段博客的引用,可能解释的更清楚:

这样我们就知道了什么是“当前 continuation”。它有什么意义?一旦我们得到了当前的 continuation 并将它保存在某处,我们就最终将程序当前的状态保存了下来——及时地冷冻下来。这就像操作系统进入休眠状态。一个 continuation 对象里保存了从我们获得它的地方重新启动程序的必要信息。操作系统在每次发生线程间的上下文切换时也是如此。唯一的区别是它保留着全部控制。请求一个 continuation 对象(在 Scheme 里,可以调用 call-with-current-continuation 函数)后,你就会获得一个包括了当前 continuation 的对象,也就是堆栈信息(在 CPS 程序里就是下一个要调用的函数),可以把这个对象保存在一个变量(或者是磁盘)里。当你用这个 continuation “重启”程序时,就会转回到你取得这个对象的那个状态,这就象切换回一个被挂起的线程或唤醒休眠的操作系统,区别是用 continuation,你可以多次地重复这一过程,而当操作系统被唤醒时,休眠信息就被销毁了,如果那些信息没有被销毁,你也就可以一次次地将它唤醒到同一点,就象重返过去一样。有了 continuation 你就有了这个控制力!

应用:消除JS回调地狱

虽然还没有完整的学过JS和前端的技术,但是在学Django的过程中接触到的一些简短的JS也觉得这门语言”似乎”有很多的缺陷(个人感觉,主要是感觉各种{({()})}嵌套确实不够优雅),但是JS又确实是一门经常被拿来讨论一些FP问题的语言(可能因为JS支持函数作为first-class还能在浏览器直接运行的原因吧).在看CPS的过程中也看到了关于JS回调地狱的问题.

回调地狱

先来丢一段js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fs.readdir(source, function (err, files) {  
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})

解决方案

当然这一堆括号和大括号看着很让人抓狂而且也很难马上掌握执行的顺序,当然这里不会来优化这段代码(我不会,哈哈),来看一个简单的例子:

1
2
3
4
5
6
7
8
9
// 第一次ajax,查询出id
$.get('http://xxx/user?name=jxy', function(data){
var id = data.id;
// 第二次ajax,根据id查询出需要的数据
$.get('http://xxx/another?id='+id, function(data){
// 这里才是真正的处理逻辑
// do something...
});
});

用async/await优化之后可以变成(据说是终极解决方案):

1
2
3
4
5
6
7
8
9
10
11
// 伪代码
// 用async修饰一个函数
async function getData(){
// 用await标记异步操作,会自动等待异步操作执行完毕之后再继续向下执行
const user = await $.get('http://xxx/user?name=jxy');
const id = user.id;
const data = await $.get('http://xxx/another?id='+id);
// 真正处理data
// do something...
};
getData();

关于async/await或者说协程,讨论起来又是无止境的了,但是基本的思想和CPS很吻合,也就是我们需要在切换上下文的时候如何来保存状态和恢复之前的状态.而js的async/await其实也只是function*+yield的一个语法糖而已,其核心就是不断的将 yield 的 next 值追加到 promise 链中,达到“一步接一步”执行的效果.

在看资料的时候还看到github上有一个Continuation.js项目,是直接使用CPS来解决回调地狱问题的,给出的例子如下:

原始js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function textProcessing(callback) {  
fs.readFile('somefile.txt', 'utf-8', function (err, contents) {
if (err) return callback(err);
//process contents
contents = contents.toUpperCase();
fs.readFile('somefile2.txt', 'utf-8', function (err, contents2) {
if (err) return callback(err);
contents += contents2;
fs.writeFile('somefile_concat_uppercase.txt', contents, function (err) {
if (err) return callback(err);
callback(null, contents);
});
});
})
;}
textProcessing(function (err, contents) {
if (err)
console.error(err);
});

使用Continuation.js之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function textProcessing(ret) {  
fs.readFile('somefile.txt', 'utf-8', cont(err, contents));
if (err) return ret(err);
contents = contents.toUpperCase();
fs.readFile('somefile2.txt', 'utf-8',
cont(err, contents2));
if (err) return ret(err);
contents += contents2;
fs.writeFile('somefile_concat_uppercase.txt', contents, cont(err));
if (err) return ret(err);
ret(null, contents);
}
textProcessing(cont(err, contents));
if (err)
console.error(err);

至少,看起来确实优雅了很多.

应用:Web应用

试想这么一个场景:用户向服务器请求了一张表单比如是注册,然后用户在网页上进行了一系列的操作之后提交了这张表单,注册成功.

在开发的过程中,一般来说我们要写一个getForm的方法来响应请求表单和postFrom的方法来响应提交表单.但是如果我们用CPS来思考这个问题呢?就会变成下面这样:用户申请了一张表单,我们生成一个函数来处理它,然后将这个函数保存起来,等到用户提交的时候,我们再调用这个函数将参数给它就可以完成注册.

在这个场景中的优势就是,我们不需要分离get和post逻辑,因为这其实都是属于用户注册这个过程中的事,而且,因为continuation可以在任何时候被调用而且不止调用一次(只要被保存了),那么优势就很明显了,我们只要缓存continuation环境,就可以节省很多的服务器性能.

我相信在web上,这个理念迟早会流行,毕竟计算机中十年不变的东西就是各种语言正在发布的新特性(hhh).

应用:JavaFlow

这只是在Google的过程中搜到的一个算是副产物吧,但是也在这里记录一下,大致就是可以在Java里使用JavaFlow来实现continuation.
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyRunnable implements Runnable {  
public void run() {
System.out.println("started!");
for( int i=0; i<10; i++ )
echo(i);
}
private void echo(int x) {
System.out.println(x);
Continuation.suspend();
}
}
Continuation c = Continuation.startWith(new MyRunnable());
System.out.println("returned a continuation");

然后就可以调用c了:
1
2
Continuation d = Continuation.continueWith(c);
System.out.println("returned another continuation");

自动CPS变换

之所以将其放在最后,是因为我也还没有时间将其搞懂,而且似乎对于理解CPS变换来说影响并不大,这里就先暂时用一张图来代替吧(王垠40行代码的注释版)

参考博客

GTF - Great Teacher Friedman

CPS变换与CPS变换编译

CPS的地位

基于CPS变换的尾递归转换算法

函数式编程与Continuation/CPS

用 continuation 开发复杂的 Web 应用程序

时间倏忽而逝

还有一篇文章先留个档,有时间再消化:
Representing Control—A Study of the CPS transformation

我因为写了一部人们把它和《禅与摩托车维修艺术》相比较的书而感到甚受恭维。我希望拙作(《时间简史》)和本书一样使人们觉得,他们不必自处于伟大的智慧及哲学的问题之外。——by霍金

在历经了多个早起的清晨和国庆回家路上的几个小时之后,也_勉强_算是走完了这一次的肖陶扩,这是一本看书名不知所云但是却发人深省的书,主要讲述了作者和儿子在一次穿越美国的长途摩托车旅行中发生的故事以及作者的思考和感受.在看到霍金对这本书的评价之后我才豁然:我们生活中处处充满了哲学,但是当我们听到哲学这个词的时候却会将其束之高阁,但是《禅》这本书却从旅行和摩托车修理的角度,将作者自己对哲学的思考和理解很好的讲述给了读者听,这是很了不起的.

骑摩托车旅游和其他的方式完全不同。坐在汽车里你只是被局限在一个小空间之内,因为已经习惯了,你意识不到从车窗向外看风景和看电视差不多。你只是个被动的观众,景物只能呆板地从窗外飞驰而过。 骑摩托车可就不同了。它没有什么车窗玻璃在面前阻挡你的视野,你会感到自己和大自然紧密地结合在了一起。你就处在景致之中,而不再是观众,你能感受到那种身临其境的震撼。脚下飞驰而过的是实实在在的水泥公路,和你走过的土地没有两样。它结结实实地躺在那儿,虽然因为车速快而显得模糊,但是你可以随时停车,及时感受它的存在,让那份踏实感深深印在你的脑海中。

我们不再是观众而是紧密地和作者的思想结合在了一起,深深的印在了我的脑海中.

在看这本书的过程中,我的脑海中就有一个词一直存在着:物我两忘,个人觉得这和作者想要表述的良质是比较接近的.虽然这里讲的禅和中国人对禅的定义是非常不一样的,但是从根本上来说,都是对于哲学的讨论.禅与摩托车可以对应于哲学中的物体和自我与世界的二元对立,而作者在书中着重讨论的也是古典和浪漫如何才能够有机的结合在一起,比如,在修理摩托车上.也就是说:如何将物体和自我,主观和客观统一起来,达到物我两忘的境界.

在讲到修理摩托车的时候:

在修理机器这方面,如果你的自我太强,往往无法把工作做好。因为你总是会被愚弄,很容易犯错,所以修理人员自大的个性对他颇为不利。如果你认识很多技术人员,我想你会同意我的观点,他们往往相当谦虚而且安静。当然,也有例外。不过即使他们起初无法保持安静和谦逊,长久工作下来,也会变成这样的个性。同时,他们还具有高度的警戒心,专注而又懂得怀疑,不会以自我为中心。

以我的理解,所谓的物我两忘或者修理摩托车的禅,就是要抛弃那一堆冰冷的说明书和零件分解,而将其视为一个整体,需要的是对于摩托车的整体的把握,在某个零件受损的时候,会冷静的从摩托车的角度来考虑这个问题,而不是发现一个零件坏了,就努力试图将其从摩托车上拆下来换一个新的上去,因为这样没有考虑到每一个零件和摩托车本体的联系,而只是将其视为一个零件,将问题视为一个任务去完成,很有可能这个零件的问题是由别的部位导致的,也有可能在你只想要拆除它的时候,因为你的目光只停留在这个零件上而造成了别的问题.修理一辆摩托车,需要的是掌握它的全部,不仅仅是所有的技术细节和零件,还有对车的_感性的或者浪漫的认识_,或者说,我们对这辆车的一生的每一个状态都很了解.

一旦真正地投入了工作之中,就可以说是在关心自己的工作,这就是关心的真正意义——对自己手中的工作产生了认同感。当一个人产生这种认同的时候,他就会看到关系的另一面——良质。

这是这本书中最能发我深思的地方了,而这本书作为计算机的必读书之一,我觉得最有意义的也就在于此,虽然科技的发展不仅仅只在计算机上,但是CS确实成为了现在科技发展最瞩目的地方之一,而作为一个CS的学生,在开始的阶段就读到这样的书,是我的幸运,它指引我去寻找理解世界的方式和获得内心平静的方法,而作为一个程序员,更不应只停留在技术细节和代码的世界中,我们需要的是浪漫抑或者说是编写软件的禅,它也让我理解了,什么是的,也就是,什么是良质的,我们应该如何去做技术,如何让做出的东西中有良质.

几分钟之后,我们顺着这条路骑到了山顶,然后又笔直地往山谷落下。一路风景十分优美。我觉得这个山谷和美国其他的山谷完全不同。往南边一点就是所有葡萄美酒的产地。山坡像波浪一样起伏,呈现出优美的曲线,而路也是蜿蜒曲折。我们的身体和车子缓缓地顺着山路向下走,同时向路边倾斜过去,几乎可以碰到树叶和树枝。高山地区的岩石和枞树远远落在身后,在我们周围是平缓的山坡和葡萄树,还有许多紫色和红色的花朵。从山谷冒出了浓郁的雾气,那是森林的气息和花香融合在了一起。在遥远的那一端,则是看不到但可以微微嗅得到的海洋气息。
…… 人只要活着就会发生不愉快的事和不幸的事。但是我现在有一种以前没有过的感觉,这种感觉并不只停留在表面,而是深入内里:我们赢了。情况正在慢慢好起来。我们几乎可以这样期待。

是的,情况正在慢慢好起来,我觉得一本书的价值不仅在于作者倾授了多少他的观点给你,而是在于阅读完这本书之后,能不能在你的脑海中留下作者思考的痕迹或者说潜意识,能够在遇到别的情况的时候,用别人(作者)的方式去思考问题从而获得答案,如果说读完一本书之后能够对其_久久不能忘怀_的话,那么这本书的价值就超出了其文字所能承载的了.

番外

在看到王垠的什么是“对用户友好”的时候,我觉得对用户友好这一点就像是需要结合理性的和感性的认识的一个例子,就在这里记录一下,可能这就是做一个好软件的禅吧.

爱因斯坦说:“Any intelligent fool can make things bigger and more complex… It takes a touch of genius - and a lot of courage to move in the opposite direction.”

确实,大师做的事情是都是简单的,道不远人,也就是霍金说的:

不必自处于伟大的智慧及哲学的问题之外

此言得之,而我们也不应该身处一个好的软件之外,不应该在使用它的时候还需要看一大堆的说明书和教程,好的软件,应该是傻瓜式的,也就是KISS原则.

很多程序员都会注意到这些机器界面的抽象,让使用者尽量少的接触到实现细节。可是他们却往往忽视了人和机器之间的界面。也许他们没有忽视它,但是他们却用非常不一样的设计思想来考虑这个问题。他们没有真正把人当成这个系统的一部分,没有像对待其它机器模块一样,提供具有良好抽象的界面给人。

一个良好的界面不应该是这样的。它给予用户的界面,应该只有一些简单的设定。用户应该用同样的方法来设置所有程序的所有参数,因为它们只不过是一个从变量到值的映射(map)。至于系统要在什么地方存储这些设定,如何找到它们,具体的格式,用户根本不应该知道。

所以我们看到,“对用户不友好”的背后,其实是程序设计的不合理使得它们缺少抽象,而不是用户的问题。

也就是说,虽然开发者每天都在讲抽象,但是却没有将用户也抽象进整个的系统中,或者说将用户和整个系统剥离了,使得用户所接触到的是冷冰冰的操作手册和一堆配置信息,而《禅》中告诉我们,操作手册是没用的,或者说写操作手册给用户看本身就不是一个的行为.

还记得邹欣在《构建之法》中讲的一个例子:微软会征集用户来测试office软件,而让程序员在单向玻璃后面看用户的操作,当用户对某一个操作很疑惑或者找不到某个功能的位置的时候,程序员在后面都会很着急,因为这个功能似乎就在那里,但是用户就是没有去点击它.当然测试结束之后团队是很羞愧的,因为自己为很好的用户交互逻辑,到了用户那里变成了反逻辑或者很难的操作.

这其实都从不同的角度在思考软件和人的关系,在编写软件的过程中程序员知道代码需要抽象,也都知道抽象的好处,但是在和用户的交互这一层上,这样的抽象却被剥离了,缺失了,不合理的菜单布局和操作逻辑比比皆是.

对于开发者来说,如何做出对用户友好的软件就是需要开发软件的禅的地方了.

主键

  • 总是应该定义主键
  • 主键要求:
    1. 不更新主键的值
    2. 不重用主键的值
    3. 不在主键中使用可能会更改的值

连接

  • 连接数据库: mysql -u{username} -p{password}
  • 选择表: USE tables;
  • 查看表/数据库: SHOW tables/databases;
  • 查看列: SHOW COLUMNS FROM tables;

检索数据

  • 单列: SELECT col FROM tables;
  • 多列: SELECT col1, col2 FROM tables;
  • 所有列: SELECT * FROM tables;
  • 检索不同的行: SELECT DISTINCT col FROM tables;
  • 限制结果: SELECT col FROM tables LIMIT 1,1

注意:

  1. 检索出来的数据都是未排序的,并且原始数据没有格式
  2. 一般不要使用*通配符,指出需要检索的列提高性能
  3. 通配符用处:检索未知列
  4. 不能部分使用DISTINCT
  5. LIMIT第一个参数为开始位置,第二个为检索的行数,开始位置从0开始

排序检索数据

  • 单列排序: SELECT col FROM table ORDER BY col
  • 多列排序: SELECT col1,col2 FROM table ORDER BY col1,col2
  • 指定排序方向: SELECT col FROM table ORDER BY col DESC
  • 多列指定方向: SELECT col1, col2 FROM table ORDER BY col1 DESC, col2

注意:

  1. 如果不明确排序顺序,不应假定检索出来的数据有顺序
  2. 多列时排序关键字有主次顺序之分
  3. MySQL默认为升序排序
  4. 如果想要对多个列降序排序,需要对每个列指定DESC关键字
  5. 位置: FROM ORDER BY LIMIT

过滤数据

  • 检查单个值: SELECT col FROM table WHERE col = ?
  • 检查不匹配: SELECT col FROM table WHERE col <> ?
  • 检查范围值: SELECT col FROM table WHERE col < ?
  • 空值检查: SELECT col FROM table WHERE col IS NULL
  • 组合WHERE子句:
    1. AND操作符: SELECT col FROM table WHERE col1 = ? AND col2 = ?
    2. OR操作符: SELECT col FROM table WHERE col1 = ? OR col2 = ?
    3. IN操作符: SELECT col FROM table WHERE col IN (val1, val2)
    4. NOT操作符: SELECT col FROM table WHERE col NOT IN (val1, val2)

注意:

  1. 数据检索和过滤应在数据库中完成,以提高应用的性能和减少带宽消耗
  2. AND和OR计算次序:AND会被优先计算,优先级需要使用圆括号来提升
  3. IN的功能和OR相当,但是IN的执行更快且可以包含其他SELECT语句建立子句

用通配符过滤数据

  • 百分号(%)通配符: SELECT col FROM table WHERE col LIKE 'xx%'
  • 下划线(_)通配符: SELECT col FROM table WHERE col LIKE '_ xx'

注意:

  1. 尾空格会干扰通配符匹配
  2. %不能匹配NULL的值
  3. 下划线只匹配单个字符
  4. 不要过度使用通配符

使用正则表达式过滤

  • SELECT col FROM table WHERE col REGEXP 'reg'
  • 匹配字符类
  • 定位符


注意:

  1. MySQL只支持正则的一个很小的子集
  2. 区分大小写需要在REGEXP后加BINARY关键字

计算字段

  • 拼接字段: SELECT Concat(col1, 'str', col2, 'str2') FROM table
  • 使用别名: SELECT col AS name FROM table
  • 执行算术计算: SELECT col*col2 AS name FROM table

注意:

  1. 计算字段用于格式化检索出的数据
  2. LTrim()和RTrim()函数可以去除字段左右两边空格

数据处理函数

常用文本处理函数:


常用日期处理函数:

  • 检索时间: SELECT col FROM table WHERE Date(col2) = 'xxxx-xx-xx'
  • 检索时间段: SELECT col FROM table WHERE Date(col) BETWEEN 'xxxx-xx-xx' AND 'xxxx-xx-xx'

注意:

  1. 总是应该使用4位数的年份

数值处理函数:

汇总数据

聚集函数

注意:

  1. 使用COUNT(*)计数包括NULL值,COUNT(col)则不包含NULL值

分组数据

  • 创建分组: SELECT col FROM table GROUP BY col
  • HAVING过滤分组: SELECT col, COUNT(*) FROM table GROUP BY col HAVING COUNT(*) > 2

注意:

  1. GROUP BY子句可以包含任意数目的列
  2. 建立分组时,指定的所有列都一起计算
  3. GROUP BY子句中列出的每个列都必须是检索列或有效表达式
  4. 除计算语句外, SELECT的每个列都需要在GROUP BY中给出
  5. 如果分组中有NULL值,将NULL作为一个分组返回
  6. GROUP BY子句必须出现在WHERE之后,ORDER BY之前
  7. WHERE在分组之前过滤,HAVING在分组之后过滤
  8. GROUP BY之后应该

使用子查询

  • 利用子查询进行过滤: SELECT col FROM table1 WHERE col IN (SELECT col2 FROM table2 WHERE col2=?)
  • 作为计算字段: SELECT col,(SELECT COUNT(*) FROM table2 WHERE col3=col4) FROM table1

注意:

  1. 列必须匹配,子查询SELECT和WHERE应保持相同的列
  2. 使用子查询时需要考虑效率和性能

联结表

  • 创建联结: SELECT col1, col2 FROM table1, table2 WHERE table1.col=table2.col
  • 内部联结: SELECT col1, col2 FROM table1 INNER JOIN table2 ON table1.col=table2.col
  • 外部联结: SELECT col1, col2 FROM table1 LEFT OUTER JOIN table2 ON table1.col=table2.col

注意:

  1. 在引用的列名可能出现二义性时需要使用完全限定列名
  2. 没有WHERE语句限定的结果将返回笛卡尔积
  3. WHERE为内部联结,是等值联结
  4. 可以使用表别名缩短sql语句和重复使用一张表

组合查询

  • 使用UNION: SELECT col FROM table1 WHERE col2=? UNION SELECT col FROM table1 WHERE col3=?

注意:

  1. UNION必须由两条或以上的SELECT组成
  2. UNION中的每个查询必须包含相同的列,表达式或聚集函数
  3. 列数据必须兼容
  4. UNION默认会去重,取消去重使用UNION ALL查询
  5. UNION只能使用一条排序语句出现在最后一个SELECT之后

全文本搜索

  • SELECT col FROM table WHERE Match(col) Against('text')
  • 对结果排序: SELECT col,Match(col) Against('text') AS rank FROM table

使用视图

  • CREATE VIEW view1 AS SELECT col FROM table
0%