PostgreSQL 与 SQL 基础:为 Fast API 打下数据基础

在构建任何动态、数据驱动的Web API时,一个稳定高效的数据存储方案是不可或缺的。对于使用Python FastAPI的开发者来说,深入理解关系型数据库的工作原理、掌握SQL这门与数据库"对话"的语言,以及学会如何在Python中操作数据库,是构建强大应用的关键一步。

本文将带你探索关系型数据库(以PostgreSQL为例)的核心概念,精通SQL的关键操作,并学习如何使用Python的psycopg2驱动连接和操作PostgreSQL,为你的FastAPI项目打下坚实的数据基础。

1. 关系型数据库与数据库管理系统(DBMS) (以PostgreSQL为例)

关系型数据库 因其清晰的结构、强大的数据一致性保障以及成熟的生态系统而备受青睐。其核心思想是将数据组织成一张张表格 (Table) 。这些表格就像电子表格一样,有行有列。更重要的是,这些表格之间可以通过预定义的关系 (Relationship) 相互连接。例如,在一个博客系统中,用户 (users) 表和 帖子 (posts) 表可以通过"用户ID"建立联系,表明哪篇帖子是由哪个用户创建的。这种结构化的方式使得数据的查询、维护和管理变得高效且可靠。

我们通常不会直接去操作底层的数据库文件。取而代之的是,我们会通过一个中间件软件------数据库管理系统 (DBMS) 来与数据库交互。DBMS可以看作是数据库的"大管家"或"智能大脑",它负责接收我们(或我们的应用程序)发出的指令(比如"帮我查一下所有价格大于100的商品"、"新建一条用户记录"),执行这些操作,并将结果返回。PostgreSQL 就是一款功能强大、开源且高度可扩展的关系型DBMS,深受开发者喜爱。

安装PostgreSQL后,你可以在一台服务器上创建多个相互独立的数据库实例 (Database Instance) ,每个实例都可以服务于不同的应用程序。这意味着你可以为你的FastAPI博客项目创建一个专属的数据库,而不会与其他应用(比如一个电商系统)的数据混淆。通常,PostgreSQL安装时会默认创建一个名为 postgres 的数据库,它可以作为连接到PostgreSQL服务器的初始入口点。

2. SQL:与数据库"流利对话"的语言

无论你选择哪款关系型DBMS------PostgreSQL、MySQL、Oracle还是SQL Server------它们都"听得懂"一门通用的语言:SQL (Structured Query Language)。SQL是与关系型数据库进行交互的标准语言。虽然不同DBMS在SQL的某些高级特性或方言上可能存在细微差别,但其核心语法和概念是高度一致的。

对于PostgreSQL,我们可以使用像PG Admin这样的可视化管理工具,在其查询工具中直接编写和执行SQL命令来创建表、插入数据、查询信息等。但更常见的是,我们的应用程序(比如用FastAPI构建的API)需要通过代码来执行这些SQL操作。

3. Python与PostgreSQL的桥梁:psycopg2 驱动

要在Python应用程序中与PostgreSQL数据库通信,我们需要一个数据库驱动程序 (Database Driver) 。对于PostgreSQL,psycopg2 是最流行和功能最完善的Python驱动之一。它实现了Python数据库API规范v2.0 (DB-API 2.0),允许我们用Python代码连接到PostgreSQL、执行SQL语句并处理结果。

安装 psycopg2:

在你的Python虚拟环境中,可以通过pip轻松安装:

bash 复制代码
pip install psycopg2-binary

(psycopg2-binary 包包含了预编译的二进制文件,安装更便捷。)

建立连接与创建游标:

在FastAPI应用启动时(或者在需要操作数据库的模块中),你需要建立到PostgreSQL的连接。一个典型的连接过程如下:

python 复制代码
# main.py (部分代码,展示连接逻辑)
import psycopg2
from psycopg2.extras import RealDictCursor # 让查询结果以字典形式返回,键为列名
import time

# 尝试连接数据库,如果失败则重试
while True:
    try:
        conn = psycopg2.connect(
            host="localhost",      # 数据库服务器地址
            database="fastapi_db", # 你创建的数据库名称
            user="your_postgres_user", #你的PostgreSQL用户名
            password="your_postgres_password", # 你的PostgreSQL密码
            cursor_factory=RealDictCursor # 重要!使查询结果返回字典而非元组
        )
        cur = conn.cursor() # 创建一个游标对象,用于执行SQL语句
        print("数据库连接成功!")
        break # 连接成功,跳出循环
    except Exception as error:
        print("连接数据库失败")
        print("错误详情: ", error)
        time.sleep(2) # 等待2秒后重试
  • psycopg2.connect(): 这是建立连接的核心函数,需要提供主机、数据库名、用户名和密码。
  • cursor_factory=RealDictCursor: 这是一个非常有用的配置。默认情况下,psycopg2查询返回的是元组 (tuple) 列表。使用 RealDictCursor,查询结果会变成字典 (dict) 列表,其中键是数据库表的列名,这使得在代码中通过列名访问数据更加直观和方便,尤其是在构建API响应时。
  • conn.cursor(): 连接成功后,我们通过 conn.cursor() 方法创建一个游标 (Cursor) 对象。游标就像一个在数据库结果集上移动的指针,我们通过它来执行SQL语句和获取结果。
  • 错误处理与重试: 在实际应用中,数据库连接可能会因为网络问题、数据库服务未启动等原因失败。上面的代码片段包含了一个简单的循环重试机制,这在生产环境中是常见的做法(尽管更健壮的方案可能还会涉及更复杂的重试策略和日志记录)。

现在我们有了连接和游标,就可以开始学习如何用SQL结合psycopg2来操作数据了。

4. SQL核心操作:与数据互动

4.1 数据查询:SELECT语句 ------ 探索你的数据宝藏

SELECT 是SQL中使用频率最高的命令,它的使命是从数据库中检索你需要的数据。

  • 选择所有列 (*):

    sql 复制代码
    SELECT * FROM products;

    在Python中使用psycopg2执行:

    python 复制代码
    # 【示例】在FastAPI路径操作中获取所有帖子
    @app.get("/posts")
    def get_all_posts():
        cur.execute("SELECT * FROM posts;") # 执行SQL查询
        posts_data = cur.fetchall()        # 获取所有查询结果
        return posts_data                  # FastAPI会自动将字典列表转为JSON

    cur.execute(sql_query) 用于执行SQL语句。对于SELECT查询,执行后结果并不会立即返回到Python变量中,而是存储在游标内部。你需要使用获取方法(如 fetchall(), fetchone(), fetchmany())来检索数据。

    • cur.fetchall(): 获取查询结果集中的所有行。
    • cur.fetchone(): 获取查询结果集中的下一行,如果结果集为空或已读取完毕,则返回None
    • cur.fetchmany(size): 获取指定数量的行。
  • 选择特定列:

    sql 复制代码
    SELECT id, name, price FROM products;
    python 复制代码
    cur.execute("SELECT id, title, content FROM posts WHERE published = TRUE;")
    published_posts = cur.fetchall()
  • 列别名 (AS) : 在SQL中使用AS为列指定别名,可以使API响应的JSON字段名更友好。

    sql 复制代码
    SELECT id AS product_id, name AS product_name FROM products;

    如果使用了 RealDictCursor,返回的字典键名会自动使用你指定的别名。

4.2 数据过滤:WHERE子句 ------ 精准定位信息

WHERE子句是SELECT语句的得力助手,它允许你根据特定条件筛选数据,只返回那些你真正感兴趣的行。

  • 相等条件 (=):

    sql 复制代码
    SELECT * FROM products WHERE id = 10;
    SELECT * FROM products WHERE name = '智能电视'; -- 文本值用单引号

    在Python中使用psycopg2时,强烈建议使用参数化查询 (Parameterized Queries) 来防止SQL注入攻击

    python 复制代码
    # 【示例】在FastAPI路径操作中获取特定ID的帖子
    @app.get("/posts/{post_id}")
    def get_one_post(post_id: int):
        cur.execute("SELECT * FROM posts WHERE id = %s;", (str(post_id),)) # 使用%s占位符
        post = cur.fetchone()
        if not post:
            raise HTTPException(status_code=404, detail="帖子未找到")
        return post
    • 参数化查询 (%s) : 在SQL语句中使用 %s 作为占位符,然后将实际的值作为元组传递给 cur.execute() 的第二个参数。psycopg2会自动处理值的转义,从而保护你的应用免受SQL注入的威胁。切勿直接将用户输入拼接到SQL字符串中!
  • 比较运算符 (>, <, >=, <=, <>!=):

    sql 复制代码
    SELECT * FROM products WHERE price > 50.00;
    python 复制代码
    cur.execute("SELECT * FROM posts WHERE rating > %s;", (3,))
    highly_rated_posts = cur.fetchall()
  • 逻辑运算符 (AND, OR):

    sql 复制代码
    SELECT * FROM products WHERE inventory > 0 AND price < 100.00;
    python 复制代码
    cur.execute("SELECT * FROM posts WHERE published = %s AND owner_id = %s;", (True, current_user_id))
  • IN 运算符: 匹配列表中的任意一个值。

    sql 复制代码
    SELECT * FROM products WHERE category_id IN (1, 5, 7);
    python 复制代码
    category_ids = (1, 5, 7) # 必须是元组
    cur.execute("SELECT * FROM products WHERE category_id IN %s;", (category_ids,))

    注意:当IN操作符与%s占位符一起使用时,值本身需要是一个元组。

  • LIKE 运算符 (模式匹配) : 与通配符 % (匹配零或多个字符) 和 _ (匹配单个字符) 结合使用。

    sql 复制代码
    SELECT * FROM products WHERE name LIKE '智能%'; -- 名称以"智能"开头
    python 复制代码
    search_term = "快速入门"
    cur.execute("SELECT * FROM posts WHERE title LIKE %s;", ('%' + search_term + '%',)) # 构造包含通配符的搜索模式
    search_results = cur.fetchall()
4.3 数据排序:ORDER BY子句 ------ 让结果井然有序

ORDER BY 用于指定查询结果的排序方式。

sql 复制代码
SELECT * FROM products ORDER BY price ASC; -- 按价格升序 (ASC是默认)
SELECT * FROM products ORDER BY created_at DESC; -- 按创建时间降序 (最新在前)
python 复制代码
cur.execute("SELECT * FROM posts ORDER BY created_at DESC, title ASC;")
sorted_posts = cur.fetchall()
4.4 结果限制与偏移:LIMITOFFSET ------ 高效分页的秘诀

在处理大量数据时,一次性返回所有结果是不明智的。LIMITOFFSET 是实现分页功能的关键。

sql 复制代码
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20; -- 跳过前20条,然后取10条
python 复制代码
page_size = 10
page_number = 3
offset_val = (page_number - 1) * page_size
cur.execute("SELECT * FROM posts ORDER BY id LIMIT %s OFFSET %s;", (page_size, offset_val))
paginated_posts = cur.fetchall()
4.5 数据修改操作:改变数据库的状态
  • 插入数据:INSERT INTO 语句

    sql 复制代码
    INSERT INTO products (name, price, inventory, category_id)
    VALUES ('新款耳机', 199.99, 50, 3);

    在Python (psycopg2)中插入数据并获取新插入记录的ID (或其他列):

    python 复制代码
    # 【示例】在FastAPI路径操作中创建新帖子
    @app.post("/posts", status_code=201)
    def create_new_post(post_payload: PostSchema): # PostSchema是Pydantic模型
        cur.execute("""
            INSERT INTO posts (title, content, published, owner_id)
            VALUES (%s, %s, %s, %s) RETURNING *;
        """, (post_payload.title, post_payload.content, post_payload.published, current_user_id))
        newly_created_post = cur.fetchone()
        conn.commit() # !!! 重要:提交事务,使更改永久生效 !!!
        return newly_created_post
    • RETURNING * (或 RETURNING id) : 这是PostgreSQL的一个强大特性,允许你在执行INSERT (同样适用于UPDATEDELETE) 操作后立即返回被操作行的数据(比如新生成的ID),非常适合在API响应中返回新创建或修改后的资源。
    • conn.commit() : 对于任何修改数据库的操作(INSERT, UPDATE, DELETE),执行完cur.execute()后,更改只是暂存在当前事务中。你必须调用 conn.commit() 才能将这些更改永久写入数据库。否则,当连接关闭或程序结束时,这些更改会丢失(回滚)。
    • conn.rollback() : 如果在事务过程中发生错误,你可以调用 conn.rollback() 来撤销当前事务中所有未提交的更改。
  • 更新数据:UPDATE 语句

    sql 复制代码
    UPDATE products SET price = 209.99, inventory = 45 WHERE id = 123;
    python 复制代码
    # 【示例】更新帖子
    @app.put("/posts/{post_id}")
    def update_existing_post(post_id: int, post_payload: PostUpdateSchema):
        cur.execute("""
            UPDATE posts SET title = %s, content = %s, published = %s
            WHERE id = %s RETURNING *;
        """, (post_payload.title, post_payload.content, post_payload.published, str(post_id)))
        updated_post_data = cur.fetchone()
        if not updated_post_data:
            raise HTTPException(status_code=404, detail="要更新的帖子未找到")
        conn.commit()
        return updated_post_data
  • 删除数据:DELETE FROM 语句

    sql 复制代码
    DELETE FROM products WHERE id = 789;
    python 复制代码
    # 【示例】删除帖子
    @app.delete("/posts/{post_id}", status_code=204)
    def delete_existing_post(post_id: int):
        cur.execute("DELETE FROM posts WHERE id = %s RETURNING *;", (str(post_id),))
        deleted_post_data = cur.fetchone()
        if not deleted_post_data:
            raise HTTPException(status_code=404, detail="要删除的帖子未找到")
        conn.commit()
        # 对于204 No Content,通常不返回响应体
        return Response(status_code=status.HTTP_204_NO_CONTENT)

    务必小心使用 DELETE 如果忘记了WHERE子句,它会删除表中的所有数据!

4.6 SQL 最佳实践回顾
  • 大写SQL关键字 :提高可读性 (如 SELECT, FROM, WHERE)。
  • 分号结尾psycopg2通常不需要在execute()的字符串末尾加分号,但如果你在PG Admin或其他工具中写SQL,记得加上。
  • 参数化查询永远,永远,永远 使用参数化查询 (%s 占位符和值元组) 来防止SQL注入,尤其是在处理用户输入时。
  • 事务管理 (commit/rollback):理解何时以及为何需要提交或回滚事务。

5. 数据库设计基础:为你的数据建模

良好的数据库设计是构建健壮、可维护应用的核心。

5.1 表 (Tables)、行 (Rows) 与列 (Columns)
  • 表 (Table) :代表应用中的一个核心"事物"或"概念",如 users, posts, comments
  • 列 (Column) :表的属性,描述"事物"的特征,如 users 表的 username, email, created_at
  • 行 (Row):表中的一条记录,代表一个具体的"事物"实例,如一个特定的用户。
5.2 数据类型 (Data Types)

为列选择正确的数据类型至关重要,它确保了数据的准确性、存储效率和查询性能。PostgreSQL提供了丰富的数据类型:

  • 数值类型 : INTEGER (整数), BIGINT (大整数), NUMERIC(precision, scale) (精确小数), REAL (单精度浮点), DOUBLE PRECISION (双精度浮点)。
  • 文本类型 : VARCHAR(n) (可变长度字符串,最大长度n), TEXT (可变长度字符串,无显式长度限制)。
  • 布尔类型 : BOOLEAN ( TRUEFALSE )。
  • 日期/时间类型 : DATE (日期), TIME (时间), TIMESTAMP (时间戳,不带时区), TIMESTAMPTZ (或 TIMESTAMP WITH TIME ZONE, 时间戳,带时区 - 推荐使用,以避免时区混淆)。
  • 其他 : UUID (通用唯一标识符), JSON, JSONB (二进制JSON,查询性能更好), ARRAY (数组) 等。
5.3 主键 (Primary Key - PK) ------ 每行的唯一"身份证"

主键是一列或多列的组合,其值能唯一标识表中的每一行。

  • 唯一性 (UNIQUE):主键值在表中不能重复。

  • 非空性 (NOT NULL):主键值不能为空。

  • 常用选择 : 通常,我们会为每个表添加一个名为 id 的列,类型为 SERIAL (自动递增的整数,PostgreSQL特有,相当于 INTEGER NOT NULL DEFAULT nextval('sequence_name')) 或 BIGSERIAL,并将其设为主键。或者使用 UUID 类型并生成UUID作为主键。

    sql 复制代码
    CREATE TABLE users (
        id SERIAL PRIMARY KEY, -- 自动递增的整数主键
        username VARCHAR(50) UNIQUE NOT NULL,
        email VARCHAR(100) UNIQUE NOT NULL
    );
5.4 外键 (Foreign Key - FK) ------ 表与表之间的"纽带"

外键是表中的一列(或多列),其值引用了另一张表的主键。它用于建立和强制表之间的链接或关系。

  • 定义关系 : 例如,posts 表可以有一个 owner_id 列作为外键,引用 users 表的 id 列,表明该帖子属于哪个用户。

  • 数据完整性 (Referential Integrity) : 外键有助于维护数据的一致性。例如,不能创建一个 owner_id 指向一个不存在的用户的帖子。

  • ON DELETE 策略 : 当被引用的父表行(如 users 表中的某用户)被删除时,需要定义子表(如 posts 表中该用户的所有帖子)如何响应:

    • CASCADE: 父行删除,所有引用它的子行也自动删除(例如,删除用户,其所有帖子也删除)。
    • SET NULL: 父行删除,子行中的外键列被设置为 NULL (前提是该外键列允许为NULL)。
    • SET DEFAULT: 父行删除,子行中的外键列被设置为其默认值。
    • RESTRICT / NO ACTION (通常是默认行为): 如果存在引用的子行,则阻止删除父行。
    sql 复制代码
    CREATE TABLE posts (
        id SERIAL PRIMARY KEY,
        title TEXT NOT NULL,
        content TEXT NOT NULL,
        owner_id INTEGER NOT NULL,
        CONSTRAINT fk_posts_owner_id FOREIGN KEY (owner_id) -- 定义外键约束
            REFERENCES users (id) ON DELETE CASCADE -- 如果用户被删除,其帖子也级联删除
    );
5.5 约束 (Constraints) ------ 数据的"规则守护者"

约束是施加在表或列上的规则,用于限制可以存入的数据,进一步保证数据的质量。

  • NOT NULL: 列值不能为空。
  • UNIQUE: 列中的所有值必须唯一(一个表可以有多个UNIQUE约束)。
  • PRIMARY KEY: 隐含 NOT NULLUNIQUE
  • FOREIGN KEY: 确保引用完整性。
  • CHECK: 定义一个布尔表达式,插入或更新的行必须满足该表达式(例如 CHECK (price > 0))。
  • DEFAULT: 如果插入时未提供值,则使用此默认值(例如 published BOOLEAN DEFAULT FALSE)。

总结:数据驱动FastAPI的坚实一步

通过本文的探索,你不仅深入了解了关系型数据库(特别是PostgreSQL)的基石,掌握了SQL这门与数据对话的通用语言的核心操作,更重要的是,你学会了如何使用Python的psycopg2驱动程序作为桥梁,在你的FastAPI应用中实际连接并操作PostgreSQL数据库。从基础的SELECT查询到复杂的数据修改,再到数据库设计的关键概念如主键、外键和约束,这些都是构建任何数据驱动型FastAPI应用不可或缺的知识。

熟练运用psycopg2执行参数化查询、管理事务,并结合良好的数据库设计原则,将使你能够高效地为你的FastAPI应用构建强大、安全且可维护的后端数据支持。

下一步建议:从原生SQL到ORM的飞跃

虽然直接使用psycopg2和原生SQL语句能让你对数据库操作有非常清晰的控制,但在大型项目中,直接编写大量SQL可能会变得繁琐且容易出错。

因此,我强烈建议你接下来深入学习对象关系映射器 (Object-Relational Mapper, ORM) ,特别是与FastAPI无缝集成的 SQLAlchemy ORM。ORM允许你使用Python类和对象来定义数据库表和行,用Python方法来执行数据库查询和操作,从而在很大程度上抽象掉了原生SQL的编写。这不仅能提高开发效率,还能使代码更具Pythonic风格,更易于维护和测试。

同时,不要停止实践SQL本身!继续探索更高级的SQL特性,例如:

  • JOIN 操作: 这是关系型数据库的精髓所在,允许你根据表之间的关系,从多张表中组合和检索数据(例如,获取一篇帖子及其作者的用户名和邮箱)。
  • 聚合函数 (COUNT, SUM, AVG, MAX, MIN) 与 GROUP BY: 用于数据统计和分析。
  • 子查询 (Subqueries): 在一个查询中嵌套另一个查询。
  • 窗口函数 (Window Functions): 用于执行复杂的分析计算。

掌握这些,你的数据处理能力将更上一层楼,为构建功能更丰富的FastAPI应用奠定更坚实的基础。

相关推荐
小陈工4 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
科技小花8 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸8 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain8 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希9 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神9 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员9 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java9 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿10 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴10 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存