读取是数据库操作里最频繁的动作
一个业务系统里,写入操作占比通常不高,大量请求都发生在读上。列表页、详情页、搜索结果、统计报表,背后都是一条条SELECT语句。SELECT写得好不好,直接影响接口响应速度和数据库负载。
本课程围绕Node.js+mysql2执行MySQLSELECT查询展开,把常见的查询形态、参数化写法和结果处理讲清楚。
SELECT在做什么
SELECT用于从一张或多张表中检索数据。它可以返回全部列,也可以只取需要的列;可以返回所有行,也可以用WHERE过滤出满足条件的行。Node.js把SELECT发给MySQL,MySQL返回匹配的行,mysql2把这些行整理成一个JavaScript数组,数组里每个元素是一个对象,对象的键就是列名。
环境准备
动手前确认:
-
系统已安装Node.js
-
MySQLServer已安装并处于运行状态
-
项目里已安装mysql2包
-
目标数据库和Employees表已存在并有数据
安装mysql2:
npm install mysql2
示例统一使用下面这张Employees表:
| employeeId | employeeName | department | salary |
|---|---|---|---|
| 1 | JohnSmith | HR | 45000 |
| 2 | EmmaDavis | IT | 65000 |
| 3 | MichaelBrown | Finance | 55000 |
| 4 | SophiaWilson | IT | 70000 |
| 5 | JamesTaylor | Sales | 50000 |
后续示例默认已经创建好MySQL连接,不再重复连接代码。
SELECT的基本语法
SELECT column_name(s) FROM table_name;
column_name(s)指定要读取的列,用*表示全部列;table_name指定从哪张表读。
查询全部记录
const sql = "SELECT * FROM Employees";
connection.query(sql, (err, results) => {
if (err) {
console.log(err.message);
return;
}
console.log(results);
});
返回结果是一个数组:
[
{ employeeId: 1, employeeName: 'John Smith', department: 'HR', salary: 45000 },
{ employeeId: 2, employeeName: 'Emma Davis', department: 'IT', salary: 65000 },
{ employeeId: 3, employeeName: 'Michael Brown', department: 'Finance', salary: 55000 },
{ employeeId: 4, employeeName: 'Sophia Wilson', department: 'IT', salary: 70000 },
{ employeeId: 5, employeeName: 'James Taylor', department: 'Sales', salary: 50000 }
]
SELECT*写起来省事,但项目里不太建议在业务查询里长期使用。原因有两个:一是表结构变化时,返回字段会跟着变,前端如果依赖字段顺序容易出问题;二是宽表里很多列用不上,全查出来会增加网络传输和内存开销。列表接口按需取列更稳妥。
只查需要的列
const sql = "SELECT employeeName, department, salary FROM Employees";
connection.query(sql, (err, results) => {
if (err) throw err;
console.log(results);
});
返回的对象里就只有这三列。字段少了,序列化成本也低,接口响应体积更小。
用WHERE过滤记录
const sql = "SELECT * FROM Employees WHERE department = 'IT'";
connection.query(sql, (err, results) => {
if (err) throw err;
console.log(results);
});
返回IT部门的员工:
[
{ employeeId: 2, employeeName: 'Emma Davis', department: 'IT', salary: 65000 },
{ employeeId: 4, employeeName: 'Sophia Wilson', department: 'IT', salary: 70000 }
]
WHERE后面跟的是过滤条件,只有条件为真的行才会出现在结果里。
用参数化查询处理外部传入的值
把变量直接拼进SQL字符串是常见的安全隐患。假设部门名来自前端:
const sql = `SELECT * FROM Employees WHERE department = '${department}'`;
如果department被构造成IT'OR'1'='1,条件就恒真,整张表的数据都会被读出来。正确做法是用?占位符:
const department = "IT";
const sql = "SELECT * FROM Employees WHERE department = ?";
connection.query(sql, [department], (err, results) => {
if (err) throw err;
console.log(results);
});
mysql2会把占位符替换成经过转义的值,字符串自动加引号,特殊字符被处理,注入串失去作用。这是mysql2在参数化查询上最实际的价值。
用ORDERBY排序
升序:
const sql = "SELECT * FROM Employees ORDER BY salary ASC";
connection.query(sql, (err, results) => {
if (err) throw err;
console.log(results);
});
结果按薪资从低到高:
John Smith - 45000
James Taylor - 50000
Michael Brown - 55000
Emma Davis - 65000
Sophia Wilson - 70000
降序:
const sql = "SELECT * FROM Employees ORDER BY salary DESC";
结果按薪资从高到低:
Sophia Wilson - 70000
Emma Davis - 65000
Michael Brown - 55000
James Taylor - 50000
John Smith - 45000
ORDERBY后面可以跟多个字段,比如先按department升序,同一部门内再按salary降序:
SELECT * FROM Employees ORDER BY department ASC, salary DESC
需要注意,排序字段如果没有索引,数据量大时MySQL会走filesort,性能开销明显。列表接口常用的排序字段建议加索引。
用LIMIT限制返回行数
const sql = "SELECT * FROM Employees LIMIT 3";
connection.query(sql, (err, results) => {
if (err) throw err;
console.log(results);
});
只返回前三行。LIMIT最常见的用途是分页,配合OFFSET使用:
SELECT * FROM Employees LIMIT 10 OFFSET 20
表示跳过前20行,取接下来的10行。深分页时OFFSET越大,MySQL扫描的行越多,性能越差。数据量大的列表建议用游标分页,比如WHEREemployeeId>上一页之后一个idLIMIT10。
用COUNT统计记录数
const sql = "SELECT COUNT(*) AS totalEmployees FROM Employees";
connection.query(sql, (err, results) => {
if (err) throw err;
console.log(results);
});
返回:
[ { totalEmployees: 5 } ]
AS给统计列起了别名,结果对象的键就是别名。分页场景里经常要同时拿列表和总数,一般拆成两条查询:一条COUNT拿总数,一条带LIMIT拿当前页数据。COUNT本身在InnoDB里需要扫描索引,大表上频繁调用要留意。
用AND组合多个条件
const sql = `
SELECT *
FROM Employees
WHERE department = 'IT'
AND salary > 60000
`;
connection.query(sql, (err, results) => {
if (err) throw err;
console.log(results);
});
返回IT部门且薪资大于60000的员工:
[
{ employeeId: 2, employeeName: 'Emma Davis', department: 'IT', salary: 65000 },
{ employeeId: 4, employeeName: 'Sophia Wilson', department: 'IT', salary: 70000 }
]
AND要求两个条件同时成立。
用OR组合多个条件
const sql = `
SELECT *
FROM Employees
WHERE department = 'IT'
OR department = 'HR'
`;
connection.query(sql, (err, results) => {
if (err) throw err;
console.log(results);
});
返回IT或HR部门的员工:
[
{ employeeId: 1, employeeName: 'John Smith', department: 'HR', salary: 45000 },
{ employeeId: 2, employeeName: 'Emma Davis', department: 'IT', salary: 65000 },
{ employeeId: 4, employeeName: 'Sophia Wilson', department: 'IT', salary: 70000 }
]
OR只要有一个条件成立就返回。AND和OR混用时要注意优先级,AND的优先级高于OR,不确定时用括号明确分组:
WHERE (department = 'IT' OR department = 'HR') AND salary > 50000
上面这条如果不加括号,含义就变成“IT部门,或者HR且薪资大于50000”,跟预期不同。
处理空结果
查询不一定有匹配的行,比如查一个不存在的部门:
const sql = "SELECT * FROM Employees WHERE department = 'Marketing'";
connection.query(sql, (err, results) => {
if (err) throw err;
if (results.length === 0) {
console.log("没有找到匹配的记录");
return;
}
console.log(results);
});
SELECT查不到数据不会报错,返回的是空数组[]。业务代码里要显式判断results.length,否则前端拿到空列表可能显示异常。详情页场景更要区分“没查到”和“查询失败”这两种情况。
查询错误的处理
表名写错、字段不存在、语法有误,都会通过err返回:
const sql = "SELECT * FROM EmployeeList";
connection.query(sql, (err, results) => {
if (err) {
console.log("错误:", err.message);
return;
}
console.log(results);
});
输出:
错误: Table 'companydb.EmployeeList' doesn't exist
实际表名是Employees,这里写成EmployeeList,MySQL直接返回表不存在的错误。回调风格里必须判断err并return,否则后面的代码会拿着undefined继续跑,报出更难定位的错误。
完整示例:条件查询+排序+分页
const mysql = require('mysql2');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'companydb'
});
const department = 'IT';
const minSalary = 60000;
const pageSize = 10;
const offset = 0;
const sql = `
SELECT employeeId, employeeName, department, salary
FROM Employees
WHERE department = ?
AND salary >= ?
ORDER BY salary DESC
LIMIT ? OFFSET ?
`;
connection.query(
sql,
[department, minSalary, pageSize, offset],
(err, results) => {
if (err) {
console.error("查询失败:", err.message);
return;
}
if (results.length === 0) {
console.log("没有符合条件的员工");
return;
}
console.log("查询结果:", results);
}
);
connection.end();
这段代码把参数化查询、条件过滤、排序、分页组合在一起,参数按SQL里?出现的顺序依次传入数组。LIMIT和OFFSET也用占位符传,避免手拼字符串。
现在写法:Promise与async/await
mysql2提供promise版本,配合async/await写起来更线性,错误处理可以用try/catch统一收口:
const mysql = require('mysql2/promise');
async function getEmployeesByDepartment(department) {
const connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'companydb'
});
try {
const sql = "SELECT employeeId, employeeName, salary FROM Employees WHERE department = ?";
const [rows] = await connection.execute(sql, [department]);
return rows;
} catch (err) {
console.error("查询失败:", err.message);
throw err;
} finally {
await connection.end();
}
}
connection.execute()走预处理语句路径,和query一样支持?占位符。解构赋值const[rows]拿到的是结果行数组,第二个元素是字段元信息,一般用不到。finally里关闭连接,保证异常时连接也能释放。
本节课程知识要点
-
SELECT返回的结果是数组,每个元素是一行记录,键为列名
-
业务查询尽量避免
SELECT*,按需取列减少传输和序列化开销 -
外部传入的值一律走
?占位符,不要拼接字符串 -
AND优先级高于OR,混用时用括号明确分组
-
查不到数据返回空数组,不报错,业务代码要显式判断length
-
排序字段和常用过滤字段建议加索引,避免filesort和全表扫描
-
深分页用游标方式替代大OFFSET
-
回调里必须判断err并return,promise版本用try/catch/finally
练习思考题
-
写一条SELECT语句,查询salary在50000到70000之间、department为IT或Finance的员工,按salary降序排列。
-
把上面的回调写法改写成mysql2/promise+async/await版本,要求查询出错时抛出异常,空结果时返回空数组。
-
分页接口需要同时返回当前页数据和总记录数,写出这两条SQL,并说明为什么不在一条SQL里完成。
-
WHEREdepartment='IT'ORdepartment='HR'ANDsalary>50000这条语句的实际过滤逻辑是什么?如何改写成“IT或HR部门且薪资大于50000”?