Как написать TVF, который принимает 3 параметра и возвращает таблицу, которая включает идентификаторы строк, соответствующих критериям из другой таблицы

#sql-server #user-defined-functions

#sql-server #определяемые пользователем функции

Вопрос:

Я новичок в функциях с табличным значением, и у меня возникают проблемы с запуском этой функции с нуля. Я пытаюсь создать функцию, которая принимает 3 параметра (@Lat decimal, @Long decimal, @RangeInMiles int). Что я хочу сделать, так это вернуть идентификаторы местоположений из таблицы местоположений, которые находятся в радиусе действия переданного параметра: @RangeInMiles.

Таблица местоположений включает следующие столбцы: loc_ID ИДЕНТИФИКАТОР PK (типичный адрес) loc_longitude loc_latity

Итак, идея состоит в том, чтобы заставить эту функцию использовать широту и длину, которые были переданы для сравнения с широтами и длинами в таблице Locations, и возвращать те идентификаторы, которые находятся в пределах переданного радиуса: @RangeInMiles.

Весь код, который у меня есть на данный момент, приведен ниже, но я зашел в тупик в предложении WHERE. Спасибо за помощь!

 CREATE FUNCTION dbo.GetAvailableSalesByDistance(@Lat decimal, @Long decimal, @RangeInMiles int)
RETURNS @LocationIds Table
(loc_ID int)
AS
BEGIN INSERT @LocationIds
SELECT Locations.loc_ID
FROM Locations
WHERE 
RETURN
END
  

Комментарии:

1. Кажется, вам нужен Pythagoras. Функции треугольника.

Ответ №1:

Вы могли бы попробовать это, следуя Пифагору:

 CREATE FUNCTION dbo.GetAvailableSalesByDistance(@Lat decimal, @Long decimal, @RangeInMiles int)
RETURNS @LocationIds Table
(loc_ID int)
AS
BEGIN INSERT INTO@LocationIds
SELECT Locations.loc_ID
FROM Locations as l
WHERE (SQRT(SQUARE(l.loc_latitude-@Lat) SQUARE(l.loc_longitude-@Long))<=@RangeInMiles)
RETURN
END
  

Если вы хотите найти ложные данные, вы можете попробовать это утверждение:

 SELECT * FROM Locations
WHERE TRY_CONVERT(numeric, loc_latitude) IS NULL
OR TRY_CONVERT(numeric, loc_longitude) IS NULL
  

Редактировать:
Чтобы сделать это правильно, вы могли бы использовать haversine:

 CREATE FUNCTION dbo.GetAvailableSalesByDistance(@Lat decimal, @Long decimal, @RangeInMiles int)
    RETURNS @LocationIds Table
    (loc_ID int)
    AS
    BEGIN INSERT INTO@LocationIds
    SELECT Locations.loc_ID
    FROM Locations as l
    WHERE 
(2 * 3961 * asin(sqrt((sin(radians((l.loc_latitude - @Lat) / 2))) ^ 2   cos(radians(@Lat)) * cos(radians(l.loc_latitude)) * (sin(radians((l.loc_longitude - @Long) / 2))) ^ 2))) <=@RangeInMiles)
    RETURN
    END
  

Он не тестировался и получен из этого источника

Комментарии:

1. Спасибо @Nikolaus. Я попробовал ваш подход, и при вызове функции я получаю следующую ошибку: Ошибка преобразования типа данных nvarchar в числовой. Это был мой вызов: ВЫБЕРИТЕ * ИЗ GetAvailableSalesByDistance(34.10, 84.52,5)

2. @JP1 Может ли быть, что loc_latidude или / и loc_longidude имеют тип nvarchar?

3. Вы правы, они есть! Что было бы лучше для этой функции: Переключить тип данных Lat и Long в таблице Locations или изменить саму функцию?

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

5. @JP1 Вы могли бы попытаться выяснить, есть ли у вас неверные данные.

Ответ №2:

Ваш оператор insert с предложением updated может быть:

 INSERT @LocationIds
        SELECT
            Locations.loc_ID 
        FROM
            Locations 
        WHERE
            SQRT(SQUARE(loc_longitude - @Long)   SQUARE(loc_latitude - @Lat)) <= @RangeInMiles
  

Ответ №3:

Это намного сложнее, чем то, что опубликовал @Nikolaus, широта и долгота находятся на сфере (Земля) и не выражаются в милях, поэтому тройная система Пифагора здесь неприменима. Следуя формуле, описанной здесь, мы получим следующую функцию в SQL Server для вычисления расстояния:

 CREATE FUNCTION dbo.spatialDistance(@lat1 DECIMAL, @lon1 DECIMAL, @lat2 DECIMAL, @lon2 DECIMAL)
RETURNS TABLE
AS
RETURN (
    SELECT 6371 * Calc2.c AS DistanceKm
         , 3958.8 * Calc2.c AS DistanceMi
      FROM (SELECT RADIANS(@lat1) AS Lat1
                 , RADIANS(@lat2) AS Lat2
                 , RADIANS(@lat2 - @lat1) AS dLat
                 , RADIANS(@lon2 - @lon1) AS dLon) conv
     CROSS
     APPLY (SELECT a = SQUARE(SIN(dLat/2.0))   SQUARE(SIN(dLon/2.0)) * COS(Lat1) * COS(Lat2)) Calc1
     CROSS
     APPLY (SELECT c = 2 * ATN2(SQRT(Calc1.a), SQRT(1 - Calc1.a))) AS Calc2
)
  

Затем вы могли бы написать желаемую функцию следующим образом:

 CREATE FUNCTION dbo.GetAvailableSalesByDistance(@Lat decimal, @Long decimal, @RangeInMiles int)
RETURNS Table
AS
RETURN (
    SELECT l.loc_id
      FROM Locations AS l
     CROSS
     APPLY dbo.spatialDistance(l.loc_latitude, l.loc_longitude, @Lat, @Long) dist
     WHERE dist.DistanceMi <= @RangeInMiles
)  
  

Обратите внимание, что я использую встроенные табличные функции. Скалярные функции и TVF с несколькими операторами, подобные тем, которые вы использовали в своем вопросе, ужасны для производительности и не должны использоваться. Смотрите здесь объяснение, если вам интересно.

Комментарии:

1. Спасибо за помощь! Это имеет смысл. Я все еще получаю сообщение об ошибке, что он не может преобразовать varchar в числовой при запуске вызова функции. Мысли? Ошибка преобразования типа данных nvarchar в числовой. Это был мой вызов: ВЫБЕРИТЕ * ИЗ GetAvailableSalesByDistance(34.10, 84.52,5)

2. @JP1 Как определяется ваша таблица местоположений? loc_latitude и loc_longitude являются nvarchar ? Если это так, TRY_CONVERT запрос, отправленный @Nikolaus, должен их выявить. Может быть, вы забыли удалить предыдущую версию GetAvailableSalesByDistance перед созданием моей версии?

3. Да, они являются nvarchar. Похоже, мне придется найти способ преобразовать эти столбцы в числовые. Еще раз спасибо за вашу помощь

4. @JP1 Они должны автоматически преобразовываться, некоторые просто завершаются неудачей. Вы пробовали SELECT * FROM Locations WHERE TRY_CONVERT(DECIMAL, loc_latitude) IS NULL OR TRY_CONVERT(DECIMAL, loc_longitude) IS NULL ? Здесь должны быть перечислены все записи, которые не удалось преобразовать. Может быть, использовать запятую вместо точки в качестве десятичного разделителя?