Skip to content

第一个 SQL 查询 ​

本节带你从零开始执行第一条 SQL 语句:连接数据库、运行查询、读懂结果。学完本节,你将能用 SELECT 检索数据、理解结果集的构成,并掌握 psql 中最常用的几种操作。

前置知识

阅读本节前,建议先了解:PostgreSQL 是什么

基础概念 ​

在敲下第一个字符之前,先把几个绕不开的概念理清。

数据库(Database):一个独立的数据容器,通常一个应用对应一个数据库。PostgreSQL 实例(Instance)下可以建多个数据库,数据库之间默认互相隔离、不能跨库查询。

表(Table):数据库里存放数据的二维结构,由列(Column)和行(Row)组成。习惯上也把行称为记录(Record),在 PostgreSQL 内部则称为元组(Tuple)。

模式(Schema):数据库内部的命名空间。一个数据库可以有多个模式,表实际的全名是 模式名.表名,如 public.users。默认创建的表都在 public 模式下。

SQL(Structured Query Language):结构化查询语言,是与关系数据库交互的通用语言。按用途分为几类:

类别全称作用代表语句
DDLData Definition Language定义结构CREATE、ALTER、DROP、TRUNCATE
DMLData Manipulation Language操作数据INSERT、UPDATE、DELETE
DQLData Query Language查询数据SELECT
DCLData Control Language权限控制GRANT、REVOKE
TCLTransaction Control Language事务控制BEGIN、COMMIT、ROLLBACK

结果集(Result Set):查询返回的行集合。SELECT 语句永远返回一个结果集,哪怕是 0 行。

psql:PostgreSQL 官方自带的命令行客户端(Interactive Terminal)。它是学习阶段最合适的工具,因为不隐藏任何细节。

NULL:表示"未知值",与空字符串、数字 0 都不同。这是 SQL 里最容易出错的概念,后文会单独说明。

语法与用法 ​

SELECT 是所有查询的基础,最小骨架如下:

sql
SELECT 列名1, 列名2 FROM 表名;
子句是否必需作用
SELECT必需指定要返回的列或表达式
FROM可选指定数据来源表;省略时对常量求值
WHERE可选过滤行
ORDER BY可选排序
LIMIT可选限制返回行数

最简形式甚至可以不带 FROM,直接对一个表达式求值:

sql
-- 对常量求值:不需要任何表
SELECT 1 + 1;
text
 ?column?
----------
        2
(1 row)

表达式可以用 AS 起别名(AS 可省略,但建议保留):

sql
SELECT
  1 + 1              AS sum_result,      -- 数值运算
  'Hello' || ' PG'   AS greeting,        -- 字符串拼接
  now()              AS server_time,     -- 函数调用
  2 * 3.14           AS circle_area;
text
 sum_result |  greeting  |         server_time          | circle_area
------------+------------+------------------------------+-------------
          2 | Hello PG   | 2026-09-21 10:05:12.334+08   |        6.28
(1 row)

psql 提供了大量以反斜杠开头的元命令(Meta-Command),用于控制客户端行为,与 SQL 语句是两个体系:

元命令作用
\l列出所有数据库
\c 数据库名连接到指定数据库
\dt列出当前数据库的表
\d 表名查看表结构
\dn列出所有模式
\du列出所有角色
\x切换纵向展开显示(宽表友好)
\timing开关 SQL 执行耗时统计
\i 文件路径执行 SQL 文件
\?查看所有元命令帮助
\q退出 psql

详细说明 ​

连接数据库 ​

先用 psql 建立连接。最简形式如下(连接参数由命令行或环境变量提供):

bash
# 连接到本机 5432 端口,以当前系统用户身份登录
psql -d demo

# 明确指定主机、端口、用户、数据库
psql -h 127.0.0.1 -p 5432 -U postgres -d demo
text
psql (18.6)
Type "help" for help.

demo=#

提示符 demo=# 的含义是:已连到 demo 数据库、当前以超级用户身份操作(普通用户的提示符末尾是 =>)。

也可以先连到默认库,再切换:

bash
psql -U postgres
text
postgres=# \c demo
You are now connected to database "demo" as user "postgres".
demo=#

用元命令确认当前所处的环境:

text
demo=# \conninfo
You are connected to database "demo" as user "postgres" via socket in "/var/run/postgresql" at port "5432".

demo=# \dt
Did not find any relations.

demo=# \l
                                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    | ICU Locale | Access privileges
-----------+----------+----------+-------------+-------------+------------+-------------------
 demo      | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            |
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            |
 template0 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            |
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            |
(4 rows)

语句的书写规则 ​

理解下面三条规则,能避免 90% 的入门报错:

  1. 语句以分号结尾。psql 只有在遇到分号时才把缓冲区内容发给服务端。少写分号时提示符会变成 demo-#,表示语句尚未结束。
  2. SQL 关键字大小写不敏感。select、SELECT、Select 完全等价;但字符串字面量区分大小写,'Alice' 与 'alice' 不同。本教程统一使用大写关键字便于阅读。
  3. 未加引号的标识符会被折叠为小写。SELECT Name FROM Users 实际查询的是 name 列和 users 表。如果需要区分大小写,必须用双引号:SELECT "Name" FROM "Users"。
text
demo=# SELECT 1
demo-# + 1;
 ?column?
----------
        2
(1 row)
sql
-- 单引号是字符串字面量,双引号是标识符引用,二者不可混用
SELECT 'hello' AS a_text_literal;
text
 a_text_literal
----------------
 hello
(1 row)

结果集的解读 ​

SELECT 的输出包含三部分:列头、数据行、行数统计。

sql
SELECT
  'PostgreSQL' AS product,
  18           AS major_version,
  true         AS is_open_source;
text
  product   | major_version | is_open_source
------------+---------------+----------------
 PostgreSQL |            18 | t
(1 row)

三点需要留意:

  • 列名默认取表达式文本。SELECT 1 + 1 的列名是 ?column?,因为表达式没有天然名字;这也是建议写 AS 别名的主要原因。
  • 布尔值显示为 t / f,而不是 true / false。这是 psql 的默认显示风格,值本身仍是布尔类型。
  • (1 row) 是客户端统计信息,不是服务端返回的数据。

NULL 的初步认识 ​

NULL 表示未知,任何与 NULL 的比较结果都不是真或假,而是 NULL(未知)。

sql
-- NULL 参与运算,结果仍是 NULL
SELECT
  NULL + 1          AS null_plus_one,
  NULL = NULL       AS null_eq_null,
  NULL IS NULL      AS null_is_null;
text
 null_plus_one | null_eq_null | null_is_null
---------------+--------------+--------------
               |              | t
(1 row)

为什么 NULL = NULL 不是 t?因为"未知值等于未知值"本身也是未知的。要判断是否为 NULL,必须用 IS NULL / IS NOT NULL,不能用 =。此外,字符串拼接时 NULL 会"传染":'a' || NULL 结果是 NULL 而不是 'a'。

生成测试数据 ​

入门阶段手边可能没有数据表,generate_series() 可以在不建表的情况下生成序列,非常适合练习:

sql
-- 生成 1 到 5 的序列
SELECT generate_series(1, 5) AS n;
text
 n
---
 1
 2
 3
 4
 5
(5 rows)
sql
-- 带步长与行数统计
SELECT
  generate_series(1, 10, 3) AS step_3,
  generate_series(1, 10)    AS natural;
text
 step_3 | natural
--------+---------
      1 |       1
      4 |       2
      7 |       3
     10 |       4
        |       5
        |       6
        |       7
        |       8
        |       9
        |      10
(10 rows)

注意上面这个例子的现象:两个集合函数长度不同时,短的一方自动用 NULL 补齐,总行数取较长者。这属于容易踩的坑,实际写查询时应避免在同一层 SELECT 里放多个长度未知的集合函数。

实战示例 ​

场景:一个电商系统需要核算某天的订单金额。为便于直接运行,这里先用临时表构造数据,再执行第一条"真正的"查询。

第 1 步:准备一张临时表

sql
-- 临时表只在当前会话可见,会话结束自动删除,适合做练习
CREATE TEMP TABLE order_demo (
  order_no   TEXT          NOT NULL,
  sku        TEXT          NOT NULL,
  qty        INTEGER       NOT NULL CHECK (qty > 0),
  unit_price NUMERIC(10, 2) NOT NULL CHECK (unit_price >= 0),
  is_paid    BOOLEAN       NOT NULL DEFAULT false,
  created_at TIMESTAMPTZ   NOT NULL DEFAULT now()
);

INSERT INTO order_demo (order_no, sku, qty, unit_price, is_paid) VALUES
  ('SO-20260921-001', 'SKU-1001', 2, 299.00, true),
  ('SO-20260921-001', 'SKU-1004', 1, 899.00, true),
  ('SO-20260921-002', 'SKU-1002', 3,  59.90, false),
  ('SO-20260921-003', 'SKU-1003', 1, 129.00, true);
text
CREATE TABLE
INSERT 0 4

第 2 步:确认表结构

text
demo=# \d order_demo
                    Table "pg_temp_3.order_demo"
   Column   |           Type           | Collation | Nullable | Default
------------+--------------------------+-----------+----------+---------
 order_no   | text                     |           | not null |
 sku        | text                     |           | not null |
 qty        | integer                  |           | not null |
 unit_price | numeric(10,2)            |           | not null |
 is_paid    | boolean                  |           | not null | false
 created_at | timestamp with time zone |           | not null | now()
Check constraints:
    "order_demo_qty_check" CHECK (qty > 0)
    "order_demo_unit_price_check" CHECK (unit_price >= 0)

第 3 步:运行第一条查询

sql
SELECT order_no, sku, qty, unit_price
FROM order_demo
ORDER BY order_no, sku;
text
     order_no      |   sku    | qty | unit_price
-------------------+----------+-----+------------
 SO-20260921-001   | SKU-1001 |   2 |     299.00
 SO-20260921-001   | SKU-1004 |   1 |     899.00
 SO-20260921-002   | SKU-1002 |   3 |      59.90
 SO-20260921-003   | SKU-1003 |   1 |     129.00
(4 rows)

第 4 步:在查询里做计算——每个明细行的小计

sql
SELECT
  order_no,
  sku,
  qty,
  unit_price,
  qty * unit_price AS line_total      -- 表达式列,不存储在表中
FROM order_demo
ORDER BY line_total DESC;
text
     order_no      |   sku    | qty | unit_price | line_total
-------------------+----------+-----+------------+------------
 SO-20260921-001   | SKU-1004 |   1 |     899.00 |     899.00
 SO-20260921-001   | SKU-1001 |   2 |     299.00 |     598.00
 SO-20260921-002   | SKU-1002 |   3 |      59.90 |     179.70
 SO-20260921-003   | SKU-1003 |   1 |     129.00 |     129.00
(4 rows)

第 5 步:只取前 2 行(Top-N)

sql
SELECT order_no, sku, qty * unit_price AS line_total
FROM order_demo
ORDER BY line_total DESC
LIMIT 2;
text
     order_no      |   sku    | line_total
-------------------+----------+------------
 SO-20260921-001   | SKU-1004 |     899.00
 SO-20260921-001   | SKU-1001 |     598.00
(2 rows)

第 6 步:用单个 SELECT 把多条常量组织成"虚拟表"

sql
-- VALUES 可以当作临时数据源使用,适合做快速验证
SELECT t.sku, t.price
FROM (VALUES
  ('SKU-1001', 299.00),
  ('SKU-1002',  59.90),
  ('SKU-1003', 129.00)
) AS t (sku, price)
ORDER BY t.price DESC;
text
   sku    | price
----------+--------
 SKU-1001 | 299.00
 SKU-1003 | 129.00
 SKU-1002 |  59.90
(3 rows)

第 7 步:查看执行耗时,建立性能感知

text
demo=# \timing
Timing is on.
demo=# SELECT count(*) FROM order_demo;
 count
-------
     4
(1 row)

Time: 0.412 ms

注意事项 ​

  • 忘记分号不会报错,但语句不会执行。提示符从 demo=# 变成 demo-# 就是信号,补上分号即可;如果写错了想放弃,输入 \r 清空缓冲区。
  • 不要用双引号写字符串。这是从 MySQL 迁移过来最常犯的错误。
sql
-- 错误!PostgreSQL 中双引号是标识符引用,这里会报 column "hello" does not exist
-- SELECT "hello";

-- 正确:字符串用单引号
SELECT 'hello' AS greeting;
  • 不要把 NULL 当成 0 或空字符串。三者语义完全不同,混用会导致统计结果错误。
sql
-- 错误!count(*) 会把 NULL 行也算进去
SELECT count(*) FROM order_demo WHERE is_paid = NULL;   -- 恒返回 0 行

-- 正确:判断 NULL 必须用 IS NULL / IS NOT NULL
SELECT count(*) FROM order_demo WHERE is_paid IS NOT NULL;
  • 表达式结果列一定显式起别名。否则列名是 ?column?,程序取值时无法按名访问,只能按下标,非常脆弱。
  • 不要依赖 SELECT *。字段顺序变化、新增列都会影响调用方,生产代码应显式列出所需列。
  • generate_series 等多集合函数不要混用。像上文例子里长度不同会自动补齐 NULL,容易产生错误数据。需要多序列时应分别查询或用 JOIN 关联。
  • 布尔值显示为 t/f 是客户端行为。在应用代码里通过驱动拿到的是真正的布尔值,不要按字符串 t/f 解析。
  • 查询不写 ORDER BY 时顺序未定义。PostgreSQL 不会保证"按物理存储顺序"返回,需要稳定顺序就必须显式排序。

最佳实践 ​

  • 学习阶段坚持使用 psql。它能显示真实的类型、真实的执行时间与完整的错误信息,而不是被 GUI 包装过的结果。
  • 每条语句都显式列出列名,不用 SELECT *,尤其是会被程序调用的 SQL。
  • 表达式与计算列一律加 AS 别名,让结果集自解释。
  • 开启 \timing 观察耗时,从第一天就建立对查询成本的直觉。
  • 把常用元命令练熟(\d、\dt、\x、\c、\q),它们能显著减少调试时间。
  • 练习脚本写成 .sql 文件并用 \i 执行,便于反复重跑与版本管理,而不是手工粘贴。
  • 遇到 NULL 相关结果异常时,先用 IS NULL 单独验证,再决定业务逻辑,不要凭直觉假设。
  • 用 CREATE TEMP TABLE 做实验。它不会污染正式库,会话结束自动清理,是安全的练手方式。

下一节

继续学习:数据库与模式的基本操作

参考链接 ​