什么是数据库?从基本概念到常见类型

科技资讯4小时前发布 jdkjadmin
0 00
什么是数据库?从基本概念到常见类型

什么是数据库?从基本概念到常见类型

用户注册、商品目录、订单记录、聊天消息、设备读数——应用程序需要保存的数据,通常不会只在程序运行期间存在。数据库就是用来组织、保存、查询和维护这些数据的工具。它让多个程序能够按规则读写数据,并在进程退出、机器重启之后继续使用这些数据。

不过,“数据库”并不等于某一种软件,也不等于“把数据放在表格里”。不同系统的结构、查询方式、一致性保证和运维成本差异很大。选型时,最重要的问题不是“哪种数据库最先进”,而是数据之间有什么关系、会怎样读写、允许什么样的故障,以及团队能否运维它。

一、数据库、DBMS 和数据模型

日常所说的“数据库”可能指一组数据,也可能指管理这些数据的软件。更精确地说:

  • 数据是应用要保存的信息,例如用户、订单和金额。
  • DBMS(数据库管理系统)负责保存、查询、更新、并发控制、权限管理等工作。SQLite、PostgreSQL、MySQL、MongoDB 都是 DBMS。
  • 数据库在具体产品中可能指一份数据集合、一个逻辑命名空间,或运行中的数据服务,含义要看上下文。
  • 数据模型描述数据如何组织、如何关联,以及可以用什么方式查询。关系模型、文档模型、键值模型和图模型是常见例子。

DBMS 与数据库也不要和应用混为一谈。比如,一个网站的应用程序发出查询,数据库服务执行查询并返回结果;应用的业务规则、用户界面和数据库的数据管理职责并不相同。

二、关系型数据库的基本概念

关系型数据库把数据组织为表。表由行和列组成:行通常代表一条记录,列代表记录的属性。例如,用户表可以包含用户编号、邮箱和创建时间。关系型数据库常用 SQL(结构化查询语言)定义结构、查询和修改数据。

表、主键与外键

主键用于唯一标识表中的一行。主键值应当唯一且非空;使用自增整数或随机生成的 ID 都很常见。外键用于表达一张表中的值引用另一张表中的记录,例如订单的 user_id 引用用户表的 id。外键约束可以阻止引用不存在用户的订单,但具体支持和默认行为取决于数据库及其设置。

表结构中的 NULL 表示缺失或未知值,不等同于空字符串、数字 0 或布尔值 false。比较 NULL 通常要用 IS NULL,而不是 = NULL。是否允许某列为 NULL,应由业务含义决定。

约束、范式与关联

约束是由数据库强制执行的数据规则。常见约束包括主键、外键、唯一约束、非空约束和检查约束。把重要规则放在数据库层,可以避免不同程序入口各自漏掉校验;但数据库约束不能替代完整的业务校验和授权。

关系型数据库常把不同实体拆分到不同表中,再用 ID 关联。例如,用户信息存放在 users 表,订单信息存放在 orders 表。这样可以避免反复复制用户邮箱等信息,减少更新时的不一致。规范化的目标之一就是减少冗余和更新异常;它并不意味着每个场景都必须拆到最细。为了读性能而有意保存冗余数据,是可以采用的设计,但需要明确如何保持同步。

事务与 ACID

事务将一组操作作为一个逻辑单元提交或回滚。常说的 ACID 包括:

  • 原子性:事务中的操作要么整体生效,要么整体不生效。
  • 一致性:事务执行前后应满足数据库约束及应用定义的规则;这不代表数据库能自动保证所有业务逻辑正确。
  • 隔离性:并发事务之间的可见性由隔离级别和实现方式决定。不同数据库的细节并不完全相同。
  • 持久性:提交成功后,数据库会按其配置和承诺保存结果。实际故障保障仍与存储、复制、备份和配置有关。

ACID 不是所有数据库都以相同方式提供的单一开关。即使同属关系型数据库,锁、隔离级别、复制和故障恢复的行为也可能不同。多服务之间的业务操作通常也不能简单地用一个本地数据库事务包住,需要另外设计协调、补偿或幂等处理。

三、用 Python 和 SQLite 创建一个可运行的数据库

SQLite 是嵌入式关系型数据库,数据库通常存放在本地文件中。Python 标准库自带 sqlite3,不必安装额外数据库服务。下面的程序会在当前目录创建 shop.db,建表、插入数据并查询订单。保存为 demo.py,使用 Python 3 运行即可:

import sqlite3

connection = sqlite3.connect("shop.db")
connection.execute("PRAGMA foreign_keys = ON")

connection.executescript("""
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY,
    email TEXT NOT NULL UNIQUE
);

CREATE TABLE IF NOT EXISTS orders (
    id INTEGER PRIMARY KEY,
    user_id INTEGER NOT NULL,
    amount_cents INTEGER NOT NULL CHECK (amount_cents >= 0),
    FOREIGN KEY (user_id) REFERENCES users(id)
);
""")

try:
    with connection:
        connection.execute(
            "INSERT OR IGNORE INTO users (id, email) VALUES (?, ?)",
            (1, "[email protected]"),
        )
        connection.execute(
            "INSERT INTO orders (user_id, amount_cents) VALUES (?, ?)",
            (1, 2599),
        )

    rows = connection.execute(
        """
        SELECT users.email, orders.amount_cents
        FROM orders
        JOIN users ON users.id = orders.user_id
        WHERE users.id = ?
        """,
        (1,),
    ).fetchall()

    for email, amount_cents in rows:
        print(email, amount_cents)
finally:
    connection.close()

运行后,示例会输出邮箱和以分为单位的金额。金额使用整数分,避免用二进制浮点数表示货币时出现精度误差。示例中的 with connection 会在代码块结束时提交事务;如果代码块中出现异常,事务会回滚。连接仍需关闭,因此使用了 finally。

这里显式执行 PRAGMA foreign_keys = ON,是因为 SQLite 的外键检查需要在连接上启用;不能假定每个连接都已开启。示例中的 INTEGER PRIMARY KEY 在 SQLite 中有特定含义,其他数据库的自增主键写法可能不同。若多进程、多机器并发写入需求较高,SQLite 的文件型部署方式未必合适,应评估客户端/服务端数据库。

四、索引、查询与性能

索引是数据库为加速特定查询维护的数据结构。没有索引时,数据库可能需要检查大量甚至全部记录;索引可以帮助定位满足条件的行。索引并非免费:它会占用空间,并增加插入、删除和更新时的维护成本。

例如,如果经常按订单所属用户查询,可以为 orders.user_id 建立索引:

CREATE INDEX IF NOT EXISTS idx_orders_user_id
ON orders(user_id);

在 SQLite 中,可以用 EXPLAIN QUERY PLAN 查看查询计划的大致信息:

EXPLAIN QUERY PLAN
SELECT id, amount_cents
FROM orders
WHERE user_id = 1;

查询计划是理解执行方式的线索,不是“加了索引就一定更快”的保证。小表全表扫描可能更划算;低选择性的字段、写入量很大的表、条件与索引列顺序不匹配等情况,也可能导致索引没有预期效果。应基于真实查询和数据规模测试,而不是给每一列都建索引。

复合索引由多个列组成,通常需要结合查询条件和排序方式设计。数据库优化器会根据统计信息、查询计划和系统版本选择执行方案。上线后还应关注慢查询、锁等待、连接数、磁盘空间和数据增长,而不仅是单条 SQL 的执行时间。

五、常见数据库类型及适用场景

关系型数据库

PostgreSQL、MySQL、MariaDB 和 SQL Server 都是常见关系型数据库。它们适合结构相对明确、实体间关系重要、需要约束和事务的业务,例如账户、订单、库存和企业管理系统。它们也能处理复杂查询,但功能、SQL 兼容性、扩展能力、许可和运维方式各不相同,不能简单认为换一个产品就无需改造。

关系型不等于“只能存固定不变的结构”。一些关系型数据库支持 JSON 列和多种扩展能力。不过,把所有内容都塞进一个 JSON 字段,可能失去清晰约束和方便关联的优势;应该根据查询方式和数据语义决定字段如何组织。

键值数据库

键值数据库按键读取对应的值,常用于缓存、会话、计数器和短期状态。Redis 是常见例子,支持多种数据结构和操作,但“把数据放进 Redis”并不自动等于可靠持久化。内存容量、持久化方式、复制、淘汰策略和故障恢复都需要按用途配置。缓存通常允许过期或重建;若把它当唯一数据来源,就必须重新评估其持久性和恢复方案。

文档数据库

文档数据库以 JSON 风格的文档组织数据,适合字段变化较多、查询通常围绕单个文档展开的场景,例如内容管理或部分目录数据。MongoDB 是常见例子。文档模型减少了某些场景下的关联操作,但不意味着不需要设计:重复字段会带来一致性问题,文档大小、索引和跨文档事务也都有边界。嵌套还是拆分,应由访问模式和更新方式决定。

宽列数据库

宽列数据库面向分布式数据和特定访问模式,常用于高写入量、按分区键读取的业务。Cassandra 是代表之一。此类系统通常要求先根据查询设计分区和表结构,不适合直接照搬关系型数据库中“任意组合条件都能高效查”的预期。分区键选错可能造成热点或查询代价过高,写入规模也不能单独作为选型理由。

图数据库

图数据库把实体表示为节点,把实体间的联系表示为边,适用于需要频繁遍历多跳关系的场景,例如关系网络、依赖图和某些推荐问题。若只是偶尔根据外键关联几张表,关系型数据库通常已经足够。图数据库的价值在于关系遍历是核心查询,而不是因为数据中出现了“关系”这个词。

时序数据库

时序数据库针对带时间戳的连续测量数据做了专门优化,常见于监控指标、传感器读数和设备状态。保留周期、降采样、时间分区和高基数标签会直接影响存储与查询成本。若数据量不大、查询简单,关系型数据库也可能胜任;不要因为数据有时间字段就自动引入专用系统。

搜索引擎与向量数据库

全文搜索引擎适合相关性排序、分词、模糊匹配和搜索分析。它通常是业务数据库的查询副本或索引,而非自动替代主数据库;索引构建和更新存在延迟时,搜索结果可能暂时落后于主数据。

向量数据库用于存储向量并执行近似近邻等相似度检索,常见于语义搜索和检索增强生成。向量相似不等同于事实正确,也不自带权限过滤、数据治理或来源校验。向量索引参数会影响召回率、延迟和资源占用,应使用业务数据评测。很多系统提供向量能力,但是否适合独立采用专用产品,要看规模、过滤条件、更新方式和运维能力。

六、如何选择:从访问模式和边界开始

选数据库前,先写清楚数据规模和工作负载:数据增长速度、读写比例、查询是否需要多表关联、是否有强一致性要求、是否允许短暂陈旧、峰值并发是多少,以及数据要保留多久。还要区分可用性(服务能否访问)、持久性(故障后数据是否还在)和一致性(并发或复制环境中读到什么结果);它们不是同一个指标。

然后检查关键约束:最大延迟、恢复时间目标(RTO)、恢复点目标(RPO)、数据所在地要求、加密和审计要求,以及团队是否能维护备份、升级、监控和故障切换。托管数据库能减少一部分运维工作,但不会自动替你做好权限设计、容量评估和恢复演练。

常见的起点是:单机、小型应用或本地工具可考虑 SQLite;需要多个服务通过网络并发访问、复杂事务与关系查询时,可评估 PostgreSQL 或 MySQL;高频缓存可使用键值系统;其余专用类型则应由明确的访问模式和测量结果驱动。这里不是固定规则,真正选型应以可验证的需求和原型测试为准。

七、常见错误与安全边界

把用户输入拼进 SQL

将用户输入直接拼接到 SQL 字符串,可能导致 SQL 注入。应使用驱动提供的参数绑定。前面的 Python 示例使用问号占位符;不同驱动的占位符语法不同,应查对应驱动文档。参数值只能绑定数据值,不能用来替代表名、列名或关键字;动态标识符应从代码维护的允许列表中选择,并按数据库规则正确引用。

# 错误:不要把不可信输入拼进 SQL
# sql = "SELECT * FROM users WHERE email = '" + email + "'"

# 正确:将数据作为参数传递
row = connection.execute(
    "SELECT id FROM users WHERE email = ?",
    (email,),
).fetchone()

参数化查询是防止 SQL 注入的重要措施,但不是完整安全方案。它不负责用户身份认证、业务授权、输入语义校验或防止滥用。数据库账户仍应遵循最小权限原则,应用只获得完成工作所需的权限。

把数据库文件或端口暴露给不可信对象

SQLite 文件不是天然安全的:拥有文件系统访问权限的人可能读取、复制或修改数据。服务端数据库应限制网络访问,使用强认证并妥善管理密钥;敏感数据还应根据威胁模型评估传输加密、静态加密、密钥轮换和审计。密码通常应使用适合密码存储的慢哈希方案处理,而不是明文或可逆加密;不应自行设计密码学算法。

认为事务可以替代所有业务规则

事务可以保证一组数据库操作按设定方式提交或回滚,但不能自动防止所有并发竞态。例如“先查询余额再扣款”若拆成多个未受保护的操作,可能出现超扣。应使用适当的原子更新、约束、锁或隔离级别,并在目标数据库上验证行为。跨服务调用通常也不属于单个数据库事务范围。

没有备份,也没有恢复测试

复制与备份不是一回事:复制可能把误删或错误更新同步到副本,不能单独替代可恢复的备份。备份策略应包含保留周期、访问控制、异地或隔离存放,并定期进行恢复演练。仅有“备份任务成功”的日志,不能证明数据确实能在故障时恢复。

过度依赖数据库默认配置

默认隔离级别、自动提交、时区、字符集、外键开关、连接池和超时行为都可能因产品或驱动而异。迁移数据库、升级版本或换驱动时,应检查这些假设。尤其不要把某一种数据库的 SQL 方言、类型转换和锁行为当成所有数据库的共同标准。

八、从小项目走向生产环境

当应用开始承载重要数据,数据库设计就不止是建表和写查询。应使用版本化迁移记录结构变化;避免上线脚本在重复执行时造成意外;为连接、查询和事务设置合理超时;监控磁盘、连接、锁、错误率和慢查询;在真实数据量或接近真实的数据上做性能测试。

还要提前考虑数据生命周期:哪些数据需要保留,如何删除或匿名化,删除请求是否覆盖缓存、搜索索引和备份中的副本。涉及个人信息或受监管数据时,应结合适用法律、组织规范和具体部署地区评估义务,不能仅凭数据库产品提供了某项功能就认为合规。

数据库的核心价值不是“保存一份文件”,而是在数据增长、并发访问和故障发生时,仍能按可理解的规则管理信息。先弄清数据关系、查询方式和容错要求,再选择合适的数据模型;从最简单可验证的方案开始,并用约束、参数化查询、备份和监控守住边界,通常比一开始追求复杂架构更可靠。

© 版权声明

相关文章