6. SELECT查询与结果集处理:基本查询、WHERE条件、ORDER BY排序、LIMIT分页、遍历结果集

数据库编程里,最核心的操作就是查询。说白了,你写再多代码,最终目的就是把数据拿出来用。SELECT 语句就是干这个的。我做了这么多年嵌入式数据库开发,可以说 80% 的调试时间都花在写查询上。今天咱们就把这块彻底讲透。

本章核心知识点:

  • 基本 SELECT 查询语法
  • WHERE 条件过滤
  • ORDER BY 排序
  • LIMIT 分页
  • 结果集遍历与处理
SELECT 查询与结果集处理知识体系 SELECT 查询 基本查询 WHERE 条件 ORDER BY 排序 LIMIT 分页 结果集遍历 SELECT * / 指定列 =, >, <, LIKE, IN ASC / DESC OFFSET 偏移 sqlite3_step / sqlite3_column

6.1 基本查询:从简单开始

最基本的 SELECT 查询,就是「给我看看这张表里有什么」。语法很简单:

SELECT 列名1, 列名2, ... FROM 表名;

如果你想看所有列,用星号 * 就行。我个人习惯在调试阶段先用 SELECT * 看看全貌,正式代码里再改成具体列名——这样性能更好,也更容易维护。

举个例子,假设我们有个 sensors 表:

-- 查询所有列
SELECT * FROM sensors;

-- 查询指定列
SELECT id, name, temperature FROM sensors;

我的经验:嵌入式设备内存有限,能用具体列名就别用 *。我在一个智能家居项目里,就因为用了 SELECT * 拉回了一堆不需要的 BLOB 数据,结果内存直接爆了。从那以后,我写 SELECT 都会明确指定列名。

6.2 WHERE 条件:精准筛选

光查全部数据不够用。你想想看,一张表里可能有几万条记录,你总得挑出你想要的。WHERE 子句就是干这个的。

SELECT 列名 FROM 表名 WHERE 条件表达式;

常用的条件运算符:

运算符 说明 示例
= 等于 WHERE id = 100
> / < 大于 / 小于 WHERE temperature > 30
>= / <= 大于等于 / 小于等于 WHERE humidity <= 60
!= 或 <> 不等于 WHERE status != 0
LIKE 模糊匹配 WHERE name LIKE 'temp%'
IN 在集合中 WHERE id IN (1,2,3)
AND / OR 逻辑组合 WHERE a > 10 AND b < 20

来看个实际例子:

-- 查询温度大于25度的传感器
SELECT id, name, temperature 
FROM sensors 
WHERE temperature > 25;

-- 多条件组合:温度大于25且湿度小于60
SELECT * FROM sensors 
WHERE temperature > 25 AND humidity < 60;

-- 模糊匹配:名字以"temp"开头的传感器
SELECT * FROM sensors 
WHERE name LIKE 'temp%';

注意:我曾经在一个项目中,用 WHERE name = 'temp_sensor' 死活查不到数据。后来发现是字符串末尾有个不可见字符。嵌入式设备的数据经常有这种「脏数据」,建议用 TRIM() 函数处理一下。

6.3 ORDER BY 排序:让数据有序

查出来的数据默认是按插入顺序排列的。但很多时候,你需要按某个字段排序。比如按温度从高到低排列。

SELECT 列名 FROM 表名 ORDER BY 列名 [ASC | DESC];
  • ASC:升序(默认,从小到大)
  • DESC:降序(从大到小)

看例子:

-- 按温度升序排列(默认)
SELECT id, name, temperature 
FROM sensors 
ORDER BY temperature;

-- 按温度降序排列
SELECT id, name, temperature 
FROM sensors 
ORDER BY temperature DESC;

-- 多字段排序:先按温度降序,温度相同再按湿度升序
SELECT * FROM sensors 
ORDER BY temperature DESC, humidity ASC;

我的习惯:多字段排序时,我会把最关键的排序字段放前面。比如查报警记录,先按严重级别降序,再按时间升序。这样最严重的报警永远在最前面。

6.4 LIMIT 分页:控制数据量

嵌入式设备的内存很金贵。一次查几万条数据回来,内存可能直接撑爆。LIMIT 就是用来限制返回行数的。

SELECT 列名 FROM 表名 LIMIT 行数 OFFSET 偏移量;

或者用简写形式:

SELECT 列名 FROM 表名 LIMIT 偏移量, 行数;

分页的核心公式:

-- 第N页,每页M条数据
SELECT * FROM sensors 
LIMIT M OFFSET (N-1) * M;

-- 比如第2页,每页10条
SELECT * FROM sensors 
LIMIT 10 OFFSET 10;

重要提醒:分页查询一定要配合 ORDER BY 使用!否则每次查出来的数据顺序可能不一样,分页就乱套了。我见过有人没加 ORDER BY,结果第一页和第二页的数据有重叠——排查了半天才发现是这个问题。

6.5 遍历结果集:C语言实战

好了,SQL 语句写好了,怎么在 C 代码里拿到数据?这就涉及到结果集遍历了。以 SQLite 为例,核心流程是三步:

  1. 准备语句sqlite3_prepare_v2()
  2. 逐行遍历sqlite3_step() 循环
  3. 读取列值sqlite3_column_xxx() 系列函数

来看完整代码:

#include <stdio.h>
#include <sqlite3.h>

void query_sensors(sqlite3 *db, int page, int page_size) {
    sqlite3_stmt *stmt;
    const char *sql;
    int rc;
    int offset = (page - 1) * page_size;
    
    // 1. 准备SQL语句
    sql = "SELECT id, name, temperature, humidity "
          "FROM sensors "
          "WHERE temperature > ? "
          "ORDER BY temperature DESC "
          "LIMIT ? OFFSET ?";
    
    rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    if (rc != SQLITE_OK) {
        printf("准备语句失败: %s\n", sqlite3_errmsg(db));
        return;
    }
    
    // 2. 绑定参数
    sqlite3_bind_double(stmt, 1, 25.0);   // 温度大于25度
    sqlite3_bind_int(stmt, 2, page_size); // 每页条数
    sqlite3_bind_int(stmt, 3, offset);    // 偏移量
    
    // 3. 遍历结果集
    printf("=== 第%d页 传感器数据 ===\n", page);
    printf("ID\t名称\t\t温度\t湿度\n");
    printf("----------------------------------------\n");
    
    while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
        int id = sqlite3_column_int(stmt, 0);
        const unsigned char *name = sqlite3_column_text(stmt, 1);
        double temp = sqlite3_column_double(stmt, 2);
        double hum = sqlite3_column_double(stmt, 3);
        
        printf("%d\t%s\t%.1f\t%.1f\n", id, name, temp, hum);
    }
    
    if (rc != SQLITE_DONE) {
        printf("遍历出错: %s\n", sqlite3_errmsg(db));
    }
    
    // 4. 清理
    sqlite3_finalize(stmt);
}

关键函数说明:

函数 作用
sqlite3_prepare_v2() 编译SQL语句,生成预处理语句
sqlite3_bind_xxx() 绑定参数到占位符 ?
sqlite3_step() 执行一步,返回 SQLITE_ROW 表示有数据
sqlite3_column_xxx() 读取当前行的列值(int, double, text 等)
sqlite3_finalize() 销毁预处理语句,释放资源

避坑指南:我曾经在遍历结果集时忘了调用 sqlite3_finalize(),结果内存泄漏了。嵌入式设备跑几天就崩了。记住:每个 sqlite3_prepare 都必须对应一个 sqlite3_finalize,就像 malloc 和 free 一样。

6.6 综合实战:带分页的传感器查询

把上面所有知识点串起来,写一个完整的查询函数:

void list_sensors_paginated(sqlite3 *db, int page) {
    sqlite3_stmt *stmt;
    int rc;
    int page_size = 5;
    int offset = (page - 1) * page_size;
    
    // 带参数的分页查询
    const char *sql = 
        "SELECT id, name, temperature, humidity, status "
        "FROM sensors "
        "WHERE status = 1 "
        "ORDER BY temperature DESC, id ASC "
        "LIMIT ? OFFSET ?";
    
    rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    if (rc != SQLITE_OK) {
        printf("SQL错误: %s\n", sqlite3_errmsg(db));
        return;
    }
    
    sqlite3_bind_int(stmt, 1, page_size);
    sqlite3_bind_int(stmt, 2, offset);
    
    printf("\n--- 传感器列表 (第%d页) ---\n", page);
    printf("ID\t名称\t\t温度\t湿度\t状态\n");
    printf("----------------------------------------\n");
    
    int count = 0;
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        int id = sqlite3_column_int(stmt, 0);
        const char *name = (const char *)sqlite3_column_text(stmt, 1);
        double temp = sqlite3_column_double(stmt, 2);
        double hum = sqlite3_column_double(stmt, 3);
        int status = sqlite3_column_int(stmt, 4);
        
        printf("%d\t%-12s\t%.1f\t%.1f\t%s\n", 
               id, name, temp, hum, 
               status ? "在线" : "离线");
        count++;
    }
    
    if (count == 0) {
        printf("没有更多数据了。\n");
    }
    
    sqlite3_finalize(stmt);
}

我的建议:实际项目中,我一般会把分页参数(页码、每页条数)封装成结构体,这样代码更清晰。另外,记得在遍历前先查一下总记录数,这样前端才知道总共有多少页。

好了,SELECT 查询这块的核心内容就这些。从基本查询到条件过滤,从排序到分页,再到 C 代码里的结果集遍历——每一步都是实战中会反复用到的。你写代码的时候,多想想「这个查询会不会返回太多数据?」「排序字段选对了没?」「参数绑定有没有漏掉?」。养成这些习惯,你的数据库编程水平会提升很快。


公众号:蓝海资料掘金营,微信deep3321