跳转至

SQL数据库基础(SQL Database Fundamentals)

一句话说明

SQL是管理和查询结构化数据的标准语言,在生信中用于存储样本元数据、查询基因注释数据库、以及用数据库思维高效分析物种丰度表。


核心概念(白话版)

1. 数据库是什么

  • 定义:数据库(Database)就是一个有组织的数据仓库,用来存储、管理和检索大量数据。
  • 白话比方:你可以把数据库想象成一个超级 Excel 文件——但它比 Excel 强太多了:
  • Excel 打开几万行就卡死,数据库能轻松处理几百万、几千万行
  • Excel 用鼠标点来点去,数据库用 SQL 语句精确查询
  • Excel 只能一个人用,数据库支持多人同时读写
  • Excel 数据容易搞乱,数据库有严格的格式约束(每列只能放规定的数据类型)

2. SQL是什么

  • 定义:SQL(Structured Query Language,结构化查询语言)是和数据库"对话"的语言。
  • 白话比方:你去图书馆借书,SQL 就是你跟图书管理员说的那些话——"帮我找所有 2024 年出版的生物学书","把这本书的作者改成张三"。SQL 就是这种"精确的请求语句"。
  • 发音:可以读作 "S-Q-L"(三个字母分开读),也可以读 "sequel"。
  • 特点
  • 不区分大小写(SELECTselect 一样),但约定俗成关键字大写,表名/列名小写
  • 每条语句以分号 ; 结尾
  • 非常接近英语自然语言,比编程语言好学

3. 为什么生信要学SQL

场景没有SQL有SQL
从10万条基因注释中找目标基因写 Python 脚本一行行扫描,慢一条 SELECT 语句,秒出结果
合并样本元数据和丰度表pandas merge + 各种对齐一条 JOIN 语句搞定
统计每个门的物种数量groupby + count + 排序GROUP BY phylum 一句话
NCBI / Ensembl / UniProt 查询网页手动点直接 SQL 查询本地镜像
存储几十万条代谢通路注释CSV 文件,打开就卡数据库秒查

总结:数据量小的时候用 pandas 够了;数据量大、要频繁查询、要多人共享时,SQL 是标配。

4. 关系型 vs 非关系型数据库

对比项关系型数据库(RDBMS)非关系型数据库(NoSQL)
数据结构表格(行+列),像 Excel文档/键值对/图,格式灵活
查询语言SQL各家不同(MongoDB 用 JSON 查询)
代表产品MySQL, PostgreSQL, SQLiteMongoDB, Redis, Elasticsearch
数据关系用外键关联多张表通常把相关数据塞到一个文档里
适用场景结构化数据、需要复杂查询半结构化/非结构化数据、高并发
生信用途样本元数据、基因注释、实验记录序列搜索引擎、日志存储

面试结论:生信工程师主要用关系型数据库,学好 SQL 就够用了。SQLite 是最轻量的选择——不需要安装服务器,Python 自带支持,非常适合学习和小型项目。


SQL 基础语法

以下所有示例都用生信场景,数据用 T2D 肠道菌群研究举例。

1. CREATE TABLE / INSERT —— 建表和插入数据

CREATE TABLE(创建表)

白话:相当于在 Excel 里新建一个工作表,然后定义每一列叫什么名字、能放什么类型的数据。

-- 创建样本元数据表
-- 白话:建一张"样本信息登记表",规定好每列放什么
CREATE TABLE samples (
    sample_id   TEXT PRIMARY KEY,   -- 样本编号,主键(唯一标识,不能重复)
    patient_id  TEXT NOT NULL,      -- 患者编号,不能为空
    group_name  TEXT NOT NULL,      -- 分组:'T2D' 或 'Control'
    age         INTEGER,            -- 年龄,整数
    bmi         REAL,               -- BMI指数,小数
    gender      TEXT,               -- 性别:'M' 或 'F'
    collect_date TEXT               -- 采样日期
);

-- 创建物种丰度表
CREATE TABLE abundance (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,  -- 自增主键
    sample_id   TEXT NOT NULL,      -- 样本编号(关联 samples 表)
    taxonomy    TEXT NOT NULL,      -- 物种分类名(如 "Bacteroides")
    tax_level   TEXT NOT NULL,      -- 分类层级:'phylum'/'genus'/'species'
    abundance   REAL NOT NULL,      -- 相对丰度值(0~1)
    read_count  INTEGER             -- 原始reads计数
);

常用数据类型

SQL类型说明举例
TEXT文本字符串样本ID、物种名、基因名
INTEGER整数reads计数、年龄、染色体编号
REAL小数(浮点数)丰度值、p值、BMI
BLOB二进制大数据序列文件(很少用)

关键约束

约束白话解释
PRIMARY KEY主键——这列的值必须唯一,用来标识每一行,像身份证号
NOT NULL不能为空——这列必须填值
UNIQUE唯一——这列不能有重复值
DEFAULT默认值——不填就自动用这个值
AUTOINCREMENT自增——每插入一行自动加1

INSERT(插入数据)

白话:相当于往 Excel 表里新增一行数据。

-- 插入单条样本数据
INSERT INTO samples (sample_id, patient_id, group_name, age, bmi, gender, collect_date)
VALUES ('S001', 'P001', 'T2D', 55, 28.3, 'M', '2024-01-15');

-- 插入多条数据(一次插入多行)
INSERT INTO samples (sample_id, patient_id, group_name, age, bmi, gender, collect_date)
VALUES
    ('S002', 'P002', 'Control', 48, 22.1, 'F', '2024-01-16'),
    ('S003', 'P003', 'T2D', 62, 30.5, 'M', '2024-01-17'),
    ('S004', 'P004', 'Control', 45, 21.8, 'F', '2024-01-18');

-- 插入丰度数据
INSERT INTO abundance (sample_id, taxonomy, tax_level, abundance, read_count)
VALUES
    ('S001', 'Bacteroides', 'genus', 0.25, 15000),
    ('S001', 'Prevotella',  'genus', 0.08, 4800),
    ('S001', 'Faecalibacterium', 'genus', 0.12, 7200),
    ('S002', 'Bacteroides', 'genus', 0.30, 18000),
    ('S002', 'Prevotella',  'genus', 0.15, 9000);

2. SELECT / WHERE / ORDER BY / LIMIT —— 查询数据

白话:SELECT 就是"查"——从表里挑出你想看的数据。

-- ============================================================
-- 基础查询:查看所有样本
-- 白话:把 samples 表里的所有数据都拿出来看看
-- ============================================================
SELECT * FROM samples;
-- * 表示"所有列"

-- 只看特定列
SELECT sample_id, group_name, age FROM samples;

-- ============================================================
-- WHERE:加条件筛选
-- 白话:只拿满足条件的行,相当于 Excel 的筛选功能
-- ============================================================

-- 只看 T2D 组的样本
SELECT * FROM samples
WHERE group_name = 'T2D';

-- 找 BMI > 25 的样本(超重)
SELECT sample_id, bmi, group_name FROM samples
WHERE bmi > 25;

-- 多条件:T2D 组 且 年龄 > 50
SELECT * FROM samples
WHERE group_name = 'T2D' AND age > 50;

-- 多条件:T2D 组 或 BMI > 30
SELECT * FROM samples
WHERE group_name = 'T2D' OR bmi > 30;

-- 模糊查询:找名字包含 "Bacteroid" 的物种
-- LIKE 是模糊匹配,% 表示"任意字符"
SELECT * FROM abundance
WHERE taxonomy LIKE '%Bacteroid%';

-- IN 查询:找指定的几个物种
SELECT * FROM abundance
WHERE taxonomy IN ('Bacteroides', 'Prevotella', 'Faecalibacterium');

-- BETWEEN:找丰度在 0.1 到 0.3 之间的
SELECT * FROM abundance
WHERE abundance BETWEEN 0.1 AND 0.3;

-- IS NULL / IS NOT NULL:找缺失值
SELECT * FROM samples
WHERE bmi IS NULL;         -- 找BMI没填的样本
SELECT * FROM samples
WHERE bmi IS NOT NULL;     -- 找BMI有值的样本

-- ============================================================
-- ORDER BY:排序
-- 白话:按某列排队,ASC = 从小到大(默认),DESC = 从大到小
-- ============================================================

-- 按丰度从高到低排序
SELECT * FROM abundance
ORDER BY abundance DESC;

-- 先按分组排,同组内按年龄排
SELECT * FROM samples
ORDER BY group_name ASC, age DESC;

-- ============================================================
-- LIMIT:只看前N条
-- 白话:只拿前几行,相当于 pandas 的 head()
-- ============================================================

-- 看丰度最高的前10个记录
SELECT * FROM abundance
ORDER BY abundance DESC
LIMIT 10;

-- 跳过前5条,取接下来的10条(分页查询)
SELECT * FROM abundance
ORDER BY abundance DESC
LIMIT 10 OFFSET 5;

3. GROUP BY / HAVING / 聚合函数 —— 分组统计

白话:GROUP BY 就是"按某列分组",然后对每组做统计(数个数、求平均、求最大值等)。相当于 pandas 的 groupby().agg()

五大聚合函数

函数白话生信例子
COUNT(*)数一数有多少行每组有多少个样本
SUM(列)求和每个样本的总 reads 数
AVG(列)求平均值每组的平均 BMI
MAX(列)求最大值某物种的最高丰度
MIN(列)求最小值某物种的最低丰度
-- 统计每组有多少个样本
SELECT group_name, COUNT(*) AS sample_count
FROM samples
GROUP BY group_name;
-- AS 是给结果列起个别名,不影响原表

-- 每组的平均年龄和平均BMI
SELECT group_name,
       AVG(age) AS avg_age,
       AVG(bmi) AS avg_bmi
FROM samples
GROUP BY group_name;

-- 每个样本中有多少个物种(genus 层级)
SELECT sample_id, COUNT(*) AS genus_count
FROM abundance
WHERE tax_level = 'genus'
GROUP BY sample_id;

-- 每个物种在所有样本中的平均丰度
SELECT taxonomy,
       AVG(abundance) AS mean_abundance,
       MAX(abundance) AS max_abundance,
       MIN(abundance) AS min_abundance
FROM abundance
WHERE tax_level = 'genus'
GROUP BY taxonomy
ORDER BY mean_abundance DESC;

-- ============================================================
-- HAVING:对分组结果再筛选
-- 白话:WHERE 是筛选"原始行",HAVING 是筛选"统计后的结果"
-- 记忆口诀:WHERE 先筛行,GROUP BY 再分组,HAVING 后筛组
-- ============================================================

-- 找出平均丰度 > 0.1 的物种(优势菌)
SELECT taxonomy, AVG(abundance) AS mean_abundance
FROM abundance
WHERE tax_level = 'genus'
GROUP BY taxonomy
HAVING AVG(abundance) > 0.1
ORDER BY mean_abundance DESC;

-- 找出在 >= 3 个样本中出现的物种(核心菌群)
SELECT taxonomy, COUNT(DISTINCT sample_id) AS sample_count
FROM abundance
WHERE tax_level = 'genus' AND abundance > 0
GROUP BY taxonomy
HAVING COUNT(DISTINCT sample_id) >= 3;

SQL 执行顺序(面试常考):

FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
  1       2        3          4        5         6         7
白话:先找到表 → 筛选行 → 分组 → 筛选组 → 选列 → 排序 → 取前N条


4. JOIN —— 多表关联查询

白话:JOIN 就是把两张表"拼"在一起。就像你有一张"样本信息表"和一张"物种丰度表",想把它们合并成一张大表来分析。相当于 pandas 的 pd.merge()

JOIN 类型图解

表A(samples)          表B(abundance)
┌──────────┐           ┌──────────┐
│ S001     │           │ S001     │
│ S002     │           │ S002     │
│ S003     │           │ S003     │
│ S004     │           │ S005     │  ← 只在B表中
└──────────┘           └──────────┘
     ↑ S004只在A表中

INNER JOIN(内连接):只取两边都有的 → S001, S002, S003
LEFT JOIN(左连接):A表全保留,B表没有的填NULL → S001, S002, S003, S004(NULL)
RIGHT JOIN(右连接):B表全保留,A表没有的填NULL → S001, S002, S003, S005(NULL)
FULL OUTER JOIN(全连接):两边都保留 → S001, S002, S003, S004(NULL), S005(NULL)

注意:SQLite 不支持 RIGHT JOIN 和 FULL OUTER JOIN,但 MySQL / PostgreSQL 支持。面试时知道概念就行。

-- ============================================================
-- INNER JOIN:只取两张表都有的样本
-- 白话:只合并两边都有对应数据的行
-- ============================================================
SELECT s.sample_id, s.group_name, s.age,
       a.taxonomy, a.abundance
FROM samples s
INNER JOIN abundance a ON s.sample_id = a.sample_id;
-- s 和 a 是表的别名(alias),写起来短
-- ON 后面写关联条件:两张表通过 sample_id 对应

-- ============================================================
-- LEFT JOIN:保留左表所有行
-- 白话:samples 表的所有样本都要显示,即使没有丰度数据也保留
-- ============================================================
SELECT s.sample_id, s.group_name,
       a.taxonomy, a.abundance
FROM samples s
LEFT JOIN abundance a ON s.sample_id = a.sample_id;
-- 如果某样本在 abundance 表中没有数据,taxonomy 和 abundance 显示 NULL

-- ============================================================
-- 实际生信例子:合并元数据和丰度,做分组比较
-- ============================================================

-- 比较 T2D 组和 Control 组的 Bacteroides 平均丰度
SELECT s.group_name,
       AVG(a.abundance) AS avg_bacteroides
FROM samples s
INNER JOIN abundance a ON s.sample_id = a.sample_id
WHERE a.taxonomy = 'Bacteroides'
GROUP BY s.group_name;

-- 找出每个样本的优势菌(丰度最高的物种)
SELECT s.sample_id, s.group_name,
       a.taxonomy, a.abundance
FROM samples s
INNER JOIN abundance a ON s.sample_id = a.sample_id
WHERE a.tax_level = 'genus'
  AND a.abundance = (
      SELECT MAX(a2.abundance)
      FROM abundance a2
      WHERE a2.sample_id = s.sample_id
        AND a2.tax_level = 'genus'
  );

5. UPDATE / DELETE —— 修改和删除数据

白话:UPDATE 是改数据,DELETE 是删数据。操作前一定要加 WHERE 条件,否则会改/删整张表!

-- ============================================================
-- UPDATE:修改数据
-- 白话:把某些行的某列值改掉
-- ============================================================

-- 修改某个样本的BMI值(发现录入错了)
UPDATE samples
SET bmi = 27.5
WHERE sample_id = 'S001';

-- 同时修改多列
UPDATE samples
SET age = 56, bmi = 28.0
WHERE sample_id = 'S001';

-- 批量修改:把所有没填性别的样本标记为 'Unknown'
UPDATE samples
SET gender = 'Unknown'
WHERE gender IS NULL;

-- ============================================================
-- DELETE:删除数据
-- 白话:删掉满足条件的行
-- ⚠️ 一定要加 WHERE!不加就删全表!
-- ============================================================

-- 删除某个样本的丰度数据
DELETE FROM abundance
WHERE sample_id = 'S004';

-- 删除丰度为0的记录(清理无效数据)
DELETE FROM abundance
WHERE abundance = 0;

-- ⚠️ 危险操作:不加 WHERE = 删除所有数据!
-- DELETE FROM abundance;  -- 千万别这么干!

6. 子查询(Subquery)

白话:子查询就是"查询里面嵌套查询"——先用内层查询查出一个中间结果,再让外层查询用这个结果。像俄罗斯套娃。

-- 找出丰度高于全体平均丰度的物种
SELECT taxonomy, abundance
FROM abundance
WHERE abundance > (
    SELECT AVG(abundance) FROM abundance
);
-- 内层:先算出全体平均丰度
-- 外层:再筛选出超过平均值的记录

-- 找出 T2D 组中有哪些样本
-- 然后查这些样本的丰度数据
SELECT * FROM abundance
WHERE sample_id IN (
    SELECT sample_id FROM samples
    WHERE group_name = 'T2D'
);

-- 找出每个物种在 T2D 和 Control 组的平均丰度差异
SELECT taxonomy,
       t2d_avg,
       ctrl_avg,
       (t2d_avg - ctrl_avg) AS diff
FROM (
    SELECT a.taxonomy,
           AVG(CASE WHEN s.group_name = 'T2D' THEN a.abundance END) AS t2d_avg,
           AVG(CASE WHEN s.group_name = 'Control' THEN a.abundance END) AS ctrl_avg
    FROM abundance a
    INNER JOIN samples s ON a.sample_id = s.sample_id
    WHERE a.tax_level = 'genus'
    GROUP BY a.taxonomy
) sub
ORDER BY ABS(diff) DESC;
-- CASE WHEN 是条件判断,相当于 Python 的 if-else
-- ABS() 是求绝对值

生信中的数据库应用

1. 存储样本元数据

在宏基因组研究中,每个样本都有很多元信息需要管理:

-- 完整的样本元数据表设计
CREATE TABLE sample_metadata (
    sample_id       TEXT PRIMARY KEY,
    project_id      TEXT NOT NULL,       -- 项目编号
    patient_id      TEXT NOT NULL,       -- 患者编号
    group_name      TEXT NOT NULL,       -- 疾病组/对照组
    age             INTEGER,
    gender          TEXT,
    bmi             REAL,
    hba1c           REAL,               -- 糖化血红蛋白(T2D关键指标)
    fasting_glucose REAL,               -- 空腹血糖
    medication      TEXT,               -- 用药情况
    sequencing_platform TEXT,           -- 测序平台(如 Illumina NovaSeq)
    read_length     INTEGER,            -- 测序读长
    total_reads     INTEGER,            -- 总reads数
    collect_date    TEXT,
    notes           TEXT                -- 备注
);

-- 实际应用:快速统计项目基本信息
SELECT group_name,
       COUNT(*) AS n,
       AVG(age) AS mean_age,
       AVG(bmi) AS mean_bmi,
       AVG(hba1c) AS mean_hba1c,
       AVG(total_reads) AS mean_reads
FROM sample_metadata
GROUP BY group_name;

2. 基因注释数据库查询

-- KEGG 通路注释表
CREATE TABLE kegg_annotation (
    gene_id     TEXT NOT NULL,
    ko_id       TEXT,                   -- KEGG Orthology 编号(如 K00001)
    pathway_id  TEXT,                   -- 通路编号(如 ko00010)
    pathway_name TEXT,                  -- 通路名称
    description TEXT                    -- 基因功能描述
);

-- 查询碳水化合物代谢相关的基因
SELECT gene_id, ko_id, pathway_name, description
FROM kegg_annotation
WHERE pathway_name LIKE '%Carbohydrate%'
ORDER BY ko_id;

-- 统计每条通路有多少个基因
SELECT pathway_id, pathway_name, COUNT(DISTINCT gene_id) AS gene_count
FROM kegg_annotation
WHERE pathway_id IS NOT NULL
GROUP BY pathway_id, pathway_name
ORDER BY gene_count DESC
LIMIT 20;

3. NCBI / Ensembl / UniProt 数据库结构概览

这些大型生物数据库底层都用关系型数据库存储:

数据库底层存储核心表你可能用到的场景
NCBI Taxonomy关系型nodes(节点层级), names(物种名)物种分类查询、构建分类树
EnsemblMySQLgene, transcript, exon, seq_region基因注释、转录本查询
UniProtOracleentry, feature, dbxref蛋白质功能注释、结构信息
GO (Gene Ontology)关系型term, graph_path, association基因GO功能富集分析
-- 模拟 NCBI Taxonomy 查询
-- 查找 Bacteroides 属下的所有物种
CREATE TABLE taxonomy (
    tax_id      INTEGER PRIMARY KEY,
    name        TEXT NOT NULL,
    rank        TEXT,                   -- kingdom/phylum/class/order/family/genus/species
    parent_id   INTEGER                -- 上一级分类的 tax_id
);

SELECT tax_id, name, rank
FROM taxonomy
WHERE parent_id = (SELECT tax_id FROM taxonomy WHERE name = 'Bacteroides' AND rank = 'genus')
  AND rank = 'species';

4. 用 SQL 思维分析物种丰度表

很多生信分析的逻辑,其实就是 SQL 的 GROUP BY + JOIN:

-- ============================================================
-- 场景1:找核心菌群(core microbiome)
-- 定义:在 >= 80% 的样本中都出现的属
-- ============================================================
SELECT taxonomy, 
       COUNT(DISTINCT sample_id) AS present_in,
       (SELECT COUNT(DISTINCT sample_id) FROM abundance) AS total_samples,
       ROUND(COUNT(DISTINCT sample_id) * 100.0 / 
             (SELECT COUNT(DISTINCT sample_id) FROM abundance), 1) AS prevalence_pct
FROM abundance
WHERE tax_level = 'genus' AND abundance > 0
GROUP BY taxonomy
HAVING ROUND(COUNT(DISTINCT sample_id) * 100.0 / 
             (SELECT COUNT(DISTINCT sample_id) FROM abundance), 1) >= 80
ORDER BY prevalence_pct DESC;

-- ============================================================
-- 场景2:Alpha多样性简易计算(物种丰富度 = Richness)
-- ============================================================
SELECT sample_id,
       COUNT(DISTINCT taxonomy) AS richness
FROM abundance
WHERE tax_level = 'genus' AND abundance > 0
GROUP BY sample_id
ORDER BY richness DESC;

-- ============================================================
-- 场景3:差异物种初筛
-- 比较T2D vs Control 每个物种的平均丰度
-- ============================================================
SELECT a.taxonomy,
       AVG(CASE WHEN s.group_name = 'T2D' THEN a.abundance END) AS t2d_mean,
       AVG(CASE WHEN s.group_name = 'Control' THEN a.abundance END) AS ctrl_mean,
       AVG(CASE WHEN s.group_name = 'T2D' THEN a.abundance END) -
       AVG(CASE WHEN s.group_name = 'Control' THEN a.abundance END) AS diff
FROM abundance a
JOIN samples s ON a.sample_id = s.sample_id
WHERE a.tax_level = 'genus'
GROUP BY a.taxonomy
ORDER BY ABS(diff) DESC
LIMIT 20;

Python + SQLite 实操

SQLite 是 Python 自带的轻量级数据库,不需要额外安装。以下代码可以直接运行。

1. 创建数据库和表

import sqlite3

# ============================================================
# 连接数据库(如果不存在会自动创建)
# ":memory:" 表示在内存中创建,程序结束就消失
# 也可以写文件路径如 "my_project.db"
# ============================================================
conn = sqlite3.connect("t2d_metagenome.db")

# 获取游标(cursor)—— 用来执行 SQL 语句的"执行器"
cur = conn.cursor()

# 创建样本元数据表
cur.execute("""
    CREATE TABLE IF NOT EXISTS samples (
        sample_id   TEXT PRIMARY KEY,
        patient_id  TEXT NOT NULL,
        group_name  TEXT NOT NULL,
        age         INTEGER,
        bmi         REAL,
        gender      TEXT
    )
""")
# IF NOT EXISTS:如果表已存在就跳过,不会报错

# 创建物种丰度表
cur.execute("""
    CREATE TABLE IF NOT EXISTS abundance (
        id          INTEGER PRIMARY KEY AUTOINCREMENT,
        sample_id   TEXT NOT NULL,
        taxonomy    TEXT NOT NULL,
        tax_level   TEXT NOT NULL,
        abundance   REAL NOT NULL,
        read_count  INTEGER
    )
""")

# 提交更改(重要!不提交的话修改不会保存)
conn.commit()
print("数据库和表创建成功!")

2. 插入数据

# ============================================================
# 插入样本数据
# 用 ? 占位符防止 SQL 注入攻击(安全写法)
# ============================================================

# 插入单条
cur.execute(
    "INSERT INTO samples VALUES (?, ?, ?, ?, ?, ?)",
    ("S001", "P001", "T2D", 55, 28.3, "M")
)

# 批量插入(推荐,效率高)
sample_data = [
    ("S002", "P002", "Control", 48, 22.1, "F"),
    ("S003", "P003", "T2D", 62, 30.5, "M"),
    ("S004", "P004", "Control", 45, 21.8, "F"),
    ("S005", "P005", "T2D", 58, 27.9, "M"),
    ("S006", "P006", "Control", 50, 23.5, "F"),
]
cur.executemany(
    "INSERT INTO samples VALUES (?, ?, ?, ?, ?, ?)",
    sample_data
)

# 插入丰度数据
abundance_data = [
    ("S001", "Bacteroides",       "genus", 0.25, 15000),
    ("S001", "Prevotella",        "genus", 0.08,  4800),
    ("S001", "Faecalibacterium",  "genus", 0.12,  7200),
    ("S001", "Roseburia",         "genus", 0.06,  3600),
    ("S002", "Bacteroides",       "genus", 0.30, 18000),
    ("S002", "Prevotella",        "genus", 0.15,  9000),
    ("S002", "Faecalibacterium",  "genus", 0.18, 10800),
    ("S002", "Roseburia",         "genus", 0.10,  6000),
    ("S003", "Bacteroides",       "genus", 0.20, 12000),
    ("S003", "Prevotella",        "genus", 0.05,  3000),
    ("S003", "Faecalibacterium",  "genus", 0.08,  4800),
    ("S004", "Bacteroides",       "genus", 0.28, 16800),
    ("S004", "Prevotella",        "genus", 0.18, 10800),
    ("S004", "Faecalibacterium",  "genus", 0.20, 12000),
    ("S005", "Bacteroides",       "genus", 0.22, 13200),
    ("S005", "Prevotella",        "genus", 0.06,  3600),
    ("S006", "Bacteroides",       "genus", 0.32, 19200),
    ("S006", "Prevotella",        "genus", 0.12,  7200),
]
cur.executemany(
    "INSERT INTO abundance (sample_id, taxonomy, tax_level, abundance, read_count) VALUES (?, ?, ?, ?, ?)",
    abundance_data
)

conn.commit()
print(f"插入了 {len(sample_data) + 1} 条样本数据")
print(f"插入了 {len(abundance_data)} 条丰度数据")

3. 查询数据

# ============================================================
# 基础查询
# ============================================================

# 查询所有样本
print("=== 所有样本 ===")
cur.execute("SELECT * FROM samples")
rows = cur.fetchall()  # 获取所有结果
for row in rows:
    print(row)
# 输出:('S001', 'P001', 'T2D', 55, 28.3, 'M') ...

# 只获取一条
cur.execute("SELECT * FROM samples WHERE sample_id = ?", ("S001",))
one_row = cur.fetchone()  # 获取第一条
print(f"\nS001样本: {one_row}")

# ============================================================
# 分组统计
# ============================================================
print("\n=== 分组统计 ===")
cur.execute("""
    SELECT group_name,
           COUNT(*) AS n,
           ROUND(AVG(age), 1) AS mean_age,
           ROUND(AVG(bmi), 1) AS mean_bmi
    FROM samples
    GROUP BY group_name
""")
for row in cur.fetchall():
    print(f"组: {row[0]}, 样本数: {row[1]}, 平均年龄: {row[2]}, 平均BMI: {row[3]}")

# ============================================================
# JOIN 查询:合并样本信息和丰度数据
# ============================================================
print("\n=== T2D vs Control 的 Bacteroides 丰度 ===")
cur.execute("""
    SELECT s.group_name,
           ROUND(AVG(a.abundance), 4) AS avg_abundance
    FROM samples s
    INNER JOIN abundance a ON s.sample_id = a.sample_id
    WHERE a.taxonomy = 'Bacteroides'
    GROUP BY s.group_name
""")
for row in cur.fetchall():
    print(f"{row[0]}: 平均丰度 = {row[1]}")

4. pandas 读取 SQL

import pandas as pd
import sqlite3

# ============================================================
# pandas 可以直接用 SQL 查询数据库,返回 DataFrame
# 这是生信最常用的方式:SQL 查 + pandas 分析
# ============================================================

conn = sqlite3.connect("t2d_metagenome.db")

# 方法一:读取整张表
df_samples = pd.read_sql("SELECT * FROM samples", conn)
print(df_samples)
print(f"形状: {df_samples.shape}")

# 方法二:用 SQL 做复杂查询,结果直接变 DataFrame
df_abundance = pd.read_sql("""
    SELECT s.sample_id, s.group_name, s.age, s.bmi,
           a.taxonomy, a.abundance
    FROM samples s
    INNER JOIN abundance a ON s.sample_id = a.sample_id
    WHERE a.tax_level = 'genus'
""", conn)

print(f"\n合并后的数据:\n{df_abundance.head(10)}")

# 方法三:拿到 DataFrame 后用 pandas 继续分析
# 例如:做透视表(样本×物种的丰度矩阵)
pivot = df_abundance.pivot_table(
    values="abundance",
    index="sample_id",
    columns="taxonomy",
    fill_value=0
)
print(f"\n丰度矩阵:\n{pivot}")

# 方法四:pandas DataFrame 写回数据库
# 把分析结果存回数据库
result_df = df_abundance.groupby(["group_name", "taxonomy"])["abundance"].mean().reset_index()
result_df.columns = ["group_name", "taxonomy", "mean_abundance"]
result_df.to_sql("group_mean_abundance", conn, if_exists="replace", index=False)
print("\n分组平均丰度已保存到数据库表 group_mean_abundance")

# 关闭连接
conn.close()

常用速查表

SQL 关键字速查

功能SQL语法pandas 等价
查所有列SELECT * FROM tabledf
查指定列SELECT col1, col2 FROM tabledf[["col1", "col2"]]
条件筛选WHERE col > 10df[df["col"] > 10]
模糊匹配WHERE col LIKE '%abc%'df[df["col"].str.contains("abc")]
排序ORDER BY col DESCdf.sort_values("col", ascending=False)
取前N行LIMIT 10df.head(10)
分组统计GROUP BY coldf.groupby("col")
计数COUNT(*).count()len()
求平均AVG(col).mean()
求和SUM(col).sum()
最大/最小MAX(col) / MIN(col).max() / .min()
去重SELECT DISTINCT coldf["col"].unique()
去重计数COUNT(DISTINCT col)df["col"].nunique()
表连接JOIN ... ON ...pd.merge(df1, df2, on=...)
分组后筛选HAVING.filter() 或手动条件
插入数据INSERT INTOdf.to_sql()
更新数据UPDATE ... SET ...df.loc[条件, "col"] = 值
删除行DELETE FROM ... WHERE ...df.drop()df[~条件]
新建表CREATE TABLE无直接等价
空值判断IS NULL / IS NOT NULL.isna() / .notna()

SQL 执行顺序(面试必背)

书写顺序:SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT
执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT

常见错误排查

错误原因修复
no such table: xxx表不存在检查表名拼写,先 CREATE TABLE
UNIQUE constraint failed主键重复检查插入的数据是否有重复ID
near "xxx": syntax errorSQL 语法错检查关键字拼写、引号匹配、逗号
no such column: xxx列名不存在检查列名拼写
database is locked其他进程占用了数据库关闭其他连接,或加 timeout

面试怎么答

Q1: SQL 的 WHERE 和 HAVING 有什么区别?

WHERE 和 HAVING 都是筛选条件,但作用阶段不同:

  • WHERE 在 GROUP BY 之前执行,筛选的是原始行,不能使用聚合函数
  • HAVING 在 GROUP BY 之后执行,筛选的是分组后的结果,可以使用聚合函数

举个生信例子:

-- WHERE:先筛选 genus 层级的数据
-- HAVING:再从分组结果中筛选平均丰度 > 0.1 的物种
SELECT taxonomy, AVG(abundance) AS mean_abd
FROM abundance
WHERE tax_level = 'genus'          -- WHERE:筛选原始行
GROUP BY taxonomy
HAVING AVG(abundance) > 0.1;      -- HAVING:筛选分组后的结果

执行顺序口诀:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT


Q2: INNER JOIN、LEFT JOIN、RIGHT JOIN 有什么区别?

三种 JOIN 的区别在于"保留哪边的数据":

JOIN 类型保留规则白话
INNER JOIN只保留两边都有匹配的行两张表都有的才要
LEFT JOIN左表全保留,右表没匹配的填 NULL以左表为主
RIGHT JOIN右表全保留,左表没匹配的填 NULL以右表为主

生信场景举例:我有一张样本元数据表(samples)和一张丰度表(abundance)。如果某些样本没有丰度数据(测序失败了),用 INNER JOIN 会丢掉这些样本,用 LEFT JOIN 会保留这些样本但丰度列显示 NULL。

实际工作中 LEFT JOIN 最常用,因为通常希望保留主表(样本表)的完整信息。


Q3: 如何优化一条慢的 SQL 查询?

常用的优化手段:

  1. 加索引(INDEX):对频繁用于 WHERE、JOIN、ORDER BY 的列建索引
    CREATE INDEX idx_sample_id ON abundance(sample_id);
    CREATE INDEX idx_taxonomy ON abundance(taxonomy);
    
  2. 避免 SELECT *:只查需要的列,减少数据传输量
  3. 用 WHERE 提前过滤:先筛选再 JOIN,减少参与运算的数据量
  4. 用 EXPLAIN 分析EXPLAIN QUERY PLAN SELECT ... 查看执行计划
  5. 避免在 WHERE 中对列做函数运算:比如 WHERE UPPER(name) = 'ABC' 会导致索引失效

在生信中,最常见的优化是对 sample_idtaxonomy 等高频查询字段建索引。


Q4: 什么是 SQL 注入?如何防止?

SQL 注入是一种安全攻击——恶意用户通过拼接 SQL 代码来破坏或窃取数据。

危险写法(字符串拼接):

# ❌ 不安全:用户输入可能包含恶意SQL
user_input = "'; DROP TABLE samples; --"
sql = f"SELECT * FROM samples WHERE sample_id = '{user_input}'"

安全写法(参数化查询):

# ✅ 安全:用 ? 占位符,数据库会自动转义
cur.execute("SELECT * FROM samples WHERE sample_id = ?", (user_input,))

面试点:生信虽然不做 Web 开发,但养成参数化查询的习惯很重要——防止数据污染和意外删除。


Q5: 你在项目中用过 SQL 吗?怎么用的?

参考答案(结合简历项目):

在该 T2D 肠道菌群宏基因组项目中,我用 Python 的 sqlite3 模块来管理实验数据:

  1. 存储样本元数据:将 120 个样本的临床信息(年龄、BMI、HbA1c、分组等)存入 SQLite 数据库,用 SQL 做快速统计和筛选
  2. 合并多源数据:用 JOIN 将样本元数据表、物种丰度表、功能注释表关联起来,一条语句就能得到"T2D 组中 Bacteroides 丰度 > 10% 的样本"
  3. 结合 pandas 分析:用 pd.read_sql() 把 SQL 查询结果直接转为 DataFrame,再做下游统计和可视化
  4. 批量处理注释数据:KEGG 通路注释有上万条记录,存在 CSV 里打开很慢,导入数据库后用 GROUP BY 快速统计每条通路的基因数量

Q6: 说一下 SQL 的执行顺序

SQL 的书写顺序和执行顺序是不同的:

书写:SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT
执行:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT

白话解释: 1. FROM:先确定要查哪张表 2. WHERE:从表中筛选出符合条件的行 3. GROUP BY:把筛选后的行分组 4. HAVING:从分组结果中再筛选 5. SELECT:选出要显示的列(这时候才处理别名 AS) 6. ORDER BY:对结果排序 7. LIMIT:取前N条

这就是为什么 WHERE 里不能用 SELECT 中的别名——因为 WHERE 执行的时候 SELECT 还没执行。


Q7: 数据库索引是什么?什么时候该建索引?

索引(INDEX)就像书的目录——没有目录你要翻遍整本书找内容,有了目录直接跳到对应页码。

-- 建索引
CREATE INDEX idx_taxonomy ON abundance(taxonomy);
-- 有了这个索引后,WHERE taxonomy = 'Bacteroides' 会快很多

该建索引的场景: - 频繁出现在 WHERE 条件中的列 - 用于 JOIN 的关联列(如 sample_id) - 用于 ORDER BY 排序的列

不该建索引的场景: - 数据量很小的表(几百行不需要索引) - 频繁 INSERT/UPDATE 的表(索引会拖慢写入速度) - 值种类很少的列(如 gender 只有 M/F 两种值,索引效果差)


延伸阅读

入门教程

资源说明链接
W3Schools SQL Tutorial最基础的 SQL 入门,有在线练习w3schools.com/sql
SQLite 官方文档SQLite 的权威参考sqlite.org/docs.html
Python sqlite3 模块文档Python 自带的 SQLite 接口文档docs.python.org/3/library/sqlite3.html
SQLBolt交互式 SQL 练习网站sqlbolt.com

生信相关

资源说明
Ensembl Public MySQLEnsembl 提供免费的 MySQL 公共服务器,可以直接 SQL 查询基因注释
UCSC Genome Browser MySQLUCSC 也提供公共 MySQL 查询接口
BioPython + SQLiteBioPython 的 BioSQL 模块支持将序列数据存入 SQL 数据库

进阶学习方向

方向说明适用场景
MySQL / PostgreSQL生产环境的关系型数据库多人协作项目、大型数据集
SQLAlchemy (Python ORM)用 Python 对象操作数据库,不用写原始 SQL大型 Python 项目
数据库设计范式(1NF/2NF/3NF)如何设计不冗余的表结构需要自己设计数据库时
窗口函数(WINDOW FUNCTION)SQL 高级分析功能复杂排名、移动平均等分析

学习建议:先把上面的 Python + SQLite 实操代码跑一遍,亲手操作过一次比看十遍文档都管用。面试时重点记住 JOIN 的区别、WHERE vs HAVING、执行顺序这三个高频考点。