后台查询接口影响响应时间最大的因素:
1.记录数据条数;
2.记录数据大小;
3.是否在索引上查找;
4.查询数据次数。
分析:
1.当数据库的一个表记录很多显然查询数据很慢。
2.当数据库的一个表记录不大,但是数据很大也可能很慢。
我们的一个用户表中一个building很大,当查询100条数据就会把服务器的内存搞爆掉。
当然查询时要查询筛选有用字段,不可以直接把记录的所有字段都查拆来。这样能减少内存消耗和提高查询速度。
3.在经常查询字段上建立索引。据说oracle上用索查询和不用索引查询在超多记录的情况下相差1000倍。
4.若出现嵌套查询显然会大大增加相应查询时间。你想想一个查询按30毫秒计算,查询1000次要多少时间。要先预处理用管道操作把能合并的查询合并到一个查询中,然后生成map,然后再处理。这是标准的用空间换时间的方案。
当然当出现了大字段的情况,把嵌套查询转换成管道查询反而会出现内存爆掉导致无法实现的问题。这种情况通常出现在后台报表查询接口中。最佳的解决方案是部分采用管道,部分采用嵌套查询。这样既兼顾了嵌套查询的内存利用率低和减少查询次数的目的。若这样优化还是出现查询超时的问题,可以把get请求换成post请求。因为get请求都有默认超时时间60秒,而post通常没有默认超时时间,除非你专门设置。
第一个例子是采用嵌套查询导致响应时间增加的部分代码:
let results=await WpUModel.find({
user_type: '经纪人',
user_name: new RegExp(searchName)
},'-_id id user_name user_type').lean().exec();
let res=[]
for (const item of results) {
let checkDetails = await WpUCModel.find({
user_id:item.id
},'-_id user_id create_time check_status check_details').sort({
create_time:1}).lean().exec();
// 同一个人可能有多次审核
for (const checkDetail of checkDetails) {
let {
create_time,check_status,check_details}=checkDetail;
let refer={
'PASS':'通过',
"REFUSE":'未通过',
"WAIT":'审核中'
}
check_status= refer[check_status];
let userID=JSON.parse(check_details).userID;
let introducer;
if(userID){
introducer=await WpUModel.findOne({
id: userID,
},'-_id id user_name user_type').lean().exec();
}
res.push({
...item,
create_time,
check_status,
introducer,
})
}
}
显然签到查询用到的内存要小,代码简单,写的快。但是缺点很显然:查询数据库次数很多,影响响应时间,随着记录数的增加影响越大。
第二个例子,下面是使用管道预先处理优化后的代码:
let results = await WpUModel.find(
{
user_type: '经纪人',
user_name: new RegExp(searchName),
},
'-_id id user_name user_type employee_num'
).lean().exec();
let idList = results.map(x2 => x2.id);
let res = [];
let checkDetailsList = await WpUCModel.aggregate([
// 第一阶段:匹配 user_id 在 idList 中的文档
{
$match: {
user_id: {
$in: idList } } },
// 第二阶段:按 user_id 分组,并将每个分组的内容放入一个数组中
{
$group: {
_id: '$user_id', // 按 user_id 分组
checkDetails: {
$push: {
user_id: '$user_id',
create_time: '$create_time',
check_status: '$check_status',
check_details: '$check_details'
}
}
}
},
// 第三阶段:重新投影,只包含需要的字段,并重命名 _id 为 user_id
{
$project: {
_id: 0, // 排除 _id 字段
user_id: '$_id', // 重命名 _id 为 user_id
checkDetails: 1 // 包含 details 字段
}
}
]);
// logger.debug('checkDetailsList:', checkDetailsList);
const filteredDataMap = new Map(checkDetailsList.map(item => [item.user_id, item.checkDetails]));
let subIdList = [];
checkDetailsList.forEach(item =>{
if(apiCommonToolUtil.checkNotEmptArray(item.checkDetails)){
item.checkDetails.forEach(x =>{
if(x && x.check_details){
let y = JSON.parse(x.check_details);
if(y && y.userID){
subIdList.push(y.userID);
}
}
});
}
});
let subUserList = await WpUModel.find(
{
id: {
$in:subIdList}
},
'-_id id user_name user_type'
).lean().exec();
const subUserMap = new Map(subUserList.map(item => [item.id, item]));
// logger.debug('subUserMap:', subUserMap);
for (const item of results) {
let checkDetails = filteredDataMap.get(item.id);
if(apiCommonToolUtil.checkNotEmptArray(checkDetails)){
checkDetails.forEach(x =>{
if(x){
let create_time = x.create_time, check_status = x.check_status, check_details = x.check_details;
// logger.debug('create_time:', create_time, 'check_status:', check_status, 'check_details:', check_details);
let refer = {
PASS: '通过',
REFUSE: '未通过',
WAIT: '审核中',
};
check_status = refer[check_status];
let userID = JSON.parse(check_details).userID;
let introducer;
if(userID){
introducer = subUserMap.get(userID);
}
// logger.debug('x:', x, 'userID:', userID, 'introducer:', introducer);
res.push({
...item,
create_time,
check_status,
introducer,
});
}
});
}
}
可以看到只进行三次数据查询就实现了第一个嵌套查询几十上百的查询。当然它也是有代价的,就是原来只需要查询一个记录现在需要一次性把成百上千的记录查询出来,这就需要用到更大的内存。代码复杂度也比第一个方案复杂的多,写代码和调试代码的时间也比第一方案要费时间。当然使用者只对响应时间敏感,开发者投入的时间是固定的一次性,使用者使用需要的时间是重复的。所以让开发者投入适当多的时间改善响应时间是值得,有利于提高效率和用户体验。有的公司只追求开发效率不讲用户体验,很容易引导开发者大量采用第一方案来提高开发效率,一旦养成了代码风气。开发的软件用户体验和竞争力可想而知。放眼都是嵌套查询和查询所有字段的代码,新来的显然也会抄来抄去。大部分开发人员在做代码搬运工,为了快速完成任务和提高每天的所谓效率,只会害了一个项目。只要你全部精力投入开发,没有无缘无故的慢也没有无缘无故的快。开发效率和软件质量同样重要,人不可能不吃不喝一直写代码,若一天投入开发的时间过多,反而早晨开发效率的下降。开发低质量的软件比开发得慢危害更大。
第三个例子:下面是一个请求耗时60秒以上的查询操作的部分代码:
let allData = {
range: '全部'};
let newRealEstateData = {
range: '新房'};
let secRealEstateData = {
range: '二手房'};
// 将结果转换成 Map 对象
let realEstateMap = new Map(allRealEstate.map(item => [item.id, item.type]));
// vr映射
let vrMap = {
};
let hpMap = {
};
let vrs = await WpVModel.find({
}, '-_id type estate_id estates').lean().exec();
let hpNewCount = 0, hpOldCount = 0;
let vrNewHouseCount = 0, vrOldHouseCount = 0;
vrs.forEach(vr => {
if (vr.type === '航拍') {
hpMap[vr.estate_id] = 1;
if (vr.estates) {
vr.estates.forEach(x => {
hpMap[x] = 1;
});
}
if(vr.estate_id && realEstateMap){
let type = realEstateMap.get(vr.estate_id);
if(type){
if(type === '新房'){
hpNewCount = hpNewCount + 1;
}else{
hpOldCount = hpOldCount + 1;
}
}
}
} else if (vr.type === '样板房') {
if(vr.estate_id && realEstateMap){
let type = realEstateMap.get(vr.estate_id);
if(type){
if(type === '新房'){
vrNewHouseCount = vrNewHouseCount + 1;
}else{
vrOldHouseCount = vrOldHouseCount + 1;
}
}
}
vrMap[vr.estate_id] = 1;
}
});
vrs = null;
for(let item of allRealEstate) {
// 样板房
if(vrMap[item.id]) {
allData['样板房vr'] = (allData['样板房vr'] || 0) + 1;
if(item.type === '新房') {
newRealEstateData['样板房vr'] = (newRealEstateData['样板房vr'] || 0) + 1;
} else {
secRealEstateData['样板房vr'] = (secRealEstateData['样板房vr'] || 0) + 1;
}
}
// 航拍
if(hpMap[item.id]) {
allData['航拍'] = (allData['航拍'] || 0) + 1;
if(item.type === '新房') {
newRealEstateData['航拍'] = (newRealEstateData['航拍'] || 0) + 1;
} else {
secRealEstateData['航拍'] = (secRealEstateData['航拍'] || 0) + 1;
}
}
let {
building, apartment} = await WpREModel.findOne({
id: item.id
}, '-_id building apartment').lean().exec();
let kmap = {
'hasSun': '日照',
'hasScenery': '景观',
'hasNoise': '噪声',
'hasPrice': '一房一价',
'east': '楼栋',
};
if(building) {
let buildingArr = JSON.parse(building);
let fmap = {
};
buildingArr.forEach(value => {
for (let key in kmap) {
if (value[key] && !fmap[key]) {
fmap[key] = 1;
let mk = kmap[key];
allData[mk] = (allData[mk] || 0) + 1;
if(item.type === '新房') {
newRealEstateData[mk] = (newRealEstateData[mk] || 0) + 1;
} else {
secRealEstateData[mk] = (secRealEstateData[mk] || 0) + 1;
}
}
}
});
}
// 户型
if(apartment) {
let imgList = JSON.parse(apartment).imgList;
imgList.forEach(async x => {
if(x.canvasImage) {
allData['户型'] = (allData['户型'] || 0) + 1;
if(item.type === '新房') {
newRealEstateData['户型'] = (newRealEstateData['户型'] || 0) + 1;
} else {
secRealEstateData['户型'] = (secRealEstateData['户型'] || 0) + 1;
}
}else{
allData['户型(无评测)'] = (allData['户型(无评测)'] || 0) + 1;
if(item.type === '新房') {
newRealEstateData['户型(无评测)'] = (newRealEstateData['户型(无评测)'] || 0) + 1;
} else {
secRealEstateData['户型(无评测)'] = (secRealEstateData['户型(无评测)'] || 0) + 1;
}
}
});
}
}
分析:这行代码超级耗时:
let vrs = await WpVModel.find({
}, '-_id type estate_id estates').lean().exec();
它耗时特别大,因为记录量太大基本无法优化,查询出来几万条。
后面的嵌套查询耗时是可以优化的。只是由于building超级大,所以不能按照第二个例子那样全部用管道处理一次性查询出来,不然会内存爆掉。
第四个例子:下面是部分采用管道来减少查询数组库的次数来达到提高查询处理速度:
let allData = {
range: '全部'};
let newRealEstateData = {
range: '新房'};
let secRealEstateData = {
range: '二手房'};
// 将结果转换成 Map 对象
let realEstateMap = new Map(allRealEstate.map(item => [item.id, item.type]));
// vr映射
let vrMap = {
};
let hpMap = {
};
let vrs = await WpVRModel.find({
}, '-_id type estate_id estates').lean().exec();
let hpNewCount = 0, hpOldCount = 0;
let vrNewHouseCount = 0, vrOldHouseCount = 0;
vrs.forEach(vr => {
if (vr.type === '航拍') {
hpMap[vr.estate_id] = 1;
if (vr.estates) {
vr.estates.forEach(x => {
hpMap[x] = 1;
});
}
if(vr.estate_id && realEstateMap){
let type = realEstateMap.get(vr.estate_id);
if(type){
if(type === '新房'){
hpNewCount = hpNewCount + 1;
}else{
hpOldCount = hpOldCount + 1;
}
}
}
} else if (vr.type === '样板房') {
if(vr.estate_id && realEstateMap){
let type = realEstateMap.get(vr.estate_id);
if(type){
if(type === '新房'){
vrNewHouseCount = vrNewHouseCount + 1;
}else{
vrOldHouseCount = vrOldHouseCount + 1;
}
}
}
vrMap[vr.estate_id] = 1;
}
});
vrs = null;
let maxIndex = 0;
let length = allRealEstate.length;
for(let i = 0; i < length; ){
let list = [];
if(i + 30 < length){
maxIndex = i + 30;
list = allRealEstate.slice(i, maxIndex);
}else{
maxIndex = length;
list = allRealEstate.slice(i, maxIndex);
}
i = maxIndex;
// logger.debug('i:', i, 'list.length:', list.length);
let idList = list.map(x2 => x2.id);
let buildingApartmentList = await WpREModel.find({
id: {
$in: idList
}
}, '-_id id building apartment').lean().exec();
// 将结果转换成 Map 对象
let buildingApartmentMap = new Map(buildingApartmentList.map(item => [item.id, item]));
list.forEach(item => {
// let item = allRealEstate[i];
// 样板房
if(vrMap[item.id]) {
allData['样板房vr'] = (allData['样板房vr'] || 0) + 1;
if(item.type === '新房') {
newRealEstateData['样板房vr'] = (newRealEstateData['样板房vr'] || 0) + 1;
} else {
secRealEstateData['样板房vr'] = (secRealEstateData['样板房vr'] || 0) + 1;
}
}
// 航拍
if(hpMap[item.id]) {
allData['航拍'] = (allData['航拍'] || 0) + 1;
if(item.type === '新房') {
newRealEstateData['航拍'] = (newRealEstateData['航拍'] || 0) + 1;
} else {
secRealEstateData['航拍'] = (secRealEstateData['航拍'] || 0) + 1;
}
}
let buildingApartmentEntity = buildingApartmentMap.get(item.id);
let kmap = {
'hasSun': '日照',
'hasScenery': '景观',
'hasNoise': '噪声',
'hasPrice': '一房一价',
'east': '楼栋',
};
if(buildingApartmentEntity && buildingApartmentEntity.building) {
let buildingArr = JSON.parse(buildingApartmentEntity.building);
let fmap = {
};
buildingArr.forEach(value => {
for (let key in kmap) {
if (value[key] && !fmap[key]) {
fmap[key] = 1;
let mk = kmap[key];
allData[mk] = (allData[mk] || 0) + 1;
if(item.type === '新房') {
newRealEstateData[mk] = (newRealEstateData[mk] || 0) + 1;
} else {
secRealEstateData[mk] = (secRealEstateData[mk] || 0) + 1;
}
}
}
});
}
// 户型
if(buildingApartmentEntity && buildingApartmentEntity.apartment) {
let imgList = JSON.parse(buildingApartmentEntity.apartment).imgList;
imgList.forEach(async x => {
if(x.canvasImage) {
allData['户型'] = (allData['户型'] || 0) + 1;
if(item.type === '新房') {
newRealEstateData['户型'] = (newRealEstateData['户型'] || 0) + 1;
} else {
secRealEstateData['户型'] = (secRealEstateData['户型'] || 0) + 1;
}
}else{
allData['户型(无评测)'] = (allData['户型(无评测)'] || 0) + 1;
if(item.type === '新房') {
newRealEstateData['户型(无评测)'] = (newRealEstateData['户型(无评测)'] || 0) + 1;
} else {
secRealEstateData['户型(无评测)'] = (secRealEstateData['户型(无评测)'] || 0) + 1;
}
}
});
}
});
}
可以看到原来7826次的查询只需要261次查询做到了。就是每次只查30个记录,来避免内存爆掉,同时大大减少了查询次数,来减少查询响应时间。