学 SQL 最容易踩的坑,不是语法难,而是你照着 A 数据库的教程写,拿到 B 数据库上跑不通。SQL 有 ISO 标准,但现实中几乎没有哪个数据库完全照标准实现——它们各有方言。这篇教程以标准为主线,把方言差异逐条标出来。文中示例均在 SQLite 3.46.1 和 PostgreSQL 16.3 实测通过。

一、SQL 是什么:五类命令
SQL(Structured Query Language)是操作关系型数据库的语言。按用途分五类:
| 分类 | 全称 | 干什么 | 代表语句 |
|---|---|---|---|
| DDL | Data Definition Language | 定义结构 | CREATE / ALTER / DROP / TRUNCATE |
| DML | Data Manipulation Language | 操作数据 | INSERT / UPDATE / DELETE |
| DQL | Data Query Language | 查询数据 | SELECT |
| DCL | Data Control Language | 权限控制 | GRANT / REVOKE |
| TCL | Transaction Control Language | 事务控制 | BEGIN / COMMIT / ROLLBACK |
多数教程只列前四类,把 TCL 漏了。事务是数据库区别于普通文件存储的核心能力,不该漏。
二、方言地图:先记住这几条
这一节是全文最值钱的部分。 下面这些差异,是你跨库写 SQL 时最先撞上的墙。
| 需求 | MySQL | PostgreSQL | SQLite | SQL 标准 |
|---|---|---|---|---|
| 自增主键 | AUTO_INCREMENT | SERIAL 或 GENERATED ... AS IDENTITY | INTEGER PRIMARY KEY(天然自增) | GENERATED ALWAYS AS IDENTITY |
| 标识符引号 | 反引号 ` | 双引号 " | 双引号 " | 双引号 " |
| 字符串拼接 | CONCAT(a,b) | a \|\| b | a \|\| b | a \|\| b |
| 分页 | LIMIT n OFFSET m / LIMIT m,n | LIMIT n OFFSET m / FETCH FIRST n ROWS ONLY | LIMIT n OFFSET m / LIMIT m,n | OFFSET m ROWS FETCH NEXT n ROWS ONLY |
| 字符串聚合 | GROUP_CONCAT(x SEPARATOR ',') | string_agg(x, ',') | group_concat(x, ',') | — |
| 空值替换 | IFNULL(a,b) / COALESCE | COALESCE | IFNULL(a,b) / COALESCE | COALESCE |
| 当前时间 | NOW() / CURDATE() | now() / current_date | datetime('now') / date('now') | CURRENT_TIMESTAMP / CURRENT_DATE |
| 布尔类型 | TINYINT(1) | BOOLEAN | 无(用 0/1) | BOOLEAN |
TRUNCATE | ✅ | ✅ | ❌(用 DELETE FROM) | ✅ |
| UPSERT | ON DUPLICATE KEY UPDATE | ON CONFLICT ... DO UPDATE | ON CONFLICT ... DO UPDATE | MERGE(较新) |
| 类型转换 | CAST(x AS t) | CAST(x AS t) / x::t | CAST(x AS t) | CAST(x AS t) |
三条实用建议:
- 写"通用 SQL"时不要用任何私有语法。
AUTO_INCREMENT、反引号、LIMIT m,n全是方言,换个库就废。 - SQL 标准语法优先。比如分页用
OFFSET ... FETCH NEXT ... ROWS ONLY(PG、SQL Server 支持;MySQL 8 不支持,SQLite 不支持——所以实战里LIMIT/OFFSET反而更通用)。 - 不确定就查官方文档,别信二手教程。网上一大半教程把 MySQL 语法当"SQL 通用语法"讲。
三、数据库操作
3.1 创建
-- SQL 标准 / MySQL / PostgreSQL
CREATE DATABASE school_db;
-- 带存在性检查(MySQL 专有;PG 不支持 CREATE DATABASE IF NOT EXISTS)
CREATE DATABASE IF NOT EXISTS school_db;SQLite 没有
CREATE DATABASE。它的"数据库"就是一个文件,sqlite3 school.db打开即创建(详见 SQLite 篇)。
3.2 切换与查看
USE school_db; -- MySQL;PG 用 \c school_db;SQLite 用 .open
SHOW DATABASES; -- MySQL;PG 用 \l;SQLite 用 .databases3.3 删除
DROP DATABASE school_db; -- 不可恢复,慎用
DROP DATABASE IF EXISTS school_db;四、表操作
4.1 建表与数据类型
同样的表,三种方言写法不同:
-- MySQL
CREATE TABLE students (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
age INT,
email VARCHAR(100) UNIQUE
);
-- PostgreSQL
CREATE TABLE students (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT,
email VARCHAR(100) UNIQUE
);
-- SQLite(类型是"建议",VARCHAR(100) 的长度不生效)
CREATE TABLE students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
email TEXT UNIQUE
);常用类型对照:
| 用途 | MySQL | PostgreSQL | SQLite |
|---|---|---|---|
| 整数 | INT / BIGINT | INTEGER / BIGINT | INTEGER |
| 短文本 | VARCHAR(n) | VARCHAR(n) / TEXT | TEXT |
| 长文本 | TEXT | TEXT | TEXT |
| 小数 | DECIMAL(p,s) | NUMERIC(p,s) | REAL / NUMERIC |
| 布尔 | TINYINT(1) | BOOLEAN | 无(0/1) |
| 日期时间 | DATETIME / TIMESTAMP | TIMESTAMP / TIMESTAMPTZ | TEXT / INTEGER |
| JSON | JSON | JSONB | TEXT(+ json() 函数) |
| 二进制 | BLOB | BYTEA | BLOB |
关于 DECIMAL 与 FLOAT:金额永远用 DECIMAL/NUMERIC,别用 FLOAT。浮点是二进制近似,0.1 + 0.2 != 0.3 在数据库里同样是灾难。
4.2 约束
CREATE TABLE students (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT CHECK (age >= 0),
email VARCHAR(100) UNIQUE,
class_id INT,
FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE CASCADE
);| 约束 | 作用 |
|---|---|
PRIMARY KEY | 主键,唯一且非空 |
NOT NULL | 非空 |
UNIQUE | 唯一 |
CHECK(表达式) | 自定义校验 |
DEFAULT | 默认值 |
FOREIGN KEY ... REFERENCES | 外键 |
SQLite 的外键默认不生效,需要
PRAGMA foreign_keys = ON。这是最容易造成静默数据损坏的坑,详见 SQLite 篇。
4.3 修改表结构(方言差异最大)
-- 添加列(三方通用)
ALTER TABLE students ADD COLUMN major VARCHAR(50);
-- 修改列类型
ALTER TABLE students MODIFY COLUMN age SMALLINT; -- MySQL
ALTER TABLE students ALTER COLUMN age TYPE SMALLINT; -- PostgreSQL
-- SQLite:不支持直接改列类型,需"建新表→拷数据→换名"
-- 改列名
ALTER TABLE students CHANGE COLUMN major department VARCHAR(50); -- MySQL
ALTER TABLE students RENAME COLUMN major TO department; -- PostgreSQL / SQLite(3.25+)
-- 删除列
ALTER TABLE students DROP COLUMN email; -- 三方均可(SQLite 需 3.35+)4.4 查看与删除
-- 查看表结构
DESCRIBE students; -- MySQL
\d students -- PostgreSQL(psql 元命令)
PRAGMA table_info(students); -- SQLite
-- 删除
DROP TABLE students; -- 删表(含结构)
DROP TABLE IF EXISTS students; -- 存在才删,建议加
TRUNCATE TABLE students; -- 清空数据保留结构(MySQL/PG)
DELETE FROM students; -- SQLite 用这个替代 TRUNCATE五、CRUD
5.1 插入
INSERT INTO students (name, age, email) VALUES
('张三', 18, 'zhangsan@example.com'),
('李四', 19, 'lisi@example.com');一次插多行比循环单行插入快得多,这是所有数据库的共识。
5.2 查询
SELECT * FROM students; -- 全部列
SELECT name, age FROM students; -- 指定列
SELECT DISTINCT major FROM students; -- 去重
SELECT * FROM students WHERE age > 18 AND major = 'CS';
SELECT * FROM students ORDER BY age DESC, name ASC; -- 多列排序
SELECT * FROM students ORDER BY age DESC LIMIT 5; -- 前 5
SELECT * FROM students ORDER BY age DESC LIMIT 5 OFFSET 10; -- 第 11-15 行别在生产代码里写 SELECT *。表的列会变,依赖 * 的代码会在加列后悄悄出错,而且会拖慢查询(多读不需要的列)。写死列名。
5.3 更新与删除
UPDATE students SET age = 20 WHERE name = '张三';
DELETE FROM students WHERE name = '王五';UPDATE/DELETE 不写 WHERE 会作用于全表,且没有确认提示。 实操建议:先用同样 WHERE 跑一遍 SELECT,确认影响范围,再改 UPDATE/DELETE。
5.4 UPSERT(方言写法不同)
-- PostgreSQL / SQLite
INSERT INTO students (id, name) VALUES (1, '张三')
ON CONFLICT (id) DO UPDATE SET name = excluded.name;
-- MySQL
INSERT INTO students (id, name) VALUES (1, '张三')
ON DUPLICATE KEY UPDATE name = VALUES(name);六、NULL:最容易出错的地方
NULL 不是"0",也不是空字符串,它是"未知"。这导致三条反直觉的规则。
6.1 比较要用 IS NULL,不能用 = NULL
SELECT * FROM students WHERE age = NULL; -- ❌ 永远返回空
SELECT * FROM students WHERE age IS NULL; -- ✅ 正确
SELECT * FROM students WHERE age IS NOT NULL;任何值和 NULL 比较,结果都是"未知",WHERE 会把它当假丢掉。
6.2 聚合函数会跳过 NULL
这是 COUNT(*) 和 COUNT(列) 的区别:
SELECT COUNT(*) AS cnt_all, COUNT(age) AS cnt_age FROM students;3|2表里 3 行,但 age 有一行是 NULL,所以 COUNT(age) = 2。SUM/AVG/MAX/MIN 同理都会跳过 NULL。AVG 的分母是"非 NULL 的行数",不是总行数——算平均分时极易出错。
6.3 空值替换
SELECT name, COALESCE(age, 0) AS safe_age FROM students; -- 三方通用,推荐
SELECT name, IFNULL(age, 0) FROM students; -- MySQL / SQLiteCOALESCE 接受多个参数,返回第一个非 NULL 值,是标准写法,优先用它。
6.4 GROUP BY 里的 NULL
GROUP BY 会把所有 NULL 归成一组。排序时 NULL 的位置各家默认不同(MySQL 升序把 NULL 排最前,PG 排最后),要固定顺序就显式写:
ORDER BY age ASC NULLS LAST -- PostgreSQL / SQLite(3.30+)
ORDER BY age IS NULL, age -- MySQL 的变通写法七、聚合与分组
SELECT
COUNT(*) AS total,
AVG(age) AS avg_age,
MIN(age) AS min_age,
MAX(age) AS max_age
FROM students;SELECT major, COUNT(*) AS cnt, AVG(age) AS avg_age
FROM students
GROUP BY major
HAVING COUNT(*) > 5
ORDER BY cnt DESC;WHERE 和 HAVING 的区别:WHERE 在分组前过滤行,HAVING 在分组后过滤组。WHERE 里不能引用聚合函数,HAVING 可以。
-- 错的:WHERE 用聚合
SELECT major FROM students WHERE COUNT(*) > 5 GROUP BY major;
-- 对的:
SELECT major FROM students GROUP BY major HAVING COUNT(*) > 5;八、连接(JOIN)
先建两张表:
CREATE TABLE classes (id INT PRIMARY KEY, name VARCHAR(50));
CREATE TABLE students (id INT PRIMARY KEY, name VARCHAR(50), class_id INT);| 连接类型 | 返回什么 | 备注 |
|---|---|---|
INNER JOIN | 两张表都匹配上的行 | 最常用,JOIN 默认即此 |
LEFT JOIN | 左表全部 + 右表匹配的,右表无匹配补 NULL | 极常用 |
RIGHT JOIN | 右表全部 + 左表匹配的 | 可用 LEFT JOIN 调换顺序替代 |
FULL OUTER JOIN | 两边所有行,各自无匹配处补 NULL | MySQL 不支持(用 LEFT UNION RIGHT 模拟) |
CROSS JOIN | 笛卡尔积(m×n 行) | 慎用,容易炸 |
| 自连接 | 表和自己 JOIN | 处理层级结构(如员工-经理) |
-- 内连接:只列出有班级的学生
SELECT s.name, c.name AS class_name
FROM students s
INNER JOIN classes c ON s.class_id = c.id;
-- 左连接:所有学生,没班级的也列出(班级名显示 NULL)
SELECT s.name, c.name AS class_name
FROM students s
LEFT JOIN classes c ON s.class_id = c.id;
-- 左连接 + 过滤出"没班级的学生"(反连接模式)
SELECT s.name
FROM students s
LEFT JOIN classes c ON s.class_id = c.id
WHERE c.id IS NULL;最后那个"反连接"写法很实用——找出"订单表里有、客户表里没有"的脏数据就是靠它。
九、子查询
-- 标量子查询:返回单个值
SELECT * FROM students WHERE age > (SELECT AVG(age) FROM students);
-- IN 子查询:返回一列
SELECT * FROM students
WHERE class_id IN (SELECT id FROM classes WHERE name LIKE 'CS%');
-- EXISTS:判断存在性(通常比 IN 快,且对 NULL 更安全)
SELECT * FROM students s
WHERE EXISTS (SELECT 1 FROM classes c WHERE c.id = s.class_id);
-- 相关子查询:子查询引用外层列
SELECT name,
(SELECT COUNT(*) FROM students s2 WHERE s2.class_id = s1.class_id) AS classmates
FROM students s1;IN vs EXISTS:当子查询结果可能含 NULL 时,NOT IN 会返回空集(因为 x != NULL 是未知)——这是经典陷阱。用 NOT EXISTS 代替 NOT IN 更安全。
十、集合运算
SELECT name FROM students_a
UNION -- 并集,去重
SELECT name FROM students_b;
SELECT name FROM students_a
UNION ALL -- 并集,不去重(更快,能保留重复)
SELECT name FROM students_b;
SELECT name FROM students_a
INTERSECT -- 交集
SELECT name FROM students_b;
SELECT name FROM students_a
EXCEPT -- 差集(A 有 B 没有)
SELECT name FROM students_b;方言:
UNION/UNION ALL三方通用。INTERSECT/EXCEPT在 MySQL 需 8.0.31+;老版本 MySQL 用INNER JOIN/NOT IN模拟。
十一、CASE 表达式
SQL 里的 if-else,用在 SELECT、WHERE、ORDER BY 里都行:
SELECT name,
CASE
WHEN age IS NULL THEN '未知'
WHEN age >= 30 THEN '资深'
ELSE '新人'
END AS tag
FROM students;-- 按自定义顺序排序(比如状态:待处理 → 处理中 → 已完成)
SELECT * FROM tasks
ORDER BY CASE status
WHEN 'pending' THEN 1
WHEN 'running' THEN 2
WHEN 'done' THEN 3
ELSE 4
END;十二、窗口函数
窗口函数能在不合并行的前提下做聚合/排名,是 2010 年之后 SQL 最重要的增强。MySQL 8.0+、PostgreSQL、SQLite 3.25+ 都支持。
SELECT
name,
age,
ROW_NUMBER() OVER (ORDER BY age) AS 行号,
RANK() OVER (ORDER BY age) AS 排名_跳号,
DENSE_RANK() OVER (ORDER BY age) AS 排名_不跳号
FROM students;-- 分组内排名:每个班级按年龄排名
SELECT name, class_id, age,
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY age DESC) AS rn
FROM students;-- 移动平均:最近 3 行的平均(时间序列分析常用)
SELECT day, amount,
AVG(amount) OVER (ORDER BY day ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS ma3
FROM sales;| 函数 | 作用 |
|---|---|
ROW_NUMBER() | 连续行号,无并列 |
RANK() | 并列跳号(1,1,3) |
DENSE_RANK() | 并列不跳号(1,1,2) |
LAG(col, n) / LEAD(col, n) | 取前 n 行 / 后 n 行的值 |
SUM()/AVG()/COUNT() OVER(...) | 窗口内聚合 |
NTILE(n) | 分成 n 桶 |
十三、CTE(公共表表达式)
WITH 让复杂查询可读,替代嵌套子查询:
WITH class_stats AS (
SELECT class_id, COUNT(*) AS cnt, AVG(age) AS avg_age
FROM students
GROUP BY class_id
)
SELECT c.name, cs.cnt, cs.avg_age
FROM class_stats cs
JOIN classes c ON c.id = cs.class_id
WHERE cs.cnt > 5;递归 CTE 能遍历树形结构(组织架构、分类树、BOM):
WITH RECURSIVE org AS (
SELECT id, name, manager_id, 1 AS level
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, o.level + 1
FROM employees e
JOIN org o ON e.manager_id = o.id
)
SELECT * FROM org ORDER BY level;十四、常用函数速查
| 用途 | MySQL | PostgreSQL | SQLite |
|---|---|---|---|
| 转大写 | UPPER(x) | UPPER(x) | UPPER(x) |
| 长度 | LENGTH(x) / CHAR_LENGTH(x) | LENGTH(x) | length(x) |
| 截取 | SUBSTRING(x,2,3) | SUBSTRING(x FROM 2 FOR 3) / LEFT(x,3) | substr(x,2,3) |
| 拼接 | CONCAT(a,b) | a \|\| b | a \|\| b |
| 当前时间 | NOW() | now() | datetime('now') |
| 格式化日期 | DATE_FORMAT(d,'%Y-%m') | to_char(d,'YYYY-MM') | strftime('%Y-%m',d) |
| 日期截断 | DATE(d) | date_trunc('month',d) | date(d,'start of month') |
| 空值替换 | IFNULL(a,b) / COALESCE | COALESCE(a,b) | IFNULL(a,b) / COALESCE |
| 数字取整 | ROUND(x,2) | ROUND(x,2) | round(x,2) |
| 类型转换 | CAST(x AS CHAR) | x::text / CAST(x AS text) | CAST(x AS TEXT) |
| 模糊匹配 | LIKE(默认忽略大小写) | LIKE(区分)/ ILIKE(忽略) | LIKE(默认忽略 ASCII 大小写) |
LIKE的大小写行为是最容易跨库翻车的地方之一:MySQL 默认不区分,PostgreSQL 默认区分。要么统一用UPPER(x) LIKE UPPER(y),要么明确用各家的大小写不敏感写法。
十五、索引与查询计划
-- 创建
CREATE INDEX idx_students_name ON students(name); -- 单列
CREATE INDEX idx_students_age_name ON students(age, name); -- 复合(顺序重要)
CREATE UNIQUE INDEX idx_students_email ON students(email); -- 唯一索引
-- 删除(注意方言)
DROP INDEX idx_students_name ON students; -- MySQL(要带 ON 表名)
DROP INDEX idx_students_name; -- PostgreSQL / SQLite复合索引的顺序有讲究:(age, name) 能加速 WHERE age=? 和 WHERE age=? AND name=?,但不能加速只有 WHERE name=? 的查询(最左前缀原则)。
看执行计划
EXPLAIN SELECT * FROM students WHERE name = '张三'; -- MySQL / PG
EXPLAIN QUERY PLAN SELECT * FROM students WHERE name = '张三'; -- SQLite
EXPLAIN ANALYZE SELECT * FROM students WHERE name = '张三'; -- PG / MySQL 8(实际执行并计时)看到 SCAN(SQLite)/ Seq Scan(PG)/ ALL(MySQL 的 type 列)就说明在全表扫描,该考虑加索引了。
十六、视图
CREATE VIEW student_summary AS
SELECT s.id, s.name, COUNT(e.course_id) AS courses
FROM students s
LEFT JOIN enrollments e ON s.id = e.student_id
GROUP BY s.id, s.name;
SELECT * FROM student_summary WHERE courses > 3;
CREATE OR REPLACE VIEW student_summary AS ...; -- 更新视图定义
DROP VIEW student_summary;视图是保存下来的查询,不是数据副本——每次查视图都会重新执行底层查询。它可以简化复杂查询、限制列级访问权限(只暴露部分列给某类用户)。
十七、事务
BEGIN; -- 或 START TRANSACTION
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 成功提交;失败用 ROLLBACK 撤销事务的原子性保证了转账这类操作要么全成、要么全不成,不会出现"钱扣了没到账"的中间态。
隔离级别
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
READ UNCOMMITTED | 可能 | 可能 | 可能 |
READ COMMITTED | 否 | 可能 | 可能 |
REPEATABLE READ | 否 | 否 | 可能 |
SERIALIZABLE | 否 | 否 | 否 |
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 标准写法
-- 查看/设置(会话级)
SELECT @@transaction_isolation; -- MySQL
SHOW transaction_isolation; -- PostgreSQL默认值差异:MySQL InnoDB 默认 REPEATABLE READ,PostgreSQL 默认 READ COMMITTED。这个差异会导致同样的代码在两个库上表现不同,做并发相关的逻辑时务必确认。
十八、权限(DCL)
-- MySQL:用户和主机绑定
GRANT SELECT, INSERT ON school_db.* TO 'app'@'localhost';
REVOKE DELETE ON school_db.* FROM 'app'@'localhost';
-- PostgreSQL:用户是角色,不绑主机
GRANT SELECT, INSERT ON ALL TABLES IN SCHEMA public TO app;
REVOKE DELETE ON students FROM app;MySQL 的
'user'@'host'是它的特色(同一用户名从不同主机连可以是不同的账号)。SQLite 没有用户概念,文件权限就是它的全部安全模型。
十九、常见错误与最佳实践
19.1 三个高频错误
① 保留字当表名/列名
-- MySQL:反引号
CREATE TABLE `user` (`id` INT, `order` INT);
-- PostgreSQL / SQLite:双引号
CREATE TABLE "user" (id INT, "order" INT);user、order、group、select 都是保留字。最好的做法是加前缀(app_user、shop_order),从根上绕开。
② 字符串拼接导致 SQL 注入
-- ❌ 危险:把用户输入直接拼进 SQL
query = "SELECT * FROM users WHERE name = '" + input + "'";永远用参数化查询(? 或 $1 占位)。这是安全底线,不是风格问题。
③ 在 WHERE 里对索引列做运算
-- ❌ 索引失效:对列做了函数运算
SELECT * FROM orders WHERE YEAR(created_at) = 2026;
-- ✅ 改成范围比较,走索引
SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';19.2 最佳实践清单
- 命名有意义:
student_id而不是sid - 不用
SELECT *:写死需要的列 - 参数化查询:防注入
- 索引按需建:查询频繁的列建,写入频繁的表别乱建(索引会拖慢写入)
- 金额用
DECIMAL,不用FLOAT UPDATE/DELETE前先SELECT验证WHERE- 大事务拆小:长事务会长期持锁
- 定期备份:
mysqldump/pg_dump/sqlite3 .backup
二十、速查表
-- —— DDL ——
CREATE DATABASE db; -- 非 SQLite
DROP DATABASE IF EXISTS db;
CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(50));
ALTER TABLE t ADD COLUMN x INT;
DROP TABLE IF EXISTS t;
TRUNCATE TABLE t; -- 非 SQLite(用 DELETE FROM)
-- —— DML ——
INSERT INTO t (a,b) VALUES (1,2);
UPDATE t SET b=3 WHERE a=1;
DELETE FROM t WHERE a=1;
-- —— DQL ——
SELECT a, b FROM t WHERE a>1 ORDER BY b DESC LIMIT 10 OFFSET 20;
SELECT k, COUNT(*) FROM t GROUP BY k HAVING COUNT(*)>1;
SELECT a.*, b.* FROM a INNER JOIN b ON a.id=b.a_id;
SELECT a FROM t1 UNION ALL SELECT a FROM t2;
SELECT x, ROW_NUMBER() OVER (PARTITION BY k ORDER BY x) FROM t;
WITH cte AS (SELECT * FROM t) SELECT * FROM cte;
-- —— TCL ——
BEGIN; ... COMMIT; -- 或 ROLLBACK;
-- —— 元信息 ——
DESCRIBE t; -- MySQL
\d t -- PostgreSQL
PRAGMA table_info(t); -- SQLite
EXPLAIN QUERY PLAN SELECT ...; -- SQLite学习资源
- 现代 SQL 教程 —— 讲标准 SQL 与各家方言差异,质量很高
- SQLZoo —— 交互式练习
- Use The Index, Luke —— 索引与查询优化的权威读物(免费)
- LeetCode 数据库题库 —— 实战练习
- 各数据库官方文档:MySQL / PostgreSQL / SQLite
学 SQL 的正确姿势是:先掌握标准,再记住方言。标准部分(
SELECT/JOIN/GROUP BY/事务)在所有数据库通用,值得花时间打牢;方言部分用到再查,本文的方言对照表可以当索引使。
参考&致谢
系列教程
数据库系列
- SQL 命令使用教程:从入门到精通 —— SQL 标准命令,含 MySQL/PostgreSQL/SQLite 方言对照
- SQLite 使用全面教程:轻量级数据库的终极指南 —— 嵌入式数据库,类型系统、外键陷阱、WAL、FTS5
- MySQL 命令行使用全面教程:从入门到精通 ——
mysql客户端工具,批处理、备份、性能诊断 - MySQL 使用全面指南:从入门到高级实践 —— MySQL 安装配置、字符集、索引、事务锁、复制
- PostgreSQL 使用全面指南:从入门到企业级应用 —— PG 认证配置、JSONB、分区、备份与高可用
- PostgreSQL 命令行使用教程:掌握 psql 工具 ——
psql元命令、脚本化、\copy与pg_dump - PostgreSQL 实现原理深度剖析:高性能数据库引擎的核心机制 —— MVCC、WAL、B-tree 内核机制与可验证观察方法

