2

I am facing a weird issue wherein on disabling/enabling certain condition in where clause, my Select query throws .net framework error.

Here is the CREATE table script.

Table test_classes:

CREATE TABLE [dbo].[test_classes]
(
    [CLASSID] [int] NOT NULL,
    [PARENTID] [int] NULL,
    [CATID] [int] NOT NULL,
    [CLASS_NAME] [nvarchar](255) NOT NULL,
    [ORIGINAL_NAME] [nvarchar](255) NULL,
    [GEOMETRY] [tinyint] NOT NULL,
    [READ_ONLY] [bit] NOT NULL,
    [DISPLAY_STYLES] [image] NULL,
    [FEATURE_COUNT] [int] NOT NULL,
    [TEMPOWNER] [int] NULL,
    [OPTIONS] [int] NOT NULL,
    [POLYGON_TYPE] [int] NULL,
    [CLASS_EXTRA] [nvarchar](1024) NULL,
    [MAPID] [int] NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

Table test_polygon:

CREATE TABLE [dbo].[test_polygon]
(
    [FID] [nvarchar](36) NOT NULL,
    [EXTENT_L] [float] NOT NULL,
    [EXTENT_T] [float] NOT NULL,
    [EXTENT_R] [float] NOT NULL,
    [EXTENT_B] [float] NOT NULL,
    [COORDINATES] [image] NULL,
    [CHAINS] [smallint] NOT NULL,
    [CLASSID] [int] NOT NULL,
    [SPATIAL_KEY] [bigint] NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

Due to word limitation (due to image datatype), here is the INSERT input: GDrive SQL Link

SELECT SQL query:

select 
    Class_Name, FID, 
    geometry::STGeomFromWKB(b1+b2,0) as polygon, 
    Class_ID, Original_Name
from 
    (Select 
         cl.Class_Name, p.FID,
         substring(CAST(p.Coordinates AS varbinary(max)),1,1) as b1,
         substring(CAST(p.Coordinates AS varbinary(max)),3,999999) as b2,
         cl.ClassID as Class_ID,
         cl.Original_Name
     From       
         test_polygon p
     Inner Join 
         test_classes cl on cl.ClassID = p.ClassID) s_polygon
--where Class_ID = 215                  --Filter#1
--where Class_Name = 'L1_County'        --Filter#2

To note, Class_ID 215 represents 'L1_County' class_name.

Problem is, if you enable Filter#1, then the output is as expected. But when I only enable Filter#2 then the query fails with .NET Error.

Expected output :

Class_Name  FID               polygon       Class_ID    Original_Name
----------- ----------------  ------------- ----------- ------------------------
L1_County   Northamptonshire  <long value>  215         B8USR_4DB8184E88092424 

Error I get :

Msg 6522, Level 16, State 1, Line 4
A .NET Framework error occurred during execution of user-defined routine or aggregate "geometry":
System.FormatException: 24119: The Polygon input is not valid because the start and end points of the exterior ring are not the same. Each ring of a polygon must have the same start and end points.

System.FormatException:
at Microsoft.SqlServer.Types.GeometryValidator.ValidatePolygonRing(Int32 iRing, Int32 cPoints, Double firstX, Double firstY, Double lastX, Double lastY)
at Microsoft.SqlServer.Types.Validator.Execute(Transition transition)
at Microsoft.SqlServer.Types.ForwardingGeoDataSink.EndFigure()
at Microsoft.SqlServer.Types.WellKnownBinaryReader.ReadLineStringPoints(ByteOrder byteOrder, UInt32 cPoints, Boolean readZ, Boolean readM)
at Microsoft.SqlServer.Types.WellKnownBinaryReader.ReadLinearRing(ByteOrder byteOrder, Boolean readZ, Boolean readM)
at Microsoft.SqlServer.Types.WellKnownBinaryReader.ParseWkbPolygonWithoutHeader(ByteOrder byteOrder, Boolean readZ, Boolean readM)
at Microsoft.SqlServer.Types.WellKnownBinaryReader.ParseWkb(OpenGisType> type) > at Microsoft.SqlServer.Types.WellKnownBinaryReader.Read(OpenGisType type, Int32 srid)
at Microsoft.SqlServer.Types.SqlGeometry.GeometryFromBinary(OpenGisType type, SqlBytes binary, Int32 srid) .

What I am trying to ask is, Why do I get error when WHERE clause has Class_Name and not when Class_ID.

I am using SQL Server 2012 Enterprise edition. Error replicates in SQL Server 2008 as well.

edit:

Estimated Execution plan for Filter#1 :

plan1

Estimated Execution plan for Filter#2 :

plan2

Prabhat G
  • 2,974
  • 1
  • 22
  • 31
  • 2
    This is likely due to different execution plans. ID filter likely uses an index and therefore returns only one row based on the index, where is name filter is using a table scan and returns many rows (including the "dodgy row)" where it then calculates geometry and ONLY then filters based on Class_Name – Alex Aug 04 '17 at 11:27
  • Just to elaborate it is probably something similar to this question: https://stackoverflow.com/questions/17427560/conversion-failed-when-converting-the-varchar-value-n-to-data-type-int . Do you get an error if you run your query without any filters? – Alex Aug 04 '17 at 11:35
  • Thanks for comments @Alex. Yes, I do get error when running without any filter. that's natural. The WKB fails for 1 specific polygon. my question was to why Filter#2 isn't filtering records like Filter#1. – Prabhat G Aug 04 '17 at 11:41
  • Different execution plans: specifically scan vs seek. If scan is chosen filtering will likely (in your case it is a fact) be applied after `STGeomFromWKB`. In SSMS you can use Query->"Display Actual Execution Plan" and Query->"Display Estimated Execution Plan" to see what is happening. – Alex Aug 04 '17 at 11:44
  • They do both Table Scan on Test_polygon. – Vojtěch Dohnal Aug 04 '17 at 11:46
  • Post execution plans in your question for both filters – Alex Aug 04 '17 at 11:47
  • @Alex : posted. They both are identical though as rightly pointed by Mr. Dohnal – Prabhat G Aug 04 '17 at 11:52
  • @Alex Yes they are, even if you add `OPTION ( HASH JOIN )` to the first query, still it runs ok and the second fails. Execution plans are totally identical. Very bizzare indeed... – Vojtěch Dohnal Aug 04 '17 at 11:53
  • 1
    Hover mouse over "Table Scan [test_polygon]" to see a little pop up. Notice that when ID filter is used you get "Predicate" specified; when name is used there is no predicate on [test_polygon], instead the filtering is applied on [test_classes]. Also relevant is that "Compute Scalar" happens before "Join". – Alex Aug 04 '17 at 12:00

1 Answers1

2

I will summarise comments:

You are seeing this issue because your table contains invalid data. The reason you do not see it when searching by test_polygon.Class_ID is that Class_ID is passed as a predicate to the table scan. When test_classes.Class_Name is used as filter the search predicate is applied to test_classes table. Since geometry::STGeomFromWKB "Compute Scalar" happens before "Join" it causes all rows of test_polygon to be evaluated by this function, including rows containing invalid data.

Update: Even though the plans look the same, they are not, as predicate conditions are different for different filters (WHERE conditions) and therefore outputs of table scans operators are different.

The is no standard way to force the order of evaluation in SQL Server query as by design you are not supposed to.

There are two options:

  1. Materialise (store in a table) the result of the sub-query. This, simply, splits the query into two separate queries, one to find records and the second query to compute data on the found results. The intermediate results are stored in a (temp) table.
  2. Use "hacks" that allow you to coerce SQL Server to evaluate query a certain way.

Below is an example of a "hack":

select 
    Class_Name, FID, 
    CASE WHEN Class_Name = Class_Name THEN geometry::STGeomFromWKB(b1+b2,0) ELSE NULL END as polygon,
    Class_ID, Original_Name
from 
    (Select 
         cl.Class_Name, p.FID,
         substring(CAST(p.Coordinates AS varbinary(max)),1,1) as b1,
         substring(CAST(p.Coordinates AS varbinary(max)),3,999999) as b2,
         cl.ClassID as Class_ID,
         cl.Original_Name
     From       
         test_polygon p
     Inner Join 
         test_classes cl on cl.ClassID = p.ClassID) s_polygon
--where Class_ID = 215                  --Filter#1
where Class_Name = 'L1_County'        --Filter#2

By adding a dummy CASE expression that looks at test_classes.Class_Name we are forcing SQL Server to evaluate it after the JOIN has been resolved.

The plan:

New Plan

Useful Article: http://dataeducation.com/cursors-run-just-fine/

Community
  • 1
  • 1
Alex
  • 4,885
  • 3
  • 19
  • 39
  • Any ideas why `Class_ID is passed as a predicate`? Because it is `int`? Another option would be to materialize the subquery first. – Vojtěch Dohnal Aug 04 '17 at 12:37
  • @VojtěchDohnal - I am not sure what you mean by the first question. Predicate is filter which appears in the `WHERE` clause. When searching by `Class_Name` it is added to predicates in `test_classes`. Nothing to do with the data type. RE: materialize the subquery first - yes you can save results to temp table. Thanks for pointing this out, I will update my answer. – Alex Aug 04 '17 at 12:43
  • Thanks both of you. It's quite satisfactory I believe, unless someone has got better reason to explain. :-) – Prabhat G Aug 04 '17 at 12:58
  • 1
    I meant why the predicate worked in Table Scan for `Class_ID` and not for `Class_Name`. Probably because ClassID attribute exists in both tables - they get joined by it. I think the question was not about finding various workarounds but to find a reason why, which you did quite well. – Vojtěch Dohnal Aug 04 '17 at 12:58
  • @VojtěchDohnal - I see what you mean now. The key problem here are not predicates themselves but rather at what stage "Compute Scalar" gets evaluated. In original query it was evaluated before the inner join and therefore all `test_polygon` records were passed into the "geometry" function. – Alex Aug 04 '17 at 13:08
  • Yes, but in the first query thanks to that predicate the Compute Scalar run only for that one Class_ID=215 record. So this was the magic why it worked though both plans looked the same. The missing hint for materializing subqueries is [a known problem for a quite long time...](https://connect.microsoft.com/SQLServer/feedback/details/218968/provide-a-hint-to-force-intermediate-materialization-of-ctes-or-derived-tables) – Vojtěch Dohnal Aug 04 '17 at 13:12
  • @VojtěchDohnal - plans are not the same. Predicates are different, therefore the outputs of the table scans are different. – Alex Aug 04 '17 at 13:14
  • For filter 1, Predicate is present for both the table scan (classes and polygon), whereas for filter 2, predicate is only at classes – Prabhat G Aug 04 '17 at 13:16