如何在 PostgreSQL 中应用 CROSS JOIN?

Posted

技术标签:

【中文标题】如何在 PostgreSQL 中应用 CROSS JOIN?【英文标题】:How to apply CROSS JOIN in PostgreSQL? 【发布时间】:2021-02-23 10:28:14 【问题描述】:

DB-Fiddle

CREATE TABLE sales (
    id SERIAL PRIMARY KEY,
    country VARCHAR(255),
    sales_date DATE,
    sales_volume DECIMAL,
    fix_costs DECIMAL
);

INSERT INTO sales
(country, sales_date, sales_volume, fix_costs
)
VALUES 

('DE', '2020-01-03', '500', '0'),
('NL', '2020-01-03', '320', '0'),
('FR', '2020-01-03', '350', '0'),
('None', '2020-01-31', '0', '2000'),

('DE', '2020-02-15', '0', '0'),
('NL', '2020-02-15', '0', '0'),
('FR', '2020-02-15', '0', '0'),
('None', '2020-02-29', '0', '5000'),

('DE', '2020-03-27', '180', '0'),
('NL', '2020-03-27', '670', '0'),
('FR', '2020-03-27', '970', '0'),
('None', '2020-03-31', '0', '4000');

预期结果:

sales_date   |   country    |   sales_volume   |     fix_costs
-------------|--------------|------------------|------------------------------------------
2020-01-03   |     DE       |       500        |     37.95  (= 2000/31 = 64.5 x 0.59)
2020-01-03   |     FR       |       350        |     26.57  (= 2000/31 = 64.5 x 0.41)
2020-01-03   |     NL       |       320        |      0.00
-------------|--------------|------------------|------------------------------------------
2020-02-15   |     DE       |         0        |     86.21  (= 5000/28 = 172.4 x 0.50)  
2020-02-15   |     FR       |         0        |     86.21  (= 5000/28 = 172.4 x 0.50)  
2020-02-15   |     NL       |         0        |      0.00
-------------|--------------|------------------|------------------------------------------    
2020-03-27   |     DE       |       180        |     20.20  (= 4000/31 = 129.0 x 0.16) 
2020-03-27   |     FR       |       970        |    108.84  (= 4000/31 = 129.0 x 0.84)   
2020-03-27   |     NL       |       670        |      0.00
-------------|--------------|------------------|-------------------------------------------

参考 this question 中的解决方案,我尝试应用以下查询以获得预期结果:

SELECT 
    sales_date, 
    country, 
    SUM(sales_volume),
       (CASE WHEN country = 'NL' THEN 0
             WHEN SUM(CASE WHEN country <> 'NL' THEN sales_volume END) OVER (PARTITION BY sales_date) > 0
             THEN ((f.fix_costs/ DAY(LAST_DAY(sales_date))) *
                   sales_volume / NULLIF(SUM(CASE WHEN country <> 'NL' THEN sales_volume END) OVER (PARTITION BY sales_date), 0)
                  )
             ELSE (f.fix_costs / DAY(LAST_DAY(sales_date))) * 1 / SUM(country <> 'NL') OVER (PARTITION by sales_date)
        END) AS imputed_fix_costs
FROM sales s

    CROSS JOIN
     (SELECT 
      LAST_DAY(sales_date) AS month_ld, 
      SUM(fix_costs) AS fix_costs
      FROM sales
      WHERE country = 'None'
      GROUP BY month_ld
     ) f
      ON f.month_ld = LAST_DAY(s.sales_date)

WHERE country <> 'None'
GROUP BY 1,2;

这个查询在 MariaDB 中没有任何问题。 但是,现在我切换到 Postgres 9.5 并且在 CROSS JOIN 上出现错误:

 ERROR:  syntax error at or near "ON"
 LINE 22:       ON f.month_ld = LAST_DAY(s.sales_date)

我需要如何修改查询以使其在 Postgres 9.5 中也能正常工作?

【问题讨论】:

交叉连接不需要连接条件,所以需要去掉ON部分 与您的问题无关,但是:Postgres 9.5 是no longer supported,您应该尽快计划升级。如果你正在迁移,你应该从 Postgres 13 开始 非常感谢旧版本的额外提示。我也尝试删除 ON 部分,但它仍然不起作用:dbfiddle.uk/… 能否解释一下 fix_costs 的逻辑。 das 0.59 来自哪里?为什么 NL 是 0.00? 如果你想要on,请使用join,而不是cross join 【参考方案1】:
    Postgres AFAIK 中没有 LAST_DAY()DAY() 函数。 CROSS JOINs 没有加入条件。

TL;DR: 重新阅读您的代码后,我发现我的结构与您的结构惊人地相似。所以,事实上,我想毕竟不需要这么大的解释。尽管如此,我还是把它作为文档放在那里。

但是,我想,您的问题最重要的部分是如何进行连接。这在我强调的第 2 步中进行了描述。


不清楚,第二个 sales_date 组中的最后一个 fix_costs 部分是如何变成 0.5 的(百分比会产生 除零错误...)

但是,这个查询可以展示解决方法:

step-by-step demo:db<>fiddle

SELECT
    s1.*,
-- 3:
    COALESCE(
        s2.fix_costs / date_part('day', s2.sales_date) *

-- 1:
            CASE WHEN s1.country != 'NL' THEN s1.sales_volume END  
                / NULLIF(SUM(s1.sales_volume) FILTER (WHERE s1.country != 'NL')  OVER (PARTITION BY s1.sales_date), 0)
   , 0)       

FROM sales s1

-- 2:
JOIN sales s2 ON s1.country <> 'None' AND s2.country = 'None' 
    AND date_trunc('month', s1.sales_date) = date_trunc('month', s2.sales_date)

1a) SUM(...) OVER (PARTITION BY ...) 窗口函数返回来自sales_datesales_volumn 值的总和。此结果将作为新列添加到结果中。

1b) 添加FILTER 子句会从此计算中删除country = 'NL' 记录

1c) NULLIF(..., 0) 是为了在计算当前sales_volumn 和先前计算的total 之间的百分比时避免除零(这将发生在第二个sales_date 组中) .

1d) CASE 子句从除法的第一部分删除 NL

1e) 计算fix_costs 值第二部分的百分比(据我了解)

2a) 自然连接条件:一个表没有None国家,第二个只有None国家记录。

2b) date_trunc('month', ...) 将日期标准化为当月的第一天。所以连接条件是:两个表的月份都适合(date_trunc()比较的值都是YYYY-MM-01

3a) date_part('day', ...) 从日期中提取日期。由于您总是添加一个月的最后一天,因此不需要明确计算。因此,可以计算出fix_costs 值的第一部分。

3b) 将两个 fix_costs 部分相乘

3c) COALESCE(..., 0) 由于过滤了NL 记录和DIV BY ZERO 错误处理,fix_costs 列中存在一些NULL 结果,此功能已修复.

TODO:为第二组添加0.5 部分。


另外:

要在 Postgres 中计算当月的最后一天,您必须执行以下操作:

date_trunc('month', the_date) + interval '1 month - 1 day'

date_trunc('month', ...) 计算月份的第一天,如上所述。然后你加一个月得到下个月的第一天。从中您可以减去一天以获得当月的最后一天。

【讨论】:

【参考方案2】:

您可能需要在这里使用 INNER JOIN,而不是 CROSS JOIN。 正如它所说的here, “在 MariaDB 中,CROSS JOIN 的语法等同于 INNER JOIN(它们可以相互替换)。”

在 PostgreSQL 中,CROSS JOIN 的工作方式不同,您的预期行为根本不匹配 - 您可能不想将所有记录相互匹配。只需将“CROSS”替换为“INNER”即可。

但是这段代码也有一个 MariaDB 特有的函数 Last_Day 的问题,它被 here 覆盖。

【讨论】:

【参考方案3】:

DB-Fiddle

SELECT
s.sales_date, 
s.country,
s.sales_volume,
f.fix_costs,

 (CASE WHEN country = 'NL' THEN 0
       
       /* Exclude NL from fixed_costs calculation */
       WHEN SUM(CASE WHEN country <> 'NL' THEN sales_volume ELSE 0 END) OVER (PARTITION BY sales_date) > 0
       THEN ((f.fix_costs/ extract(day FROM (date_trunc('month', sales_date + INTERVAL '1 month') - INTERVAL '1 day'))) *
              sales_volume / 
              NULLIF(SUM(s.sales_volume) FILTER (WHERE s.country != 'NL')  OVER (PARTITION BY s.sales_date), 0)
              )
              
        /* Divide fixed_cots equaly among countries in case of no sale*/      
        ELSE (f.fix_costs / extract(day FROM (date_trunc('month', sales_date + INTERVAL '1 month') - INTERVAL '1 day')))
              / SUM(CASE WHEN country <> 'NL' THEN 1 ELSE 0 END) OVER (PARTITION by sales_date)
              
        END) AS imputed_fix_costs
        
FROM sales s

JOIN

  (SELECT 
  (date_trunc('MONTH', sales_date)) AS month_ld,
  SUM(fix_costs) AS fix_costs
  FROM sales
  WHERE country = 'None'
  GROUP BY month_ld) f ON f.month_ld = date_trunc('month', s.sales_date)

WHERE country NOT IN ('None')
GROUP BY 1,2,3,4
ORDER BY 1;

【讨论】:

以上是关于如何在 PostgreSQL 中应用 CROSS JOIN?的主要内容,如果未能解决你的问题,请参考以下文章

如何应用 cross_val_score 来交叉验证我们自己的模型

如何在 BigQuery 中使用 SPLIT 和 CROSS APPLY 函数

如何在单个 Java 应用程序中使用两个 PostgreSQL DB 版本?

如何在 Django 应用程序中访问 PostgreSQL 数据库

Redshift:将 FULL OUTER 替换为 CROSS JOIN

如何在 JavaEE 应用程序中为 PostgreSQL 热备设置配置连接故障转移?