【问题标题】:What's the best way to execute SQL Query on large excel file using vb.net?使用 vb.net 在大型 excel 文件上执行 SQL 查询的最佳方法是什么?
【发布时间】:2014-04-20 00:25:57
【问题描述】:

设置环境:

我正在使用带有 .NET 框架 4 的 vb.net 开发 Excel 2010 应用程序级插件。


我的目标:

  1. 让用户输入多个名称进行搜索
  2. 使用名称列表在 LARGE 电子表格(超过 30,000 行)上执行 SQL 查询
  3. 返回记录集并粘贴到新工作表中

性能是我的首要任务。我想知道利用 .NET 框架最快的方法。

在我的代码中使用 ADO 连接对象是可行的,但该过程需要的时间太长(5 - 8 秒)。


这是我在名为 wells 的表上使用的 SQL 查询:

    SELECT * 
    FROM wells 
    WHERE padgroup in 

    (SELECT padgroup 
     FROM wells 
     WHERE name LIKE 'TOMCHUCK 21-30'
             OR name LIKE 'FEDERAL 41-25PH')


这是表格的一部分:


我现在正在使用此代码创建一个 ADO 连接对象来检索我的结果:

    'Create Recordset Object
    rsCon = CreateObject("ADODB.Connection")
    rsData = CreateObject("ADODB.Recordset")

    rsCon.Open(szConnect)
    rsData.Open(mySQLQueryToExecute, rsCon, 0, 1, 1)

    'Check to make sure data is received, then copy the data
    If Not rsData.EOF Then

        TargetRange.Cells(1, 1).CopyFromRecordset(rsData)

    Else

        MsgBox("No records returned from : " & SourceFile, vbCritical)

    End If

    'Clean up the Recordset object
    rsData.Close()
    rsData = Nothing
    rsCon.Close()
    rsCon = Nothing


据我所知,Excel 电子表格以 Open XML 格式存储,.NET 框架包含对解析 XML 的本机支持。

在研究之后,我发现了几个不同的选择:


有人可以提供一个关于什么是最好的使用方法的指针吗?我真的很感激。


附加说明:

  • 所有查询都需要能够在不连接到 在线数据库
  • 我只需访问电子表格一次即可从行中提取原始数据


现在我只是将电子表格嵌入为项目资源。

然后,在运行时创建文件,运行查询,将结果存储在内存中,然后删除文件。

   'Create temp file path in the commonapplicationdata folder
    Dim excelsheetpath As StringBuilder

    excelsheetpath = New StringBuilder(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData))

    excelsheetpath.Append("\MasterList.xlsm")

    'Save resources into temp location in HD
    System.IO.File.WriteAllBytes(excelsheetpath.ToString, My.Resources.MasterList)

    'Now call the function to use ADO to get records from the MasterList.xlsm file just created
    GetData(excelsheetpath.ToString, "Sheet1", "A1:S40000", True, False)

    'Store the results in-memory and display by adding to a datagridview control (in a custom task pane)

    'Delete the spreadsheet
    System.IO.File.Delete(excelsheetpath.ToString())

【问题讨论】:

  • 这是更大应用程序的一部分吗?它是一个开放类型名称和关闭类型的东西吗?他们加载数据的方式以及加载数据的时间取决于应用程序的类型,虽然它很乱我喜欢使用 Excel 互操作,但它似乎是功能最丰富的选项,尽管如果你只需要数据而不需要有关单元格的信息,您可能最好使用 Linq,您可以 Get Some More Information on Linq and Excel Here
  • @user2140261 该链接看起来很有希望,我会试一试并报告。现在,为了加载数据,我只是将 excel 工作表嵌入为项目资源。然后,在代码中,我临时创建文件、运行查询、删除文件。我更新了我的问题以包含此代码。
  • 这可能会评估每一行的子查询。我会独立运行查询并使用它来手动构建第一个。使用不同的。
  • 运行子查询。使用子查询的结果在 ('3492','3494') 中建立一个硬编码的 WHERE padgroup。您可能会遇到对每一行都运行子查询的情况。非常简单 - 在 ('3492','3494') 中测试 WHERE padgroup
  • @Blam 我把SELECT * FROM wells WHERE padgroup in (3492, 3494) 测试过了,差别很小。仍然平均大约 5-8 秒。另外,我只是使用 VBA 运行了这个查询(没有任何额外的代码),它仍然需要 5-8 秒。我认为这与设置 ADO 连接的时间有关,而不是查询本身

标签: .net vb.net excel vsto openxml


【解决方案1】:

您以错误的方式执行 VSTO ;) 不要将 SQL 与 Excel 一起使用。如果您需要速度,请利用 VSTO 和本机 Excel API。您可以跳过 ADODB/OLEDB 层的开销,直接进入 Excel 对象模型以使用 Excel 中超快的 Autofilter、SpecialCells 方法将仅可见单元格放入多区域范围,以及 Value快速将范围复制到数组的方法。

这是一个示例 VSTO 2010 自定义工作簿,可快速搜索包含“aba”、“cat”或“zon”的 a list of 58k words for words

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Xml.Linq;
using Microsoft.Office.Tools.Excel;
using Microsoft.VisualStudio.Tools.Applications.Runtime;
using Excel = Microsoft.Office.Interop.Excel;
using Office = Microsoft.Office.Core;

namespace ExcelWorkbook1
{
    public partial class ThisWorkbook
    {
        private void ThisWorkbook_Startup(object sender, System.EventArgs e)
        {
            const int Sheet1 = 1; // you can use Linq to find a sheet by name if needed
            const int ColumnB = 2;
            List<List<object>> results = Query(Sheet1, ColumnB, "aba", "cat", "zon");

            foreach (List<object> record in results)
            {
                System.Diagnostics.Debug.Print("{0,-10} {1,30} {2}", record[0], record[1], record[2]);
            }
        }

        private void ThisWorkbook_Shutdown(object sender, System.EventArgs e)
        {
        }

        /// <summary>
        ///     Removes any existing Excel autofilters from the worksheet
        /// </summary>
        private void ClearFilter(Microsoft.Office.Interop.Excel._Worksheet worksheet)
        {
            if (worksheet.AutoFilter != null)
            {
                worksheet.Cells.AutoFilter();
            }
        }

        /// <summary>
        ///     Applies an Excel Autofilter to the worksheet for search for an array of substring predicates
        /// </summary>
        private void ApplyFilter(Microsoft.Office.Interop.Excel._Worksheet worksheet, int column, params string[] predicates)
        {
            string[] criteria = new string[predicates.Length];
            int i = 0;

            ClearFilter(worksheet);

            foreach (string value in predicates)
            {
                criteria[i++] = String.Concat("=*", value, "*");
            }

            worksheet.Cells.AutoFilter(column, criteria, Excel.XlAutoFilterOperator.xlOr); 
        }

        /// <summary>
        ///     Returns a list of rows that are hits on a search for an array of substrings in Column B of Sheet1
        /// </summary>
        private List<List<object>> Query(int sheetIndex, int columnIndex, params string[] words)
        {
            Microsoft.Office.Interop.Excel._Worksheet worksheet;
            Excel.Range range;
            List<List<object>> records = new List<List<object>>();
            List<object> record;
            object[,] cells;
            object value;
            int row, column, rows, columns;
            bool hit;

            try
            {
                worksheet = (Microsoft.Office.Interop.Excel._Worksheet)Globals.ThisWorkbook.Sheets[sheetIndex];
                if (null == worksheet)
                {
                    return null;
                }

                // apply the autofilter
                ApplyFilter(worksheet, columnIndex, words);

                // get the 
                range = worksheet.Range["$A:$C"].SpecialCells(Excel.XlCellType.xlCellTypeVisible);
                foreach (Excel.Range subrange in range.Areas)
                {
                    // copy the cells to a multidimensional array for perfomance
                    cells = subrange.Value;

                    // transform the multidimensional array to a List
                    for (row = cells.GetLowerBound(0), rows = cells.GetUpperBound(0); row <= rows; row++)
                    {
                        record = new List<object>();
                        hit = false;

                        for (column = cells.GetLowerBound(1), columns = cells.GetUpperBound(1); column <= columns; column++)
                        {
                            value = cells[row, column];
                            hit = hit || (null != value);

                            if (hit)
                            {
                                record.Add(cells[row, column]);
                            }
                        }

                        if (hit)
                        {
                            records.Add(record);
                        }
                    }
                }
            }
            catch { }
            finally
            {
                // use GC.Collect over Marshal.ReleaseComObject() to release all RCWs per http://stackoverflow.com/a/17131389/1995977 and more
                cells = null;
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }

            return records;
        }

        #region VSTO Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InternalStartup()
        {
            this.Startup += new System.EventHandler(ThisWorkbook_Startup);
            this.Shutdown += new System.EventHandler(ThisWorkbook_Shutdown);
        }

        #endregion

    }
}

【讨论】:

    【解决方案2】:

    Excel 2010 文件并不完全是 XML。获取一个 XLSX(或 XMSM)文件,并将其重命名为 .zip 扩展名。然后将其解压缩到一个新文件夹。子文件夹中的文件将是 XML 文件,是的,但实际的 XLSX 文件是一个 zip 文件,其中包含一组包含 XML 文件的文件夹。

    我认为,您最好的选择是使用 ACE 驱动程序(不再支持 JET)并通过 ODBC 访问它。如果这还不够快,您可以在特定时间提取数据并将其上传到数据库,您可以从中运行查询;查询应该更快,但可能会过时。

    【讨论】:

    • 感谢您的想法 - 我会尝试并报告。另外,我忘记在问题中指定这一点 - 所有查询都需要能够在不连接到在线数据库的情况下执行。
    【解决方案3】:

    我的解决方案:

    我尝试了三种不同的方法:

    • 使用 SQL 的 ADO 连接对象(最慢)
    • VSTO 和 Excel 的自动过滤器(可靠)
    • LINQ to XML(最快)

    LINQ to XML 提供了最佳性能。我将表格转换为 XML 文件:


    然后,在我的代码中,我使用 StringReader 来引入 XMLwellData 文件(保存为项目资源)。

        'welldoc will be the file to do queries on using LINQ to XML
        Dim stream As System.IO.StringReader
        stream = New StringReader(My.Resources.XMLwellData)
    
        welldoc = XDocument.Load(stream)
    
        'clean up stream now that it's no longer needed
        stream.Close()
        stream.Dispose()
    
    
        '***** later in the code perform my query on XML file *********
    
            Dim query = _
         From well In welldoc.<wellList>.<well> _
         Where well.<name>.Value Like "TOMCHUCK 21-30" _
         Select well
    
         For Each well in query
    
            MessageBox.Show(well.<padgroup>.value)
    
         Next
    

    这非常简单,可以做我想做的事,而且最棒的是它很快。

    感谢您的所有帮助和建议。这对我来说很重要。


    使用 Excel 的自动过滤器的替代方法


    如果您尝试使用其他答案中建议的代码,则只会过滤两个值:

        worksheet.Cells.AutoFilter(column, criteria, Excel.XlAutoFilterOperator.xlOr);
    


    因此,对于filter with multiple criteria using Excel's Auotfilter,您必须将参数作为数组传递并在 xlFilterValues 上进行过滤。


        Dim wrkbk As Excel.Workbook
        Dim wrksht As Excel.Worksheet
        Dim myRange As Excel.Range
        Dim cell As Excel.Range
    
        'You would add all of your wellnames to search to this List
        Dim wellNames As New List(Of String)
    
        wrksht = wrkbk.Sheets(1)
    
        'In my excel file there is a Named Range which includes the all the information
        myRange = wrksht.Range("WellLookUpTable")
    
        'Notice, for `Criteria1:=` you MUST convert the List to an array
        With wrksht.Range("WellLookUpTable")
            .AutoFilter(Field:=2, Criteria1:=wellNames.ToArray, Operator:=Excel.XlAutoFilterOperator.xlFilterValues)
        End With
    
        myRange = wrksht.Range("A2", wrksht.Range("A2").End(Excel.XlDirection.xlDown)).Cells.SpecialCells(Excel.XlCellType.xlCellTypeVisible)
    
        For Each cell In myRange
    
            'column 11 is padgroup
            MessageBox.Show(cell.Offset(0, 11).Value)
    
        Next
    

    【讨论】:

    • 有趣的解决方案,但是,它是苹果到橘子。 ADO 和 VSTO 都查询存储在 Excel 工作表中的数据,而 XML-LINQ 查询编译成资源的 XML 文件。前两个允许在 Excel UI 中创建/修改/删除数据,后者不允许。最快的选择是将字符串常量的索引数组编译到您的 .Net 源代码中;)很高兴它为您解决。
    • @SteveJansen 我同意 - 我很难说一个真的比另一个更好。我只知道对于我的应用程序,XML 解决方案提供了我所需要的。再次感谢您的回答。浏览代码教会了我一些事情
    猜你喜欢
    • 2012-07-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-17
    • 1970-01-01
    • 2020-01-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多