七的博客

PostgreSQL11快速入门(七)-常用的数据查询

PostgreSQL

PostgreSQL11快速入门(七)-常用的数据查询

前面一个章节中 , 有提到如何使用基本的 SQL SELECT 语句来查询数据。

但是在实际的项目开发中,几乎不可能会有这么简单的查询 SQL 来写,所以很有必要学习下常用的数据查询语法。主要包括以下几种:

  • 单条件查询
  • 组合条件查询
  • 分组查询
  • 连接
  • 子查询
  • 聚合函数
  • 窗口函数

1. 示例数据库表定义

首先需要定义数据库表来演示下面的查询,这里计划使用 2 张表以及模拟一部分数据。 业务场景为电力采集行业的电表设备以及电表采集的数据:

1.1 电表档案

CREATE TABLE meter (
    id SERIAL PRIMARY KEY,       -- 电表主键ID 
    meter_no CHAR(8) NOT NULL,   -- 电表编号,8位数字
    location VARCHAR(255),       -- 电表安装位置
    installation_date DATE       -- 安装日期
);

造一点数据:

INSERT INTO meter (id, meter_no, location, installation_date) VALUES
(1, '00000001', '3号楼101', '2019-01-01'),
(2, '00000002', '3号楼102', '2019-01-15'),
(3, '00000003', '3号楼103', '2019-02-01'),
(4, '00000004', '3号楼104', '2019-03-01'),
(5, '00000005', '3号楼105', '2019-04-01');

表中的数据:

id meter_no location installation_date
1 00000001 3号楼101 2019-01-01
2 00000002 3号楼102 2019-01-15
3 00000003 3号楼103 2019-02-01
4 00000004 3号楼104 2019-03-01
5 00000005 3号楼105 2019-04-01

1.2 电表采集的电量数据

CREATE TABLE meter_data (
    id SERIAL PRIMARY KEY,   -- 自增的主键 , 用于唯一标识每条记录,无业务含义
    meter_id INTEGER NOT NULL, -- 电表的ID。 meter表的主键
    timestamp TIMESTAMP NOT NULL, -- 采集电表数据的时间戳 
    value NUMERIC(10,2) NOT NULL -- 电表读数 , 最多 10 位数字,其中 2 位小数。
);

造一点数据:

INSERT INTO meter_data (id, meter_id, timestamp, value) VALUES
(10001, 1, '2019-05-01 00:00:00', 150.00),
(10003, 1, '2019-05-01 01:00:00', 153.00),
(10007, 2, '2019-05-01 00:00:00', 75.00),
(10015, 3, '2019-05-01 00:00:00', 200.00),
(10020, 6, '2019-05-01 00:00:00', 120.00);

表中的数据:

id meter_id timestamp value
10001 1 2019-05-01 00:00:00 150.00
10003 1 2019-05-01 01:00:00 153.00
10007 2 2019-05-01 00:00:00 75.00
10015 3 2019-05-01 00:00:00 200.00
10020 6 2019-05-01 00:00:00 120.00

2. 查询语法分析

首先看下 select 语句的语法规则:

SELECT column1, column2, ...
FROM table_name
WHERE condition
GROUP BY column
HAVING condition
ORDER BY column
LIMIT count;

上面的语法为:

  • column1 , column2 等是要查询的列名,也可以使用 * 号表示所有列。
  • table_name 是要查询的表名。
  • WHERE 子句指定查询条件 , 只返回满足条件的行。
  • GROUP BY 子句用于对结果集进行分组。
  • HAVING 子句用于对分组后的结果进行过滤。
  • ORDER BY 子句用于对结果集进行排序。
  • LIMIT 子句用于限制返回的行数。

3. 常规查询

第一个小节,先从比较简单的又比较常见的查询语句开始。

  • 查询 meter_data 表中所有的数据:
SELECT * FROM meter_data;
  • 查询指定列的数据:
SELECT meter_id, timestamp, value FROM meter_data;
  • 查询满足条件的数据,例如查询电表 1001 的数据:
SELECT * FROM meter_data WHERE meter_id = 1001;
  • 使用比较运算符和逻辑运算符来查询电表 1001 在指定时间范围内的数据:
SELECT * FROM meter_data
WHERE meter_id = 1001 
   AND timestamp BETWEEN '2023-06-01 09:00:00' AND '2023-06-01 10:00:00';
  • 查询读数大于 1000 的电表数据:
SELECT * FROM meter_data WHERE value > 1000;
  • 将查询结果按时间排序:
SELECT * FROM meter_data ORDER BY timestamp;
  • 将查询结果按电表 ID 分组,计算每个电表的平均读数:
SELECT meter_id, AVG(value) AS avg_value
FROM meter_data
GROUP BY meter_id;
  • 查询平均读数大于 2000 的电表 ID:
SELECT meter_id, AVG(value) AS avg_value
FROM meter_data
GROUP BY meter_id
HAVING AVG(value) > 2000;

上面这些场景基本包含了大部分简单的 SQL 应用,复杂的 SQL 也就是在这些的基础上稍微改动。 有点类似于英语的长难句,简单句搞懂了,长难句进行分解的话也可以比较快的看得懂。

2. 连接查询

连接查询是将两个或多个表按照某个条件合并起来,形成一个新的结果集的查询方式。

换句话来说,就是查询的时候先确定一张主表作为最后的结果集,然后被关联的表选择性的连接到主表的结果集上。 在实际的业务开发过程中,关联查询是最常用,也是最需要熟练掌握的查询方式。

PG 支持多种类型的连接查询 , 包括内连接、外连接、交叉连接等。

2.1 内连接 INNER JOIN

内连接 ( INNER JOIN ) 是比较常用的一种连接类型 , 它根据连接条件从两个表中返回满足条件的记录。

内连接也可以称作等值连接,就是两张表都要满足条件的部分,才会作为最终的结果输出。 inner joinjoin 是等价的。

内连接示意图,图片来源于 w3schools

语法:

SELECT column1, column2, ...
FROM table1
INNER JOIN table2 ON condition;

例如 , 查询所有有电表读数的电表的表号、位置和读数数据:

SELECT m.meter_no, m.location, md.timestamp, md.value
FROM meter m
INNER JOIN meter_data md ON m.id = md.meter_id;

查询结果如下:

meter_no location timestamp value
00000001 3号楼101 2019-05-01 00:00:00 150.00
00000001 3号楼101 2019-05-01 01:00:00 153.00
00000002 3号楼102 2019-05-01 00:00:00 75.00
00000003 3号楼103 2019-05-01 00:00:00 200.00

内连接只会返回在 metermeter_data 表中都存在的记录。结果包括电表的表号、位置以及相应的读数数据。

没有读数数据的电表00000004和00000005不会出现在结果中 , 因为它们在 meter_data 表中没有对应的记录。

meter_data 表中 meter_id 为6的记录也查不出来 , 因为 meter 表中没有对应的电表信息。

内连接关联过程

2.2 左外连接 LEFT JOIN

左外连接是以左边的表作为主表,查询出来符合条件的内容。 最终查询出来的结果是左表全部符合条件的内容,右表符合条件的也将查询出来,但是可能出现右表的内容为空的现象。

左外连接示意图,图片来源于 w3schools

语法:

-- 左外连接
SELECT column1, column2, ...
FROM table1
LEFT JOIN table2 ON condition;

例如使用 LEFT JOIN 查询来获取所有电表的表号、安装位置 , 以及它们的读数数据:

SELECT m.meter_no, m.location, md.timestamp, md.value
FROM meter m
LEFT JOIN meter_data md ON m.id = md.meter_id;

查询结果如下:

meter_no location timestamp value
00000001 3号楼101 2019-05-01 00:00:00 150.00
00000001 3号楼101 2019-05-01 01:00:00 153.00
00000002 3号楼102 2019-05-01 00:00:00 75.00
00000003 3号楼103 2019-05-01 00:00:00 200.00
00000004 3号楼104 NULL NULL
00000005 3号楼105 NULL NULL

左外连接关联过程

左外连接查询返回左表 ( meter ) 中的所有记录 , 以及右表(meter_data)中匹配的记录。对于左表中存在但右表中找不到匹配的记录,右表的字段将返回 NULL。

在结果中 , 电表00000004和00000005 虽然在 meter_data 表中没有对应的读数数据 , 但因为它们存在于 meter 表中,所以也会出现在查询结果里,只是对应的 timestampvalue 字段为 NULL。

可以看出左外连接以左表为主 , 保留左表的所有记录这个特点。

2.3 右外连接 RIGHT JOIN

右外连接跟左外连接是相反的过程。以右边边的表作为主表,查询出来符合条件的内容。 最终查询出来的结果是右表全部符合条件的内容,左表符合条件的也将查询出来,但是可能出现左表的内容为空的现象。

在实际项目开发过程中,右外连接我使用的比较少,左外连接使用的更多一点。

右外连接示意图,图片来源于 w3schools

语法:

-- 右外连接
SELECT column1, column2, ...
FROM table1
RIGHT JOIN table2 ON condition;

举例使用右外连接 来查询有读数数据的 电表信息 , 包括电表可能不存在于 meter 表中的情况,这种情况通常是脏数据:

SELECT m.meter_no, m.location, md.timestamp, md.value  
FROM meter m
RIGHT JOIN meter_data md ON m.id = md.meter_id;

查询结果如下:

meter_no location timestamp value
00000001 3号楼101 2019-05-01 00:00:00 150.00
00000001 3号楼101 2019-05-01 01:00:00 153.00
00000002 3号楼102 2019-05-01 00:00:00 75.00
00000003 3号楼103 2019-05-01 00:00:00 200.00
NULL NULL 2019-05-01 00:00:00 120.00

meter_id = 6 的读数数据出现在查询结果里 , 但对应的 meter_nolocation 字段为 NULL , 因为在 meter 表中没有 id = 6 的电表信息。

右外连接过程

2.4 全外连接 FULL OUTER JOIN

全外连接查询返回左表和右表中的所有记录。如果其中一个表中的记录在另一个表中没有匹配 , 则缺失的字段将返回 NULL。

右外连接示意图,图片来源于 w3schools

语法:

SELECT column1, column2, ...
FROM table1
FULL OUTER JOIN table2 ON condition;

使用全外连接查询两张表 :

SELECT m.meter_no, m.location, md.timestamp, md.value
FROM meter m
FULL OUTER JOIN meter_data md ON m.id = md.meter_id;

查询结果如下:

meter_no location timestamp value
00000001 3号楼101 2019-05-01 00:00:00 150.00
00000001 3号楼101 2019-05-01 01:00:00 153.00
00000002 3号楼102 2019-05-01 00:00:00 75.00
00000003 3号楼103 2019-05-01 00:00:00 200.00
00000004 3号楼104 NULL NULL
00000005 3号楼105 NULL NULL
NULL NULL 2019-05-01 00:00:00 120.00

meter 表中 id 为 4 和 5 的行在 meter_data 表中没有匹配,但这些行仍然会返回,对应的 timestampvalue 列用 NULL 填充。

meter_data 表中 meter_id 为 6 的行在 meter 表中没有匹配,但这一行仍然会返回 , 对应的 meter_nolocation 列用 NULL 填充。

总结下就是 全外连接返回两个表中的所有行 , 无论它们是否有匹配。

2.5 交叉连接 CROSS JOIN

交叉连接 ( CROSS JOIN ) 返回两个表的笛卡尔积 , 即每个表中的每一行与另一个表中的每一行组合。

语法:

SELECT column1, column2, ...
FROM table1
CROSS JOIN table2;

使用交叉连接查询两张表 :

SELECT m.meter_no, m.location, md.timestamp, md.value
FROM meter m
CROSS JOIN meter_data md;

查询结果(只列举一部分,共计 25条记录 ):

meter_no location timestamp value
00000001 3号楼101 2019-05-01 00:00:00 150.00
00000001 3号楼101 2019-05-01 01:00:00 153.00
00000001 3号楼101 2019-05-01 00:00:00 75.00
00000001 3号楼101 2019-05-01 00:00:00 200.00
00000001 3号楼101 2019-05-01 00:00:00 120.00
00000002 3号楼102 2019-05-01 00:00:00 150.00
00000002 3号楼102 2019-05-01 01:00:00 153.00

meter 表中的每一行与 meter_data 表中的每一行都进行了组合。结果集的行数等于两个表行数的乘积,上面的查询结果条数共计为 5 x 5 =25 条记录。

注意:交叉连接通常与其他条件一起使用 , 如果有大表的话,尽量避免生成过大的结果集,否则有极大的性能问题。

3. 子查询

子查询是指嵌套在另一个 SQL 语句中的 SELECT 语句 , 它可以出现在 SELECT、FROM、WHERE 等子句中,并作为外部查询的数据源或过滤条件。

子查询的分类有好多种,多为入门系列的课程详细就不做详细的展开,举几个常用的子查询方式。

3.1 WHERE 条件中的子查询

比如要 查询读数值大于平均读数值的电表的表号、安装位置和读数数据。

SELECT m.meter_no, m.location, md.timestamp, md.value
FROM meter m
JOIN meter_data md ON m.id = md.meter_id
WHERE md.value > (
    SELECT AVG(value) FROM meter_data  --  这里就是子查询语句
);

这个应该比较好理解,子查询计算了电表读数表中所有读数的平均数值,然后外层查询去使用这个平均值作为条件 , 筛选出读数值大于平均值的电表读数记录。

3.2 子查询作为临时表

查询每个电表最新的读数数据,写 SQL:

SELECT m.meter_no, m.location, md.timestamp, md.value
FROM meter m
JOIN (
    SELECT meter_id, MAX(timestamp) AS latest_timestamp   --  查询每个表最新的一条读数数据
    FROM meter_data
    GROUP BY meter_id
) latest_data ON m.id = latest_data.meter_id
JOIN meter_data md ON latest_data.meter_id = md.meter_id AND latest_data.latest_timestamp = md.timestamp;

子查询的结果作为一个临时表去跟与 meter 表和 meter_data 表进行关联,这样我们可以获取每个电表最新的读数数据。

3.3 SELECT 子句中的子查询

比如查询每个电表的读数数据,并计算每个读数与该电表平均读数的差值。

SELECT 
    m.meter_no,
    m.location,
    md.timestamp,
    md.value,
    (md.value - (SELECT AVG(value) FROM meter_data WHERE meter_id = m.id)) AS diff_from_avg   -- 这个是子查询
FROM meter m
JOIN meter_data md ON m.id = md.meter_id;

这个子查询计算了每个电表的平均读数值,然后用在在 SELECT 子句中给每一行计算读数值与该电表平均读数的差值。

注意,这种子查询的方式查询效率不会太高,平时尽量少用这种写法。

4. 聚合函数

聚合函数对一组值执行计算,返回单个结果值。PG 提供了多种内置的聚合函数 , 如 COUNT、SUM、AVG、MAX、MIN 等,看到这些函数应该大概就知道是什么意思,这些函数在 Excel 中也是类似的用法。

需要注意的是,聚合函数通常与 GROUP BY 子句一起使用 , 对分组后的数据进行聚合计算,单独使用通常没有很大意义。

基本的语法:

SELECT aggregate_function(column)
FROM table
WHERE condition;

4.1 sum 函数

使用 sum 函数 计算每个电表的读数总和

SELECT meter_id, SUM(value) AS total_value
FROM meter_data
GROUP BY meter_id;

4.2 avg 函数

使用 AVG 函数 计算每个电表的平均读数

SELECT meter_id, AVG(value) AS avg_value
FROM meter_data
GROUP BY meter_id;

4.3 max min 函数

使用 MAX 和 MIN 函数 查找每个电表的最大和最小读数。

SELECT 
    meter_id,
    MAX(value) AS max_value,
    MIN(value) AS min_value
FROM meter_data
GROUP BY meter_id;

4.4 组合使用函数

组合使用下这几个函数,查找每个电表的读数记录数量、读数总和、平均读数、最大读数和最小读数。

  SELECT
      meter_id,
      COUNT(*) AS reading_count,
      SUM(value) AS total_value,
      AVG(value) AS avg_value,
      MAX(value) AS max_value,
      MIN(value) AS min_value
  FROM meter_data
  GROUP BY meter_id;

5. 窗口函数

窗口函数是在一组相关的行上执行计算,并返回每一行的结果。它类似于聚合函数,但不会将结果集减少为单个行。

语法:

SELECT column1, column2, ...,
       window_function() OVER (
         [PARTITION BY partition_expression]
         [ORDER BY sort_expression [ASC | DESC] [NULLS {FIRST | LAST}]]
         [frame_clause]
       )
FROM table;

举几个常见的例子:

5.1 ROW_NUMBER 函数

为每个电表的读数记录添加一个按时间戳排序的序号。

SELECT
    meter_id,
    timestamp,
    value,
    ROW_NUMBER() OVER (PARTITION BY meter_id ORDER BY timestamp) AS row_num
FROM meter_data;

查询结果如下:

meter_id timestamp value row_num
1 2019-05-01 00:00:00 150.00 1
1 2019-05-01 01:00:00 153.00 2
2 2019-05-01 00:00:00 75.00 1
3 2019-05-01 00:00:00 200.00 1
6 2019-05-01 00:00:00 120.00 1

可以看到窗口函数为每个电的记录生成一个唯一的序号。其中 PARTITION BY meter_id 指定按电表 ID 进行分组 , ORDER BY timestamp 指定按时间戳排序。

5.2 RANK 函数

RANK 函数为给记录按读数值大小生成排名。

举个例子,查询每个电表的读数记录按读数值大小排名,并处理并列排名。

SELECT
    meter_id,
    timestamp,
    value,
    RANK() OVER (PARTITION BY meter_id ORDER BY value DESC) AS rank
FROM meter_data;

查询结果如下:

meter_id timestamp value rank
1 2019-05-01 01:00:00 153.00 1
1 2019-05-01 00:00:00 150.00 2
2 2019-05-01 00:00:00 75.00 1
3 2019-05-01 00:00:00 200.00 1
6 2019-05-01 00:00:00 120.00 1

RANK 函数为每个电表的记录按读数值大小生成排名。其中 PARTITION BY meter_id 指定按电表 ID 进行分组 , ORDER BY value DESC 指定按读数值降序排列。并列的读数值会获得相同的排名,且不会影响后续排名的序号。

5.3 LAG 函数

LAG 函数主要用于计算记录跟记录之间的差值。

SELECT
    meter_id,
    timestamp,
    value,
    value - LAG(value, 1, 0) OVER (PARTITION BY meter_id ORDER BY timestamp) AS diff
FROM meter_data;

LAG (value, 1, 0) 函数返回每个电表分组中当前行的前一行的 value 值 , 如果前一行不存在 , 则返回默认值 0。

PARTITION BY meter_id 指定按电表ID分组 , ORDER BY timestamp 指定按时间戳排序。

查询结果中的 diff 列表示当前读数与前一次读数的差值。

查询结果如下:

meter_id timestamp value diff
1 2019-05-01 00:00:00 150.00 150
1 2019-05-01 01:00:00 153.00 3
2 2019-05-01 00:00:00 75.00 75
3 2019-05-01 00:00:00 200.00 200
6 2019-05-01 00:00:00 120.00 120

通过以上几个例子可以发现,窗口函数有点像 group by 一样,将窗口进行分组,在有些场景下,可以实现多条 SQL 嵌套出来实现的效果。

参考链接