География SQL Server: уменьшить размер (десятичная точность) текста WKT

Для моего фермерского приложения хранимая процедура извлекает границы загона/поля, которые хранятся как тип данных SQL Server geography, для отображения на мобильном устройстве пользователя.

Функции SQL Server .ToString() и .STAsText() отображают каждую вершину как пару широта/долгота с точностью до 15 знаков после запятой. Из этого ответа 15 знаков после запятой определяют местоположение с точностью до ширины атома! С точностью до метра мне было бы достаточно.

В результате чрезмерно точная полезная нагрузка становится чрезвычайно большой и слишком медленной для использования на больших фермах.

Из моих географических данных SQL Server я хотел бы получить WKT, отформатированный до 4 или 5 десятичных знаков. Я не смог найти встроенных методов, но мои лучшие выводы:

  • Postgis и Google Cloud BigQuery имеют функцию ST_SNAPTOGRID, что было бы идеально, и
  • Regex может быть полезным, например. этот ответ, но SQL Server, похоже, не заменяет регулярное выражение.

Я бы подумал, что это общая проблема: есть ли простое решение?


person Merenzo    schedule 17.07.2020    source источник
comment
Какую версию SQL Server вы используете?   -  person iamdave    schedule 17.07.2020
comment
@iamdave - я использую Microsoft SQL Azure 12.0.2000.8 с уровнем совместимости 140 (= SQL Server 2017).   -  person Merenzo    schedule 21.07.2020


Ответы (2)


Редактировать

Я полагаю, что, возможно, неправильно понял ваш вопрос, и вы хотите передать WKT, а не двоичные представления полигонов? Если это так, мой ответ ниже по-прежнему показывает вам, как отрезать некоторые десятичные знаки (без их округления). Просто не заворачивайте stuff(...) FOR XML в STGeomFromText, и вы получите измененный WKT.


При работе с geography типами данных может быть удобно поддерживать очень подробную «основную» версию, из которой вы создаете и сохраняете менее подробные версии в соответствии с вашими требованиями.

Простой способ создать эти полигоны упрощенной сложности — использовать полезно названный Reduce, которая, я думаю, действительно поможет вам в этой ситуации.

Если вы предпочитаете пойти по пути уменьшения количества знаков после запятой, вам придется либо написать собственную функцию CLR, либо войти в чудесный мир манипуляций со строками SQL Server!

SQL-запрос

declare @DecimalPlaces int = 4; -- Specify the desired number of lat/long decimals

with g as(
    select p.g  -- Original polygon, for comparison purposes
          ,geography::STGeomFromText('POLYGON(('    -- stripped apart and then recreated polygon from text, using a custom string split function.  You won't be able to use the built in STRING_SPLIT here as it doesn't guarantee sort order.
            + stuff((select ', ' + left(s.item,charindex('.',s.item,0) + @DecimalPlaces) + substring(s.item,charindex(' ',s.item,0),charindex('.',s.item,charindex(' ',s.item,0)) - charindex(' ',s.item,0) + 1 + @DecimalPlaces)
                     from dbo.fn_StringSplitMax(replace(replace(p.g.STAsText(),'POLYGON ((',''),'))',''),', ',null) as s
                     for xml path(''), type).value('.', 'NVARCHAR(MAX)')    -- STUFF and FOR XML mimics GROUP_CONCAT functionality seen in other SQL languages, to recombine shortened Points back into a Polygon string
                   ,1,2,''
                   )
            + '))', 4326).MakeValid() as x  -- Remember to make the polygon valid again, as you have been messing with the Point data
    from(values(geography::STGeomFromText('POLYGON((-121.973669 37.365336,-121.97367 37.365336,-121.973642 37.365309,-121.973415 37.365309,-121.973189 37.365309,-121.973002 37.365912,-121.972815 37.366515,-121.972796 37.366532,-121.972776 37.366549,-121.972627 37.366424,-121.972478 37.366299,-121.972422 37.366299,-121.972366 37.366299,-121.972298 37.366356,-121.97223 37.366412,-121.97215 37.366505,-121.97207 37.366598,-121.971908 37.366794,-121.971489 37.367353,-121.971396 37.367484,-121.971285 37.36769,-121.971173 37.367897,-121.971121 37.368072,-121.971068 37.368248,-121.971028 37.36847,-121.970987 37.368692,-121.970987 37.368779,-121.970987 37.368866,-121.970949 37.368923,-121.970912 37.36898,-121.970935 37.36898,-121.970958 37.36898,-121.970975 37.368933,-121.970993 37.368887,-121.971067 37.368807,-121.97114 37.368726,-121.971124 37.368705,-121.971108 37.368685,-121.971136 37.368698,-121.971163 37.368712,-121.97134 37.368531,-121.971516 37.368351,-121.971697 37.368186,-121.971878 37.368021,-121.972085 37.367846,-121.972293 37.36767,-121.972331 37.367629,-121.972369 37.367588,-121.972125 37.367763,-121.97188 37.367938,-121.971612 37.36815,-121.971345 37.368362,-121.971321 37.36835,-121.971297 37.368338,-121.971323 37.368298,-121.97135 37.368259,-121.971569 37.368062,-121.971788 37.367865,-121.971977 37.367716,-121.972166 37.367567,-121.972345 37.367442,-121.972524 37.367317,-121.972605 37.367272,-121.972687 37.367227,-121.972728 37.367227,-121.972769 37.367227,-121.972769 37.367259,-121.972769 37.367291,-121.972612 37.367416,-121.972454 37.367542,-121.972488 37.367558,-121.972521 37.367575,-121.972404 37.367674,-121.972286 37.367773,-121.972194 37.367851,-121.972101 37.367928,-121.972046 37.36799,-121.971991 37.368052,-121.972008 37.368052,-121.972025 37.368052,-121.972143 37.367959,-121.972261 37.367866,-121.972296 37.367866,-121.972276 37.36794,-121.972221 37.36798,-121.972094 37.368097,-121.971966 37.368214,-121.971956 37.368324,-121.971945 37.368433,-121.971907 37.368753,-121.971868 37.369073,-121.97184 37.369578,-121.971812 37.370083,-121.971798 37.370212,-121.971783 37.370342,-121.971542 37.370486,-121.971904 37.370324,-121.972085 37.37028,-121.972266 37.370236,-121.972559 37.370196,-121.972852 37.370155,-121.973019 37.370155,-121.973186 37.370155,-121.973232 37.370136,-121.973279 37.370116,-121.973307 37.370058,-121.973336 37.370001,-121.973363 37.369836,-121.973391 37.369671,-121.973419 37.369227,-121.973446 37.368784,-121.973429 37.368413,-121.973413 37.368041,-121.973361 37.367714,-121.973308 37.367387,-121.973285 37.367339,-121.973262 37.36729,-121.973126 37.3673,-121.972989 37.36731,-121.973066 37.36728,-121.973144 37.367251,-121.973269 37.367237,-121.973393 37.367223,-121.973443 37.367158,-121.973493 37.367093,-121.973518 37.36702,-121.973543 37.366947,-121.973582 37.366618,-121.973622 37.366288,-121.97366 37.365826,-121.973698 37.365363,-121.973669 37.365336))', 4326))) as p(g)
)
-- select various versions of the polygons into the same column for overlay comparison in SSMS
select 'Original' as l
      ,g
from g
union all
select 'Short' as l
      ,x
from g
union all
select 'Original Reduced' as l
      ,g.Reduce(10)
from g
union all
select 'Short Reduced' as l
      ,x.Reduce(10)
from g;

Выход

Здесь интересно отметить разницу в длине двоичного представления geog (простое количество отображаемых символов). Как я упоминал выше, простое использование функции Reduce может сделать то, что вам нужно, поэтому вам нужно протестировать различные подходы, чтобы увидеть, как лучше всего сократить передачу данных.

+------------------+--------------------+------+
|        l         |         g          |  Len |
+------------------+--------------------+------+
| Original         | 0xE6100000010484...| 4290 |
| Short            | 0xE6100000010471...| 3840 |
| Original Reduced | 0xE6100000010418...|  834 |
| Short Reduced    | 0xE610000001041E...| 1184 |
+------------------+--------------------+------+

Визуальное сравнение

Функция разделения строк

Поскольку данные полигонов могут быть чертовски огромными, вам понадобится разделитель строк, который может обрабатывать более 4 или 8 тысяч символов. В моем случае я предпочитаю подход на основе xml:

create function [dbo].[fn_StringSplitMax]
(
    @str nvarchar(max) = ' '                -- String to split.
    ,@delimiter as nvarchar(max) = ','      -- Delimiting value to split on.
    ,@num as int = null                     -- Which value to return.
)
returns table
as
return
    with s as
    (       -- Convert the string to an XML value, replacing the delimiter with XML tags
        select convert(xml,'<x>' + replace((select @str for xml path('')),@delimiter,'</x><x>') + '</x>').query('.') as s
    )
    select rn
          ,item     -- Select the values from the generated XML value by CROSS APPLYing to the XML nodes
    from(select row_number() over (order by (select null)) as rn
              ,n.x.value('.','nvarchar(max)') as item
        from s
              cross apply s.nodes('x') as n(x)
        ) a
    where rn = @num
        or @num is null;
person iamdave    schedule 17.07.2020
comment
Да, я хочу передать WKT. - person Merenzo; 21.07.2020
comment
@Merenzo Согласно редактированию в верхней части моего ответа, вы пробовали решение в моем ответе? - person iamdave; 21.07.2020
comment
Да, @iamdave, он отлично работает для POLYGON, и я уверен, что его можно адаптировать для работы с MULTIPOLYGON. - person Merenzo; 23.07.2020
comment
@Merenzo Отличные новости! Если это был ответ на ваш вопрос, пожалуйста, отметьте его как таковой, чтобы другие пользователи, столкнувшиеся с этим вопросом, сталкивались с ним. - person iamdave; 23.07.2020

Проходя через отличный ответ @iamdave и используя тот же подход, похоже, что нам нужно разделить только точки... Я думаю, что мы можем игнорировать все круглые скобки и запятые и игнорировать префикс POLYGON (что означает, что он будет работать через другие типы GEOGRAPHY, такие как MULTIPOLYGON.)

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

Это работает для меня (используя тестовые данные @iamdave):

DECLARE @wkt NVARCHAR(MAX), @wktShort NVARCHAR(MAX);
DECLARE @decimalPlaces int = 4;
SET @wkt  = 'POLYGON((-121.973669 37.365336,-121.97367 37.365336,-121.973642 37.365309,-121.973415 37.365309,-121.973189 37.365309,-121.973002 37.365912,-121.972815 37.366515,-121.972796 37.366532,-121.972776 37.366549,-121.972627 37.366424,-121.972478 37.366299,-121.972422 37.366299,-121.972366 37.366299,-121.972298 37.366356,-121.97223 37.366412,-121.97215 37.366505,-121.97207 37.366598,-121.971908 37.366794,-121.971489 37.367353,-121.971396 37.367484,-121.971285 37.36769,-121.971173 37.367897,-121.971121 37.368072,-121.971068 37.368248,-121.971028 37.36847,-121.970987 37.368692,-121.970987 37.368779,-121.970987 37.368866,-121.970949 37.368923,-121.970912 37.36898,-121.970935 37.36898,-121.970958 37.36898,-121.970975 37.368933,-121.970993 37.368887,-121.971067 37.368807,-121.97114 37.368726,-121.971124 37.368705,-121.971108 37.368685,-121.971136 37.368698,-121.971163 37.368712,-121.97134 37.368531,-121.971516 37.368351,-121.971697 37.368186,-121.971878 37.368021,-121.972085 37.367846,-121.972293 37.36767,-121.972331 37.367629,-121.972369 37.367588,-121.972125 37.367763,-121.97188 37.367938,-121.971612 37.36815,-121.971345 37.368362,-121.971321 37.36835,-121.971297 37.368338,-121.971323 37.368298,-121.97135 37.368259,-121.971569 37.368062,-121.971788 37.367865,-121.971977 37.367716,-121.972166 37.367567,-121.972345 37.367442,-121.972524 37.367317,-121.972605 37.367272,-121.972687 37.367227,-121.972728 37.367227,-121.972769 37.367227,-121.972769 37.367259,-121.972769 37.367291,-121.972612 37.367416,-121.972454 37.367542,-121.972488 37.367558,-121.972521 37.367575,-121.972404 37.367674,-121.972286 37.367773,-121.972194 37.367851,-121.972101 37.367928,-121.972046 37.36799,-121.971991 37.368052,-121.972008 37.368052,-121.972025 37.368052,-121.972143 37.367959,-121.972261 37.367866,-121.972296 37.367866,-121.972276 37.36794,-121.972221 37.36798,-121.972094 37.368097,-121.971966 37.368214,-121.971956 37.368324,-121.971945 37.368433,-121.971907 37.368753,-121.971868 37.369073,-121.97184 37.369578,-121.971812 37.370083,-121.971798 37.370212,-121.971783 37.370342,-121.971542 37.370486,-121.971904 37.370324,-121.972085 37.37028,-121.972266 37.370236,-121.972559 37.370196,-121.972852 37.370155,-121.973019 37.370155,-121.973186 37.370155,-121.973232 37.370136,-121.973279 37.370116,-121.973307 37.370058,-121.973336 37.370001,-121.973363 37.369836,-121.973391 37.369671,-121.973419 37.369227,-121.973446 37.368784,-121.973429 37.368413,-121.973413 37.368041,-121.973361 37.367714,-121.973308 37.367387,-121.973285 37.367339,-121.973262 37.36729,-121.973126 37.3673,-121.972989 37.36731,-121.973066 37.36728,-121.973144 37.367251,-121.973269 37.367237,-121.973393 37.367223,-121.973443 37.367158,-121.973493 37.367093,-121.973518 37.36702,-121.973543 37.366947,-121.973582 37.366618,-121.973622 37.366288,-121.97366 37.365826,-121.973698 37.365363,-121.973669 37.365336))';

-- Split on '.', then get the next N decimals, and find the index of the first non-number.
-- Then recombine the fragments, skipping the unwanted numbers.
WITH points AS (
    SELECT value, LEFT(value, @decimalPlaces) AS decimals, PATINDEX('%[^0-9]%', value) AS indx
    FROM STRING_SPLIT(@wkt, '.')
)
SELECT @wktShort = STRING_AGG(IIF(indx < @decimalPlaces, '', decimals) + SUBSTRING(value, indx, LEN(value)), '.') 
FROM points;

Сравнивая оригинал с укороченным, мы видим, что каждое число усечено до 4 dp:

SELECT @wkt AS Text UNION ALL SELECT @wktShort;

введите здесь описание изображения

person Merenzo    schedule 23.07.2020