MySQL LEFT JOIN与WHERE函数调用产生错误结果

时间:2018-09-27 11:23:45

标签: mysql left-join mysql-function

从MySQL 5.7开始,我正在执行LEFT JOIN,而WHERE子句调用我的用户定义函数。它找不到应找到的匹配行。

[出于本文的目的,我最初将实际代码简化了一些。但是,鉴于用户的建议响应,我发布了可能相关的实际代码。]

我的用户功能是:

CREATE FUNCTION `jfn_rent_valid_email`(
    rent_mail_to varchar(1),
    agent_email varchar(45),
    contact_email varchar(60)
)
RETURNS varchar(60)
BEGIN
    IF rent_mail_to = 'A' AND agent_email LIKE '%@%' THEN
        RETURN agent_email;
    ELSEIF contact_email LIKE '%@%' THEN
        RETURN contact_email;
    ELSE
        RETURN NULL;
    END IF
END

我的查询是:

SELECT r.RentCode, r.MailTo, a.AgentEmail, co.Email,
   jfn_rent_valid_email(r.MailTo, a.AgentEmail, co.Email) 
AS ValidEmail
FROM rents r
LEFT JOIN contacts co ON r.RentCode = co.RentCode -- this produces one match
LEFT JOIN link l ON r.RentCode = l.RentCode -- there will be no match in `link` on this
LEFT JOIN agents a ON l.AgentCode = a.AgentCode -- there will be no match in `agents` on this
WHERE  r.RentCode = 'ZAKC17' -- this produces one match
AND (jfn_rent_valid_email(r.MailTo, a.AgentEmail, co.Email) IS NOT NULL)

这不会产生任何行。

但是。如果a.AgentEmail IS NULL如果我从更改为

AND (jfn_rent_valid_email(r.MailTo, a.AgentEmail, co.Email) IS NOT NULL)

AND (jfn_rent_valid_email(r.MailTo, NULL, co.Email) IS NOT NULL)

确实正确地产生了匹配的行:

RentCode, MailTo, AgentEmail, Email,      ValidEmail
ZAKC17,   N,      <NULL>,     name@email, name@email

因此,当a.AgentEmailNULL(来自不匹配的LEFT JOIN行)时,为什么世界上确实将其作为a.AgentEmail传递给函数行为与将其作为文字NULL传递的方式不同吗?

[BTW:我相信我过去曾在MS SQL Server下使用过这种构造,并且按预期工作。另外,我可以将AND (jfn_rent_valid_email(r.MailTo, a.AgentEmail, co.Email) IS NOT NULL)的测试反转为AND (jfn_rent_valid_email(r.MailTo, a.AgentEmail, co.Email) IS NULL),但我仍然没有匹配。好像任何引用a....作为函数的参数都不会导致匹配行...]

2 个答案:

答案 0 :(得分:1)

最有可能是优化程序将LEFT JOIN变成INNER JOIN的问题。当优化器认为生成的NULL行的WHERE条件始终为false(在这种情况下不是)时,可以执行此操作。

您可以使用EXPLAIN命令查看查询计划,根据查询的变体,您可能会看到不同的表顺序。

如果该函数的实际逻辑是通过一个函数调用检查所有电子邮件,那么使用仅将一个电子邮件地址作为参数并用于每个电子邮件列的函数可能会更好。

您可以尝试不使用以下功能:

SELECT r.RentCode, r.MailTo, a.AgentEmail, co.Email,
   jfn_rent_valid_email(r.MailTo, a.AgentEmail, co.Email) 
AS ValidEmail
FROM rents r
LEFT JOIN contacts co ON r.RentCode = co.RentCode -- this produces one match
LEFT JOIN link l ON r.RentCode = l.RentCode -- there will be no match in `link` on this
LEFT JOIN agents a ON l.AgentCode = a.AgentCode -- there will be no match in `agents` on this
WHERE  r.RentCode = 'ZAKC17' -- this produces one match
AND ((r.MailTo='A' AND a.AgentEmail LIKE '%@%') OR co.Email LIKE '%@%' )

或将函数包装在子查询中:

SELECT q.RentCode, q.MailTo, q.AgentEmail, q.Email, q.ValidEmail
FROM (
  SELECT r.RentCode, r.MailTo, a.AgentEmail, co.Email,
   jfn_rent_valid_email(r.MailTo, a.AgentEmail, co.Email) AS ValidEmail
  FROM rents r
    LEFT JOIN contacts co ON r.RentCode = co.RentCode -- this produces one match
    LEFT JOIN link l ON r.RentCode = l.RentCode -- there will be no match in `link` on this
    LEFT JOIN agents a ON l.AgentCode = a.AgentCode -- there will be no match in `agents` on this
  WHERE  r.RentCode = 'ZAKC17' -- this produces one match
) as q
WHERE q.ValidEmail IS NOT NULL

答案 1 :(得分:0)

WHERE子句中更改对函数的调用以读取

jfn_rent_valid_email(r.MailTo, IFNULL(a.AgentEmail, NULL), IFNULL(co.Email, NULL)) IS NOT NULL

解决了问题。

看来,优化器认为,如果对{{1}的 plain 引用不正确,则该函数会错误地猜测函数将在NULL不匹配的情况下返回LEFT JOIN }作为任何参数传递。但是,如果列引用位于任何类型的 expression 内部,则优化器会退出。因此,将其包装在看似毫无意义的“虚拟” a.AgentEmail中就足以恢复正确的行为。

我将其标记为可接受的解决方案,因为它是迄今为止最简单的解决方法,需要最少的代码更改/完整的查询重写。

但是,由于@slaakso在此主题中的此处用于分析问题的帖子,因此受到了充分的感谢。请注意,他指出该行为已在MySQL 8中修复/更改,因此不需要此解决方法,因此仅在MySQL 5.7或更早版本中才需要。