将 MySQL 视图转换为 Postgres

Posted

技术标签:

【中文标题】将 MySQL 视图转换为 Postgres【英文标题】:Convert MySQL view to Postgres 【发布时间】:2015-10-07 23:04:43 【问题描述】:

我继承了将生产 mysql 数据库转换为 Postgres 的需要。这主要是使用简单的 SQL 语句来创建表/函数(使用 Navicat 生成半自动转换)来处理的,但现在我在转换有点复杂的视图时遇到了问题。

研究表明,这可能是由于两个 DB 处理子查询(WHERE 语句)的方式不同,也许这只是语法上的差异。这里的业务逻辑是未知的,因为代码库是从另一个开发人员那里继承而来的。

运行以下命令(使用 Laravel 迁移/php 脚本):

SELECT 
parent.is_owner AS is_owner,
parent.brand AS first_name,
parent.id AS id,
(SELECT count(c.id)
 FROM campaigns c
 WHERE((
       (c.user_id = parent.id)
       OR
       (c.user_id = child.id)
       )
       AND
       (c.campaign_status_id = 4)
))
AS current_campaigns,
(SELECT count(c.id)
    FROM campaigns c
    WHERE
        ((
        (c.user_id = parent.id)
        OR (c.user_id = child.id)
        )
        AND (c.campaign_status_id = 5)
))
AS past_campaigns,
(SELECT count(c.id)
    FROM campaigns c
    WHERE
        ((
         (c.user_id = parent.id)
         OR (c.user_id = child.id))
         AND (c.campaign_status_id = 2)
        ))
    AS pending_campaigns,
(SELECT count(c.id)
    FROM    campaigns c
    WHERE ((
            (c.user_id = parent.id)
            OR (c.user_id = child.id)
            )
            AND (c.invoice_status = '1')
        ))
    AS past_invoices
FROM ((users parent LEFT JOIN campaigns mc ON
     ((parent.id = mc.user_id)))
    LEFT JOIN users child ON ((child.parent_owner = parent.id)
    ))
WHERE
(
    (parent.is_owner = 1)
    OR (child.is_retailer = 1)
)
GROUP BY parent.id
ORDER BY parent.brand

...触发错误

SQLSTATE[42803]: Grouping error: 7 ERROR:  subquery uses ungrouped column "child.id" from outer query
  LINE 1: ...c where (((c.user_id = parent.id) or (c.user_id = child.id)) ...

谁能建议如何格式化,以便 Postgres 运行子查询?

顺便说一句,这里在 Laravel 迁移脚本中使用的 PHP 代码是:

...

DB::unprepared("CREATE VIEW client AS
select parent.is_owner AS is_owner,parent.brand AS first_name,parent.id AS id
   ,(select count(c.id) from campaigns c where (((c.user_id = parent.id) or (c.user_id = child.id)) and (c.campaign_status_id = 4))) AS current_campaigns
   ,(select count(c.id) from campaigns c where (((c.user_id = parent.id) or (c.user_id = child.id)) and (c.campaign_status_id = 5))) AS past_campaigns
   ,(select count(c.id) from campaigns c where (((c.user_id = parent.id) or (c.user_id = child.id)) and (c.campaign_status_id = 2))) AS pending_campaigns
   ,(select count(c.id) from campaigns c where (((c.user_id = parent.id) or (c.user_id = child.id)) and (c.invoice_status = '1'))) AS past_invoices
from ((users parent
left join campaigns mc on((parent.id = mc.user_id)))
left join users child on((child.parent_owner = parent.id)))
where ((parent.is_owner = 1) or (child.is_retailer = 1))
group by parent.id
order by parent.brand;");

更新,已修复:

太棒了。所有人的意见都很好。

@patrick 和@ErwinBrandstetter 的解决方案都有效。我会支持帕特里克在这里,因为我的角色是“按原样”转换系统。将来可能有重构的空间,但在这个阶段,我觉得弄乱(或改进)别人的管道胶带解决方案是有风险的(即代码库在某些地方似乎过于复杂,没有文档的迹象,我在没有更多关于业务逻辑的背景信息的情况下,我不愿意四处寻找或尝试核心改进)。我怀疑模型的某些部分可能无论如何都需要大修,所以[原文如此]-fix 在这里受到青睐。

我怀疑某些点击抖动可能会生成原始查询...试图让原始开发人员从怀疑中受益,并假设存在一些需要快速(即鼠标)周转的业务压力。复杂的 SQL 不是我的强项,但我很高兴我的直觉是正确的,查询一开始就没有必要复杂。也许这个视图是一个计划外的螺栓 - 不是一开始就设计的。不管明智与否,我可能会尝试使用基于 ORM 的方法来解决问题。

我在最后一分钟参与了这个项目,正在运行清理以重新启动(原始开发人员是“放手”),所以我正在使用一个几乎没有记录的代码库,其中充满了未知的功能。像伞兵一样奔跑。值得庆幸的是,这个视图问题出现到了拼图的最后一块。谢谢你:-)

【问题讨论】:

这里的问题是,与 MySQL 不同,PostgreSQL 实际上遵循 SQL 标准。这意味着如果您进行分组,则所有列都需要在分组中或需要聚合(最小/最大/平均/等)函数。如果您可以将表定义和少量示例数据添加到 sqlfiddle,我可以轻松地为您创建查询:) 我在任何地方都没有看到您的 Postgres 版本?确定最佳答案至关重要。此外,该错误是由于查询中的歧义造成的,并且您没有提供解决方法的信息。基本上:您想计算与多个孩子相关联的广告系列多次还是一次? 祝您重新启动顺利,并保持您的手指交叉。 +1 提出恰当的问题和后续行动。 【参考方案1】:

哦,我的,哦,我的。毫无疑问,开发人员的右手无名指有抽搐,因为该语句有不少于 74 个括号。以下是仅使用 8 个括号和 14 行而不是 54 行的方法:

SELECT 
  parent.is_owner AS is_owner,
  parent.brand AS first_name,
  parent.id AS id,
  sum(CASE WHEN c.campaign_status_id = 4 THEN 1 ElSE 0 END) AS current_campaigns,
  sum(CASE WHEN c.campaign_status_id = 5 THEN 1 ElSE 0 END) AS past_campaigns,
  sum(CASE WHEN c.campaign_status_id = 2 THEN 1 ElSE 0 END) AS pending_campaigns,
  sum(CASE WHEN c.invoice_status = '1' THEN 1 ElSE 0 END) AS past_invoices,
FROM users parent
LEFT JOIN users child ON child.parent_owner = parent.id
LEFT JOIN campaigns c ON c.user_id = parent.id OR c.user_id = child.id
WHERE parent.is_owner = 1 OR child.is_retailer = 1
GROUP BY parent.is_owner, parent.brand, parent.id
ORDER BY parent.brand;

没有子选择意味着这段代码在启动时会运行得更快。就像 Wolph 在他的评论中提到的那样,选择列表中未包含在聚合函数中的每一列都必须出现在 GROUP BY 子句中,这由 SQL 标准指定,但被 MySQL 忽略了。

通过使用CASE 构造来避免子选择:列列表中的条件表达式求值。请注意,子选择中过滤的重复子句现在作为JOIN 子句执行,主查询中的每列仅评估campaigns 中的一个相关列。从 CASE 语句中发出 10 并将其包装在 sum() 函数中是在单个查询中执行多个不同计数的绝妙技巧。

正如 Wolph 在此答案下方的评论中指出的那样,该子句

sum(CASE WHEN c.campaign_status_id = 4 THEN 1 ElSE 0 END) AS current_campaigns

也可以更简洁地写成

sum((c.campaign_status_id = 4)::integer) AS current_campaigns

这可能比CASE 语句要快一些,因为在编写 PostgreSQL 的 C 语言中将布尔值转换为整数不需要任何操作(C 中的布尔值是 1 或 0)。易读性肯定更差(更不用说使用两倍的括号了!)。

【讨论】:

优秀的答案,我 +1 :) 我不确定计数,在这种情况下不应该是 SUM() 吗?此外,这可能比 case 语句更容易:(c.campaign_status_id = 4)::integer @Wolph 很好地发现了sum() 问题(转换现有代码时出现的那些讨厌的错误!),回答更新并为您的评论+1,这是值得的。演员表不太有用,或者看起来如此,因为CASE 想要一个布尔表达式。 我的意思是你可以在使用演员表时完全放下箱子。将布尔值转换为整数将根据真假给出 0 或 1 @Wolph 哦,这确实很漂亮。无论如何,就易读性而言,几乎低于腰带。关于性能影响的任何见解?我会把它粘贴到我的答案中。 根据我的经验,Postgres 可以很好地优化两者。这只是我个人的偏好,因为我发现案例陈述太冗长而无法阅读【参考方案2】:

问题中缺少解释,但可能的用例是:

计算每个用户“拥有”的广告系列数量。一个用户可以有子用户,子用户的活动应该添加到父用户。

除了@Patrick decluttered in his demo 令人难以置信的嘈杂语法之外,查询也是模棱两可的(并且可能完全错误):

如果我们可以假设:

引用完整性:子用户仅引用现有父用户,使用FOREIGN KEY 约束强制执行。

父母和孩子被可靠地标记为is_owner/is_retailer,这些列只包含值01见下文。

这个查询可以完成这项工作:

SELECT CASE WHEN u.is_retailer = 1 THEN u.parent_owner
            WHEN u.is_owner = 1    THEN u.id END        AS user_id
     , max(u.is_owner)                                  AS is_owner
     , max(u.brand) FILTER (WHERE u.is_owner = 1)       AS first_name
     , count(*) FILTER (WHERE c.campaign_status_id = 4) AS current_campaigns
     , count(*) FILTER (WHERE c.campaign_status_id = 5) AS past_campaigns
     , count(*) FILTER (WHERE c.campaign_status_id = 2) AS pending_campaigns
     , count(*) FILTER (WHERE c.invoice_status = '1')   AS past_invoices
FROM   users          u
LEFT   JOIN campaigns c ON u.id = c.user_id
                       AND (c.campaign_status_id IN (4, 5, 2) OR 
                            c.invoice_status = '1')  -- exclude irrelevant early
WHERE  1 IN (u.is_owner, u.is_retailer)  -- parent & child, may be redundant
GROUP  BY 1
ORDER  BY 2;

应该很快。确保为大表设置合适的索引。 如果没有其他选项,则此条件是多余的:

   WHERE  1 IN (u.is_owner, u.is_retailer)

我“按原样”使用您的数据模型,但您可能应该只有 boolean 列:

is_childtrue 给孩子,false 给父母。 is_owner:所有者为true,零售商为false

使用 Postgres 9.4 中引入的新 aggregate FILTER 子句:

How can I simplify this game statistics query?

【讨论】:

感谢@ErwinBrandstetter。这也有效。我将来可能会追求这个,但现在(由于当前的项目范围)正在使用 Patrick 的 [sic]-fix 运行。请参阅对原始问题的更新。

以上是关于将 MySQL 视图转换为 Postgres的主要内容,如果未能解决你的问题,请参考以下文章

MySQL:在视图中将小数转换为百分比

在 MySQL 中创建视图并将行转换为列

将UIView转换从视图A转换为视图B.

如何将视图控制器动画转换转换为 Segue 动画转换

带有子查询的 MySQL 视图

mysql的查询表与查询视图的问题