【问题标题】:Microsoft Access condense multiple lines in a tableMicrosoft Access 在表格中压缩多行
【发布时间】:2011-07-07 15:47:34
【问题描述】:

我有一个关于 MS Access 2007 的问题,希望有人能解答。我有一个长而简单的表格,其中包含客户姓名和交货日期。我想通过在一个新字段“ALLDays”中列出名称和所有日期来总结此表,同时仍保留所有数据。

源表如下所示:

Name         Day  
CustomerA    Monday  
CustomerA    Thursday  
CustomerB    Tuesday  
CustomerB    Friday  
CustomerC    Wednesday  
CustomerC    Saturday  

我想要一个返回如下结果的查询:

Name         ALLDays  
CustomerA    Monday, Thursday  
CustomerB    Tuesday, Friday  
CustomerC    Wednesday, Saturday  

谢谢。

【问题讨论】:

  • 参见stackoverflow.com/questions/1920552/…,特别是关于 ADODB 记录集的注释。
  • 通常,您会使用交叉表查询。转到“创建”选项卡以创建查询,然后为设计视图查询设计。添加表格以查看内容。然后选择“设计”选项卡,“交叉表”。

标签: ms-access


【解决方案1】:

Thomas's GetList function 很棒,但是对于我的大型数据库来说太慢了。我认为减速可能是由于使用了 ADO 造成的,所以我重写了 GetList 以使用原生 DAO 调用。

这个版本大约快 3 倍

Option Compare Database
Option Explicit

' Concatenate multiple values in a query. From:
' https://stackoverflow.com/questions/5174362/microsoft-access-condense-multiple-lines-in-a-table/5174843#5174843
'
' Note that using a StringBuilder class from here:
' https://codereview.stackexchange.com/questions/67596/a-lightning-fast-stringbuilder/154792#154792
' offers no code speed up

Public Function GetListOptimal( _
    SQL As String, _
    Optional fieldDelim As String = ", ", _
    Optional recordDelim As String = vbCrLf _
    ) As String

    Dim dbs As Database
    Dim rs As Recordset
    Dim records() As Variant
    Dim recordCount As Long

    ' return values
    Dim ret As String
    Dim recordString As String
    ret = ""
    recordString = ""

    ' index vars
    Dim recordN As Integer
    Dim fieldN As Integer
    Dim currentField As Variant

    ' array bounds vars
    Dim recordsLBField As Integer
    Dim recordsUBField As Integer
    Dim recordsLBRecord As Integer
    Dim recordsUBRecord As Integer

    ' get data from db
    Set dbs = CurrentDb
    Set rs = dbs.OpenRecordset(SQL)
    recordCount = rs.recordCount

    ' Guard against no records returned
    If recordCount = 0 Then
        GetListOptimal = ""
        Exit Function
    End If

    records = rs.GetRows(recordCount)

    ' assign bounds of data
    recordsLBField = LBound(records, 1)    ' should always be 0, I think
    recordsUBField = UBound(records, 1)
    recordsLBRecord = LBound(records, 2)    ' should always be 0, I think
    recordsUBRecord = UBound(records, 2)

    ' FYI vba will loop thorugh every For loop at least once, even if
    ' both LBound and UBound are 0.  We already checked to ensure that
    ' there is at least one record, and that also ensures that
    ' there is at least one record.  I think...
    ' Can a SQL query return >0 records with 0 fields each?
    For recordN = recordsLBRecord To recordsUBRecord
        For fieldN = recordsLBField To recordsUBField
            ' Only add fieldDelim after at least one field
            If recordString <> "" Then
                recordString = recordString & fieldDelim
            End If

            ' records is indexed (field, record) for some reason
            currentField = records(fieldN, recordN)

            ' Guard against null-valued fields
            If Not IsNull(currentField) Then
                recordString = recordString & CStr(currentField)
            End If
        Next fieldN

        ' Only add recordDelim after at least one record
        If ret <> "" Then
            ret = ret & recordDelim
        End If
        ret = ret & recordString

        recordString = ""   ' Re-initialize to ensure no old data problems
    Next recordN

    ' adds final recordDelim at end output
    ' not sure when this might be a good idea
    ' TODO: Implement switch parameter to control
    ' this, rather than just disabling it
    ' If ret <> "" Then
    '    ret = ret & recordDelim
    ' End If

    ' Cleanup db objects
    Set dbs = Nothing
    Set rs = Nothing

    GetListOptimal = ret
    Exit Function
End Function

调用签名是相同的,尽管可能存在它们给出不同结果的极端情况。

此版本还有一个好处是不需要您添加手动引用as MarredCheese pointed out

【讨论】:

  • 对于上述改进的解决方案,小修复。因为recordCount 只有在移动到最后一条记录时才会返回正确的结果If rs.recordCount &gt; 0 Then rs.MoveLast recordCount = rs.recordCount rs.MoveFirst End If
【解决方案2】:

这是一个不需要 VBA 的简单解决方案。它使用更新查询将值连接到字段上。

我会用我正在使用的例子来展示它。

我有一个表“emails_by_team”,它有两个字段“team_id”和“email_formatted”。我想要的是在一个字符串中收集给定团队的所有电子邮件。

1) 我创建了一个表“team_more_info”,其中包含两个字段:“team_id”和“team_emails”

2) 用“emails_by_team”中的所有“team_id”填充“team_more_info”

3) 创建一个更新查询,将“emails_by_team”设置为 NULL
查询名称:team_email_collection_clear

UPDATE team_more_info 
SET team_more_info.team_emails = Null;

4) 这里的诀窍是:创建更新查询
查询名称:team_email_collection_update

UPDATE team_more_info INNER JOIN emails_by_team 
  ON team_more_info.team_id = emails_by_team.team_id 
SET team_more_info.team_emails = 
    IIf(IsNull([team_emails]),[email_formatted],[team_emails] & "; " & [email_formatted]);

5) 为使信息保持最新,创建一个在需要时运行两个查询的宏

第一:team_email_collection_clear

第二个:team_email_collection_update

QED

【讨论】:

    【解决方案3】:

    由于这只是一小部分选项,另一种没有 VBA 的方法是设置一系列 IIF 语句并连接结果。

    SELECT name, 
       IIF(SUM(IIF(day = "Monday",1,0)) >0, "Monday, ") & 
       IIF(SUM(IIF(day = "Tuesday",1,0)) >0, "Tuesday, ") & 
       IIF(SUM(IIF(day = "Wednesday",1,0)) >0, "Wednesday, ") & 
       IIF(SUM(IIF(day = "Thursday",1,0)) >0, "Thursday, ") &
       IIF(SUM(IIF(day = "Friday",1,0)) >0, "Friday, ") &
       IIF(SUM(IIF(day = "Saturday",1,0)) >0, "Saturday, ") &
       IIF(SUM(IIF(day = "Sunday",1,0)) >0, "Sunday, ") AS AllDays
    FROM Table1
    GROUP BY name
    

    如果你是完美主义者,你甚至可以像这样去掉最后一个逗号

    SELECT name, 
    LEFT(
       IIF(SUM(IIF(day = "Monday",1,0)) >0, "Monday, ") & 
       IIF(SUM(IIF(day = "Tuesday",1,0)) >0, "Tuesday, ") & 
       IIF(SUM(IIF(day = "Wednesday",1,0)) >0, "Wednesday, ") & 
       IIF(SUM(IIF(day = "Thursday",1,0)) >0, "Thursday, ") &
       IIF(SUM(IIF(day = "Friday",1,0)) >0, "Friday, ") &
       IIF(SUM(IIF(day = "Saturday",1,0)) >0, "Saturday, ") &
       IIF(SUM(IIF(day = "Sunday",1,0)) >0, "Sunday, "),
    LEN(
       IIF(SUM(IIF(day = "Monday",1,0)) >0, "Monday, ") & 
       IIF(SUM(IIF(day = "Tuesday",1,0)) >0, "Tuesday, ") & 
       IIF(SUM(IIF(day = "Wednesday",1,0)) >0, "Wednesday, ") & 
       IIF(SUM(IIF(day = "Thursday",1,0)) >0, "Thursday, ") &
       IIF(SUM(IIF(day = "Friday",1,0)) >0, "Friday, ") &
       IIF(SUM(IIF(day = "Saturday",1,0)) >0, "Saturday, ") &
       IIF(SUM(IIF(day = "Sunday",1,0)) >0, "Sunday, ")
    ) - 2
    )
    AS AllDays
    FROM Table1
    GROUP BY name
    

    您也可以考虑将它们保存在单独的列中,因为如果从其他人访问此查询,这可能会更有用。例如,通过这种方式只查找具有星期二的实例会更容易。比如:

    SELECT name, 
    IIF(SUM(IIF(day = "Monday",1,0)) >0, "Monday") AS Monday,  
    IIF(SUM(IIF(day = "Tuesday",1,0)) >0, "Tuesday") AS Tuesday,
    IIF(SUM(IIF(day = "Wednesday",1,0)) >0, "Wednesday") AS Wednesday,
    IIF(SUM(IIF(day = "Thursday",1,0)) >0, "Thursday") AS Thursday,
    IIF(SUM(IIF(day = "Friday",1,0)) >0, "Friday") AS Friday,
    IIF(SUM(IIF(day = "Saturday",1,0)) >0, "Saturday") AS Saturday,
    IIF(SUM(IIF(day = "Sunday",1,0)) >0, "Sunday") AS Sunday
    FROM Table1
    GROUP BY name
    

    【讨论】:

      【解决方案4】:

      通常你必须编写一个函数来创建一个连接列表。这是我用过的:。

      Public Function GetList(SQL As String _
                                  , Optional ColumnDelimeter As String = ", " _
                                  , Optional RowDelimeter As String = vbCrLf) As String
      'PURPOSE: to return a combined string from the passed query
      'ARGS:
      '   1. SQL is a valid Select statement
      '   2. ColumnDelimiter is the character(s) that separate each column
      '   3. RowDelimiter is the character(s) that separate each row
      'RETURN VAL: Concatenated list
      'DESIGN NOTES:
      'EXAMPLE CALL: =GetList("Select Col1,Col2 From Table1 Where Table1.Key = " & OuterTable.Key)
      
      Const PROCNAME = "GetList"
      Const adClipString = 2
      Dim oConn As ADODB.Connection
      Dim oRS As ADODB.Recordset
      Dim sResult As String
      
      On Error GoTo ProcErr
      
      Set oConn = CurrentProject.Connection
      Set oRS = oConn.Execute(SQL)
      
      sResult = oRS.GetString(adClipString, -1, ColumnDelimeter, RowDelimeter)
      
      If Right(sResult, Len(RowDelimeter)) = RowDelimeter Then
          sResult = Mid$(sResult, 1, Len(sResult) - Len(RowDelimeter))
      End If
      
      GetList = sResult
      oRS.Close
      oConn.Close
      
      CleanUp:
          Set oRS = Nothing
          Set oConn = Nothing
      
      Exit Function
      ProcErr:
          ' insert error handler
          Resume CleanUp
      
      End Function
      

      Remou 的版本增加了一个特性,你可以传递一个值数组而不是 SQL 语句。


      示例查询可能如下所示:

      SELECT SourceTable.Name
          , GetList("Select Day From SourceTable As T1 Where T1.Name = """ & [SourceTable].[Name] & """","",", ") AS Expr1
      FROM SourceTable
      GROUP BY SourceTable.Name;
      

      【讨论】:

      • 顺便说一句,我显然已经删除了使用 PROCNAME 常量的错误处理程序。
      • 非常感谢您的帮助,但我在这方面还很陌生,您的回答有点超出我的水平。你能告诉我具体情况吗?我的源表称为 CustomerRoutes。这两个字段是名称和日期。所以我创建一个模块并编写什么确切的代码?以及我究竟如何调用 GetList 函数,我假设我在 SQL 模式下创建了一个新查询并编写了类似 Select("Name, Day from...?谢谢。
      • @Sam_H - 首先,您需要将代码放入模块中。然后,您使用 OP 中提到的“SourceData”中的不同客户创建一个普通查询(因此,每个客户一行)。您可能需要将其设为 Group By 查询。然后,您将在该查询中的一列中输入一个公式:=GetList("Select Day From SourceData As S1 Where S1.Day = " &amp; SourceData.Col)。通过这种方式,您正在使用主查询中的列在 GetList 函数中构建 Select 查询。
      • @MartinVerner - 这不是 GetString 函数的限制。我怀疑您从中调用该函数的查询正在将列截断为 255 个字符。在一系列场景中,备忘录/长文本字段(这实际上是我们在这里想要的)被 Access 悄悄截断。此链接已过时,但谈到了我认为当前版本的 Access 中仍然存在的相同问题:allenbrowne.com/ser-63.html
      • +1。在Dim oConn As ADODB.Connection 收到错误user-defined type not defined 的读者请注意:这是因为“您需要先设置对'Microsoft ActiveX Data Objects'(简称ADO)的引用。选择工具-> 引用。从弹出的对话框中向上,向下滚动,直到找到类似 Microsoft ActiveX Data Objects 2.7 Library 的条目(选择您看到的最高数字)。选中此条目旁边的复选框,然后单击确定。”来源:p2p.wrox.com/excel-vba/…
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-21
      • 1970-01-01
      • 2015-10-23
      相关资源
      最近更新 更多