PostgreSQL:按多列权重排序

时间:2018-02-23 22:46:53

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

我正在使用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...

然而,我必须通过各种可能的组合进行变异,这非常糟糕。

4 个答案:

答案 0 :(得分:1)

总结这样的权重:

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;

答案 1 :(得分:1)

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),他们只是希望您按属性排序,除非您想跳过一些箍,说你真的是这个意思。

答案 2 :(得分: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 |

答案 3 :(得分:1)

我就是这样做的。

第一:权重

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

-- 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

由于您说您可能有数百万行,因此可以在表completion_level中添加resources列;为了提高效率。

进行查询以获取completion_level的作品,您可以在视图中使用它。但是当您需要数据快速而简单并且您有 MILLIONS 行时,最好通过&#34; 默认设置数据&#34;在一列或另一个表中。

当您有视图时,每次运行它时,它都会重新创建数据。如果您已将它放在桌面上,那么它速度很快,您不必重新创建任何内容,只需查询数据。

但是如何处理completion_level? 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;

因此,每次更新权重或更新OLD数据时,只需运行函数

即可
SELECT update_resources_completion_level();

最后:如果我添加列怎么办?

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