SQL Server 2016에서 공간 데이터를위한 MakeValid () 대신 사용


13

LINESTRINGOracle에서 SQL Server로 옮기는 매우 큰 지리 데이터 테이블이 있습니다 . Oracle에서는이 데이터에 대해 여러 가지 평가가 수행되며 SQL Server의 데이터에 대해서도 평가가 수행되어야합니다.

문제 : SQL Server는 LINESTRINGOracle보다 유효한 요구 사항이 더 엄격합니다 . "LineString 인스턴스는 두 개 이상의 연속 포인트 간격 동안 자체적으로 겹칠 수 없습니다." 단지 우리 LINESTRING의 비율이 그 기준을 충족시키지 못하기 때문에 데이터를 평가하는 데 필요한 기능이 실패합니다. SQL Server에서 데이터의 유효성을 검사 할 수 있도록 데이터를 조정해야합니다.

예를 들면 다음과 같습니다.

LINESTRING그 자체로 배가 되는 매우 간단한 유효성 검사 :

select geography::STGeomFromText(
    'LINESTRING (0 0 1, 0 1 2, 0 -1 3)',4326).IsValidDetailed()
24413: Not valid because of two overlapping edges in curve (1).

MakeValid그것에 대해 함수를 실행 :

select geography::STGeomFromText(
    'LINESTRING (0 0 1, 0 1 2, 0 -1 3)',4326).MakeValid().STAsText()
LINESTRING (0 -0.999999999999867, 0 0, 0 0.999999999999867)

불행하게도 MakeValid함수는 점의 순서를 변경하고 3 차원을 제거하여 사용할 수 없게합니다. 3 차원을 재정렬하거나 제거하지 않고이 문제를 해결하는 다른 접근법을 찾고 있습니다.

어떤 아이디어?

내 실제 데이터에는 수백 / 수천 점이 있습니다.

답변:


12

SQL Server에서 처음으로 공간 데이터를 가지고 놀고 있다는 것을 명심하십시오 (그래서 이미이 첫 번째 부분을 알고있을 것입니다). 그러나 SQL Server가 (xyz) 좌표를 true로 취급하지 않는다는 것을 알아내는 데 시간이 걸렸습니다. 3D 값인 경우 선택적인 "고도"값 Z를 사용하여 값을 (위도 경도)로 처리합니다.이 값은 유효성 검사 및 기타 기능에 의해 무시됩니다.

증거:

select geography::STGeomFromText('LINESTRING (0 0 1, 0 1 2, 0 -1 3)', 4326)
    .IsValidDetailed()

24413: Not valid because of two overlapping edges in curve (1).

첫 번째 예제는 (0 0 1), (0 1 2) 및 (0 -1 3) 이 3D 공간에서 동일 선상에 있지 않기 때문에 이상하게 보였습니다 (저는 수학자이므로 이러한 용어로 생각하고있었습니다). IsValidDetailed(과MakeValid )는 이들을 (0 0), (0 1) 및 (0, -1)로 처리하여 겹치는 선을 만듭니다.

그것을 증명하기 위해 X와 Z를 바꾸면 다음과 같이 검증됩니다.

select geography::STGeomFromText('LINESTRING (1 0 0, 2 1 0, 3 -1 0)', 4326)
    .IsValidDetailed()

24400: Valid

수학적 3D 공간의 점이 아니라 지구 표면에서 추적 된 영역 또는 경로로 생각하면 실제로 의미가 있습니다.


문제의 두 번째 부분은 Z (및 M) 포인트 값이 함수를 통해 SQL에 의해 보존되지 않는다는 것입니다 .

Z 좌표는 라이브러리에서 수행 한 계산에 사용되지 않으며 라이브러리 계산을 통해 전달되지 않습니다.

불행히도 이것은 의도적으로 설계된 것입니다. 이것은 2010 년에 Microsoft에보고되었으며 요청은 "수정되지 않음"으로 마감되었습니다. 토론이 적절하다는 것을 알 수 있으며 그 이유는 다음과 같습니다.

MakeValid가 공간 요소를 분할하고 병합하기 때문에 Z 및 M 할당은 모호합니다. 이 과정에서 종종 포인트가 생성, 제거 또는 이동됩니다. 따라서 MakeValid (및 기타 구성)는 Z 및 M 값을 떨어 뜨립니다.

예를 들면 다음과 같습니다.

DECLARE @a geometry = geometry::Parse('POINT(0 0 2 2)');
DECLARE @b geometry = geometry::Parse('POINT(0 0 1 1)');
SELECT @a.STUnion(@b).AsTextZM()

Z 및 M 값은 점 (0 0)에 대해 모호합니다. 반 정확한 결과를 반환하는 대신 Z와 M을 완전히 떨어 뜨리기로 결정했습니다.

방법을 정확히 알고 있다면 나중에 할당 할 수 있습니다. 또는 입력시 객체 생성 방식을 변경하거나 객체의 두 가지 버전 (유효한 버전과 모든 기능을 유지하는 버전)을 유지할 수 있습니다. 시나리오를 더 잘 설명하고 객체로 수행하는 작업을 설명하면 추가 해결 방법을 제공 할 수 있습니다.

또한 이미 본 것처럼 포인트 순서 변경, MULTILINESTRING 또는 POINT 객체 반환과 같은 MakeValid다른 예기치 않은 작업을 수행 할 수도 있습니다 .


내가 만난 한 가지 아이디어는 대신 MULTIPOINT 객체로 저장하는 것입니다 .

문제는 선 스트링이 실제로 이전에 선으로 추적 한 두 점 사이의 연속적인 선 섹션을 되돌릴 때입니다. 정의에 따라 기존 점을 다시 추적하는 경우 선 스트링은 더 이상이 포인트 세트를 나타낼 수있는 가장 단순한 형상이 아니며 MakeValid ()는 대신 여러 줄로 된 문자열을 제공하며 Z / M 값을 잃게됩니다.

불행히도 GPS 데이터 또는 이와 유사한 작업을 수행하는 경우 경로의 어느 시점에서 경로를 다시 추적했을 가능성이 높으므로 이러한 시나리오에서 선 스트링이 항상 유용하지는 않습니다. 데이터는 규칙적인 시점에 샘플링 된 객체의 개별 위치를 나타내므로 다 지점입니다.

귀하의 경우에는 잘 검증됩니다.

select geometry::STGeomFromText('MULTIPOINT (0 0 1, 0 1 2, 0 -1 3)',4326)
    .IsValidDetailed()

24400: Valid

이것을 LINESTRINGS로 유지 해야하는 경우 MakeValidZ를 유지하면서 소스 X 또는 Y 포인트 중 일부를 약간의 값으로 약간 조정하는 자체 버전을 작성해야 합니다. 다른 객체 유형으로 변환하십시오).

아직 코드를 작성 중이지만 여기에서 시작 아이디어를 살펴보십시오.


편집 테스트 중에 발견 한 몇 가지 사항은 다음과 같습니다.

  • 지오메트리 객체가 유효하지 않으면 그 기능을 많이 사용할 수 없습니다. 을 읽을 수 없으며를 읽 거나 반복하여 사용할 STGeometryType수 없습니다 . 를 사용할 수없는 경우 기본적으로 지리적 객체의 텍스트 표현을 조작하는 데 문제가 있습니다.STNumPointsSTPointNMakeValid
  • 를 사용 STAsText()하면 유효하지 않은 객체의 텍스트 표현도 반환하지만 Z 또는 M 값은 반환하지 않습니다. 대신, 우리는 원하는 AsTextZM()나 ToString().
  • 호출 하는 함수 를 만들 수 없으므로 RAND()(함수가 결정적이어야 함), 나는 계속해서 더 큰 값으로 점점 조금씩 움직였다. 나는 데이터의 정확성이 무엇인지, 또는 작은 변화에 얼마나 견딜 수 있는지 전혀 모른다. 따라서 자신의 재량에 따라이 기능을 사용하거나 수정하십시오.

이 루프가 영원히 계속되게 할 수있는 가능한 입력이 있는지 전혀 모른다. 경고를 받았습니다.

CREATE FUNCTION dbo.FixBadLineString (@input geography) RETURNS geography
AS BEGIN
DECLARE @output geography

IF @input.STIsValid() = 1   --send valid objects back as-is
  SET @output = @input;
ELSE IF LEFT(@input.IsValidDetailed(),6) = '24413:'
--"Not valid because of two overlapping edges in curve"
BEGIN
  --make a new MultiPoint object from the LineString text
  DECLARE @mp geography = geography::STGeomFromText(
      REPLACE(@input.AsTextZM(), 'LINESTRING', 'MULTIPOINT'), 4326);
  DECLARE @newText nvarchar(max); --to build output
  DECLARE @point int 
  DECLARE @tinynum float = 0;

  SET @output = @input;
  --keep going until it validates
  WHILE @output.STIsValid() = 0
  BEGIN
    SET @newText = 'LINESTRING (';
    SET @point = 1
    SET @tinynum = @tinynum + 0.00000001

    --Loop through the points, add a bit and append to the new string
    WHILE @point <= @mp.STNumPoints()
    BEGIN
      SET @newText = @newText + convert(varchar(50),
               @mp.STPointN(@point).Long + @tinynum) + ' ';
      SET @newText = @newText + convert(varchar(50),
               @mp.STPointN(@point).Lat - @tinynum) + ' ';
      SET @newText = @newText + convert(varchar(50), 
               @mp.STPointN(@point).Z) + ', ';
      SET @tinynum = @tinynum * -2
      SET @point = @point + 1
    END

    --close the parens and make the new LineString object
    SET @newText = LEFT(@newText, LEN(@newText) - 1) + ')'
    SET @output = geography::STGeomFromText(@newText, 4326);
  END; --this will loop if it is still invalid
  RETURN @output;
END;
--Any other unhandled error, just send back NULL
ELSE SET @output = NULL;

RETURN @output;
END

문자열을 파싱하는 대신 MultiPoint동일한 점 집합을 사용하여 새 객체 를 만들도록 선택하여 반복하여 찔러서 새 LineString을 다시 어셈블 할 수있었습니다. 다음은 코드를 테스트하는 코드입니다.이 값 중 3 개 (샘플 포함)는 유효하지 않지만 수정되었습니다.

declare @geostuff table (baddata geography)

INSERT INTO @geostuff (baddata)
          SELECT geography::STGeomFromText('LINESTRING (0 0 1, 0 1 2, 0 -1 3)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (0 2 0, 0 1 0.5, 0 -1 -14)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (0 0 4, 1 1 40, -1 -1 23)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (1 1 9, 0 1 -.5, 0 -1 3)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (6 6 26.5, 4 4 42, 12 12 86)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (0 0 2, -4 4 -2, 4 -4 0)',4326)

SELECT baddata.AsTextZM() as before, baddata.IsValidDetailed() as pretest,
 dbo.FixBadLineString(baddata).AsTextZM() as after,
 dbo.FixBadLineString(baddata).IsValidDetailed() as posttest 
FROM @geostuff

좋은 답변 감사합니다. BradC. 내 질문에 이것을 포함시키지 않았지만 실제 데이터에는 수백 / 수천 개의 포인트가 포함되어 있으므로 "@tinynum * 2"는 지속되지 않았습니다. 대신 "@tinynum"을 완전히 삭제하고 0과 0.000000003 사이의 난수를 사용했습니다. 나는 데이터에 대해 이것을 실행했으며 지금까지 22k가 완료되었으며 모두 LINESTRING으로 검증되었습니다.
— CaptainSlock

3

이것은 BradC의 FixBadLineString기능으로 0에서 0.000000003 사이의 난수를 사용하도록 조정되어 LINESTRINGs많은 수의 포인트 로 스케일링 하고 좌표 변경을 최소화합니다.

CREATE FUNCTION dbo.FixBadLineString (@input geography) RETURNS geography
AS BEGIN
DECLARE @output geography

IF @input.STIsValid() = 1   --send valid objects back as-is
  SET @output = @input;
ELSE IF LEFT(@input.IsValidDetailed(),6) = '24413:'
--"Not valid because of two overlapping edges in curve"
BEGIN
  --make a new MultiPoint object from the LineString text
  DECLARE @mp geography = geography::STGeomFromText(
      REPLACE(@input.AsTextZM(), 'LINESTRING', 'MULTIPOINT'), 4326);
  DECLARE @newText nvarchar(max); --to build output
  DECLARE @point int 

  SET @output = @input;
  --keep going until it validates
  WHILE @output.STIsValid() = 0
  BEGIN
    SET @newText = 'LINESTRING (';
    SET @point = 1

    --Loop through the points, add/subtract a random value between 0 and 3E-9 and append to the new string
    WHILE @point <= @mp.STNumPoints()
    BEGIN
      SET @newText = @newText + convert(varchar(50),
        CAST(@mp.STPointN(@point).Long AS NUMERIC(18,9)) + 
          CAST(ABS(CHECKSUM(PWDENCRYPT(N''))) / 644245094100000000 AS NUMERIC(18,9))) + ' ';
      SET @newText = @newText + convert(varchar(50),
        CAST(@mp.STPointN(@point).Lat AS NUMERIC(18,9)) - 
          CAST(ABS(CHECKSUM(PWDENCRYPT(N''))) / 644245094100000000 AS NUMERIC(18,9))) + ' ';
      SET @newText = @newText + convert(varchar(50), 
               @mp.STPointN(@point).Z) + ', ';
      SET @point = @point + 1
    END

    --close the parens and make the new LineString object
    SET @newText = LEFT(@newText, LEN(@newText) - 1) + ')'
    SET @output = geography::STGeomFromText(@newText, 4326);
  END; --this will loop if it is still invalid
  RETURN @output;
END;
--Any other unhandled error, just send back NULL
ELSE SET @output = NULL;

RETURN @output;
END

1
정말 좋아 보인다, 나는 PWDENCRYPT기능 에 대해 몰랐다 . 당신은 생략 할 수 있었고 ABS그것은 양수 또는 음수를 반환했을 것이므로, 우리는 항상 X에 더하고 Y에서 빼는 것이 아닙니다.
— BradC
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.