我们知道,在应用程序中与数据库进行交互是一个比较耗时的过程,首先应用程序需要与应用程序建立连接,然后将请求发送到数据库,数据库执行操作,然后将结果集返回。所以在程序中,要尽量晚的与数据库建立连接,并且较早的释放连接。

然而在很多时候,我们需要频繁的查询和更新数据库中的记录,比如我们的一张表中有1000条记录,假设有一个场景,需要一条一条的判断这1000条记录,如果不存在,插入;如果存在,更新某一个字段。这种场景很常见,比如银行的用户转账或者汇款,在完成之后需要更新账户余额等操作。

最近项目中也遇到了类似的情况,通过实践也简单总结了一些如何提高应用程序执行效率的方法,当然这些都是通过减少和数据库进行交互以及当数据达到一定程度,通过批量实现的。下面就简要介绍一下。

一 场景

最近在项目中要实现一个类似大众点评团购这种筛选的功能,用户可以根据系统提供的城市,商区,美食类别列表来进行筛选,选好之后,下面就列出所有的满足条件的美食。这里有一点需要注意,系统要提供热门商区,和热门菜系,并且,当各种条件选择之后,在条件列表的后面需要显示符合该条件的美食个数,如下图中的数字。这些条件之间是联动的,当其他条件变化时,字数也会跟着变化,比如,当选择甜品饮料时,在徐汇区的衡山路商区中满足条件的甜品有12个,当切换到自助餐的时候,个数变为了2,并且如果该条件下徐汇区中某一商区没有自助餐,则该商区都不要显示。

二 解决方法

其实,这些筛选条件,可以通过一个服务动态下发,当用户打开网页时,系统会请求所有的筛选项。 可以为该筛选项建立一张表,并在后台维护一个服务以一定的时间间隔更新这张表。 这里,我们以城市-商区-美食分类为例,来说明如何实现该功能。

我们可以建立一张表名位QueryFilter表, 该表字段为包含 城市ID,商区ID,美食分类ID,美食个数这几个字段,该表大体结构如下:

该表中仅存有ID,如果要现实对应的名称,可以关联对应的城市表,商区表,美食分类表,等等。

表的含义为:

比如对于第一条记录,城市为上海(CityID=2),所有商区(DistrictID=0),美食类别为川菜(CuisineID=4),的满足条件的美食个数为4(ProductCount=4)

比如对于第五条记录,城市为上海(CityID=2),商区为衡山路(DistrictID=71),美食类别为甜品(CuisineID=7),的满足条件的美食个数为3(ProductCount=3)

根据CityID,DistrictID,和CusineID来统计ProductCount 的语句和页面上用户选中这些条件后页面详情列表展示的接口其实是同一个接口,我们这里只需要统计美食个数。

后台需要有一个服务,以一定的时间间隔,遍历所有城市,所有商区,所有美食分类,然后更新这样一张表。

三 优化

很自然,最先想到的可能是如下方法,这里仅列出思路:

  1. 查找出所有的CityID,所有的DistirctID,所有的CuisineID,这些可以从相应的表中查出来。
  2. 然后,对于一个CityID,DistrictID,CuisineID的组合,调用接口,到数据库中所有美食表中去统计该组合条件下美食的个数。
  3. 然后检查QueryFilter中,是否存在CityID,DistrictID,CuisineID这样一条记录。
  • 如果不存在,插入CityID,DistrictID,CuisineID,以及对应的ProductCount
  • 不过存在,更新ProductCount

代码如下:

List<CityModel> allCitys;
List<BusinessDistrictModel> allDistricts;
List<CuisineModel> allCuisines;

allCitys = CityRepository.GetAllCitys();
allDistricts = BusinessDistrictRepository.GetAllBusinessDistricts();
allCuisines = CuisineRepository.GetAllCuisines();

foreach (CityModel city in allCitys)
{
    foreach (CuisineModel cuisine in allCuisines)
    {
        foreach (BusinessDistrictModel district in allDistricts)
        {
            //查找 城市-商区-美食类表- 的所有美食个数
            int productCount = QueryFilterRepository.GetProductCountProduct(city, cuisine, district);
            //查找该记录是否已经存在
            QueryFilterModel queryFilters = QueryFilterRepository.Get(city, cuisine, district);

            if (queryFilters == null)
            {
                //不存在,插入该条记录
                QueryFilterRepository.Insert(city, cuisine, district, productCount);
            }
            else
            {
                //存在在,更新该记录个数
                QueryFilterRepository.Update(city, cuisine, district, productCount);
            }
        }
    }
}

可以看到,在这段代码中有一个城市-商区-美食类别的三重循环,在最内层的循环体内,有三条需要与数据库进行交互的语句, 一条是统计美食个数,一个是获取是否存在该记录,一个是插入或者更新操作。假设如果有10个城市,总共有50个商区,有30个美食类别,那么与数据库进行交互个次数为10*50*30*3=45000次,很显然,如果数据量比较大,每一次查询比较耗时的时候,这种效率是没办法忍受的,而且数据库的负担也很大。在我自己的机器上,一次这样遍历下来,花了大概半个小时。即使这是一个运行在服务器上的后台的服务也是没有办法忍受。

优化一 : 减少与数据库的交互

可以看到,上面的问题在于,与数据库的交互次数过多,一般的在循环操作中去与数据库交互大多是会存在性能问题。所以,为了减少与数据库的交互,我们一次性把所有的满足条件的美食及该美食所在的城市ID,商区ID,美食类别ID,从数据库中查询出来放到集合List<FoodsModel>中,然后把QueryFilter表中的所有记录一次性查出来,存在List<QueryFilterModel>中。然后我们再创建一个名为readyToInsert的List< QueryFilterModel >集合以及一个readyToUpdate的List< QueryFilterModel >集合来保存我们所有需要插入或者更新的记录。优化后的代码逻辑如下:

  1. 查找出来所有的美食及其对应的CityID,DistrictID,CuisineID,放在AllFoodList集合中。
  2. 查询QueryFilter表中的所有记录,放在AllQueryFilterList集合中
  3. 查找出所有的CityID,所有的DistirctID,所有的CuisineID,这些可以从相应的表中查出来。
  4. 然后,对于一个CityID,DistrictID,CuisineID的组合,在AllFoodList中查找满足条件的记录个数,然后构造一个QueryFilterModel对象。
  5. 然后检查AllQueryFilterList中,是否存在CityID,DistrictID,CuisineID这样一条记录。
  • 如果不存在,将先前构造的QueryFilterModel放到readyToInsert集合中
  • 不过存在,将先前构造的QueryFilterModel放到readyToUpdate集合中

大概代码如下:

List<FoodModel> allFoods;
List<QueryFilterModel> allQueryFilters;
List<QueryFilterModel> readyToInsert;
List<QueryFilterModel> readyToUpdate;

List<CityModel> allCitys;
List<BusinessDistrictModel> allDistricts;
List<CuisineModel> allCuisines;

allCitys = CityRepository.GetAllCitys();
allDistricts = BusinessDistrictRepository.GetAllBusinessDistricts();
allCuisines = CuisineRepository.GetAllCuisines();

allQueryFilters = QueryFilterRepository.GetAllQueryFilters();
allFoods = FoodRepository.GetAllFoods();
readyToInsert = new List<QueryFilterModel>();
readyToUpdate = new List<QueryFilterModel>();
foreach (CityModel city in allCitys)
{
    foreach (CuisineModel cuisine in allCuisines)
    {
        foreach (BusinessDistrictModel district in allDistricts)
        {
            //查找 城市-商区-美食类表- 的所有美食个数
            int productCount = allFoods.Count(x => x.CityId == city.Id && x.CuisineId == cuisine.Id && x.DistrictId == district.Id);
            //查找该记录是否已经存在
            QueryFilterModel queryFilter = allQueryFilters.Find(x => x.CityId == city.Id && x.CuisineId == cuisine.Id && x.DistrictId == district.Id);

            QueryFilterModel queryModel = new QueryFilterModel {
                CityId = city.Id,
                CuisineId = cuisine.Id,
                DistrictId = district.Id,
                ProductCount = productCount };

            if (queryFilter == null)
            {
                //不存在,插入待插入结合
                readyToInsert.Add(queryModel);
            }
            else
            {
                //存在在,插入待更新结合
                readyToUpdate.Add(queryModel);
            }
        }
    }
}

改造后的代码,只有两次与数据库的交互,一次是获取allFoods,一次是获取allQueryFilters, 在循环提中,我们仅对allFoods集合进行统计,然后再在allQueryFilters中查询是否存在,并插入到相应的待更新或者待插入列表中,这些都是在内存中进行的操作,速度极快。经过这一项改进,我们将与数据库的交互从45000次减少到了10次以内。

优化二:批量插入与更新

在优化一中,我们得到了readyToInsert和readyToUpdate两个集合,这两个表示待插入和待更新的集合,只有在服务第一次跑的时候才有readyToInsert,后面该集合为空,因为全部都是readyToUpdate。

有了这两个集合,我们需要与数据库进行交互,把这些数据更新回数据库。这里又有两种选择,一种是for循环,在循环中一次次插入,一种是批量发送到SQLServer中进行批量更新。

如果采用第一种方式,那么就又会增加与数据库的交互,虽然比没优化之前的在三层循环中对数据库进行操作要好,但是还是会极大影响性能。所以我们采用批量更新和插入的策略。

关于SQLServer批量更新和插入记录的方式有很多种,比如ADO.NET 2.0种的SqlBulkCopy, SQLServer 2008中的表值参数(Table-Valued Parameter)。

这里我们使用SQLServer2008中的表值参数,其基本思路是,在SQLServer中新建一个存储过程,该存储过程定义了一个和QueryFilter表结构一致的表变量:

CREATE PROC BulkInsert
    (
      @QueryFilter QueryFilter READONLY
    )
AS
    INSERT  INTO QueryFilter
            ( CityID ,
              DistrictID ,
              CuisineID ,
              ProductCount
            )
            SELECT  qf.CityID ,
                    qf.DistrictID ,
                    qf.CuisineID ,
                    qf.ProductCount
            FROM    @QueryFilter AS qf

然后在C# 中,我们通过构建一个DataTable的类型,该类型要和存储过程中定义的表值参数中的类型一致,且ColumnName也应该一致,方法如下:

public static void Insert(List<QueryFilterModel> readyToInsert)
{
    //构造表结构
    DataTable queryFilterTable = new DataTable();
    queryFilterTable.Columns.Add(new DataColumn("CityID", typeof(long)));
    queryFilterTable.Columns.Add(new DataColumn("DistrictID", typeof(long)));
    queryFilterTable.Columns.Add(new DataColumn("CuisineID", typeof(long)));
    queryFilterTable.Columns.Add(new DataColumn("ProductCount", typeof(int)));

    //填充值
    foreach (QueryFilterModel model in readyToInsert)
    {
        DataRow dr = queryFilterTable.NewRow();
        dr["CityID"] = model.CityId;
        dr["DistrictID"] = model.DistrictId;
        dr["CuisineID"] = model.CuisineId;
        dr["ProductCount"] = model.ProductCount;
        queryFilterTable.Rows.Add(dr);
    }

    SqlConnection connection = new SqlConnection(myConnString);
    connection.Open();
    //BulkInsert存储过程
    SqlCommand cmd = new SqlCommand("BulkInsert", connection);
    cmd.CommandType = CommandType.StoredProcedure;
    //将表值参数传递给存储过程
    SqlParameter sqlParam = cmd.Parameters.AddWithValue("@QueryFilter", queryFilterTable);
    sqlParam.SqlDbType = SqlDbType.Structured;
    cmd.ExecuteNonQuery();
    connection.Close();
}

然后我们调用该方法,把我们之前搜集的所有需要插入的记录放进去即可批量更新到数据库里面了。这里仅列出了插入方法,更新的方法逻辑和这个类似,这里不再赘述。另外,我们在编写批量查询的存储过程的时候,还需要考虑一些诸如锁,数据完整性比如是否加事务等,这里为了演示没有做任何处理。

这样,我们通过与数据库的两次交互,就将所有的待变更的记录更新到了数据库中。

经过这两点的优化,整个服务运行的时间从之前的近半小时缩短为10秒内。

优化三:充分使用SQL强大的查询统计功能

现在,已经通过后台的服务将所有CityID,DistrictID,CuisineID组合的美食个数插入到了QueryFilter表中,现在,我们要查找所有热门的DistrictID,热门的CuisineID,并且统计所有DistrictID和CuisineID在不同组合下,美食的个数,用于用户在点击筛选菜单时显示在该条件后面的美食的个数。

要解决这一问题也有两种方式,一种是,直接将所有的根据传递进来的CityID中到QueryFilter中把所有记录查找出来,然后再在内存中,使用LINQ对集合进行统计。结果出来之后,再到City,District及Cuisine表中找到Id对应的中文名称。这些操作,如果在C#里面在内存中进行处理,会发现非常麻烦,而且占用内存,比如需要使用group by,需要Order,需要Sum,然后需要在内存中去连接City,District及Cuisine实体查找出名称,还要构造相应的对象,这些临时的中间对象会给GC带来很大压力。

其实这些统计查询功能正是SQL语句的强项,所以我们完全可以把查询条件下发中的统计完全使用SQL语句完成,比如在SQL中,可以很方便的使用查询,统计等操作,也可以很容易使用join操作来链表查询ID对应的中文名称,这样我们只需要把用户选中的CityID,DistrictID作为参数传到存储过程中,然后直接将查询结果以DataTable返回过来。

比如,要查找所有热门商区,我们只需要编写以下SQL语句,统计出,所有的商区包含的全部美食个数,倒序排列,取前十:

SELECT TOP 10
        DistrictID,
        SUM(ProductCount) AS ProductCounts
FROM    QueryFilter
WHERE   CityID = 2
        AND ProductCount > 0
        AND CuisineID = 0
GROUP BY DistrictID
ORDER BY ProductCounts DESC

查询结果如下:

如果要根据DistrictID显示是那个商区,也很简单,只需要inner join关联商区表即可。

可以看到,通过将统计,连表查询逻辑,从.NET 中移至SQLServer中,使得原本复杂难以控制的C# 代码变为了简洁充满语义的SQL语句。不仅提高了处理效率,减少了内存使用,而且使得代码更好维护。

四 结论

本文通过实践,讨论了在应用程序和数据库进行交互时值的注意和可能会影响性能的几个问题:

  • 由于数据库交互是一个比较耗时和耗资源的过程,所以应用程序中应该尽量减少与数据库的交互,在一些处理过程中,可以一次性查询出来,然后在内存中进行处理,处理好了之后,通过批量方式批量更新回数据库以持久化。
  • 对于一些需要用到的统计,排序,查询等功能,可以将一些业务逻辑直接写到存储过程中,直接利用数据库SQL语言强大统计查询分析功能,在数据库中完成一些诸如统计,排序,连表查询功能,然后将处理结果返回处理。这样减少应用程序中对数据处理导致的内存消耗以及增加的代码复杂性。使得程序更加简洁和易维护。

这些问题其实都很简单,但是处理好了可以提高应用程序的性能,希望本文对您在处理应用程序与数据库进行交互时,遇到性能问题时有所帮助。

.NET应用程序与数据库交互的若干问题的更多相关文章

  1. 说说Java程序和数据库交互的乱码解决

    本文就本人遇到的问题进行讲解 1.通过jdbc直连方式,连接Mysql数据库,从程序向数据库中写入数据出现的乱码解决方案. 当通过程序向Student表中写入一条数据时,写入数据库的内容会产生乱码. ...

  2. OAF_JDBC系列1 - 数据库交互取值方式(案例)

    2014-06-15 Created By BaoXinjian

  3. 辅助的写与数据库交互的XML文件的类

    现在企业级WEB应用中与数据库交互的XML文件都是通过插件自动生成的,不过有些时候修改比较老的项目的时候也是需要手动的来做这一动作的!如下代码就是一个实现上述的功能的辅助类,在此记录一下以备后用! p ...

  4. Java豆瓣电影爬虫——减少与数据库交互实现批量插入

    节前一个误操作把mysql中record表和movie表都清空了,显然我是没有做什么mysql备份的.所以,索性我把所有的表数据都清空的,一夜回到解放前…… 项目地址:https://github.c ...

  5. 利用NHibernate与MySQL数据库交互

    本文章使用Visual Studio作为开发工具,并建立在已经安装MySQL数据库的前提. NHibernate是一个面向.NET环境的对象/关系数据库映射工具.官网:http://nhibernat ...

  6. C#通过窗体应用程序操作数据库(增删改查)

    为了体现面向对象的思想,我们把“增删改查”这些函数封装到一个数据库操作类里: 为了便于窗体程序与数据库之间进行数据交互,我们建一个具有数据库行数据的类,通过它方便的在窗体程序与数据库之间传输数据: 我 ...

  7. 10分钟系列:NetCore3.1+EFCore三步快速完成数据库交互

    前言 做程序开发,不管是什么语言什么数据库,其中的ORM(对象关系映射)是必不可少的,但是不管选择哪一种ORM,都需要了解其中的运行机制,配置帮助类等等. 所以很多ORM都开始进行升级封装,我们只需要 ...

  8. windows环境30分钟从0开始快速搭建第一个docker项目(带数据库交互)

    前言 小白直接上手 docker  构建我们的第一个项目,简单粗暴,后续各种概念边写边了解,各种概念性的内容就不展开,没了解过的点击 Docker 教程 进行初步了解. Docker 是一个开源的应用 ...

  9. 使用Mybatis的一些基本配置及Mybatis与数据库交互测试验证

    1.简介 什么是MyBatis? MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.My ...

随机推荐

  1. 动画requestAnimationFrame

    前言 在研究canvas的2D pixi.js库的时候,其动画的刷新都用requestAnimationFrame替代了setTimeout 或 setInterval 但是jQuery中还是采用了s ...

  2. ASP.NET Core 中间件之压缩、缓存

    前言 今天给大家介绍一下在 ASP.NET Core 日常开发中用的比较多的两个中间件,它们都是出自于微软的 ASP.NET 团队,他们分别是 Microsoft.AspNetCore.Respons ...

  3. 12、Struts2表单重复提交

    什么是表单重复提交 表单的重复提交: 若刷新表单页面, 再提交表单不算重复提交. 在不刷新表单页面的前提下: 多次点击提交按钮 已经提交成功, 按 "回退" 之后, 再点击 &qu ...

  4. 【HanLP】HanLP中文自然语言处理工具实例演练

    HanLP中文自然语言处理工具实例演练 作者:白宁超 2016年11月25日13:45:13 摘要:HanLP是hankcs个人完成一系列模型与算法组成的Java工具包,目标是普及自然语言处理在生产环 ...

  5. 免费道路 bzoj 3624

    免费道路(1s 128MB)roads [输入样例] 5 7 21 3 04 5 13 2 05 3 14 3 01 2 14 2 1 [输出样例] 3 2 04 3 05 3 11 2 1 题解: ...

  6. CSS常见技巧

    一.CSS Sprite(雪碧图|精灵图)指什么? 有什么作用? CSS雪碧 即CSS Sprite,也有人叫它CSS精灵,是一种CSS图像合并技术,该方法是将小图像和背景图片合并到一张图片上,然后利 ...

  7. 关于SMARTFORMS文本编辑器出错

    最近在做ISH的一个打印功能,SMARTFORM的需求本身很简单,但做起来则一波三折. 使用环境是这样的:Windows 7 64bit + SAP GUI 740 Patch 5 + MS Offi ...

  8. swift 中关于open ,public ,fileprivate,private ,internal,修饰的说明

    关于 swift 中的open ,public ,fileprivate,private, internal的区别 以下按照修饰关键字的访问约束范围 从约束的限定范围大到小的排序进行说明 open,p ...

  9. Lucene4.4.0 开发之排序

    排序是对于全文检索来言是一个必不可少的功能,在实际运用中,排序功能能在某些时候给我们带来很大的方便,比如在淘宝,京东等一些电商网站我们可能通过排序来快速找到价格最便宜的商品,或者通过排序来找到评论数最 ...

  10. Atitit 管理原理与实践attilax总结

    Atitit 管理原理与实践attilax总结 1. 管理学分类1 2. 我要学的管理学科2 3. 管理学原理2 4. 管理心理学2 5. 现代管理理论与方法2 6. <领导科学与艺术4 7. ...