JOIN 替代 SELECT 子查询

Posted

技术标签:

【中文标题】JOIN 替代 SELECT 子查询【英文标题】:JOIN Alternative to SELECT Subquery 【发布时间】:2016-01-27 16:44:52 【问题描述】:

我正在尝试将 SELECT 子查询转换为 JOIN 语句,因此它适用于 Netezza。我最初在一个 Oracle 数据库上工作,其中以下查询运行良好,但 Netezza 不支持 SELECT 语句中的子查询。我设法将 SELECT 子查询放入 ON 语句,但 Netezza 也不支持。

我的查询试图通过查找该日期的每日销售数量和历史价格来查找单个产品销售的每日收入。

关于如何将此声明分解为 Netezza 可以接受的内容的任何建议?我也很想知道我的查询的任何重组是否会提高其效率。

我原来的 Oracle SQL 查询:

Select
    SALES.DATE,
    SALES.PRODUCT,
    SALES.QUANTITY,
    (
        Select PRICE
        from
        (
            Select PRODUCT_ID, PRICE, max(EFF_DATE) as EFF_DATE
            from HIST_PRICING
            Where
                PRODUCT_ID = SALES.PRODUCT and
                SALES.DATE > EFF_DATE
            GROUP BY
                PRODUCT_ID, PRICE
        )
    ) as PRICE,
    (SALES.QUANTITY * PRICE) as REVENUE
FROM SALES_RECORDS SALES
;

将子查询移至 JOIN ON 语句:

SELECT
    SALES.DATE,
    SALES.PRODUCT,
    SALES.QUANTITY,
    H.PRICE,
    (SALES.QUANTITY * H.PRICE) as REVENUE
FROM SALES_RECORDS SALES
LEFT JOIN HIST_PRICING H ON
    SALES.PRODUCT = H.PRODUCT and
    SALES.DATE =
        (
            Select MAX(EFF_DATE) AS MOST_RECENT
            FROM HIST_PRICING
            WHERE SALES.PRODUCT = HIST_PRICING.PRODUCT
              AND EFF_DATE <= SALES.DATE
            GROUP BY SALES.PRODUCT
        )

作为参考,这里是我的表格数据的简化示例。

╔═════════════════════════════════════╗
║           SALES_RECORDS             ║
╠═══════════╦═════════╦═══════════════╣
║   DATE    ║ PRODUCT ║ QUANTITY_SOLD ║
╠═══════════╬═════════╬═══════════════╣
║ 1/1/2015  ║ SHOES   ║           500 ║
║ 2/5/2015  ║ SHOES   ║          1200 ║
║ 3/7/2015  ║ TOYS    ║           600 ║
║ 3/9/2015  ║ SHOES   ║           100 ║
║ 5/10/2015 ║ HATS    ║           400 ║
╚═══════════╩═════════╩═══════════════╝
╔══════════════════════════════╗
║          HIST_PRICING        ║
╠═══════════╦═════════╦════════╣
║ EFF_DATE  ║ PRODUCT ║  PRICE ║
╠═══════════╬═════════╬════════╣
║ 1/1/2015  ║ SHOES   ║ $50    ║
║ 1/1/2015  ║ TOYS    ║ $10    ║
║ 1/1/2015  ║ HATS    ║ $20    ║
║ 2/15/2015 ║ SHOES   ║ $45    ║
║ 2/15/2015 ║ HATS    ║ $15    ║
║ 3/1/2015  ║ HATS    ║ $20    ║
║ 5/1/2015  ║ TOYS    ║ $15    ║
║ 8/1/2015  ║ SHOES   ║ $55    ║
╚═══════════╩═════════╩════════╝

【问题讨论】:

我不确定您是否需要使用LEFT JOIN。您的所有产品都存在于HIST_PRICING 表中吗?此外,您不需要在子查询中进行分组。 【参考方案1】:

如果您可以在FROM 子句中进行内联视图...或者,如果您对CREATE VIEW 具有 DBA 权限,那么您可以这样做:

Select
    SALES."DATE",
    SALES.PRODUCT,
    SALES.QUANTITY,
    PRICES.PRICE,
    (SALES.QUANTITY * PRICES.PRICE) as REVENUE
FROM SALES_RECORDS SALES LEFT JOIN
        (
            Select PRODUCT_ID, PRICE, max(EFF_DATE) as EFF_DATE
            from HIST_PRICING
            GROUP BY
                PRODUCT_ID, PRICE
        ) PRICES ON PRICES.PRODUCT_ID = SALES.PRODUCT AND PRICES.EFF_DATE <= SALES."DATE"
;

否则,您可以这样做:

Select
    SALES."DATE",
    SALES.PRODUCT,
    SALES.QUANTITY,
    PRICES.PRICE,
    (SALES.QUANTITY * PRICES.PRICE) as REVENUE
FROM SALES_RECORDS SALES LEFT JOIN HIST_PRICING PRICES ON PRICES.PRODUCT_ID = SALES.PRODUCT AND PRICES.EFF_DATE <= SALES."DATE"
WHERE NOT EXISTS ( SELECT 'later price for product prior to sales date'
                   FROM   hist_pricing p2
                   WHERE  p2.product_id = prices.product_id
                   AND    p2.eff_date <= sales."DATE"
                   -- NOTE: too simple - assumes you never have two prices for the same product on the same date.  
                   -- If that can happen, you need to adjust the logic below to include a tie-breaker.
                   AND    p2.eff_date > prices.eff_date )
;                   

Oracle 有各种方法来改进这两者(例如,MAX() KEEP)。但这是重新表达原始 SQL 并摆脱标量子查询的两种相当普通的 SQL 方法。

【讨论】:

【参考方案2】:

对于任务通过查找该日期的每日销售量和历史价格来查找单个产品销售的每日收入我建议采用这种方法:

在第一步中定义(延长)产品价格有效期。 这是通过一个简单的分析函数完成的(如果 Netezza 不支持,则可以选择自联接)。

select PRODUCT_ID, EFF_DATE eff_date_from, 
nvl(lead(EFF_DATE-1) over (partition by product_id order by EFF_DATE),to_date('1/1/2100','mm/dd/yyyy')) eff_date_to,
PRICE from HIST_PRICING order by PRODUCT_ID, EFF_DATE

生成带有 PRICE 和有效 FROM - 有效 TO 日期的价格表。 请注意,这两个日期都包含在内(用 -1 天完成),最后一个 TO 日期是在遥远的将来,这允许使用 BETWEEN 进行简单过滤。

注意 - 这仅适用于 DATE(没有时间组件)。如果有效性列也包含时间,则只需减去最小单位,例如 1 秒。

PRODUCT_ID EFF_DATE_FROM       EFF_DATE_TO              PRICE
---------- ------------------- ------------------- ----------
HATS       01.01.2015 00:00:00 14.02.2015 00:00:00         20 
HATS       15.02.2015 00:00:00 28.02.2015 00:00:00         15 
HATS       01.03.2015 00:00:00 01.01.2100 00:00:00         20 
SHOES      01.01.2015 00:00:00 14.02.2015 00:00:00         50 
SHOES      15.02.2015 00:00:00 31.07.2015 00:00:00         45 
SHOES      01.08.2015 00:00:00 01.01.2100 00:00:00         55 
TOYS       01.01.2015 00:00:00 30.04.2015 00:00:00         10 
TOYS       01.05.2015 00:00:00 01.01.2100 00:00:00         15

查询是对产品的简单连接(如果历史记录表可能不完整,则使用带有一些虚拟价格的外部连接)并使用 sales_date 限制价格有效性。

Select
    SALES."DATE",
    SALES.PRODUCT_ID,
    SALES.QUANTITY,
    (SALES.QUANTITY * PRICE) as REVENUE,
    PRICE
from  SALES_RECORDS  SALES
join 
(  
select PRODUCT_ID, EFF_DATE eff_date_from, 
nvl(lead(EFF_DATE-1) over (partition by product_id order by EFF_DATE),to_date('1/1/2100','mm/dd/yyyy')) eff_date_to,
PRICE from HIST_PRICING order by PRODUCT_ID, EFF_DATE
) DAILY_PRICE
on SALES.PRODUCT_ID = DAILY_PRICE.PRODUCT_ID and
 SALES."DATE" BETWEEN DAILY_PRICE.eff_date_from and DAILY_PRICE.eff_date_to
;

【讨论】:

以上是关于JOIN 替代 SELECT 子查询的主要内容,如果未能解决你的问题,请参考以下文章

SELECT 中的子查询或 JOIN 中的子查询?

查询语言系列—JOIN 语句

JOIN 或 Correlated 子查询与 exists 子句,哪个更好

Sql查询left join

在 JOIN 中重写慢速 SQL(子)查询

mysql子查询