【问题标题】:Crosstab with a large or undefined number of categories具有大量或未定义类别的交叉表
【发布时间】:2012-10-10 21:55:40
【问题描述】:

我真正的问题与记录大量反病毒产品中的哪一个同意给定样本是给定反病毒家族的成员有关。该数据库有数百万个样本,每个样本都有数十个反病毒产品投票。我想问一个查询,例如“对于包含名称 'XYZ' 的恶意软件,哪个样本得票最多,哪些供应商投票支持它?”并得到如下结果:

"BadBadVirus"  
                     V1  V2  V3  V4  V5  V6  V7  
Sample 1 - 4 votes    1   0   1   0   0   1   1      
Sample 2 - 5 votes    1   0   1   0   1   1   1   
Sample 3 - 5 votes    1   0   1   0   1   1   1  

 total     14         3       3       2   3   3  

这可能用来告诉我供应商 2 和供应商 4 要么不知道怎么做 检测此恶意软件,或者他们将其命名为不同的名称。


我将尝试稍微概括我的问题,同时希望不会破坏您帮助我的能力。假设我有五个选民(Alex、Bob、Carol、Dave、Ed)被要求查看五张照片(P1、P2、P3、P4、P5)并决定照片的“主要主题”是什么。对于我们的示例,我们将假设它们仅限于“Cat”、“Dog”或“Horse”。不是每个选民都会对每件事都投票。

数据以这种形式存在于数据库中:

Photo, Voter, Decision
(1, 'Alex', 'Cat')
(1, 'Bob', 'Dog')
(1, 'Carol', 'Cat')
(1, 'Dave', 'Cat')
(1, 'Ed', 'Cat')
(2, 'Alex', 'Cat')
(2, 'Bob', 'Dog')
(2, 'Carol', 'Cat')
(2, 'Dave', 'Cat')
(2, 'Ed', 'Dog')
(3, 'Alex', 'Horse')
(3, 'Bob', 'Horse')
(3, 'Carol', 'Dog')
(3, 'Dave', 'Horse')
(3, 'Ed', 'Horse')
(4, 'Alex', 'Horse')
(4, 'Bob', 'Horse')
(4, 'Carol', 'Cat')
(4, 'Dave', 'Horse')
(4, 'Ed', 'Horse')
(5, 'Alex', 'Dog')
(5, 'Bob', 'Cat')
(5, 'Carol', 'Cat')
(5, 'Dave', 'Cat')
(5, 'Ed', 'Cat')

目标是,给定我们正在寻找的照片主题,我们想知道有多少选民认为这是该照片的主要观点,但也列出了哪些选民认为。

Query for: "Cat"
      Total  Alex  Bob Carol Dave Ed
1 -     4      1    0    1     1   1
2 -     3      1    0    1     1   0 
3 -     0      0    0    0     0   0 
4 -     1      0    0    1     0   0 
5 -     4      0    1    1     1   1
------------------------------------
total  12      2    1    4     3   2 

Query for: "Dog"
      Total  Alex  Bob Carol Dave Ed
1 -     1     0      1   0    0    0
2 -     2     0      1   0    0    1
3 -     1     0      0   1    0    0 
4 -     0     0      0   0    0    0 
5 -     1     1      0   0    0    0 
------------------------------------
total   5     1      2   1    0    1 

我可以以我存储的格式处理数据吗?

我很难获得执行此操作的查询 - 尽管将数据转储出来然后编写一个程序来执行此操作很简单,但如果可以的话,我真的希望能够在数据库中执行此操作.

感谢您的任何建议。

【问题讨论】:

    标签: sql postgresql pivot aggregate crosstab


    【解决方案1】:
    create table vote (Photo integer, Voter text, Decision text);
    insert into vote values
    (1, 'Alex', 'Cat'),
    (1, 'Bob', 'Dog'),
    (1, 'Carol', 'Cat'),
    (1, 'Dave', 'Cat'),
    (1, 'Ed', 'Cat'),
    (2, 'Alex', 'Cat'),
    (2, 'Bob', 'Dog'),
    (2, 'Carol', 'Cat'),
    (2, 'Dave', 'Cat'),
    (2, 'Ed', 'Dog'),
    (3, 'Alex', 'Horse'),
    (3, 'Bob', 'Horse'),
    (3, 'Carol', 'Dog'),
    (3, 'Dave', 'Horse'),
    (3, 'Ed', 'Horse'),
    (4, 'Alex', 'Horse'),
    (4, 'Bob', 'Horse'),
    (4, 'Carol', 'Cat'),
    (4, 'Dave', 'Horse'),
    (4, 'Ed', 'Horse'),
    (5, 'Alex', 'Dog'),
    (5, 'Bob', 'Cat'),
    (5, 'Carol', 'Cat'),
    (5, 'Dave', 'Cat'),
    (5, 'Ed', 'Cat')
    ;
    

    猫的查询:

    select photo,
        alex + bob + carol + dave + ed as Total,
        alex, bob, carol, dave, ed
    from crosstab($$
        select
            photo, voter,
            case decision when 'Cat' then 1 else 0 end
        from vote
        order by photo
        $$,'
        select distinct voter
        from vote
        order by voter
        '
    ) as (
        photo integer,
        Alex integer,
        Bob integer,
        Carol integer,
        Dave integer,
        Ed integer
    );
     photo | total | alex | bob | carol | dave | ed 
    -------+-------+------+-----+-------+------+----
         1 |     4 |    1 |   0 |     1 |    1 |  1
         2 |     3 |    1 |   0 |     1 |    1 |  0
         3 |     0 |    0 |   0 |     0 |    0 |  0
         4 |     1 |    0 |   0 |     1 |    0 |  0
         5 |     4 |    0 |   1 |     1 |    1 |  1
    

    如果选民数量很大或未知,则可以动态完成:

    do $do$
    declare
    voter_list text;
    r record;
    begin
    
    drop table if exists pivot;
    
    voter_list := (
        select string_agg(distinct voter, ' ' order by voter) from vote
        );
    
    execute(format('
        create table pivot (
            decision text,
            photo integer,
            Total integer,
            %1$s
        )', (replace(voter_list, ' ', ' integer, ') || ' integer')
    ));
    
    for r in
    select distinct decision from vote
    loop
        execute (format($f$
            insert into pivot
            select
                %3$L as decision,
                photo,
                %1$s as Total,
                %2$s
            from crosstab($ct$
                select
                    photo, voter,
                    case decision when %3$L then 1 else 0 end
                from vote
                order by photo
                $ct$,$ct$
                select distinct voter
                from vote
                order by voter
                $ct$
            ) as (
                photo integer,
                %4$s
            );$f$,
            replace(voter_list, ' ', ' + '),
            replace(voter_list, ' ', ', '),
            r.decision,
            replace(voter_list, ' ', ' integer, ') || ' integer'
        ));
    end loop;
    end; $do$;
    

    上面的代码创建了包含所有决定的表格数据透视表:

    select * from pivot where decision = 'Cat';
    

    【讨论】:

    • 非常感谢您,Clodoaldo!选民的数量并非无法控制(44),但他们投票的东西数量巨大(300万)。我会回来反馈即将发生的事情。谢谢!
    【解决方案2】:

    您的愿望意味着传输一些数据(名称) 进入列标题,即结果表的架构。 因为这介于不方便和不可能之间, 我建议只对 sql 中的数据进行排序和求和, 并在数据库之外完成其余的工作。

    SELECT Photo, Voter
    FROM data
    WHERE Decision = '...'
    ORDER BY Photo, Voter
    

    SELECT Photo, COUNT(*) AS Total
    FROM data
    WHERE Decision = '...'
    GROUP BY Photo
    ORDER BY Photo;
    

    【讨论】:

      【解决方案3】:

      使用与 Clodoaldo 相同的示例数据(“创建表投票...”)并使用 plpythonu 函数 make_pivot_table(如下),您可以运行:

      create temp table pivot_data on commit drop as 
          select * from vote where decision = 'Cat' union select photo, null, null from vote;
      
      select * from make_pivot_table('{photo}', 'voter',  'decision', 'count', 'pivot_data',
        'pivot_result', false);
      
      select * from pivot_result order by photo;
      

      make_pivot_table 函数定义为:

      -- make_pivot_table
      -- python version 0.9
      -- last edited 2015-08-11 
      
      create or replace function
       make_pivot_table(row_headers text[], category_field text, value_field text,
        value_action text, input_table text, output_table text, keep_result boolean)
      returns void as
      $$
      # imports
      from collections import defaultdict
      import operator
      import string
      
      # constants
      BATCH_SIZE = 100
      VALID_ACTIONS = ('count', 'sum', 'min', 'max')
      NULL_CATEGORY_NAME = 'NULL_CATEGORY'
      TOTAL_COL = 'total'
      
      # functions
      def table_exists(tablename):
          plan = plpy.prepare("""select table_schema, table_name from
              information_schema.Tables where table_schema not in ('information_schema',
              'pg_catalog') and table_name = $1""", ["text"])
          rows = plpy.execute(plan, [input_table], 2)
          return bool(rows)
      
      def make_rowkey(row):
          return tuple([row[header] for header in row_headers])
      
      def quote_if_needed(value):
          return plpy.quote_literal(value) if isinstance(value, basestring) else str(value)
      
      # assumes None is never a value in the dct
      def update_if(dct, key, new_value, op, result=True):
          current_value = dct.get(key)
          if current_value is None or op(value, current_value) == result:
              dct[key] = new_value
      
      def update_output_table(output_table, row_headers, colname, value):
          pg_value = plpy.quote_literal(value) if isinstance(value, basestring) else value
          sql = 'update %s set %s = %s where ' % (output_table, plpy.quote_ident(colname), 
                                                  pg_value)
          conditions = []
          for index, row_header in enumerate(row_headers):
              conditions.append('%s = %s' % (plpy.quote_ident(row_header),
                                             quote_if_needed(rowkey[index])))
          sql += ' and '.join(conditions)
          plpy.execute(sql)
      
      
      # -----------------
      
      if not table_exists(input_table):
          plpy.error('input_table %s dones not exist' % input_table)
      
      if value_action not in VALID_ACTIONS:
          plpy.error('%s is not a recognised action' % value_action)
      
      # load the data into a dict
      count_dict = defaultdict(int)
      sum_dict = defaultdict(float)
      total_dict = defaultdict(float)
      min_dict = dict()
      max_dict = dict()
      categories_seen = set()
      rowkeys_seen = set()
      do_total = value_action in ('count', 'sum')
      
      cursor = plpy.cursor('select * from %s' % plpy.quote_ident(input_table))
      while True:
          rows = cursor.fetch(BATCH_SIZE)
          if not rows:
              break
          for row in rows:
              rowkey = make_rowkey(row)
              rowkeys_seen.add(rowkey)
              category = row[category_field]           
              value = row[value_field]
              dctkey = (rowkey, category)
      
              # skip if value field is null
              if value is None:
                  continue
      
              categories_seen.add(category)
      
              if value_action == 'count':
              count_dict[dctkey] += 1
              total_dict[rowkey] += 1
          if value_action == 'sum':
                  sum_dict[dctkey] += value
                  total_dict[rowkey] += value
              if value_action == 'min':
                  update_if(min_dict, dctkey, value, operator.lt)
              if value_action == 'max':
                  update_if(max_dict, dctkey, value, operator.gt)
      
      plpy.notice('seen %s summary rows and %s categories' % (len(rowkeys_seen),
                                                              len(categories_seen)))
      
      # get the columns types
      coltype_dict = dict()
      input_type_sql = 'select * from %s where false' % plpy.quote_ident(input_table)
      input_type_result = plpy.execute(input_type_sql)
      for index, colname in enumerate(input_type_result.colnames()):
          coltype_num = input_type_result.coltypes()[index]
          coltype_sql = 'select typname from pg_type where oid = %s' % coltype_num
          coltype = list(plpy.cursor(coltype_sql))[0]
          plpy.notice('%s: %s' % (colname, coltype['typname']))
          coltype_dict[colname] = coltype['typname']
      
      plpy.execute('drop table if exists %s' % plpy.quote_ident(output_table))
      sql_parts = []
      if keep_result:
          sql_parts.append('create table %s (' % plpy.quote_ident(output_table))
      else:
          sql_parts.append('create temp table %s (' % plpy.quote_ident(output_table))
      
      cols = []
      for row_header in row_headers:
          cols.append('%s %s' % (plpy.quote_ident(row_header), coltype_dict[row_header]))
      
      cat_type = 'bigint' if value_action == 'count' else coltype_dict[value_field]
      
      for col in sorted(categories_seen):
          if col is None:
              cols.append('%s %s' % (plpy.quote_ident(NULL_CATEGORY_NAME), cat_type))
          else:
              cols.append('%s %s' % (plpy.quote_ident(col), cat_type))
      
      if do_total:
          cols.append('%s %s' % (TOTAL_COL, cat_type))
      
      sql_parts.append(',\n'.join(cols))
      if keep_result:
          sql_parts.append(')')
      else:
          sql_parts.append(') on commit drop')
      plpy.execute('\n'.join(sql_parts))
      
      dict_map = {'count': count_dict, 'sum': sum_dict, 'min': min_dict, 'max': max_dict }
      value_dict = dict_map[value_action]
      for rowkey in rowkeys_seen:
          sql = 'insert into %s values (' % plpy.quote_ident(output_table)
          sql += ', '.join([quote_if_needed(part) for part in rowkey])
          sql += ')'
          plpy.execute(sql)
      
      if do_total:
          for rowkey, value in total_dict.iteritems():
              update_output_table(output_table, row_headers, TOTAL_COL, value)
      
      for (rowkey, category), value in value_dict.iteritems():
          # put in cateogry value
          colname = NULL_CATEGORY_NAME if category is None else category
          update_output_table(output_table, row_headers, colname, value)
      
      $$ language plpythonu
      

      【讨论】:

        猜你喜欢
        • 2017-11-18
        • 2018-01-05
        • 2021-09-21
        • 2022-06-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多