1. 先别急着敲命令:数据库基本操作到底在练什么
很多人一听到“数据库技术基本操作”,第一反应就是打开终端敲几个 SQL 语句,或者去网上找“xx数据库十五天入门”的视频跟着敲一遍。但实际上,真正能让你在项目里游刃有余的基本操作,不是背命令,而是建立一套“数据是怎么被组织、存储和查询”的心智模型。
先说结论:数据库基本操作至少包含四个层面——环境与工具链的搭建、关系型数据库的增删改查(CRUD)、非关系型数据库的文档操作,以及用内存里的数据表工具(比如 Pandas)去做探索式分析。这四个层面对应着实际工作中最常见的四种场景:初始化项目时建库建表、业务后端对数据做持久化读写、面对脏数据时快速清洗、以及对着一张几十万行的表做即时统计分析。
从相关热搜词也能看出来,大家搜索比较集中的方向是 sqlite3 基本操作、MongoDB 数据库基本操作、Pandas 基本操作,以及 Homebrew 的基本操作。这四个词组合起来,恰好就是一套完整的“数据库技术入门到能干活”的路线图:SQLite 解决单机轻量存储,MongoDB 解决文档型数据的灵活读写,Pandas 解决数据分析,Homebrew 解决环境安装。
所以这篇文章我不打算只讲某一个数据库,而是把这条路线完整地走一遍。适合三类读者:一是刚学完 SQL 语法但不知道实际项目怎么落地的学生,二是需要在自己电脑上搭一套开发环境的后端新人,三是想搞清楚“为什么同事操作数据和我不太一样”的初级数据分析师。
2. 环境准备逃不掉:macOS 上通过 Homebrew 搞定 SQLite 与 MongoDB
2.1 为什么 Homebrew 是绕不开的第一步
很多教程默认你已经装好了数据库,然后直接讲语法。但实际在 macOS 上,数据库的安装方式远不止一种:有官网下载安装包、有 Docker 容器、有 Homebrew 包管理器。我个人的建议很明确:用 Homebrew。理由有三点——第一,Homebrew 会把可执行文件统一软链到/opt/homebrew/bin下,不用自己配置 PATH;第二,卸载时干净,不会像官网 pkg 安装包那样在系统里留一堆残留;第三,升级命令简单,一条brew upgrade就能把所有工具软件一并更新。
先检查 Homebrew 是否已经安装,终端执行:
brew --version如果提示没有这个命令,那就先装 Homebrew。装的过程本身也是 macOS 开发者绕不开的一次实操:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"装完后建议顺手跑一下诊断,确认没有路径问题:
brew doctor这个命令会检查 Homebrew 运行环境和目录权限,尤其是当你以前手动装过其他版本的软件,它可能提示你有冲突。不要忽略这些提示,路径权限出错会导致后面 brew services 启动 MongoDB 时找不到数据目录。
2.2 SQLite 的特殊地位:macOS 自带但建议升级
SQLite 有一个非常好的特性——它是被编译进 macOS 系统自带的。理论上你直接输入sqlite3就能进入命令行交互环境。但系统自带版本通常比较老,有些新特性(比如更严格的约束检查、更好的 JSON 函数支持)用不上。所以我一般会用 Homebrew 装一个最新版本:
brew install sqlite3注意一个坑:Homebrew 安装的 SQLite 是 keg-only 的,也就是说它不会被自动加入 PATH,因为系统已经存在一个/usr/bin/sqlite3。如果你希望终端里敲sqlite3用的是新版,需要手动把这个 keg-only 的路径加入环境变量。在.zshrc里加上:
export PATH="/opt/homebrew/opt/sqlite3/bin:$PATH"然后运行:
source ~/.zshrc which sqlite3看到指向/opt/homebrew/opt/sqlite3/bin/sqlite3就说明成功了。
2.3 MongoDB 的安装与启动:不止 brew install 这么简单
MongoDB 的安装和 SQLite 不太一样,它没有嵌入到系统里,也不像 SQLite 那样只是个文件库。MongoDB 是独立的服务器程序,安装后需要启动服务才能连接。用 Homebrew 安装 MongoDB Community Edition 需要先加一个官方 tap:
brew tap mongodb/brew这一步很关键。MongoDB 官方并不把所有版本都放在 Homebrew 的主仓库里,而是维护了一个独立的仓库地址。不加这个 tap 直接brew install mongodb,大概率会提示找不到 formula。
brew install mongodb-community安装完成后,启动服务有两种方式。一种是前台启动,适合调试时看日志:
mongod --config /opt/homebrew/etc/mongod.conf另一种是注册为后台服务,适合平时开发:
brew services start mongodb-community使用 brew services 后,MongoDB 会随开机自启,而且日志会被统一管理。查看服务状态用:
brew services list这里常见的问题是端口被占用。如果你本机已经装了别的数据库或服务占用了 27017 端口,brew services 启动时会报错。这时先查端口占用:
lsof -i :27017如果有进程占用,修改/opt/homebrew/etc/mongod.conf里的port配置,重新指定一个端口,比如 27018。
环境装好以后,才算真正进入数据库基本操作的正题。这一块很多人忽视了,总觉得“能连上数据库就行”,但安装路径不对、版本混乱、服务管理方式不懂,到后面做项目部署时会吃大亏。
3. 关系型基本功:以 SQLite 为例把增删改查走一遍
3.1 建库建表:文件即数据库,这条特性帮了大忙
SQLite 最大的特点是零配置,数据库就是一个独立的磁盘文件。这一点在学习和原型开发阶段特别方便,不用启动服务,不用配置账号密码,不用操心端口。你用命令行创建一个数据库的完整过程如下:
sqlite3 test.db这行命令会进入一个交互式 shell,同时在当前目录生成 test.db 文件。如果在里面执行建表语句,这个文件就会成为包含数据的数据库文件。项目结束后想清理,直接删除这个文件就行,非常干净。
进入 shell 后,第一步是建表。以一个最常见的用户表为例:
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE, age INTEGER, created_at TEXT DEFAULT CURRENT_TIMESTAMP );这条语句包含了关系型数据库建表的几个核心概念:主键、非空约束、唯一约束、默认值。我特别想强调AUTOINCREMENT这个关键字的作用——它让自增主键在删除最大 ID 后不会复用之前的数值。虽然性能比不带 AUTOINCREMENT 的普通自增稍差一点,但在业务上能避免主键冲突和记录错位的问题。
3.2 插入、查询、更新、删除:每一条都要理解执行代价
建表之后就可以插入数据了。单行插入:
INSERT INTO users (name, email, age) VALUES ('张三', 'zhangsan@example.com', 28);批量插入时,SQLite 支持一次插入多行,这在初始化测试数据时非常高效:
INSERT INTO users (name, email, age) VALUES ('李四', 'lisi@example.com', 32), ('王五', 'wangwu@example.com', 25), ('赵六', 'zhaoliu@example.com', 41);查询是日常开发中使用频率最高的操作,重点带条件查询和排序:
SELECT name, email FROM users WHERE age > 26 ORDER BY age DESC;这一步我建议你刻意加上EXPLAIN QUERY PLAN来看 SQLite 是怎么执行这条语句的:
EXPLAIN QUERY PLAN SELECT name, email FROM users WHERE age > 26 ORDER BY age DESC;执行完你会发现,在没有索引的情况下,SQLite 会对整个表做全表扫描。理解了这一点,你才算真正明白为什么数据库教程总强调“查询优化”。对于只有几万行的小表,全表扫描无所谓;但到了几百万行,这个代价就是指数量级的差距。
更新和删除需要特别注意的是——一定要带 WHERE 条件。这是新手最容易犯的错误:
UPDATE users SET age = 30 WHERE name = '张三'; DELETE FROM users WHERE id = 5;如果不带 WHERE,执行DELETE FROM users;,那么全表数据会被清空。这种事在开发环境里出了也就是重来一遍,但如果在生产环境发生,那就不是玩笑。任何关系型数据库的教学内容里,我都要强调这一点:执行 UPDATE 和 DELETE 前,先写 SELECT 确认范围。
3.3 事务:让多条操作具备“要么全成功,要么全失败”的能力
事务是关系型数据库的精髓之一,也是 Sqlite 基本操作里最容易被跳过的部分。事务要解决的核心问题是:当多条 SQL 必须作为一个整体执行时,任何一条失败都不能让数据库处于中间状态。
在 sqlite3 命令行里执行事务的完整流程:
BEGIN TRANSACTION; INSERT INTO users (name, email, age) VALUES ('钱七', 'qianqi@example.com', 35); UPDATE users SET age = age + 1 WHERE name = '钱七'; COMMIT;执行COMMIT之前,数据对其它连接不可见。如果中途发现某条语句错了,执行ROLLBACK,刚才所有操作都会回滚,数据库恢复原状。
关于 SQLite 事务,有一个实际开发中容易踩的坑:SQLite 在默认日志模式下(journal mode 为 delete),写事务会对整个数据库文件加锁。这意味着,如果你同时在多个连接里往同一张表写数据,可能出现database is locked的报错。由并发写场景的话,我建议把 journal 模式改为 WAL:
PRAGMA journal_mode = WAL;WAL 模式显著提升了读写并发能力,代价是会产生额外的-wal和-shm文件。这是 SQLite 官方推荐的生产级配置,也是我在实际项目里比较常用的设置。
3.4 命令行之外的便捷操作:导入导出与备份
sqlite3 命令行最实用的几个便捷操作包括:
- 从 CSV 导入数据:
.mode csv先切换为 CSV 模式,再用.import指定文件路径和表名。 - 导出整个数据库结构:
.schema查看所有表的建表语句,这在迁移到 MySQL 或者 PostgreSQL 时特别好用。 - 备份数据库:直接复制 test.db 文件,但更稳妥的方式是用
.backup命令,它会生成一致性快照,避免文件拷贝过程中产生损坏。
sqlite3 test.db ".backup test_backup.db"我记得曾经在项目里因为没有用.backup而是直接拷贝 db 文件,结果拷贝时数据库正好在执行写操作,文件处于不一致状态,打开后跑了好几条修复命令才恢复正常。所以备份这种基础操作,也值得用正确的方式做。
4. 从 SQL 思维切到文档思维:MongoDB 的集合与 CRUD 实操
4.1 为什么总有人 SQL 转 Mongo 转不过弯
从 SQLite 切到 MongoDB,最大的难点不是语法,而是思维模型的切换。SQLite 里的一切都是表、行、列,结构固定,每一行的字段都一样。MongoDB 里没有表的概念,取而代之的是“集合”,没有行的概念,取而代之的是“文档”,文档本质上是 BSON 格式的 JSON,每条文档的字段可以完全不同。
用一个简单例子说明。在关系型数据库里,给用户表加一个“昵称”字段,必须执行ALTER TABLE users ADD COLUMN nickname TEXT;,如果表里已经有几十万行数据,这个 DDL 操作可能锁表造成短暂不可用。在 MongoDB 里,想加字段不需要预先定义,直接往文档里写入新字段即可,集合里已有的文档不会受影响。
所以 MongoDB 适合什么场景?适合业务字段变动频繁、数据结构需要灵活演进的场景,比如日志存储、用户画像、内容管理。但不适合什么场景?不适合强一致性要求极高、复杂事务、需要跨表关联查询的业务逻辑。这是选型层面的认知,比任何语法都重要。
4.2 建库、建集合、插文档:MongoDB 的完整 CRUD
连接 MongoDB 后,输入以下命令查看当前有哪些数据库:
show dbs和关系型数据库不同,MongoDB 建库不用专门执行“CREATE DATABASE”,只需要用use切换到目标库,然后在里面写入第一条数据,库就自动创建了:
use shop切换到一个不存在的库时,MongoDB 并不会立刻报错,库会在第一次写入时真正落盘。
接着往集合里插入文档。这里的“集合”对应关系型数据库的“表”,但不用预先定义结构:
db.users.insertOne({ name: "张三", email: "zhangsan@example.com", age: 28, tags: ["vip", "new_user"], address: { city: "上海", district: "浦东" } });insertOne插入单条文档,insertMany支持一次插入多条。和 SQLite 每条记录固定列数不同,MongoDB 允许同一个集合里存在不同结构的文档。
4.3 查询过滤与更新操作:不同写法对应不同执行策略
MongoDB 的查询语法是 JSON 风格的。查找年龄大于 26 的用户:
db.users.find({ age: { $gt: 26 } });$gt是条件操作符,相当于 SQL 里的>。完整条件操作符对照表如下:
| SQL 操作 | MongoDB 写法 |
|---|---|
= | { age: 26 } |
> | { age: { $gt: 26 } } |
< | { age: { $lt: 26 } } |
>= | { age: { $gte: 26 } } |
<= | { age: { $lte: 26 } } |
IN | { age: { $in: [26, 28, 30] } } |
AND | { age: { $gt: 26 }, city: "上海" } |
OR | { $or: [ { age: { $lt: 20 } }, { city: "上海" } ] } |
更新操作需要区分两个概念:更新字段和替换文档。更新一个字段用:
db.users.updateOne( { name: "张三" }, { $set: { age: 30 } } );这里$set是修改指定字段的关键字,原有文档其它字段保持不变。如果不用$set,而是写成db.users.updateOne({ name: "张三" }, { age: 30 });,MongoDB 会用第二个参数直接替换整个文档,那么原本的 name、email、tags 等字段全部丢失。这个错误在生产环境出现的频率远比你想象的高,务必记住$set。
删除操作:
db.users.deleteOne({ name: "张三" }); db.users.deleteMany({ age: { $lt: 18 } });和 SQLite 一样的原则:先查再删,确认条件匹配到的是预期的数据。
4.4 索引:MongoDB 查询性能的分水岭
MongoDB 在插入数据时不会自动为age这样的字段建索引,所以像db.users.find({ age: { $gt: 26 } })这样的查询在数据量大之后会变慢。为age创建索引:
db.users.createIndex({ age: 1 });1表示升序索引,-1表示降序。创建索引后,可以用explain检查查询是否真的用到了索引:
db.users.find({ age: { $gt: 26 } }).explain("executionStats");重点查看totalDocsExamined这个字段。如果这个值和集合总文档数相等,说明是全集合扫描;如果远小于总文档数,说明索引生效了,扫描的文档数大幅降低。建立索引同样有代价——每次写入时都要更新索引结构,所以索引不是建得越多越好,而是要为高频查询条件精准建立。
5. 用 Pandas 把数据表的操作搬到内存里:数据库命令之外的必修课
5.1 为什么数据库操作要学 Pandas
数据库基本操作如果只停留在 sqlite3 和 mongosh 命令行里,会遇到一个尴尬场景:你导出一张表的数据,想要做复杂清洗和聚合分析,SQL 语句越来越长,可读性和可维护性直线下降。这时候就需要把数据拉进内存,用 Pandas 的 DataFrame 来处理。
Pandas 解决的问题是:数据库 SQL 擅长的是结构化增删改查,但数据探索、缺失值处理、多表拼接、分组聚合、可视化前的数据整形,这些工作用 SQL 写起来绕,用 Pandas 写起来却更直白。我实际工作中的习惯是:数据库负责存储和基础过滤,Pandas 负责分析和清洗,两者配合。
5.2 DataFrame 读取数据库数据:直接对接 SQLite
Pandas 读取 SQLite 表非常方便,先装好依赖:
pip install pandas读取数据库只需要一行核心代码:
import pandas as pd import sqlite3 conn = sqlite3.connect("test.db") df = pd.read_sql_query("SELECT * FROM users", conn) conn.close()这个df就是一个 DataFrame,结构类似数据库的表,有行列索引。读取后,你可以先看一下数据的概览:
df.head() df.info() df.describe()head()返回前几行预览,info()显示每列的数据类型和非空数量,describe()对数值列做统计摘要。这三行代码是数据探索的起点。
5.3 SQL 与 Pandas 的对应关系:转换思路比记函数重要
用一张对照表来说明 SQL 和 Pandas 的对应关系,比较容易建立起迁移思维:
| 操作 | SQL 写法 | Pandas 写法 |
|---|---|---|
| 筛选列 | SELECT name, age FROM users | df[["name", "age"]] |
| 筛选行 | WHERE age > 26 | df[df["age"] > 26] |
| 排序 | ORDER BY age DESC | df.sort_values("age", ascending=False) |
| 分组聚合 | SELECT city, COUNT(*) FROM users GROUP BY city | df.groupby("city").size() |
| 去重 | SELECT DISTINCT city FROM users | df["city"].unique() |
| 取平均 | SELECT AVG(age) FROM users | df["age"].mean() |
写法和 SQL 差别很大,但背后做的事情一一对应。筛选行那一步是新手最容易算错的地方——df[df["age"] > 26]内层的df["age"] > 26是按行比较生成的布尔序列,外层的df[...]用布尔序列过滤行。
5.4 数据清洗的基本操作:缺失值、重复值、类型转换
真实项目导出的数据库表几乎不可能是干净的,常见问题包括:空值、重复行、类型不对。这三个问题的处理是 Pandas 基本操作里最实用的部分。
查看缺失值:
df.isnull().sum()处理缺失值两种思路:
# 删除缺失值所在行 df_clean = df.dropna() # 用指定值填充 df_filled = df.fillna({"age": df["age"].mean()})删除重复行:
df.drop_duplicates(subset=["email"], keep="first")subset参数指定按哪些列判断重复,keep="first"表示保留第一条。
类型转换也是高频操作。数据库里年龄可能存成了字符串,读取到 DataFrame 后是这样:
df["age"] = df["age"].astype(int)如果转换时报错,说明这一列里存在无法解析的非数字文本。这时先用pd.to_numeric加errors="coerce"参数处理:
df["age"] = pd.to_numeric(df["age"], errors="coerce")无法解析的内容会被转换为 NaN,之后再按缺失值处理。这种处理方式避免了“一条脏数据让整个脚本崩溃”的情况。
Pandas 和数据库结合使用还有一个重要场景:把处理好的数据写回数据库。假设你清洗完之后想导回 SQLite:
conn = sqlite3.connect("test.db") df_clean.to_sql("users_clean", conn, if_exists="replace", index=False) conn.close()if_exists="replace"会覆盖现有表,index=False避免把 DataFrame 的索引当成额外列写进去。这一步做下来,才算完成了一次完整的数据处理闭环:数据库导出、内存清洗、再写回。
6. 单链表基本操作实验与数据库索引:数据结构怎么影响数据库行为
6.1 热搜词为什么会有“单链表的基本操作实验”
在数据库技术这个主题下,单链表基本操作实验出现在热搜里并不奇怪。计算机专业的同学在学数据结构时都做过单链表的插入、删除、遍历实验,但往往只是当成一门理论课作业。等到后来接触数据库,才发现当初链表实验里学的指针跳转逻辑,和数据库索引的结构原理有着千丝万缕的联系。
单链表每个节点只存两个东西:数据和指向下一个节点的指针。遍历时必须从开头一个节点一个节点地顺着指针走,时间复杂度是 O(n)。如果想跳着访问,链表做不到,因为内存地址不连续。这就是为什么数据库索引不使用链表作为主要结构——它在等值查找和范围查找上的性能都不够好。
6.2 从链表到 B+ 树:数据库索引为什么没有用链表
如果让你设计一个“快速查找某条记录”的结构,第一反应可能是链表?不是,链表只适合按顺序访问。数组虽然支持按下标快速访问,但插入和删除需要移动大量元素。二叉查找树解决了部分问题,但在磁盘存储场景下,树的高度太高,每次访问一个节点都要经历一次磁盘 IO。
数据库实际使用的索引结构是 B+ 树。它的核心特点是:所有数据都存放在叶子节点,叶子节点之间通过指针连接,形成有序链表。非叶子节点只存索引键值,用来导航。这里就有意思了——链表在 B+ 树里是真实存在的,而且是很关键的设计。叶子节点的指针把有序的记录串成了一条链,使得范围查找(比如查年龄大于 26 且小于 40 的所有用户)只需要顺着叶子节点的链表顺序扫描,不需要反复回溯上层节点。
从单链表到 B+ 树,是一步步“加复杂约束”的过程:
- 单链表:只能线性访问,O(n)。
- 双向链表:能前向和后向遍历,但查找还是 O(n)。
- 跳表:增加了多级索引,查找变快,Redis 的 ZSet 就用它。
- B+ 树:磁盘友好型多叉树,叶子节点链表化,专为范围查询优化。
6.3 实操验证:为什么索引能让 SQLite 查询变快
前面在 SQLite 部分提到过EXPLAIN QUERY PLAN,现在我们做一个具体的实验来验证索引的作用。
先插入 10 万行测试数据:
WITH RECURSIVE cnt(x) AS ( SELECT 1 UNION ALL SELECT x + 1 FROM cnt LIMIT 100000 ) INSERT INTO users (name, email, age) SELECT 'user' || x, x || '@example.com', (x % 80) + 18 FROM cnt;然后执行查询计划:
EXPLAIN QUERY PLAN SELECT * FROM users WHERE age = 30;此时应该是全表扫描。创建索引:
CREATE INDEX idx_users_age ON users(age);再次执行同样的查询计划,你会看到查询计划的输出从“扫描全表”变成了“通过索引查找”。这个变化在十万行的数据量下可能只有几十毫秒的差距,但到了千万级数据量,全表扫描可能需要几秒甚至更慢,而索引查找只需要几毫秒。
这个实验恰好把数据结构理论和数据库操作串成了一个完整的故事:你写的每一个 WHERE 条件,数据库都要在底层决定用哪种“链表/树”结构去查找;索引就是预先建好的“跳表/树”,让查找不再线性扫描。理解这个逻辑,比背下来“加索引能加速查询”这句话有用得多。
7. 踩过几次坑之后总结的实操经验
最后分享几条我在同时使用 SQLite、MongoDB、Pandas 时踩过坑之后沉淀下来的经验,这些在官方文档里大多不会重点提到。
第一,SQLite 的并发写入限制是真实存在的。我在一个内部工具里遇到过两个进程同时往同一个 SQLite 文件写数据,频繁出现database is locked。最终解决方案就是前面说的开启 WAL 模式,同时把并发写入改成了串行队列。如果你的工具只是单进程使用,SQLite 是完全够用的,没必要为了“并发”硬上 PostgreSQL 或 MySQL。
第二,MongoDB 的文档结构设计比关系型表结构更需要在前期花时间思考。关系型数据库强迫你想好字段,MongoDB 不强迫,但这种自由度是把双刃剑。同一个集合里如果不同文档字段差异过大,查询时很容易漏掉数据,出现“为什么查不到”的问题。我现在会维护一份字段字典,记录每个集合的常用字段和数据类型,哪怕 MongoDB 允许动态加字段,也尽量保持文档结构有规律。
第三,Pandas 处理大型数据时要注意内存开销。一个 5000 万行的表,读取全部到 DataFrame 就可能把内存吃满。碰到这种情况,我的做法有两种:一是用read_sql_query时只选取需要的列,不要SELECT *;二是加上分块读取:
chunks = pd.read_sql_query("SELECT * FROM users", conn, chunksize=10000) result = pd.concat(chunk for chunk in chunks)分块读取牺牲少量性能,但能保住程序不崩。
第四,命令行工具是基本操作的起点,但不是终点。无论 sqlite3、mongosh 还是 Pandas REPL,真正高效的日常使用方式是写脚本文件。SQLite 的 SQL 用.read执行,MongoDB 的 JS 脚本用mongosh script.js执行,Pandas 的分析逻辑写进 Python 文件后python analysis.py执行。把常用操作固化成脚本,数据库基本操作才算真正从“会敲命令”升级到了“能稳定复现”。
数据库技术的基本操作,说白了就是不停练习“从数据里取出你需要的内容”这一件事。数据放在 SQLite 里,练习 SQL 和事务;数据放在 MongoDB 里,练习文档模型和索引;数据进了 DataFrame,练习清洗和分析。三条线都走通了,你对数据库技术的理解才算真正落地。