数据分析并不总是需要搭建一套完整的数据仓库或商业智能平台。很多中小型应用里,核心数据就存放在MySQL中,只要能连上数据库、写好聚合查询,再配合JavaScript把结果展示出来,就能快速得到一个可用的分析功能。本文选用Node.js作为中间层,使用mysql2驱动连接MySQL,通过Express提供JSON接口,前端利用Chart.js绘制图表,实现销售额统计、客单价分析和用户复购率等基础指标。

一、准备MySQL数据与Node.js环境
先创建一张订单表orders作为分析对象。实际业务里订单表通常包含用户标识、消费金额和下单时间,这里只保留这三个关键字段,方便聚焦分析流程。金额字段使用DECIMAL(10,2)而不是FLOAT,是为了避免浮点数在金额计算中出现精度误差。建表语句如下:
CREATE DATABASE IF NOT EXISTS sales_db DEFAULT CHARACTER SET utf8mb4; USE sales_db; CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); INSERT INTO orders (user_id, amount, created_at) VALUES (1, 129.90, '2024-01-05 10:12:00'), (2, 59.00, '2024-01-06 14:30:00'), (1, 240.00, '2024-01-12 09:20:00'), (3, 89.90, '2024-02-03 16:45:00'), (2, 199.00, '2024-02-14 11:00:00'), (1, 310.00, '2024-03-01 10:00:00');
Node.js侧推荐使用mysql2包的Promise版本,它支持连接池和预处理语句。先初始化项目并安装依赖:npm install mysql2 express。连接数据库时使用createPool比createConnection更适合Web服务,因为连接池可以复用连接,避免每次请求都建立新的TCP连接。下面的代码创建了一个包含10个连接上限的连接池:
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'sales_db',
waitForConnections: true,
connectionLimit: 10
});
连接池创建后不会立即发起连接,而是在第一次执行查询时按需建立。这样做的好处是应用启动更快,也方便在数据库尚未就绪时继续启动Web服务。后续的数据库操作都通过pool.query()或pool.execute()完成,其中execute适合带参数的预处理语句,后面会用到。
二、使用SQL聚合函数完成核心统计
数据分析的核心是聚合。MySQL提供COUNT、SUM、AVG、MIN、MAX等聚合函数,配合GROUP BY可以按维度汇总数据。以按月统计销售额为例,使用DATE_FORMAT把下单时间格式化为月份字符串,再按该字段分组,就能得到每个月的订单数和销售总额。
SELECT DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS order_count,
SUM(amount) AS total_sales,
AVG(amount) AS avg_order_value
FROM orders
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY month;
这个查询返回的结果已经可以直接用于展示,但如果你想做进一步计算,比如计算环比增长率,可以把结果交给JavaScript处理。下面的addGrowthRate函数接收SQL查询返回的数组,为每一行计算相比上一个月的销售额增长百分比:
function addGrowthRate(rows) {
return rows.map((row, index) => {
const previous = rows[index - 1];
const growth = previous
? ((row.total_sales - previous.total_sales) / previous.total_sales) * 100
: null;
return { ...row, growth_rate: growth ? growth.toFixed(2) + '%' : '-' };
});
}
复购率是另一个常见指标,指的是下过两单及以上的用户占比。可以先按用户分组统计订单数,再在外层查询中计算符合条件的用户数量。SQL如下:
SELECT COUNT(DISTINCT user_id) AS total_users,
COUNT(DISTINCT CASE WHEN order_count > 1 THEN user_id END) AS repeat_users
FROM (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
) AS user_orders;
在Node.js中执行上述查询时,要注意不要把用户输入直接拼接进SQL字符串。如果分析功能允许传入日期范围,应该使用?占位符和参数数组。Node.js的mysql2驱动会自动转义参数,防止SQL注入。下面是一个安全的区间统计方法:
async function getStatsByRange(startDate, endDate) {
const sql = `
SELECT DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS order_count,
SUM(amount) AS total_sales
FROM orders
WHERE created_at BETWEEN ? AND ?
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY month;
`;
const [rows] = await pool.execute(sql, [startDate, endDate]);
return rows;
}
三、通过REST接口与前端图表展示数据
有了查询函数,下一步是把它暴露为HTTP接口。用Express创建一个简单的服务,定义/api/stats/monthly路由,调用前面写好的统计函数并返回JSON。前端通过fetch获取数据后,可以交给Chart.js绘制柱状图。先来看服务端代码:
const express = require('express');
const app = express();
const port = 3000;
app.get('/api/stats/monthly', async (req, res) => {
try {
const rows = await getMonthlyStats();
const data = addGrowthRate(rows);
res.json(data);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
接口返回的JSON结构是一个数组,每项包含month、order_count、total_sales、avg_order_value和growth_rate。前端拿到这个数组后,用map提取标签和数据列即可。下面是完整的网页代码,使用Chart.js CDN绘制柱状图。为了演示,页面直接请求本机接口:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>销售数据分析</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<canvas id="salesChart" width="800" height="400"></canvas>
<script>
async function loadData() {
const response = await fetch('/api/stats/monthly');
const data = await response.json();
const labels = data.map(item => item.month);
const sales = data.map(item => item.total_sales);
const ctx = document.getElementById('salesChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '月销售额',
data: sales,
backgroundColor: 'rgba(54, 162, 235, 0.6)'
}]
},
options: {
scales: {
y: { beginAtZero: true }
}
}
});
}
loadData();
</script>
</body>
</html>
如果不想依赖外部CDN,完全可以用原生JavaScript生成HTML表格,把统计结果逐行渲染出来。表格方式对移动端不太友好,但胜在零依赖。对于简单报表,可以直接构造<table>元素并追加到<div>容器中。这里不使用document.write,避免覆盖页面内容。
四、优化查询性能与常见避坑点
随着订单数据量增加,聚合查询会扫描整张表,速度会明显下降。为created_at和user_id字段创建索引可以有效提升过滤和分组效率。尤其是WHERE created_at BETWEEN ? AND ?这类范围查询,索引能减少大量磁盘I/O。创建索引的SQL如下:
CREATE INDEX idx_orders_created_at ON orders(created_at); CREATE INDEX idx_orders_user_id ON orders(user_id);
除了索引,缓存也是分析模块常用的优化手段。如果统计结果不要求实时更新,可以设置一个定时任务,每隔几分钟执行一次聚合查询并把结果写入内存或Redis,接口直接读取缓存。这样数据库的负载会大幅降低。需要实时数据时再回源查询,但要控制查询频率,避免前端频繁刷新导致数据库压力过大。
金额精度是另一个容易忽略的坑。MySQL的DECIMAL类型在JavaScript中会被转换为字符串,如果直接参与数值计算,需要先用Number或parseFloat转换。涉及金额统计时,更推荐把金额乘以100转成整数,以“分”为单位进行运算和传输,展示时再除以100。这样可以避开二进制浮点数无法精确表示十进制小数的问题。
另外,生产环境要统一处理异步错误。Express 4不会自动捕获async路由中的异常,所以每个异步处理器都要用try...catch包裹,或者引入一个错误处理中间件。日志中记录SQL执行耗时和返回行数,方便后续定位慢查询。连接池的connectionLimit不宜设置过大,一般与数据库最大连接数和应用实例数匹配,避免连接数耗尽。
MySQLJavaScript数据分析修改时间:2026-09-30 11:35:41