目录
三、对于有大数据量的mysql表来说,使用LIMIT分页存在很严重的性能问题。
有一个这样的场景,一张小表A,里面存储了一些ID,大约几百个
在任何类型的ORM中,聚合(aggregation)都是造成混乱的根源,而Django也是如此。该文档提供了各种示例,演示了如何使用Django的ORM对数据进行分组(group)和聚合(aggregation),但是我决定从另一个角度进行研究。
在本文中,我将QuerySet和SQL并排放置。如果您最喜欢SQL语言,那么这是适合您的Django GROUP BY速查表。
为了演示不同的GROUP BY查询,我将使用Django内置django.contrib.auth应用程序中的模型。

让我们计算一下我们有多少用户:

对行进行计数非常普遍,以至于Django在QuerySet上就为其包含了一个函数。与其他QuerySet不同,我们接下来将看到它count返回一个数字。
Django还提供了其它两种方法来对表中的行数。
我们将从从aggregate开始:

为了使用aggregate我们导入了聚合函数Count。在这种情况下,我们使用主键列的名称id来计数表中的所有行。
聚合返回的结果是一个字典,如下所示:

返回字典的默认键名key是id__count。最好不要依赖此命名约定,而是提供自己的名称:

使用aggregate我们得到了将聚合函数(比如Count, Max, Sum)应用于整个表后的结果,这很有用,但是通常我们希望将表中的记录分成各个组(group),然后在对每个组应用聚合函数。
现在让我们根据用户的活动状态分组, 再来统计每个组的人数:

这次我们使用了annotate。为了生产GROUP BY我们使用的组合values和annotate:
values('is_active'):根据什么分组
annotate(total=Count('id')):对什么进行聚合
返回结果是is_active的值以及每种状态的人数。
顺序很重要:在调用values方法之前使用annotate不会以注释形式添加聚合函数结果。
要将聚合函数应用于过滤后的查询集,可以在查询中的任何位置使用filter。例如,仅统计公司职员(staff)的活动状态计数:

像过滤器一样,可在查询语句中的任何位置使用order_by对查询集进行排序:

注意:你可以同时对表中本身的字段(is_active)和聚合后生成的字段(total)进行排序。
对同一组数据使用多个聚合函数,请一次添加多个注释:

该查询将产生活动和不活动用户的数量,以及每个组中用户加入的最后日期。
就像执行多个聚合函数一样,我们可能还希望按表中多个字段进行分组。例如,按活动状态和人员状态分组:

该查询的结果包括和is_active,is_staff以及每个组中的用户数。
GROUP BY的另一个常见用例是按表达式分组。例如,计算每年加入的用户数:

请注意,要从我们在第一次调用时使用特殊表达式的日期开始获取年份values()。查询的结果是一个dict,键的名称为date_joined__year。
有时内置表达式还不够,您需要汇总更复杂的表达式。例如,对注册后已登录过的用户进行分组:

这里的表达相当复杂。我们首先使用annotate来构建表达式,然后通过在以下对的调用中引用该表达式,将其标记为GROUP BY键values()。从这里开始,它是完全一样的。
使用条件聚合,您只能聚合组的一部分。当您有多个聚合时,条件会派上用场。例如,按签约年份计算职员和编外用户的数量:

上面的SQL语句来自PostgreSQL,它与SQLite一起是当前唯一支持FILTER语法快捷方式(正式称为“选择性聚合”)的数据库后端。对于其他数据库后端,ORM将CASE ... WHEN代替使用。
该HAVING用于过滤聚合函数的结果,例如查找在哪些年份有100个以上的用户注册了:

如何按distinct分组
对于某些聚合函数,比如COUNT,有时希望仅对不同的事件进行计数。例如统计每个用户活动状态有多少个不同的姓氏:

聚合字段通常只是一个更大问题的第一步。例如按用户活动状态的唯一姓氏百分比是多少:

第一个annotate()定义聚合字段。第二种annotate()使用聚合函数构造表达式。
到目前为止,我们仅在单个模型中使用了数据,但是聚合通常用于跨关系。更简单的方案是一对一或外键关系。例如,假设我们UserProfile与User之间具有一对一的关系,并且我们想按配置文件类型对用户进行计数:

就像GROUP BY表达式一样,在values中使用关系将按该字段分组。请注意,结果中的用户配置文件类型的名称将为“ user_profile__type”。
一种更复杂的关系类型是多对多关系。例如计算每个用户是多少个组的成员:

一个用户可以是多个组的成员。为了计算用户所属的组数,我们在User模型中使用了相关的名称“组” 。如果未显式设置相关名称(related_name),则Django将自动以format生成名称{related model model}_set。例如,group_set。
最近项目使用了PostgreSQL 简单学习join语法以及原理,以后有时间搞一下SQLite源码。
PostgreSQL JOIN 子句用于把来自两个或多个表的行结合起来,基于这些表之间的共同字段。
在 PostgreSQL 中,JOIN 有五种连接类型:
CROSS JOIN :交叉连接
INNER JOIN:内连接
LEFT OUTER JOIN:左外连接
RIGHT OUTER JOIN:右外连接
FULL OUTER JOIN:全外连接
创建company表和department表 其中company表存储员工基本信息 department表存储部门信息。
company表定义以及初始化数据如下:
DROP TABLE COMPANY;
CREATE TABLE COMPANY(
ID INT PRIMARY KEY NOT NULL,
NAME TEXT NOT NULL,
AGE INT NOT NULL,
ADDRESS CHAR(50),
SALARY REAL
);
- INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (1, 'Paul', 32, 'California', 20000.00 );
-
- INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (2, 'Allen', 25, 'Texas', 15000.00 );
-
- INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (3, 'Teddy', 23, 'Norway', 20000.00 );
-
- INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (4, 'Mark', 25, 'Rich-Mond ', 65000.00 );
-
- INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (5, 'David', 27, 'Texas', 85000.00 );
-
- INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (6, 'Kim', 22, 'South-Hall', 45000.00 );
-
- INSERT INTO COMPANY VALUES (7, 'James', 24, 'Houston', 10000.00 );
-
- INSERT INTO COMPANY VALUES (8, 'Paul', 24, 'Houston', 20000.00);
-
- INSERT INTO COMPANY VALUES (9, 'James', 44, 'Norway', 5000.00);
-
- INSERT INTO COMPANY VALUES (10, 'James', 45, 'Texas', 5000.00);

department表定义以及初始化如下:
CREATE TABLE DEPARTMENT(
ID INT PRIMARY KEY NOT NULL,
DEPT CHAR(50) NOT NULL,
EMP_ID INT NOT NULL
);
- INSERT INTO DEPARTMENT (ID, DEPT, EMP_ID) VALUES (1, 'IT Billing', 1 );
-
- INSERT INTO DEPARTMENT (ID, DEPT, EMP_ID) VALUES (2, 'Engineering', 2 );
-
- INSERT INTO DEPARTMENT (ID, DEPT, EMP_ID) VALUES (3, 'Finance', 7 );

交叉连接(CROSS JOIN)把第一个表的每一行与第二个表的每一行进行匹配。如果两个输入表分别有 x 和 y 行,则结果表有 x*y 行。
SELECT EMP_ID, NAME, DEPT FROM COMPANY CROSS JOIN DEPARTMENT;

其相应查询计划如下所示:

内连接(INNER JOIN)根据连接谓词结合两个表(table1 和 table2)的列值来创建一个新的结果表。查询会把 table1 中的每一行与 table2 中的每一行进行比较,找到所有满足连接谓词的行的匹配对。当满足连接谓词时,A 和 B 行的每个匹配对的列值会合并成一个结果行。内连接(INNER JOIN)是最常见的连接类型,是默认的连接类型。INNER 关键字是可选的。
SELECT EMP_ID, NAME, DEPT FROM COMPANY INNER JOIN DEPARTMENT ON COMPANY.ID = DEPARTMENT.EMP_ID;


对于左外连接,首先执行一个内连接。然后,对于表 T1 中不满足表 T2 中连接条件的每一行,其中 T2 的列中有 null 值也会添加一个连接行。因此,连接的表在 T1 中每一行至少有一行。

首先,执行内部连接。然后,对于表T2中不满足表T1中连接条件的每一行,其中T1列中的值为空也会添加一个连接行。这与左联接相反;对于T2中的每一行,结果表总是有一行。

首先,执行内部连接。然后,对于表 T1 中不满足表 T2 中任何行连接条件的每一行,如果 T2 的列中有 null 值也会添加一个到结果中。此外,对于 T2 中不满足与 T1 中的任何行连接条件的每一行,将会添加 T1 列中包含 null 值的到结果中。


由以上操作可知 大多数连接会使用Hash Join算法来实现。postgreSQL中join算法有三种nested loop join merge join 以及hash join
nested loop join: The right relation is scanned once for every row found in the left relation. This strategy is easy to implement but can be very time consuming. (However, if the right relation can be scanned with an index scan, this can be a good strategy. It is possible to use values from the current row of the left relation as keys for the index scan of the right.)
EXPLAIN SELECT * FROM COMPANY JOIN DEPARTMENT ON DEPARTMENT.EMP_ID = COMPANY.ID WHERE company."id" = 1;
1. 表company按照id来过滤得到结果
2. 对于过滤后结果每一行,利用id从department表中进行匹配
merge join: Each relation is sorted on the join attributes before the join starts. Then the two relations are scanned in parallel, and matching rows are combined to form join rows. This kind of join is more attractive because each relation has to be scanned only once. The required sorting might be achieved either by an explicit sort step, or by scanning the relation in the proper order using an index on the join key.
set enable_hashjoin=off;
EXPLAIN SELECT * FROM COMPANY JOIN DEPARTMENT ON DEPARTMENT.emp_id = COMPANY.ID;

hash join: the right relation is first scanned and loaded into a hash table, using its join attributes as hash keys. Next the left relation is scanned and the appropriate values of every row found are used as hash keys to locate the matching rows in the table.
set enable_hashjoin=on;
EXPLAIN SELECT * FROM COMPANY JOIN DEPARTMENT ON DEPARTMENT.emp_id = COMPANY.ID WHERE company."id" = 1;

EXPLAIN SELECT * FROM COMPANY JOIN DEPARTMENT ON DEPARTMENT.emp_id = COMPANY.ID WHERE company.age = 32;

首先顺序扫描department表 构建hash表 key=department.emp_id 即关联字段,
然后顺序扫描company表 用company表Id来匹配hash表key 如果匹配成功 则输出。
hash join和merge join被关联的两个表都只扫描一次, nested loop join则被关联的表其中一个扫描一次, (如果前一个表的扫描结果有多行输出)另一个扫描多次.
参考一下hash join实现源码:
将主驱动表的关联字段作为key,主驱动表需要的字段作为value来构建hash表。
遍历被驱动表的每一行 计算该行是否与hash表中key相同 如果key相同则将被驱动表相应字段和命中hash表key对应的value一起输出,作为结果中的一行。由于hash表的使用,被驱动表的每一行查找时间复杂度为常数。
- for (j = 0; j < length(inner); j++)
-
- hash_key = hash(inner[j]);
-
- append(hash_store[hash_key], inner[j]);
-
- for (i = 0; i < length(outer); i++)
-
- hash_key = hash(outer[i]);
-
- for (j = 0; j < length(hash_store[hash_key]); j++)
-
- if (outer[i] == hash_store[hash_key][j])
-
- output(outer[i], inner[j]);
解释如下:
- //利用 inner 表, 来构造 hash 表(放在内存里)
-
- for (j = 0; j < length(inner); j++)
-
- {
-
- hash_key = hash(inner[j]);
-
- append(hash_store[hash_key], inner[j]);
-
- }
-
- //对 outer 表的每一个元素, 进行遍历
-
- for (i = 0; i < length(outer); i++)
-
- {
-
- //拿到 outer 表中的 某个元素, 进行 hash运算, 得到其 hash_key 值
-
- hash_key = hash(outer[i]);
-
- //用上面刚得到的 hash_key值, 来 对 hash 表进行 探测(假定hash表中有此key 值)
-
- //采用 length (hash_store[hash_Key]) 是因为,hash算法构造完hash 表后,有可能出现一个key值处有多个元素的情况。
-
- //对 拥有相同 的 (此处是上面刚运算的,特定的)hash_key 值的各个元素的遍历
-
- for (j = 0; j < length(hash_store[hash_key]); j++)
-
- {
-
- //如果找到了匹配值,则输出一行结果
-
- if (outer[i] == hash_store[hash_key][j])
-
- output(outer[i], inner[j]);
-
- }
-
- }
MySQL版本:5.5.28-0ubuntu0.12.04.2-log
我还认为JOIN总是比MySQL中的子查询更好,但EXPLAIN是一种更好的判断方式。 这是一个子查询比JOIN更好的例子。
这是我的3个子查询的查询:
- EXPLAIN SELECT vrl.list_id,vrl.ontology_id,vrl.position,l.name AS list_name, vrlih.position AS previous_position, vrl.moved_date
- FROM `vote-ranked-listory` vrl
- INNER JOIN lists l ON l.list_id = vrl.list_id
- INNER JOIN `vote-ranked-list-item-history` vrlih ON vrl.list_id = vrlih.list_id AND vrl.ontology_id=vrlih.ontology_id AND vrlih.type='PREVIOUS_POSITION'
- INNER JOIN list_burial_state lbs ON lbs.list_id = vrl.list_id AND lbs.burial_score < 0.5
- WHERE vrl.position <= 15 AND l.status='ACTIVE' AND l.is_public=1 AND vrl.ontology_id < 1000000000
- AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=43) IS NULL
- AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=55) IS NULL
- AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=246403) IS NOT NULL
- ORDER BY vrl.moved_date DESC LIMIT 200;
EXPLAIN显示:
- +----+--------------------+----------+--------+-----------------------------------------------------+--------------+---------+-------------------------------------------------+------+--------------------------+
- | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
- +----+--------------------+----------+--------+-----------------------------------------------------+--------------+---------+-------------------------------------------------+------+--------------------------+
- | 1 | PRIMARY | vrl | index | PRIMARY | moved_date | 8 | NULL | 200 | Using where |
- | 1 | PRIMARY | l | eq_ref | PRIMARY,status,ispublic,idx_lookup,is_public_status | PRIMARY | 4 | ranker.vrl.list_id | 1 | Using where |
- | 1 | PRIMARY | vrlih | eq_ref | PRIMARY | PRIMARY | 9 | ranker.vrl.list_id,ranker.vrl.ontology_id,const | 1 | Using where |
- | 1 | PRIMARY | lbs | eq_ref | PRIMARY,idx_list_burial_state,burial_score | PRIMARY | 4 | ranker.vrl.list_id | 1 | Using where |
- | 4 | DEPENDENT SUBQUERY | list_tag | ref | list_tag_key,list_id,tag_id | list_tag_key | 9 | ranker.l.list_id,const | 1 | Using where; Using index |
- | 3 | DEPENDENT SUBQUERY | list_tag | ref | list_tag_key,list_id,tag_id | list_tag_key | 9 | ranker.l.list_id,const | 1 | Using where; Using index |
- | 2 | DEPENDENT SUBQUERY | list_tag | ref | list_tag_key,list_id,tag_id | list_tag_key | 9 | ranker.l.list_id,const | 1 | Using where; Using index |
- +----+--------------------+----------+--------+-----------------------------------------------------+--------------+---------+-------------------------------------------------+------+--------------------------+
与JOIN相同的查询是:
- EXPLAIN SELECT vrl.list_id,vrl.ontology_id,vrl.position,l.name AS list_name, vrlih.position AS previous_position, vrl.moved_date
- FROM `vote-ranked-listory` vrl
- INNER JOIN lists l ON l.list_id = vrl.list_id
- INNER JOIN `vote-ranked-list-item-history` vrlih ON vrl.list_id = vrlih.list_id AND vrl.ontology_id=vrlih.ontology_id AND vrlih.type='PREVIOUS_POSITION'
- INNER JOIN list_burial_state lbs ON lbs.list_id = vrl.list_id AND lbs.burial_score < 0.5
- LEFT JOIN list_tag lt1 ON lt1.list_id = vrl.list_id AND lt1.tag_id = 43
- LEFT JOIN list_tag lt2 ON lt2.list_id = vrl.list_id AND lt2.tag_id = 55
- INNER JOIN list_tag lt3 ON lt3.list_id = vrl.list_id AND lt3.tag_id = 246403
- WHERE vrl.position <= 15 AND l.status='ACTIVE' AND l.is_public=1 AND vrl.ontology_id < 1000000000
- AND lt1.list_id IS NULL AND lt2.tag_id IS NULL
- ORDER BY vrl.moved_date DESC LIMIT 200;
输出是:
- +----+-------------+-------+--------+-----------------------------------------------------+--------------+---------+---------------------------------------------+------+----------------------------------------------+
- | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
- +----+-------------+-------+--------+-----------------------------------------------------+--------------+---------+---------------------------------------------+------+----------------------------------------------+
- | 1 | SIMPLE | lt3 | ref | list_tag_key,list_id,tag_id | tag_id | 5 | const | 2386 | Using where; Using temporary; Using filesort |
- | 1 | SIMPLE | l | eq_ref | PRIMARY,status,ispublic,idx_lookup,is_public_status | PRIMARY | 4 | ranker.lt3.list_id | 1 | Using where |
- | 1 | SIMPLE | vrlih | ref | PRIMARY | PRIMARY | 4 | ranker.lt3.list_id | 103 | Using where |
- | 1 | SIMPLE | vrl | ref | PRIMARY | PRIMARY | 8 | ranker.lt3.list_id,ranker.vrlih.ontology_id | 65 | Using where |
- | 1 | SIMPLE | lt1 | ref | list_tag_key,list_id,tag_id | list_tag_key | 9 | ranker.lt3.list_id,const | 1 | Using where; Using index; Not exists |
- | 1 | SIMPLE | lbs | eq_ref | PRIMARY,idx_list_burial_state,burial_score | PRIMARY | 4 | ranker.vrl.list_id | 1 | Using where |
- | 1 | SIMPLE | lt2 | ref | list_tag_key,list_id,tag_id | list_tag_key | 9 | ranker.lt3.list_id,const | 1 | Using where; Using index |
- +----+-------------+-------+--------+-----------------------------------------------------+--------------+---------+---------------------------------------------+------+----------------------------------------------+
rows列的比较告诉了区别,而JOIN的查询是使用Using temporary; Using filesort Using temporary; Using filesort 。
当然,当我运行两个查询时,第一个查询在0.02秒内完成,第二个查询在1分钟后仍未完成,因此EXPLAIN正确解释了这些查询。
如果我在list_tag表上没有INNER JOIN,即如果我删除了
AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=246403) IS NOT NULL
从第一个查询和相应的:
INNER JOIN list_tag lt3 ON lt3.list_id = vrl.list_id AND lt3.tag_id = 246403
从第二个查询开始,EXPLAIN为两个查询返回相同的行数,这两个查询的运行速度相同。
仅当第二个连接表的数据明显多于主表时才会出现差异。 我有过如下经历......
我们有一个十万条目的用户表和他们的会员数据(友谊)约三十万条目。 这是一个加入声明,以便接收朋友和他们的数据,但有很大的延迟。 但是在成员资格表中只有少量数据的情况下工作正常。 一旦我们将其更改为使用子查询,它就可以正常工作。
但同时,连接查询正在使用条目少于主表的其他表。
所以我认为连接和子查询语句工作正常,它取决于数据和情况。
子查询是解决形式问题的逻辑上正确的方法,“从A获取事实,以B中的事实为条件”。 在这种情况下,在子查询中粘贴B比进行连接更合乎逻辑。 从实际意义上说,它也更安全,因为你不必因为多次匹配B而从A中获取重复的事实时要谨慎。
然而,实际上,答案通常归结为性能。 一些优化器在给出连接和子查询时会吮吸柠檬,而另一些优化者则以另一种方式吮吸柠檬,这是特定于优化器,特定于DBMS的版本和查询特定的。
从历史上看,显式连接通常会获胜,因此连接的既定智慧更好,但优化器一直在变得越来越好,所以我更喜欢先以逻辑连贯的方式编写查询,然后在性能限制要求时进行重组。
子查询通常用于将单个行作为原子值返回,但它们可用于将值与多个行与IN关键字进行比较。 它们几乎可以在SQL语句中的任何有意义的点上使用,包括目标列表,WHERE子句等。 可以使用简单的子查询作为搜索条件。 例如,在一对表之间:
SELECT title FROM books WHERE author_id = (SELECT id FROM authors WHERE last_name = 'Bar' AND first_name = 'Foo');
请注意,对子查询的结果使用常规值运算符要求只返回一个字段。 如果您对检查一组其他值中是否存在单个值感兴趣,请使用IN:
SELECT title FROM books WHERE author_id IN (SELECT id FROM authors WHERE last_name ~ '^[A-E]');
这显然不同于LEFT-JOIN,你只想加入表A和B中的东西,即使连接条件没有在表B中找到任何匹配的记录,等等。
如果您只是担心速度,则必须检查数据库并编写一个好的查询,看看性能是否有任何显着差异。
在大多数情况下, JOIN比子查询更快,并且子查询的速度非常快。
在JOIN RDBMS可以创建一个更适合您的查询的执行计划,并且可以预测应该加载哪些数据以进行处理并节省时间,这与子查询不同,在子查询中它将运行所有查询并加载所有数据以执行处理。
子查询的好处是它们比JOIN更具可读性:这就是大多数新SQL用户更喜欢它们的原因; 这是简单的方法; 但是在性能方面,JOINS在大多数情况下都更好,即使它们也不难读。
使用EXPLAIN查看数据库如何对数据执行查询。 在这个答案中有一个巨大的“取决于”......
当PostgreSQL认为一个子查询比另一个更快时,它可以将子查询重写为连接或子查询的连接。 这一切都取决于数据,索引,相关性,数据量,查询等。
首先,要比较两者,首先应将查询与子查询区分开来:
对于第一类查询,良好的RDBMS将联接和子查询视为等效,并将生成相同的查询计划。
这些天甚至mysql都这样做。
尽管如此,有时却没有,但这并不意味着连接总是会赢 - 我在mysql中使用子查询提高了性能。 (例如,如果存在阻止mysql规划器正确估计成本的事情,并且规划器没有看到连接变量和子查询变量相同,那么子查询可以通过强制某个路径来胜过连接)。
结论是,如果要确定哪个更好,那么您应该测试连接和子查询变体的查询。
对于第二个类 ,比较没有意义,因为这些查询不能使用连接重写,在这些情况下,子查询是执行所需任务的自然方式,您不应该区别它们。
子查询具有即时计算聚合函数的能力。 例如,查找该书的最低价格,并获得以此价格出售的所有书籍。 1)使用子查询:
- SELECT titles, price
- FROM Books, Orders
- WHERE price =
- (SELECT MIN(price)
- FROM Orders) AND (Books.ID=Orders.ID);
2)使用JOIN
- SELECT MIN(price)
- FROM Orders;
- -----------------
- 2.99
-
- SELECT titles, price
- FROM Books b
- INNER JOIN Orders o
- ON b.ID = o.ID
- WHERE o.price = 2.99;
摘自MySQL手册 ( 13.2.10.11重写子查询作为连接 ):
LEFT [OUTER] JOIN可以比等效的子查询更快,因为服务器可能能够更好地优化它 - 这一事实并非仅针对MySQL Server。
所以子查询可能比LEFT [OUTER] JOIN慢,但在我看来,它们的强度略高于可读性。
我认为引用答案中未充分强调的是特定(使用)案例可能产生的重复和问题结果的问题。
(虽然Marcelo Cantos确实提到过)
我将引用斯坦福大学关于SQL的Lagunita课程的例子。
- +------+--------+------+--------+
- | sID | sName | GPA | sizeHS |
- +------+--------+------+--------+
- | 123 | Amy | 3.9 | 1000 |
- | 234 | Bob | 3.6 | 1500 |
- | 345 | Craig | 3.5 | 500 |
- | 456 | Doris | 3.9 | 1000 |
- | 567 | Edward | 2.9 | 2000 |
- | 678 | Fay | 3.8 | 200 |
- | 789 | Gary | 3.4 | 800 |
- | 987 | Helen | 3.7 | 800 |
- | 876 | Irene | 3.9 | 400 |
- | 765 | Jay | 2.9 | 1500 |
- | 654 | Amy | 3.9 | 1000 |
- | 543 | Craig | 3.4 | 2000 |
- +------+--------+------+--------+
(向特定大学和专业提出的申请)
- +------+----------+----------------+----------+
- | sID | cName | major | decision |
- +------+----------+----------------+----------+
- | 123 | Stanford | CS | Y |
- | 123 | Stanford | EE | N |
- | 123 | Berkeley | CS | Y |
- | 123 | Cornell | EE | Y |
- | 234 | Berkeley | biology | N |
- | 345 | MIT | bioengineering | Y |
- | 345 | Cornell | bioengineering | N |
- | 345 | Cornell | CS | Y |
- | 345 | Cornell | EE | N |
- | 678 | Stanford | history | Y |
- | 987 | Stanford | CS | Y |
- | 987 | Berkeley | CS | Y |
- | 876 | Stanford | CS | N |
- | 876 | MIT | biology | Y |
- | 876 | MIT | marine biology | N |
- | 765 | Stanford | history | Y |
- | 765 | Cornell | history | N |
- | 765 | Cornell | psychology | Y |
- | 543 | MIT | CS | N |
- +------+----------+----------------+----------+
让我们试着找一些申请CS专业(不论大学)的学生的GPA分数
使用子查询:
- select GPA from Student where sID in (select sID from Apply where major = 'CS');
-
- +------+
- | GPA |
- +------+
- | 3.9 |
- | 3.5 |
- | 3.7 |
- | 3.9 |
- | 3.4 |
- +------+
此结果集的平均值为:
- select avg(GPA) from Student where sID in (select sID from Apply where major = 'CS');
-
- +--------------------+
- | avg(GPA) |
- +--------------------+
- | 3.6800000000000006 |
- +--------------------+
使用连接:
- select GPA from Student, Apply where Student.sID = Apply.sID and Apply.major = 'CS';
-
- +------+
- | GPA |
- +------+
- | 3.9 |
- | 3.9 |
- | 3.5 |
- | 3.7 |
- | 3.7 |
- | 3.9 |
- | 3.4 |
- +------+
此结果集的平均值:
- select avg(GPA) from Student, Apply where Student.sID = Apply.sID and Apply.major = 'CS';
-
- +-------------------+
- | avg(GPA) |
- +-------------------+
- | 3.714285714285714 |
- +-------------------+
很明显,第二次尝试在我们的用例中产生了误导性的结果,因为它计算重复计算平均值。 同样显而易见的是,使用基于连接的语句的distinct将不会消除该问题,因为它将错误地保留3.9分中的三分之一。 正确的情况是考虑到我们实际上有两(2)名学生符合我们的查询标准,得出3.9分的两(2)次出现。
在某些情况下,除了任何性能问题之外,在某些情况下,子查询是最安全的方式。
根据我的观察,如两个案例,如果一个表有少于100,000个记录,那么连接将快速工作。
但是如果一个表有超过100,000个记录,那么子查询是最好的结果。
我有一个表,我在下面的查询中创建了500,000条记录,其结果时间就像
- SELECT *
- FROM crv.workorder_details wd
- inner join crv.workorder wr on wr.workorder_id = wd.workorder_id;
结果:13.3秒
- select *
- from crv.workorder_details
- where workorder_id in (select workorder_id from crv.workorder)
结果:1.65秒
在2010年,我会加入这些问题的作者,并会强烈投票支持JOIN 。 但是有了更多的经验(特别是在MySQL中)我可以说:是的子查询可以更好。 我在这里读过多个答案。 有人说,子查询更快,但缺乏一个很好的解释。 我希望我能提供这个(非常)迟到的答案:
首先,让我说最重要的是: 有不同形式的子查询
第二个重要声明: 规模问题
如果您使用子查询,您应该知道 ,DB-Server如何执行子查询。 特别是如果子查询被评估一次或每行! 另一方面,现代DB-Server能够进行大量优化。 在某些情况下,子查询有助于优化查询,但较新版本的DB-Server可能会使优化过时。
SELECT moo, (SELECT roger FROM wilco WHERE moo = me) AS bar FROM foo
请注意,对foo每个结果行执行子查询。 尽可能避免这种情况,它可能会大大减慢对大型数据集的查询速度。 但是如果子查询没有引用foo ,它可以由DB服务器优化为静态内容,并且只能被评估一次。
SELECT moo FROM foo WHERE bar = (SELECT roger FROM wilco WHERE moo = me)
如果幸运的话,DB会在内部将其优化为JOIN 。 如果没有,您的查询将在巨大的数据集上变得非常非常慢,因为它将为foo每一行执行子查询,而不仅仅是select-type中的结果。
- SELECT moo, bar
- FROM foo
- LEFT JOIN (
- SELECT MIN(bar), me FROM wilco GROUP BY me
- ) ON moo = me
这是有趣的。 我们将JOIN与子查询结合起来。 在这里,我们得到了子查询的真正优势。 想象一下在wilco有数百万行的数据集,但只有少数几个不同的me 。 我们现在有一个较小的临时表来加入,而不是加入一个巨大的表。 这可以导致更快的查询,具体取决于数据库大小。 您可以使用CREATE TEMPORARY TABLE ...和INSERT INTO ... SELECT ...获得相同的效果,这可以在非常复杂的查询上提供更好的可读性(但可以将数据集锁定在可重复的读隔离级别)。
- SELECT moo, bar
- FROM (
- SELECT moo, CONCAT(roger, wilco) AS bar
- FROM foo
- GROUP BY moo
- HAVING bar LIKE 'SpaceQ%'
- ) AS temp_foo
- GROUP BY bar
- ORDER BY bar
您可以在多个级别中嵌套子查询。 如果您必须对结果进行分组或排序,这可以对大型数据集有所帮助。 通常,DB-Server为此创建一个临时表,但有时您不需要对整个表进行排序,只需对结果集进行排序。 这可能会提供更好的性能,具体取决于表的大小。
子查询不能替代JOIN ,您不应该像这样使用它们(尽管可能)。 在我看来,正确使用子查询是用来快速替换CREATE TEMPORARY TABLE ... 一个好的子查询以某种方式减少数据集,您无法在JOIN的ON语句中完成。 如果子查询具有关键字GROUP BY或DISTINCT ,并且最好不位于select字段或where语句中,那么它可能会大大提高性能。
我只是考虑同样的问题,但我在FROM部分使用子查询。 我需要从大表连接和查询,“slave”表有2800万条记录,但结果只有128个这么小的结果大数据! 我正在使用MAX()函数。
首先我使用LEFT JOIN,因为我认为这是正确的方法,mysql可以优化等。第二次只是为了测试,我重写为JOIN的子选择。
LEFT JOIN运行时:1.12s SUB-SELECT运行时:0.06s
subselect比连接快18倍! 就在chokito adv。 该子选择看起来很糟糕,但结果......
有些人说“有些RDBMS可以将子查询重写为连接,或者当它认为一个子查询比另一个更快时可以连接到子查询 。”但是这个语句适用于简单的情况,当然不适用于带有子查询的复杂查询,这实际上导致了表现上的问题。
如果您想使用join加速查询:
对于“内部连接/连接”,不要使用where条件而是在“ON”条件下使用它。 例如:
- select id,name from table1 a
- join table2 b on a.name=b.name
- where id='123'
-
- Try,
-
- select id,name from table1 a
- join table2 b on a.name=b.name and a.id='123'
对于“左/右连接”,不要在“开”状态下使用,因为如果使用左/右连接,它将获得任何一个表的所有行。因此,不要在“开”中使用它。 所以,尝试使用“Where”条件
在旧的Mambo CMS上运行一个非常大的数据库:
- SELECT id, alias
- FROM
- mos_categories
- WHERE
- id IN (
- SELECT
- DISTINCT catid
- FROM mos_content
- );
0秒
- SELECT
- DISTINCT mos_content.catid,
- mos_categories.alias
- FROM
- mos_content, mos_categories
- WHERE
- mos_content.catid = mos_categories.id;
~3秒
EXPLAIN显示它们检查完全相同的行数,但一个需要3秒,一个接近瞬间。 故事的道德启示? 如果性能很重要(何时不是?),请尝试多种方式,看看哪一个最快。
和...
- SELECT
- DISTINCT mos_categories.id,
- mos_categories.alias
- FROM
- mos_content, mos_categories
- WHERE
- mos_content.catid = mos_categories.id;
0秒
同样,相同的结果,检查的行数相同。 我的猜测是,DISTINCT mos_content.catid需要比DISTINCT mos_categories.id更长的时间来计算。
包含子查询的许多Transact-SQL语句也可以表示为连接。 其他问题只能通过子查询提出。 在Transact-SQL中,包含子查询的语句与不包含子查询的语义等效版本之间通常没有性能差异。 但是,在某些必须检查存在的情况下,连接会产生更好的性能。 否则,必须为外部查询的每个结果处理嵌套查询,以确保消除重复项。 在这种情况下,联接方法会产生更好的结果。
所以,如果你需要类似的东西
select * from t1 where exists select * from t2 where t2.parent=t1.id
尝试使用连接。 在其他情况下,它没有任何区别。
我说:为子查询创建函数消除了cluttter的问题,并允许您为子查询实现额外的逻辑。 所以我建议尽可能为子查询创建函数。
代码杂乱是一个大问题,业界几十年来一直在努力避免它。
自己的一个网站,由于单表的数据记录高达了一百万条,造成数据访问很慢,Google分析的后台经常报告超时,尤其是页码大的页面更是慢的不行。
先让我们熟悉下基本的sql语句,来查看下我们将要测试表的基本信息
use infomation_schema
SELECT * FROM TABLES WHERE TABLE_SCHEMA = ‘dbname’ AND TABLE_NAME = ‘product’
查询结果:

从上图中我们可以看到表的基本信息:
表行数:866633
平均每行的数据长度:5133字节
单表大小:4448700632字节
关于行和表大小的单位都是字节,我们经过计算可以知道
平均行长度:大约5k
单表总大小:4.1g
表中字段各种类型都有varchar、datetime、text等,id字段为主键
1. 直接用limit start, count分页语句, 也是我程序中用的方法:
select * from product limit start, count
当起始页较小时,查询没有性能问题,我们分别看下从10, 100, 1000, 10000开始分页的执行时间(每页取20条), 如下:
select * from product limit 10, 20 0.016秒
select * from product limit 100, 20 0.016秒
select * from product limit 1000, 20 0.047秒
select * from product limit 10000, 20 0.094秒
我们已经看出随着起始记录的增加,时间也随着增大, 这说明分页语句limit跟起始页码是有很大关系的,那么我们把起始记录改为40w看下(也就是记录的一般左右) select * from product limit 400000, 20 3.229秒
再看我们取最后一页记录的时间
select * from product limit 866613, 20 37.44秒
难怪搜索引擎抓取我们页面的时候经常会报超时,像这种分页最大的页码页显然这种时
间是无法忍受的。
从中我们也能总结出两件事情:
1)limit语句的查询时间与起始记录的位置成正比
2)mysql的limit语句是很方便,但是对记录很多的表并不适合直接使用。
2. 对limit分页问题的性能优化方法
利用表的覆盖索引来加速分页查询
我们都知道,利用了索引查询的语句中如果只包含了那个索引列(覆盖索引),那么这种情况会查询很快。
因为利用索引查找有优化算法,且数据就在查询索引上面,不用再去找相关的数据地址了,这样节省了很多时间。另外Mysql中也有相关的索引缓存,在并发高的时候利用缓存就效果更好了。
在我们的例子中,我们知道id字段是主键,自然就包含了默认的主键索引。现在让我们看看利用覆盖索引的查询效果如何:
这次我们之间查询最后一页的数据(利用覆盖索引,只包含id列),如下:
select id from product limit 866613, 20 0.2秒
相对于查询了所有列的37.44秒,提升了大概100多倍的速度
那么如果我们也要查询所有列,有两种方法,一种是id>=的形式,另一种就是利用join,看下实际情况:
SELECT * FROM product WHERE ID > =(select id from product limit 866613, 1) limit 20
查询时间为0.2秒,简直是一个质的飞跃啊,哈哈
另一种写法
SELECT * FROM product a JOIN (select id from product limit 866613, 20) b ON a.ID = b.id
查询时间也很短,赞!
其实两者用的都是一个原理嘛,所以效果也差不多
在我们使用查询语句的时候,经常要返回前几条或者中间某几行数据,这个时候怎么办呢?不用担心,mysql已经为我们提供了这样一个功能。
- SELECT * FROM table LIMIT [offset,] rows | `rows OFFSET offset `
- (LIMIT offset, `length`)
- SELECT
- *
- FROM table
- where condition1 = 0
- and condition2 = 0
- and condition3 = -1
- and condition4 = -1
- order by id asc
- LIMIT 2000 OFFSET 50000
LIMIT 子句可以被用于强制 SELECT 语句返回指定的记录数。LIMIT 接受一个或两个数字参数。参数必须是一个整数常量。如果给定两个参数,第一个参数指定第一个返回记录行的偏移量,第二个参数指定返回记录行的最大数目。初始记录行的偏移量是 0(而不是 1): 为了与 PostgreSQL 兼容,MySQL 也支持句法: LIMIT # OFFSET #。
mysql> SELECT * FROM table LIMIT 5,10; // 检索记录行 6-15
//为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为 -1:
mysql> SELECT * FROM table LIMIT 95,-1; // 检索记录行 96-last.
//如果只给定一个参数,它表示返回最大的记录行数目: mysql> SELECT * FROM table LIMIT 5; //检索前 5 个记录行
//换句话说,LIMIT n 等价于 LIMIT 0,n。
MySql分页sql语句,如果和MSSQL的TOP语法相比,那么MySQL的LIMIT语法要显得优雅了许多。使用它来分页是再自然不过的事情了。
最基本的分页方式:
SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT ...
在中小数据量的情况下,这样的SQL足够用了,唯一需要注意的问题就是确保使用了索引:
举例来说,如果实际SQL类似下面语句,那么在category_id, id两列上建立复合索引比较好:
SELECT * FROM articles WHERE category_id = 123 ORDER BY id LIMIT 50, 10
子查询的分页方式:
随着数据量的增加,页数会越来越多,查看后几页的SQL就可能类似:
SELECT * FROM articles WHERE category_id = 123 ORDER BY id LIMIT 10000, 10
一言以蔽之,就是越往后分页,LIMIT语句的偏移量就会越大,速度也会明显变慢。
此时,我们可以通过子查询的方式来提高分页效率,大致如下:
- SELECT * FROM articles WHERE id >=
- (SELECT id FROM articles WHERE category_id = 123 ORDER BY id LIMIT 10000, 1) LIMIT 10
JOIN分页方式
- SELECT * FROM `content` AS t1
- JOIN (SELECT id FROM `content` ORDER BY id desc LIMIT ".($page-1)*$pagesize.", 1) AS t2
- WHERE t1.id <= t2.id ORDER BY t1.id desc LIMIT $pagesize;
经过我的测试,join分页和子查询分页的效率基本在一个等级上,消耗的时间也基本一致。
explain SQL语句:
- id select_type table type possible_keys key key_len ref rows Extra
- 1 PRIMARY <derived2> system NULL NULL NULL NULL 1
- 1 PRIMARY t1 range PRIMARY PRIMARY 4 NULL 6264 Using where
- 2 DERIVED content index NULL PRIMARY 4 NULL 27085 Using index
为什么会这样呢?因为子查询是在索引上完成的,而普通的查询时在数据文件上完成的,通常来说,索引文件要比数据文件小得多,所以操作起来也会更有效率。
实际可以利用类似策略模式的方式去处理分页,比如判断如果是一百页以内,就使用最基本的分页方式,大于一百页,则使用子查询的分页方式。
查询从第1000000之后的30条记录:
- SQL代码1:平均用时6.6秒 SELECT * FROM `cdb_posts` ORDER BY pid LIMIT 1000000 , 30
-
- SQL代码2:平均用时0.6秒 SELECT * FROM `cdb_posts` WHERE pid >= (SELECT pid FROM
- `cdb_posts` ORDER BY pid LIMIT 1000000 , 1) LIMIT 30
因为要取出所有字段内容,第一种需要跨越大量数据块并取出,而第二种基本通过直接根据索引字段定位后,才取出相应内容,效率自然大大提升。对limit的优化,不是直接使用limit,而是首先获取到offset的id,然后直接使用limit size来获取数据。
可以看出,越往后分页,LIMIT语句的偏移量就会越大,两者速度差距也会越明显。
实际应用中,可以利用类似策略模式的方式去处理分页,比如判断如果是一百页以内,就使用最基本的分页方式,大于一百页,则使用子查询的分页方式。
优化思想:避免数据量大时扫描过多的记录


为了保证index索引列连续,可以为每个表加一个自增字段,并且加上索引
Mysql的分页查询十分简单,但是当数据量大的时候一般的分页就吃不消了。
传统分页查询:SELECT c1,c2,cn… FROM table LIMIT n,m
MySQL的limit工作原理就是先读取前面n条记录,然后抛弃前n条,读后面m条想要的,所以n越大,偏移量越大,性能就越差。
推荐分页查询方法:
1、尽量给出查询的大致范围
2、子查询法
3、高性能MySQL一书中提到的只读索引方法
优化前SQL:
优化后SQL:
分别在于,优化前的SQL需要更多I/O浪费,因为先读索引,再读数据,然后抛弃无需的行。而优化后的SQL(子查询那条)只读索引(Cover index)就可以了,然后通过member_id读取需要的列。
4、第一步用用程序读取出ID,然后再用IN方法读取所需记录
程序读ID:
最后示例:
select * from t_students limit100000,20
select * from t_students where id>=(select id from t_students order by id limit 100000,1) limit 20 --推荐 ,利用索引
select t.* from t_students t join (select id from t_students order by id limit 100000,20) a on t.id=a.id --推荐 ,利用索引
创建测试表
CREATE TABLE test_order ( client_id INT NOT NULL, order_date TIMESTAMP NOT NULL, filler TEXT NOT NULL );
插入测试数据
INSERT INTO test_order
SELECT s1.id,
(CURRENT_DATE - INTERVAL '1000 days')::DATE
+ generate_series(1, s1.id%1000),
repeat(' ', 20)
FROM generate_series(1, 10000) s1 (id);
CREATE INDEX idx_test_order_client_id_order_date ON test_order (client_id, order_date DESC);
执行普通SQL
不走索引
EXPLAIN ANALYZE SELECT client_id, max(order_date) FROM test_order GROUP BY client_id;
"Execution time: 5741.682 ms"
使用索引
EXPLAIN ANALYZE SELECT DISTINCT ON (client_id) client_id, order_date FROM test_order ORDER BY client_id, order_date DESC;
"Execution time: 4628.510 ms"
优化后SQL
EXPLAIN ANALYZE WITH RECURSIVE skip AS ( (SELECT client_id, order_date FROM test_order ORDER BY client_id, order_date DESC LIMIT 1) UNION ALL (SELECT (SELECT min(client_id) FROM test_order WHERE client_id > skip.client_id ) AS client_id, (SELECT max(order_date) FROM test_order WHERE client_id = ( SELECT min(client_id) FROM test_order WHERE client_id > skip.client_id ) ) AS order_date FROM skip WHERE skip.client_id IS NOT NULL) ) SELECT * FROM skip;
"Execution time: 865.889 ms"
查询结果
client_id; order_date 1;"2014-03-09 00:00:00" 2;"2014-03-10 00:00:00" 3;"2014-03-11 00:00:00" 4;"2014-03-12 00:00:00" 5;"2014-03-13 00:00:00" 6;"2014-03-14 00:00:00" 7;"2014-03-15 00:00:00" 8;"2014-03-16 00:00:00" 9;"2014-03-17 00:00:00" 10;"2014-03-18 00:00:00" 11;"2014-03-19 00:00:00" 12;"2014-03-20 00:00:00" 13;"2014-03-21 00:00:00" 14;"2014-03-22 00:00:00" 15;"2014-03-23 00:00:00" 16;"2014-03-24 00:00:00" 17;"2014-03-25 00:00:00" 18;"2014-03-26 00:00:00" 19;"2014-03-27 00:00:00" 20;"2014-03-28 00:00:00" 21;"2014-03-29 00:00:00" 22;"2014-03-30 00:00:00" 23;"2014-03-31 00:00:00" 24;"2014-04-01 00:00:00"
(比如说巡逻车辆ID,环卫车辆的ID,公交车,微公交的ID)。
另外有一张日志表B,每条记录中的ID是来自前面那张小表的,但不是每个ID都出现在这张日志表中,比如说一天可能只有几十个ID会出现在这个日志表的当天的数据中。
(比如车辆的行车轨迹数据,每秒上报轨迹,数据量就非常庞大)。
那么我怎么快速的找出今天没有出现的ID呢。
(哪些巡逻车辆没有出现在这个片区,是不是偷懒了?哪些环卫车辆没有出行,哪些公交或微公交没有出行)?
select id from A where id not in (select id from B where time between ? and ?);
这个QUERY会很慢,有什么优化方法呢。
当然,你还可以让车辆签到的方式来解决这个问题,但是总有未签到的,或者没有这种设计的时候,那么怎么解决呢?
-- A create table a(id int primary key, info text); -- B create table b(id int primary key, aid int, crt_time timestamp); create index b_aid on b(aid); -- a表插入1000条 insert into a select generate_series(1,1000), md5(random()::text); -- b表插入500万条,只包含aid的500个id。 insert into b select generate_series(1,5000000), generate_series(1,500), clock_timestamp(); 优化前: select * from a where id not in (select aid from b); 执行时间:大于1min 优化后: select * from a where id not in (with recursive skip as ( ( select min(aid) aid from b where aid is not null ) union all ( select (select min(aid) aid from b where b.aid > s.aid and b.aid is not null) from skip s where s.aid is not null ) -- 这里的where s.aid is not null 一定要加,否则就死循环了. ) select aid from skip where aid is not null); 执行时间:46 msec
create table subregions (
id smallint primary key,
name text not null,
parent_id smallint null references subregions(id)
);
insert into subregions values
(1,'World',null),
(2,'Africa',1),
(5,'South America',419),
(9,'Oceania',1),
(11,'Western Africa',2),
(13,'Central America',419),
(14,'Eastern Africa',2),
(15,'Northern Africa',2),
(17,'Middle Africa',2),
(18,'Southern Africa',2),
(19,'Americas',1),
(21,'Northern America',19),
(29,'Caribbean',419),
(30,'Eastern Asia',142),
(34,'Southern Asia',142),
(35,'South-Eastern Asia',142),
(39,'Southern Europe',150),
(53,'Australia and New Zealand',9),
(54,'Melanesia',9),
(57,'Micronesia',9),
(61,'Polynesia',9),
(142,'Asia',1),
(143,'Central Asia',142),
(145,'Western Asia',142),
(150,'Europe',1),
(151,'Eastern Europe',150),
(154,'Northern Europe',150),
(155,'Western Europe',150),
(419,'Latin America and the Caribbean',19);
And you wanted to make a pretty tree like this:
World
Africa
Eastern Africa
Middle Africa
Northern Africa
Southern Africa
Western Africa
Americas
Latin America and the Caribbean
Caribbean
Central America
South America
Northern America
Asia
Central Asia
Eastern Asia
South-Eastern Asia
Southern Asia
Western Asia
Europe
Eastern Europe
Northern Europe
Southern Europe
Western Europe
Oceania
Australia and New Zealand
Melanesia
Micronesia
Polynesia
Here's how you'd do it:
with recursive my_expression as (
--start with the "anchor", i.e. all of the nodes whose parent_id is null:
select
id,
name as path,
name as tree,
0 as level
from subregions
where
parent_id is null
union all
--then the recursive part:
select
current.id as id,
previous.path || ' > ' || current.name as path,
repeat(' ', previous.level + 1) || current.name as tree,
previous.level + 1 as level
from subregions current
join my_expression as previous on current.parent_id = previous.id
)
select
tree
from my_expression
order by
path
路径间加入父节点和分割
select
path
from my_expression
order by
path
输出结果:
World
World > Africa
World > Africa > Eastern Africa
World > Africa > Middle Africa
World > Africa > Northern Africa
World > Africa > Southern Africa
World > Africa > Western Africa
World > Americas
World > Americas > Latin America and the Caribbean
World > Americas > Latin America and the Caribbean > Caribbean
World > Americas > Latin America and the Caribbean > Central America
World > Americas > Latin America and the Caribbean > South America
World > Americas > Northern America
World > Asia
World > Asia > Central Asia
World > Asia > Eastern Asia
World > Asia > South-Eastern Asia
World > Asia > Southern Asia
World > Asia > Western Asia
World > Europe
World > Europe > Eastern Europe
World > Europe > Northern Europe
World > Europe > Southern Europe
World > Europe > Western Europe
World > Oceania
World > Oceania > Australia and New Zealand
World > Oceania > Melanesia
World > Oceania > Micronesia
World > Oceania > Polynesia
PL/pgSQL函数在第一次被调用时,其函数内的源代码(文本)将被解析为二进制指令树,但是函数内的表达式和SQL命令只有在首次用到它们的时候,PL/pgSQL解释器才会为其创建一个准备好的执行规划,随后对该表达式或SQL命令的访问都将使用该规划。如果在一个条件语句中,有部分SQL命令或表达式没有被用到,那么PL/pgSQL解释器在本次调用中将不会为其准备执行规划,这样的好处是可以有效地减少为PL/pgSQL函数里的语句生成分析和执行规划的总时间,然而缺点是某些表达式或SQL命令中的错误只有在其被执行到的时候才能发现。
由于PL/pgSQL在函数里为一个命令制定了执行计划,那么在本次会话中该计划将会被反复使用,这样做往往可以得到更好的性能,但是如果你动态修改了相关的数据库对象,那么就有可能产生问题,如:
CREATE FUNCTION populate() RETURNS integer AS $$
DECLARE
-- 声明段
BEGIN
PERFORM my_function();
END;
$$ LANGUAGE plpgsql;
在调用以上函数时,PERFORM语句的执行计划将引用my_function对象的OID。在此之后,如果你重建了my_function函数,那么populate函数将无法再找到原有my_function函数的OID。要解决该问题,可以选择重建populate函数,或者重新登录建立新的会话,以使PostgreSQL重新编译该函数。要想规避此类问题的发生,在重建my_function时可以使用CREATE OR REPLACE FUNCTION命令。
鉴于以上规则,在PL/pgSQL里直接出现的SQL命令必须在每次执行时均引用相同的表和字段,换句话说,不能将函数的参数用作SQL命令的表名或字段名。如果想绕开该限制,可以考虑使用PL/pgSQL中的EXECUTE语句动态地构造命令,由此换来的代价是每次执行时都要构造一个新的命令计划。
使用PL/pgSQL函数的一个非常重要的优势是可以提高程序的执行效率,由于原有的SQL调用不得不在客户端与服务器之间反复传递数据,这样不仅增加了进程间通讯所产生的开销,而且也会大大增加网络IO的开销。
PL/pgSQL的结构:
PL/pgSQL是一种块结构语言,函数定义的所有文本都必须在一个块内,其中块中的每个声明和每条语句都是以分号结束,如果某一子块在另外一个块内,那么该子块的END关键字后面必须以分号结束,不过对于函数体的最后一个END关键字,分号可以省略,如:
[ <