SQL Join на ближайшую доступную дату

в настоящее время у меня есть эти таблицы:

CREATE TABLE #SECURITY_TEMP (ID CHAR(30))
CREATE TABLE #SECURITY_TEMP_PRICE_HISTORY (ID CHAR(30), PRICEDATE DATE, PRICE FLOAT)
CREATE TABLE #SECURITY_POST (ID CHAR(30), SECPOS int)

INSERT INTO #SECURITY_TEMP (ID) VALUES ('APPL') ,('VOD'),('VOW3'), ('AAA')
INSERT INTO #SECURITY_TEMP_PRICE_HISTORY (ID,PRICEDATE, PRICE) VALUES 
('APPL', '20150101',10.4), ('APPL', '20150116',15.4), ('APPL', '20150124',22.4), 
('VOD', '20150101', 30.5), ('VOD', '20150116',16.5), ('VOD', '20150124',16.5), 
('VOW3', '20150101', 45.5), ('VOW3', '20150116',48.8) ,('VOW3', '20150124',50.55), 
('AAA', '20100118', 0.002)

INSERT INTO #SECURITY_POST (ID,SECPOS) VALUES ('APPL', 100), ('VOD', 350), ('VOW3', 400)

Я хочу иметь чистую таблицу, которая показывает мне идентификатор безопасности, положение безопасности и последнюю доступную цену для этой безопасности при прохождении даты.

теперь, когда я делаю следующее:

SELECT sec.ID, sec.SECPOS, t.PRICE
FROM #SECURITY_POST as SEC INNER JOIN #SECURITY_TEMP_PRICE_HISTORY as t
ON sec.ID = t.ID
WHERE t.PriceDate = '20150101'
GROUP BY sec.ID, secPos, t.price

я получаю правильный результат

 1. ID  SECPOS  PRICE 
 2. APPL  100   10.4
 3. VOD   350   30.5
 4. VOW3  400   45.5

однако могут быть отдельные обстоятельства, когда цена акции недоступна. Поэтому в этом смысле я хочу иметь возможность получите самую последнюю доступную цену.

делаешь

SELECT sec.ID, sec.SECPOS, t.PRICE  
FROM #SECURITY_POST as SEC INNER JOIN
     #SECURITY_TEMP_PRICE_HISTORY as t
     ON sec.ID = t.ID
WHERE t.PriceDate = '20150117'
GROUP BY sec.ID, secPos, t.price

возвращает 0 строк из-за отсутствия данных и делает

SELECT sec.ID, sec.SECPOS, t.PRICE  
FROM #SECURITY_POST as SEC INNER JOIN
     #SECURITY_TEMP_PRICE_HISTORY as t
     ON sec.ID = t.ID
WHERE t.PriceDate <= '20150117'
GROUP BY sec.ID, sec.secPos, t.price
HAVING sec.secpos <> 0

возвращает повторяющиеся строки.

я пробовал множество различных методологий и я просто не могу получить выходной я хочу. Кроме того, я также хотел бы иметь возможность получить один столбец с ценой, ближайшей к дате (назовите его START_DATE) и один столбец с ценой, ближайшей ко второй дате (назовите его END_DATE) и один столбец будет позиция Price@END_DATE - Price@START_DATE. Цена всегда берется из одного и того же #SECURITY_TEMP_PRICE_HISTORY.

однако мои знания SQL просто смущают, и я не мог придумать хороший эффективный способ сделать это. Любая помощь будет оценена. Также обратите внимание, что #SECURITY_PRICE_HISTORY table may contain more securities than the #SECURITY_POST таблица.

1 ответов


это должно сделать трюк. OUTER APPLY является оператором соединения, который (как CROSS APPLY) позволяет производной таблице иметь внешнюю ссылку.

SELECT
   s.ID,
   s.SecPos,
   t.Price
   t.PriceDate
FROM
   #SECURITY_POST s
   OUTER APPLY (
      SELECT TOP 1 *
      FROM #SECURITY_TEMP_PRICE_HISTORY t
      WHERE
         s.ID = t.ID
         AND t.PriceDate <= '20150117'
      ORDER BY t.PriceDate DESC
   ) t
;

вы также можете рассмотреть возможность пометки цен безопасности, которые очень старые, или ограничение поиска для последней безопасности на определенный период (неделю или месяц или что-то).

убедитесь, что ваша таблица истории цен имеет индекс с (ID, PriceDate) так что поиск подзапросов может использовать диапазон ищет и ваш производительность может быть хорошей. Убедитесь, что вы делаете любую дату математику в таблице безопасности, а не в таблице истории, или вы заставите подзапрос поиска цены быть не sargable, что было бы плохо для производительности, поскольку диапазон ищет не будет возможно.

если цена за безопасность не найдена,OUTER APPLY все равно позволит строке существовать, поэтому цена будет отображаться как NULL. Если вы хотите, чтобы ценные бумаги не показывались при отсутствии соответствующей цены, используйте CROSS APPLY.

для вашего вторая часть вопроса, Вы можете сделать это с двумя OUTER APPLY операции, вот так:

DECLARE
   @StartDate date = '20150101',
   @EndDate date = '20150118';

SELECT
   S.ID,
   S.SecPos,
   StartDate = B.PriceDate,
   StartPrice = B.Price,
   EndDate = E.PriceDate,
   EndPrice = E.Price,
   Position = B.Price - E.Price
FROM
   #SECURITY_POST S
   OUTER APPLY (
      SELECT TOP 1 *
      FROM #SECURITY_TEMP_PRICE_HISTORY B
      WHERE
         S.ID = B.ID
         AND B.PriceDate <= @StartDate
      ORDER BY B.PriceDate DESC
   ) B
   OUTER APPLY (
      SELECT TOP 1 *
      FROM #SECURITY_TEMP_PRICE_HISTORY E
      WHERE
         S.ID = E.ID
         AND E.PriceDate <= @EndDate
      ORDER BY E.PriceDate DESC
   ) E
;

С вашими данными это дает следующий результирующий набор:

ID    SecPos  StartDate   StartPrice  EndDate     EndPrice  Position
----  ------  ----------  ----------  ----------  --------  --------
APPL  100     2015-01-01  10.4        2015-01-16  15.4       -5
VOD   350     2015-01-01  30.5        2015-01-16  16.5       14
VOW3  400     2015-01-01  45.5        2015-01-16  48.8       -3.3

наконец, хотя не все согласны, я бы рекомендовал вам назвать свой ID столбцы с именем таблицы, как в SecurityID вместо ID. По моему опыту использование ID только приводит к проблемам.

Примечание: есть способ решить эту проблему с помощью Row_Number() оконной функции. Если у вас относительно мало ценовых точек по сравнению с количеством акций, и вы ищете цены для большинства акций в таблице истории, то вы можете получить лучшую производительность с помощью этого метода. Однако, если существует большое количество ценовых пунктов на акцию или вы фильтруете только несколько акций, вы можете получить лучшую производительность с помощью метода, который я показал вам.