`
xiangxingchina
  • 浏览: 524045 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

多线程往Oracle数据库里插入数据的优化

阅读更多
昨 天做了一个东西,要实现解析txt文件,然后入库的功能。开始试验了一下单线程插入,速度实在太慢了,半个小时才插入了2W多条数据,后来改用Java的 线程池启用了30个线程,并发的执行,插入100W条数据用了一个多小时。后来又对业务层的事务做了一些调整,每1000条insert之后才提交一次, 一共开了20个线程,最后100W条数据入库一共用了14分钟不到,平均一分钟7.1W条左右。 代码如下:
  1. /**
  2. * 分析Apache日志的定时任务.每天运行数次.
  3. *
  4. * @author <a href="mailto:HL_Qu@hotmail.com">Along</a>
  5. *
  6. * @version $Revision$
  7. *
  8. * @since 2009-2-9
  9. */
  10. public class ApacheLogAnalysisTask {
  11.         /**
  12.          * Logger for this class
  13.          */
  14.         private static final Log logger = LogFactory.getLog(ApacheLogAnalysisTask.class);
  15.        
  16.         //总线程数
  17.         private static final int THREAD_COUNT = 20;
  18.        
  19.         //每个线程插入的日志数
  20.         private static final long LOG_COUNT_PER_THREAD = 1000;
  21.        
  22.         //日志文件的位置
  23.         private static final String LOG_FILE = Property.LOG_FILE_PATH + "formatLog.txt";
  24.        
  25.         private IObjectActionDetailService objectActionDetailService;
  26.        

  27.         public void setObjectActionDetailService(IObjectActionDetailService objectActionDetailService) {
  28.                 this.objectActionDetailService = objectActionDetailService;
  29.         }

  30.         public void execute() {
  31.                 this.multiAnalysisLog();
  32.         }
  33.        
  34.         private void multiAnalysisLog() {
  35.                 ExecutorService exePool = Executors.newFixedThreadPool(THREAD_COUNT);
  36.                
  37.                 FileReader fr = null;
  38.                 BufferedReader br = null;
  39.                 long beginLine = 1;
  40.                 long endLine = 0;
  41.                 String logFileBack = Property.LOG_FILE_PATH + "/formatLog_" + DateUtil.getSystemCurrentDate() + "_" + System.currentTimeMillis() + ".txt";
  42.                
  43.                 try {
  44.                         //文件拷贝出来一个新的,并将原先的删除.
  45.                         FileUtil.copyfile(LOG_FILE, logFileBack, true);
  46.                         FileUtil.deleteFile(LOG_FILE);
  47.                         System.out.println(logFileBack);
  48.                        
  49.                         fr = new FileReader(logFileBack);
  50.                         br = new BufferedReader(fr);
  51.                        
  52.                         while ((br.readLine()) != null){
  53.                                 endLine++;
  54.                                
  55.                                 //每个线程分配固定的行数
  56.                                 if((endLine - beginLine + 1) == LOG_COUNT_PER_THREAD) {
  57.                                         exePool.execute(new AnalysisLogTask(logFileBack, beginLine, endLine));
  58.                                         beginLine = endLine + 1;
  59.                                 }
  60.                         }
  61.                        
  62.                         //最后一个线程
  63.                         if (endLine > beginLine) {
  64.                                 exePool.execute(new AnalysisLogTask(logFileBack, beginLine, endLine));
  65.                         }

  66.                 } catch (FileNotFoundException e) {
  67.                         e.printStackTrace();
  68.                 } catch (IOException e) {
  69.                         e.printStackTrace();
  70.                 } finally {
  71.                         if (br != null) {
  72.                                 try {
  73.                                         br.close();
  74.                                         br = null;
  75.                                 } catch (IOException e) {
  76.                                         if (logger.isErrorEnabled()) {
  77.                                                 logger.error("run()", e);
  78.                                         }

  79.                                         e.printStackTrace();
  80.                                 }
  81.                         }
  82.                        
  83.                         if (fr != null) {
  84.                                 try {
  85.                                         fr.close();
  86.                                         fr = null;
  87.                                 } catch (IOException e) {
  88.                                         if (logger.isErrorEnabled()) {
  89.                                                 logger.error("run()", e);
  90.                                         }

  91.                                         e.printStackTrace();
  92.                                 }
  93.                         }
  94.                        
  95.                         exePool.shutdown();
  96.                         while (true) {
  97.                                 if (exePool.isTerminated()) {
  98.                                         System.out.println("ShutDown");
  99.                                         FileUtil.deleteFile(logFileBack);
  100.                                         break;
  101.                                 }
  102.                         }
  103.                        
  104.                 }
  105.         }
  106.        
  107.         private class AnalysisLogTask implements Runnable {
  108.                 //起始行
  109.                 private long beginLine;
  110.                
  111.                 //结束行
  112.                 private long endLine;
  113.                
  114.                 private String logFilePath;

  115.                 public AnalysisLogTask(String logFilePath, long beginLine, long endLine) {
  116.                         super();
  117.                         this.logFilePath = logFilePath;
  118.                         this.beginLine = beginLine;
  119.                         this.endLine = endLine;
  120.                 }

  121.                 @Override
  122.                 public void run() {
  123.                         FileReader fr = null;
  124.                         BufferedReader br = null;
  125.                         String tempStr = null;
  126.                         String[] tempArray = null;
  127.                         long currentLine = 0;
  128.                         List <ObjectActionDetail> resultList = new ArrayList<ObjectActionDetail>();
  129.                        
  130.                         ObjectActionDetail tempObjectActionDetailVO = null;
  131.                         try {
  132.                                 fr = new FileReader(logFilePath);
  133.                                 br = new BufferedReader(fr);
  134.                                 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  135.                                
  136.                                 //跳过前置的行数
  137.                                 if (beginLine != 1) {
  138.                                         while ((currentLine < (beginLine-1)) && br.readLine() != null) {
  139.                                                 ++currentLine;
  140.                                         }
  141.                                 }
  142.                                
  143.                                 while ((tempStr = br.readLine()) != null && currentLine++ < endLine) {
  144.                                         tempArray = tempStr.split("\t");
  145.                                         tempObjectActionDetailVO = new ObjectActionDetail();

  146.                                         tempObjectActionDetailVO.setIp(tempArray[0]);
  147.                                         tempObjectActionDetailVO.setActionTime(sdf.parse(tempArray[1]));
  148.                                         tempObjectActionDetailVO.setSrcObjTypeId(Integer.parseInt(tempArray[2]));
  149.                                         tempObjectActionDetailVO.setSrcObjId(Integer.parseInt(tempArray[3]));
  150.                                         tempObjectActionDetailVO.setTarObjTypeId(Integer.parseInt(tempArray[4]));
  151.                                         tempObjectActionDetailVO.setTarObjId(Integer.parseInt(tempArray[5]));
  152.                                         tempObjectActionDetailVO.setActionId(Integer.parseInt(tempArray[6]));
  153.                                        
  154.                                         tempObjectActionDetailVO.setScore(0);
  155.                                         tempObjectActionDetailVO.setStatus(1);
  156.                                        
  157.                                         resultList.add(tempObjectActionDetailVO);
  158.                                 }
  159.                                
  160.                                 logger.info("Thread:" + Thread.currentThread().getName() + "   beginLine=" + beginLine + "   endLine=" + endLine);
  161.                                
  162.                                 objectActionDetailService.insertObjectActionDetailBatch(resultList);
  163.                         } catch (FileNotFoundException e) {
  164.                                 if (logger.isErrorEnabled()) {
  165.                                         logger.error("run()", e);
  166.                                 }

  167.                                 e.printStackTrace();
  168.                         } catch (IOException e) {
  169.                                 if (logger.isErrorEnabled()) {
  170.                                         logger.error("run()", e);
  171.                                 }

  172.                                 e.printStackTrace();
  173.                         } catch (ParseException e) {
  174.                                 if (logger.isErrorEnabled()) {
  175.                                         logger.error("run()", e);
  176.                                 }

  177.                                 e.printStackTrace();
  178.                         } finally {
  179.                                 if (br != null) {
  180.                                         try {
  181.                                                 br.close();
  182.                                                 br = null;
  183.                                         } catch (IOException e) {
  184.                                                 if (logger.isErrorEnabled()) {
  185.                                                         logger.error("run()", e);
  186.                                                 }

  187.                                                 e.printStackTrace();
  188.                                         }
  189.                                 }
  190.                                
  191.                                 if (fr != null) {
  192.                                         try {
  193.                                                 fr.close();
  194.                                                 fr = null;
  195.                                         } catch (IOException e) {
  196.                                                 if (logger.isErrorEnabled()) {
  197.                                                         logger.error("run()", e);
  198.                                                 }

  199.                                                 e.printStackTrace();
  200.                                         }
  201.                                 }
  202.                         }
  203.                 }
  204.         }
  205. }

今天又试验了一下先将整个文件拆分成小的单个文件,然后每个线程再解析自己的文件。测试后感觉这样的效果不好,明显不如多线程从一个文章读数据好,27分钟插入了100W条数据,平均一分钟3.7W条左右。怀疑是多线程读取文件,本地磁盘的I/O受限导致性能低下。 代码如下:

  1. /**
  2. * 分析Apache日志的定时任务.每天运行数次.
  3. *
  4. * @author <a href="mailto:HL_Qu@hotmail.com">Along</a>
  5. *
  6. * @version $Revision$
  7. *
  8. * @since 2009-2-9
  9. */
  10. public class ApacheLogAnalysisTask {
  11.         /**
  12.          * Logger for this class
  13.          */
  14.         private static final Log logger = LogFactory.getLog(ApacheLogAnalysisTask.class);
  15.        
  16.         //总线程数
  17.         private static final int THREAD_COUNT = 10;
  18.        
  19.         //每个线程插入的日志数
  20.         private static final long LOG_COUNT_PER_THREAD = 3000;
  21.        
  22.         //日志文件的位置
  23.         private static final String LOG_FILE = Property.LOG_FILE_PATH + "/formatLog.txt";
  24.        
  25.         private IObjectActionDetailService objectActionDetailService;
  26.        

  27.         public void setObjectActionDetailService(IObjectActionDetailService objectActionDetailService) {
  28.                 this.objectActionDetailService = objectActionDetailService;
  29.         }

  30.         public void execute() {
  31.                 this.multiAnalysisLog();
  32.         }
  33.        
  34.         private void multiAnalysisLog() {
  35.                 ExecutorService exePool = Executors.newFixedThreadPool(THREAD_COUNT);
  36.                
  37.                 FileReader fr = null;
  38.                 FileWriter fw = null;
  39.                 BufferedReader br = null;
  40.                 int threadCount = 0;
  41.                 long tempLineCount = 0;
  42.                 String tempReadLineStr = null;
  43.                 long now = System.currentTimeMillis();
  44.                 String logFileBackFile = Property.LOG_FILE_PATH + "/old/formatLog_" + DateUtil.getSystemCurrentDate() + "_" + now + ".txt";
  45.                 String logFilePerThreadName = Property.LOG_FILE_PATH + "/old/formatLog_" + DateUtil.getSystemCurrentDate() + "_" + now;
  46.                
  47.                 try {
  48.                         //文件拷贝出来一个新的,并将原先的删除.
  49.                         FileUtil.copyfile(LOG_FILE, logFileBackFile, true);
  50.                         //FileUtil.deleteFile(LOG_FILE);
  51.                        
  52.                         fr = new FileReader(logFileBackFile);
  53.                         br = new BufferedReader(fr);
  54.                         fw = new FileWriter(logFilePerThreadName + "_" + ++threadCount + ".txt");
  55.                        
  56.                         while ((tempReadLineStr = br.readLine()) != null){
  57.                                 tempLineCount++;
  58.                                 fw.append(tempReadLineStr).append("\r\n");
  59.                                
  60.                                 //每个线程分配固定的行数
  61.                                 if(tempLineCount == LOG_COUNT_PER_THREAD) {
  62.                                         fw.flush();
  63.                                         fw.close();
  64.                                         exePool.execute(new AnalysisLogTask(logFilePerThreadName + "_" + threadCount + ".txt"));

  65.                                         //创建新的文件,临时变量清零.
  66.                                         fw = new FileWriter(logFilePerThreadName + "_" + ++threadCount + ".txt");
  67.                                         tempLineCount = 0;
  68.                                 }
  69.                         }
  70.                        
  71.                         //最后一个线程有文件则写入执行,没有,则删除最后一个建立的文件.
  72.                         if (tempLineCount != 0) {
  73.                                 fw.flush();
  74.                                 fw.close();
  75.                                 exePool.execute(new AnalysisLogTask(logFilePerThreadName + "_" + threadCount + ".txt"));
  76.                         } else {
  77.                                 fw.flush();
  78.                                 fw.close();
  79.                                
  80.                                 FileUtil.deleteFile(logFilePerThreadName + "_" + threadCount + ".txt");
  81.                         }

  82.                 } catch (FileNotFoundException e) {
  83.                         e.printStackTrace();
  84.                 } catch (IOException e) {
  85.                         e.printStackTrace();
  86.                 } finally {
  87.                         if (br != null) {
  88.                                 try {
  89.                                         br.close();
  90.                                         br = null;
  91.                                 } catch (IOException e) {
  92.                                         if (logger.isErrorEnabled()) {
  93.                                                 logger.error("run()", e);
  94.                                         }

  95.                                         e.printStackTrace();
  96.                                 }
  97.                         }
  98.                        
  99.                         if (fr != null) {
  100.                                 try {
  101.                                         fr.close();
  102.                                         fr = null;
  103.                                 } catch (IOException e) {
  104.                                         if (logger.isErrorEnabled()) {
  105.                                                 logger.error("run()", e);
  106.                                         }

  107.                                         e.printStackTrace();
  108.                                 }
  109.                         }
  110.                        
  111.                         FileUtil.deleteFile(logFileBackFile);
  112.                         logger.info("File has deleted:" + logFileBackFile);
  113.                        
  114.                         exePool.shutdown();
  115.                         //判断是不是所有的任务都执行完毕,执行完删除日志文件.
  116.                         while (true) {
  117.                                 if (exePool.isTerminated()) {
  118.                                         logger.info("All task has shutdown.");
  119.                                         break;
  120.                                 }
  121.                         }
  122.                        
  123.                 }
  124.         }
  125.        
  126.         private class AnalysisLogTask implements Runnable {

  127.                 //每个线程要处理的日志文件
  128.                 private String logFilePath;

  129.                 public AnalysisLogTask(String logFilePath) {
  130.                         super();
  131.                         this.logFilePath = logFilePath;
  132.                 }

  133.                 @Override
  134.                 public void run() {
  135.                         logger.info("Thread:" + Thread.currentThread().getName() + " running.");
  136.                        
  137.                         FileReader fr = null;
  138.                         BufferedReader br = null;
  139.                         String tempStr = null;
  140.                         String[] tempArray = null;
  141.                         List <ObjectActionDetail> resultList = new ArrayList<ObjectActionDetail>();
  142.                        
  143.                         ObjectActionDetail tempObjectActionDetailVO = null;
  144.                         try {
  145.                                 fr = new FileReader(logFilePath);
  146.                                 br = new BufferedReader(fr);
  147.                                 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  148.                                
  149.                                 while ((tempStr = br.readLine()) != null) {
  150.                                         tempArray = tempStr.split("\t");
  151.                                         tempObjectActionDetailVO = new ObjectActionDetail();

  152.                                         tempObjectActionDetailVO.setIp(tempArray[0]);
  153.                                         tempObjectActionDetailVO.setActionTime(sdf.parse(tempArray[1]));
  154.                                         tempObjectActionDetailVO.setSrcObjTypeId(Integer.parseInt(tempArray[2]));
  155.                                         tempObjectActionDetailVO.setSrcObjId(Integer.parseInt(tempArray[3]));
  156.                                         tempObjectActionDetailVO.setTarObjTypeId(Integer.parseInt(tempArray[4]));
  157.                                         tempObjectActionDetailVO.setTarObjId(Integer.parseInt(tempArray[5]));
  158.                                         tempObjectActionDetailVO.setActionId(Integer.parseInt(tempArray[6]));
  159.                                        
  160.                                         tempObjectActionDetailVO.setScore(0);
  161.                                         tempObjectActionDetailVO.setStatus(1);
  162.                                        
  163.                                         resultList.add(tempObjectActionDetailVO);
  164.                                 }
  165.                                 objectActionDetailService.insertObjectActionDetailBatch(resultList);
  166.                         } catch (FileNotFoundException e) {
  167.                                 if (logger.isErrorEnabled()) {
  168.                                         logger.error("run()", e);
  169.                                 }

  170.                                 e.printStackTrace();
  171.                         } catch (IOException e) {
  172.                                 if (logger.isErrorEnabled()) {
  173.                                         logger.error("run()", e);
  174.                                 }

  175.                                 e.printStackTrace();
  176.                         } catch (ParseException e) {
  177.                                 if (logger.isErrorEnabled()) {
  178.                                         logger.error("run()", e);
  179.                                 }

  180.                                 e.printStackTrace();
  181.                         } finally {
  182.                                 if (br != null) {
  183.                                         try {
  184.                                                 br.close();
  185.                                                 br = null;
  186.                                         } catch (IOException e) {
  187.                                                 if (logger.isErrorEnabled()) {
  188.                                                         logger.error("run()", e);
  189.                                                 }

  190.                                                 e.printStackTrace();
  191.                                         }
  192.                                 }
  193.                                
  194.                                 if (fr != null) {
  195.                                         try {
  196.                                                 fr.close();
  197.                                                 fr = null;
  198.                                         } catch (IOException e) {
  199.                                                 if (logger.isErrorEnabled()) {
  200.                                                         logger.error("run()", e);
  201.                                                 }

  202.                                                 e.printStackTrace();
  203.                                         }
  204.                                 }
  205.                                
  206.                                 //删除本线程负责的日志文件
  207.                                 FileUtil.deleteFile(logFilePath);
  208.                                 logger.info("Thread:" + Thread.currentThread().getName() + "   logFilePath has deleted:" + logFilePath);
  209.                                 logger.info("Thread:" + Thread.currentThread().getName() + " shutdown.");
  210.                         }
  211.                 }
  212.         }
  213. }

后来又找系统管理员优化了一下网络,现在数据入库的速度是100W条7分钟 。相信应用和数据库移动到生产环境中,性能会进一步提升。

分享到:
评论

相关推荐

    Springboot Druid多数据源 多线程

    通过Druid的监控和多线程支持,可以有效优化系统性能,提高数据处理效率。 总结起来,这个项目展示了如何利用Spring Boot和Druid实现多数据源切换,以及如何在Spring Boot中配置和使用多线程来提升数据库操作的并发...

    java 多线程操作数据库

    示例代码中使用了JDBC(Java Database Connectivity)API来建立与Oracle数据库的连接,并通过`DriverManager.getConnection()`方法获取连接。由于数据库连接是一种昂贵的资源,应避免在每个线程中重复创建连接,通常...

    Java操作Oracle数据库-多线程.rar

    2. **Oracle数据库操作**:一旦建立了连接,我们可以通过`Statement`或`PreparedStatement`对象来执行SQL语句,包括查询、插入、更新和删除数据。在本示例中,还涉及到调用Oracle数据库中的函数和存储过程,这通常...

    Oracle数据库性能优化浅析

    综上所述,Oracle数据库性能优化是一个复杂而细致的过程,需要综合考虑查询的特点、数据的结构以及系统的配置等多个因素。通过遵循上述原则和技巧,可以有效提升Oracle数据库的查询性能和整体运行效率。

    易语言oracle数据库连接模块源码

    Oracle数据库是全球广泛使用的大型关系型数据库管理系统,适用于处理大量数据和并发事务。 Oracle数据库连接模块的核心在于如何建立和管理与Oracle服务器的通信。在这个模块中,通常会用到Oracle的数据访问接口,如...

    json格式数据到入oracle数据库java源码

    - 多线程:利用多线程并发处理不同部分的数据,提高导入速度。但是要注意数据库连接和事务管理的同步问题。 -流式解析:如使用Jackson的`JsonParser`,可以在解析过程中处理数据,减少内存占用。 5. 数据映射与...

    c#批量导入excel数据到oracle数据库.rar

    此外,对于大型数据集,可能还需要考虑多线程处理、分批导入等优化策略,以减少内存占用和提高导入速度。同时,确保数据导入的事务一致性也是很重要的,特别是在数据校验失败或网络问题时,能够回滚已导入的部分。 ...

    C#.NET中如何批量插入大量数据到数据库中

    在C#.NET中批量插入大量数据到数据库是一个常见的任务,特别是在处理诸如从Excel文件导入数据等场景时。这里,我们将探讨如何使用C#.NET高效地完成这个任务,并提供一个简单的示例来说明整个过程。 首先,我们需要...

    Excel导入Oracle数据库关键代码

    同时,根据数据量大小,可以考虑是否采用多线程并行处理。 8. **扩展性**:描述中提到代码有很好的扩展性,意味着可能设计了灵活的接口或抽象类,方便添加新的数据处理逻辑,比如支持其他类型的数据库,或者增加...

    JAVAweb连接oracle数据库工程

    在Java Web开发中,连接Oracle数据库是一项常见的任务,它涉及到多层架构中的数据访问层(DAO,Data Access Object)设计,以及JDBC(Java Database Connectivity)技术的应用。本工程主要展示了如何通过Java代码与...

    实现ORACLE与SQLSERVER数据库间的数据抽取与转换工具

    对于Oracle数据库,我们可以使用PL/SQL语句或者Oracle的SQL*Plus工具来实现。在SQL Server中,我们可以利用T-SQL语言进行数据检索。 接下来是数据转换,这个阶段的目标是将从源数据库抽取的数据转化为目标数据库...

    HIbernate与oracle数据库应用例子

    通过上述步骤,开发者可以在Java应用中高效地使用Hibernate与Oracle数据库进行数据操作。实践过程中,应不断优化和调整,以适应不同场景的需求。在实际项目中,还需要关注性能监控、异常处理、安全防护等方面,确保...

    socket 大数据并列接收存数据库小列子(带多线程模拟数据)

    在这个"socket大数据并行接收存数据库小例子"中,我们主要探讨的是如何利用Socket来处理大数据,并通过多线程技术提高数据处理的效率,最终将接收到的数据存储到数据库中。 首先,Socket是TCP/IP协议栈中的一个接口...

    Windows环境下的Oracle数据库查询工具

    总的来说,这个Windows环境下的Oracle数据库查询工具提供了一个友好且功能丰富的界面,以帮助用户更方便地执行SQLPlus脚本,同时提供了多线程支持以及变量和参数功能,提升了工作效率。对于Oracle数据库的日常管理和...

    百度百科多线程爬虫Java源码,数据存储采用了Oracle11g.zip

    源码通常包含主程序类、爬虫类、数据处理类和数据库连接类等,通过阅读源码可以学习到如何构建多线程爬虫,以及如何将抓取到的数据有效地存储到Oracle数据库中。 总结来说,这个项目提供了关于Java多线程爬虫技术和...

    java操作pageoffice自带印章连接oracle数据库

    在实际应用中,可能还需要考虑错误处理、多线程安全、资源管理等细节。例如,当数据库连接失败时,程序应有适当的异常处理机制;对于并发访问,应保证印章操作的互斥性,防止多个用户同时修改同一份文档。 总的来说...

    JAVA将一个数据中数据定时自动复制(抽取)到另一个数据库

    5. **数据复制**:获取到数据后,你需要将它们插入到目标数据库。同样,使用`PreparedStatement`可以执行INSERT语句。注意处理可能出现的并发问题,比如确保数据的一致性和避免重复插入。 6. **异常处理和日志记录*...

    Oracle数据库表序列ID自增生成器

    Oracle数据库在设计和管理大型数据系统时扮演着关键角色,其中序列(Sequence)是一种非常重要的对象,用于生成唯一的整数序列,通常用作表的主键。本篇将深入探讨Oracle数据库表序列ID自增生成器及其相关知识点。 ...

    Oracle数据库知识 深入内存数据库 共60页.pptx

    此外,它设计了针对内存优化的数据存储结构和算法,同时支持多进程或多线程共享数据存储,可以是嵌入式部署,也可以采用客户机-服务器模式。 TimesTen的性能表现非常出色,例如,它可以实现每秒数十万次的事务处理...

    VC++链接ORACLE数据库

    综上所述,VC++连接Oracle数据库涉及到环境配置、库的引用、数据库连接、SQL执行、结果处理等多个步骤。理解并熟练掌握这些知识点是开发高效稳定数据库应用程序的关键。在实际开发中,你可能还需要考虑线程安全、...

Global site tag (gtag.js) - Google Analytics