【问题标题】:PostgreSQL: Order by multiple column weightsPostgreSQL:按多列权重排序
【发布时间】:2018-02-23 22:46:53
【问题描述】:

我使用的是 PostgreSQL 9.4。我有一个 resources 表,其中包含以下列:

id
name
provider
description
category

假设这些列都不是必需的(id 除外)。我希望资源具有完成级别,这意味着每列具有 NULL 值的资源将处于 0% 完成级别。

现在,每列都有一个百分比权重。比方说:

name: 40%
provider: 30%
description: 20%
category: 10%

因此,如果资源具有提供者和类别,则其完成级别为60%

这些权重百分比可能随时更改,因此始终包含完成级别值的 completion_level 列将无法解决(可能有数百万资源)。例如,在任何时候,description 的百分比权重都可能从 20% 减少到 10%,类别的权重从 10% 减少到 20%。也许甚至可以创建其他列并拥有自己的权重。

最终目标是能够按完成级别对资源进行排序。

我不确定如何处理这个问题。我目前正在使用 Rails,因此几乎所有与数据库的交互都是通过 ORM 进行的,我相信在这种情况下不会有太大帮助。

我发现的唯一一个有点类似于解决方案(而不是真的)的查询是执行以下操作:

SELECT * from resources
ORDER BY CASE name IS NOT NULL AND
              provider IS NOT NULL AND
              description is NOT NULL AND
              category IS NOT NULL THEN 100
WHEN name is NULL AND provider IS NOT NULL...

但是,我必须通过每种可能的组合进行变异,这非常糟糕。

【问题讨论】:

    标签: ruby-on-rails postgresql rails-activerecord


    【解决方案1】:

    SQL Fiddle 中添加一个权重表:

    PostgreSQL 9.6 架构设置

    CREATE TABLE resource_weights
        (  id int primary key check(id = 1)
         , name numeric
         , provider numeric
         , description numeric
         , category numeric);
    
    INSERT INTO resource_weights
        (id, name, provider, description, category)
    VALUES
        (1, .4, .3, .2, .1);
    
    CREATE TABLE resources
        (  id int
         , name varchar(50)
         , provider varchar(50)
         , description varchar(50)
         , category varchar(50));
    
    INSERT INTO resources
        (id, name, provider, description, category)
    VALUES
        (1, 'abc', 'abc', 'abc', 'abc'),
        (2, NULL, 'abc', 'abc', 'abc'),
        (3, NULL, NULL, 'abc', 'abc'),
        (4, NULL, 'abc', NULL, NULL);
    

    然后像这样在运行时计算你的权重

    查询 1

    select r.*
         , case when r.name is null then 0 else w.name end
         + case when r.provider is null then 0 else w.provider end
         + case when r.description is null then 0 else w.description end
         + case when r.category is null then 0 else w.category end weight
      from resources r
     cross join resource_weights w
     order by weight desc
    

    Results

    | id |   name | provider | description | category | weight |
    |----|--------|----------|-------------|----------|--------|
    |  1 |    abc |      abc |         abc |      abc |      1 |
    |  2 | (null) |      abc |         abc |      abc |    0.6 |
    |  3 | (null) |   (null) |         abc |      abc |    0.3 |
    |  4 | (null) |      abc |      (null) |   (null) |    0.3 |
    

    【讨论】:

      【解决方案2】:

      SQL 的 ORDER BY 几乎可以通过任何表达式对事物进行排序;特别是,您可以按总和订购。 CASE 也相当通用(如果有点冗长)和一个表达式,所以你可以说:

      case when name is not null then 40 else 0 end
      

      它或多或少等同于 Ruby 中的 name.nil?? 0 : 40

      把它们放在一起:

      order by case when name        is not null then 40 else 0 end
             + case when provider    is not null then 30 else 0 end
             + case when description is not null then 20 else 0 end
             + case when category    is not null then 10 else 0 end
      

      有点冗长,但它会做正确的事情。将其转换为 ActiveRecord 相当容易:

      query.order(Arel.sql(%q{
          case when name        is not null then 40 else 0 end
        + case when provider    is not null then 30 else 0 end
        + case when description is not null then 20 else 0 end
        + case when category    is not null then 10 else 0 end
      }))
      

      或在另一个方向:

      query.order(Arel.sql(%q{
          case when name        is not null then 40 else 0 end
        + case when provider    is not null then 30 else 0 end
        + case when description is not null then 20 else 0 end
        + case when category    is not null then 10 else 0 end
        desc
      }))
      

      您需要 Arel.sql 调用以避免 Rails 5.2+ 中的弃用警告,因为他们不再希望您使用 order(some_string),他们只希望您按属性排序,除非您想跳过一些障碍说你是认真的。

      【讨论】:

        【解决方案3】:

        这样总结权重:

        SELECT * FROM resources
        ORDER  BY (CASE WHEN name        IS NULL THEN 0 ELSE 40 END
                 + CASE WHEN provider    IS NULL THEN 0 ELSE 30 END
                 + CASE WHEN description IS NULL THEN 0 ELSE 20 END
                 + CASE WHEN category    IS NULL THEN 0 ELSE 10 END) DESC;
        

        【讨论】:

          【解决方案4】:

          这就是我的做法。

          第一:权重

          由于您说权重会不时变化,因此您必须创建一个结构来处理这些变化。它可以是一个简单的表。对于这个解决方案,它将被称为权重。

          -- Table: weights
          CREATE TABLE weights(id serial, table_nane text, column_name text, weight numeric(5,2));
          
          id | table_name | column_name  | weight
          ---+------------+--------------+--------
          1  | resources  | name         | 40.00
          2  | resources  | provider     | 30.00
          3  | resources  | description  | 20.00
          4  | resources  | category     | 10.00
          

          因此,当您需要将类别从 10 更改为 20 或/和描述从 20 更改为 10 时,您需要更新此结构。

          第二个:completion_level

          既然你说你可以有几百万行,那么在表resources中有completion_level列是可以的;出于效率目的。

          查询completion_level 工作,您可以在视图中看到它。但是当您需要快速简单的数据并且您有 MILLIONS 行时,最好将数据设置为“default”列或另一个表中。

          当您拥有一个视图时,每次运行它时,它都会重新创建数据。当你已经把它放在桌子上时,它很快,你不必重新创建任何东西,只需查询数据。

          但是如何处理完成级别? TRIGGERS

          您必须为resources 表创建触发器。因此,每当您更新或插入数据时,它都会创建完成级别。

          首先将列添加到resources 表中

          ALTER TABLE resources ADD COLUMN completion_level numeric(5,2);
          

          然后你创建触发器:

          CREATE OR REPLACE FUNCTION update_completion_level() RETURNS trigger AS $$
          BEGIN
          NEW.completion_level := (
                 CASE WHEN NEW.name IS NULL THEN 0 
                  ELSE (SELECT weight FROM weights WHERE column_name='name') END
               + CASE WHEN NEW.provider    IS NULL THEN 0
                  ELSE (SELECT weight FROM weights WHERE column_name='provider') END
               + CASE WHEN NEW.description IS NULL THEN 0
                  ELSE (SELECT weight FROM weights WHERE column_name='description') END
               + CASE WHEN NEW.category    IS NULL THEN 0
                  ELSE (SELECT weight FROM weights WHERE column_name='category') END
              );
          RETURN NEW;
          END $$ LANGUAGE plpgsql;
          
          CREATE TRIGGER resources_completion_level
            BEFORE INSERT OR UPDATE
            ON resources
            FOR EACH ROW
            EXECUTE PROCEDURE update_completion_level();
          

          注意:表weights 有一个名为table_name 的列;以防万一您想将此功能扩展到其他表。在这种情况下,您应该更新触发器并在查询中添加 AND table_name='resources'

          使用此触发器,每次更新或插入时,您都会准备好 completion_level,因此获取此数据将是对 resources 表的简单查询;)

          第三:旧数据和权重更新呢?

          由于触发器仅适用于更新和插入,那么旧数据呢?或者如果我更改列的权重会怎样?

          好吧,对于这些情况,您可以使用 函数 为每一行重新创建所有 completion_level

          CREATE OR REPLACE FUNCTION update_resources_completion_level() RETURNS void AS $$
          BEGIN
              UPDATE resources set completion_level = (
                 CASE WHEN name IS NULL THEN 0 
                  ELSE (SELECT weight FROM weights WHERE column_name='name') END
               + CASE WHEN provider IS NULL THEN 0
                  ELSE (SELECT weight FROM weights WHERE column_name='provider') END
               + CASE WHEN description IS NULL THEN 0
                  ELSE (SELECT weight FROM weights WHERE column_name='description') END
               + CASE WHEN category IS NULL THEN 0
                  ELSE (SELECT weight FROM weights WHERE column_name='category') END
              );
          END $$ LANGUAGE plpgsql;
          

          所以每次更新权重或更新旧数据时,只需运行函数

          SELECT update_resources_completion_level();
          

          最后:如果我添加列会怎样?

          好吧,您必须在weights 表中插入新列并更新函数(触发器和update_resources_completion_level())。设置好所有内容后,运行函数update_resources_completion_level() 以根据更改设置所有权重:D

          【讨论】:

            猜你喜欢
            • 2019-08-24
            • 1970-01-01
            • 2019-01-17
            • 1970-01-01
            • 1970-01-01
            • 2011-10-22
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多