24

When trying to compare software versions 5.12 to 5.8, version 5.12 is newer, however mathematically 5.12 is less than 5.8. How would I compare the two versions so that a newer version returns 'Y'?

SELECT CASE WHEN 5.12 > 5.8 THEN 'Y' ELSE 'N' END

Possible Solutions

  1. Add a 0 after the decimal in 5.8 so that it compares 5.08 to 5.12, however it seems like this would require a bit of code.
  2. Simply compare values after the decimal (ie. 12 > 8), however this fails when the version rolls to 6.0.
  3. Use reverse logic and assume that if 5.12 is less than 5.8 to return 'Y'. I believe this would fail when the version rolls to 6.0.
Kermit
  • 33,827
  • 13
  • 85
  • 121
  • 13
    Version of SQL Server? If 2008+ `DECLARE @V1 VARCHAR(10) = '5.12', @V2 VARCHAR(10) = '5.8';SELECT CASE WHEN CAST('/' + @V1 + '/' AS HIERARCHYID) > CAST('/' + @V2 + '/' AS HIERARCHYID) THEN 'Y' ELSE 'N' END` – Martin Smith Dec 18 '13 at 16:30
  • @MartinSmith You're late. – Kermit Dec 18 '13 at 19:28
  • 1
    @MartinSmith solution works great – Ghini Antonio Jul 24 '19 at 13:04
  • @MartinSmith's HierarchyID solution works well for this case. Seems HierarchyID only supports up to 5 digits between the 'dots' in a version number, so use with caution. – CJBS Feb 24 '20 at 23:26
  • @CJBS I don't think hierarchyid has any such limit on depth. It is intended for general hierarchical data – Martin Smith Apr 29 '21 at 06:46

18 Answers18

15

You could use hierarchyid Which you can use by putting a / at the end and start of the string and casting it

e.g.

SELECT CASE WHEN cast('/5.12/' as hierarchyid) > cast('/5.8/' as hierarchyid) THEN 'Y' ELSE 'N' END

That returns a Y

Eva Lacy
  • 1,257
  • 10
  • 24
8
declare @v1 varchar(100) = '5.12'
declare @v2 varchar(100) = '5.8'

select 
    case 
    when CONVERT(int, LEFT(@v1, CHARINDEX('.', @v1)-1)) < CONVERT(int, LEFT(@v2, CHARINDEX('.', @v2)-1)) then 'v2 is newer'
    when CONVERT(int, LEFT(@v1, CHARINDEX('.', @v1)-1)) > CONVERT(int, LEFT(@v2, CHARINDEX('.', @v2)-1)) then 'v1 is newer'
    when CONVERT(int, substring(@v1, CHARINDEX('.', @v1)+1, LEN(@v1))) < CONVERT(int, substring(@v2, CHARINDEX('.', @v2)+1, LEN(@v1))) then 'v2 is newer'
    when CONVERT(int, substring(@v1, CHARINDEX('.', @v1)+1, LEN(@v1))) > CONVERT(int, substring(@v2, CHARINDEX('.', @v2)+1, LEN(@v1))) then 'v1 is newer'
    else 'same!'

    end
Sean
  • 14,359
  • 13
  • 74
  • 124
  • And this can easily be extended out to _n_-part version numbers as necessary. – DaveE Jul 19 '13 at 19:59
  • Thanks for you answer, I didn't find a way to easily extend this to n-part then I posted my own answer: https://stackoverflow.com/a/65698263/11159476 with this you can also for example compare a version "1.2" with "1.2.1" (will return -1, v1 < v2) as "1.2" will be considered as "1.2.0", it is not an usual check but in case during time a digit is added to version a "1.2" will actually be considered equal to "1.2.0". – gluttony Jan 13 '21 at 08:39
8

There was a very good solution from a duplicate question here: How to compare SQL strings that hold version numbers like .NET System.Version class?

After playing with the query for a while, I learned that it was not able to compare the last part when there are 4 or more parts (say, if the version number was 1.2.3.4, it would always treat the last one as 0). I have fixed that issue as well as came up with another function to compare two version numbers.

CREATE Function [dbo].[VersionNthPart](@version as nvarchar(max), @part as int) returns int as
Begin

Declare
    @ret as int = null,
    @start as int = 1,
    @end as int = 0,
    @partsFound as int = 0,
    @terminate as bit = 0

  if @version is not null
  Begin
    Set @ret = 0
    while @partsFound < @part
    Begin
      Set @end = charindex('.', @version, @start)
      If @end = 0 -- did not find the dot. Either it was last part or the part was missing.
      begin
        if @part - @partsFound > 1 -- also this isn't the last part so it must bail early.
        begin
            set @terminate = 1
        end
        Set @partsFound = @part
        SET @end = len(@version) + 1; -- get the full length so that it can grab the whole of the final part.
      end
      else
      begin
        SET @partsFound = @partsFound + 1
      end
      If @partsFound = @part and @terminate = 0
      begin
            Set @ret = Convert(int, substring(@version, @start, @end - @start))
      end
      Else
      begin
            Set @start = @end + 1
      end
    End
  End
  return @ret
End
GO

CREATE FUNCTION [dbo].[CompareVersionNumbers]
(
    @Source nvarchar(max),
    @Target nvarchar(max),
    @Parts int = 4
)
RETURNS INT
AS
BEGIN
/*
-1 : target has higher version number (later version)
0 : same
1 : source has higher version number (later version)
*/ 
    DECLARE @ReturnValue as int = 0;
    DECLARE @PartIndex as int = 1;
    DECLARE @SourcePartValue as int = 0;
    DECLARE @TargetPartValue as int = 0;
    WHILE (@PartIndex <= @Parts AND @ReturnValue = 0)
    BEGIN
        SET @SourcePartValue = [dbo].[VersionNthPart](@Source, @PartIndex);
        SET @TargetPartValue = [dbo].[VersionNthPart](@Target, @PartIndex);
        IF @SourcePartValue > @TargetPartValue
            SET @ReturnValue = 1
        ELSE IF @SourcePartValue < @TargetPartValue
            SET @ReturnValue = -1
        SET @PartIndex = @PartIndex + 1;
    END
    RETURN @ReturnValue
END

Usage/Test case:

declare @Source as nvarchar(100) = '4.9.21.018'
declare @Target as nvarchar(100) = '4.9.21.180'
SELECT [dbo].[CompareVersionNumbers](@Source, @Target, DEFAULT) -- default version parts are 4

SET @Source = '1.0.4.1'
SET @Target = '1.0.1.8'
SELECT [dbo].[CompareVersionNumbers](@Source, @Target, 4) -- typing out # of version parts also works

SELECT [dbo].[CompareVersionNumbers](@Source, @Target, 2) -- comparing only 2 parts should be the same

SET @Target = '1.0.4.1.5'
SELECT [dbo].[CompareVersionNumbers](@Source, @Target, 4) -- only comparing up to parts 4 so they are the same
SELECT [dbo].[CompareVersionNumbers](@Source, @Target, 5) -- now comparing 5th part which should indicate that the target has higher version number
Community
  • 1
  • 1
Stephen C
  • 193
  • 3
  • 7
7

I recommend to create a SQL CLR function:

public partial class UserDefinedFunctions
{
    [SqlFunction(Name = "CompareVersion")] 
    public static bool CompareVersion(SqlString x, SqlString y)
    {
        return Version.Parse(x) > Version.Parse(y);
    }
}

Notes:

  • SqlString has explicit cast to string.
  • Pass full version string as of a.b.c.d
abatishchev
  • 98,240
  • 88
  • 296
  • 433
6

I encountered this when trying to filter SQL rows based on semantic versioning. My solution was a bit different, in that I wanted to store configuration rows tagged with a semantic version number and then select rows compatible with a running version of our software.

Assumptions:

  • My software will include a configuration setting containing the current version number
  • Data-driven configuration rows will include a min version number
  • I need to be able to select configuration rows where min <= current.

Examples:

  • Version 1.0.0 should include: 1.0.0, 1.0.0-*, 1.0.0-beta.1
  • Version 1.0.0 should exclude: 1.0.1, 1.1.0, 2.0.0
  • Version 1.1.0-beta.2 should include: 1.0.0, 1.0.1, 1.1.0-beta.1, 1.1.0-beta.2
  • Version 1.1.0-beta.2 should exclude: 1.1.0, 1.1.1, 1.2.0, 2.0.0, 1.1.1-beta.1

The MSSQL UDF is:

CREATE FUNCTION [dbo].[SemanticVersion] (
    @Version nvarchar(50)
)
RETURNS nvarchar(255)

AS
BEGIN

    DECLARE @hyphen int = CHARINDEX('-', @version)
    SET @Version = REPLACE(@Version, '*', ' ')
    DECLARE 
        @left nvarchar(50) = CASE @hyphen WHEN 0 THEN @version ELSE SUBSTRING(@version, 1, @hyphen-1) END,
        @right nvarchar(50) = CASE @hyphen WHEN 0 THEN NULL ELSE SUBSTRING(@version, @hyphen+1, 50) END,
        @normalized nvarchar(255) = '',
        @buffer int = 8

    WHILE CHARINDEX('.', @left) > 0 BEGIN
        SET @normalized = @normalized + CASE ISNUMERIC(LEFT(@left, CHARINDEX('.', @left)-1))
            WHEN 0 THEN LEFT(@left, CHARINDEX('.', @left)-1)
            WHEN 1 THEN REPLACE(STR(LEFT(@left, CHARINDEX('.', @left)-1), @buffer), SPACE(1), '0')
        END  + '.'
        SET @left = SUBSTRING(@left, CHARINDEX('.', @left)+1, 50)
    END
    SET @normalized = @normalized + CASE ISNUMERIC(@left)
        WHEN 0 THEN @left
        WHEN 1 THEN REPLACE(STR(@left, @buffer), SPACE(1), '0')
    END

    SET @normalized = @normalized + '-'
    IF (@right IS NOT NULL) BEGIN
        WHILE CHARINDEX('.', @right) > 0 BEGIN
            SET @normalized = @normalized + CASE ISNUMERIC(LEFT(@right, CHARINDEX('.', @right)-1))
                WHEN 0 THEN LEFT(@right, CHARINDEX('.', @right)-1)
                WHEN 1 THEN REPLACE(STR(LEFT(@right, CHARINDEX('.', @right)-1), @buffer), SPACE(1), '0')
            END  + '.'
            SET @right = SUBSTRING(@right, CHARINDEX('.', @right)+1, 50)
        END
        SET @normalized = @normalized + CASE ISNUMERIC(@right)
            WHEN 0 THEN @right
            WHEN 1 THEN REPLACE(STR(@right, @buffer), SPACE(1), '0')
        END
    END ELSE 
        SET @normalized = @normalized + 'zzzzzzzzzz'

    RETURN @normalized

END

SQL tests include:

SELECT CASE WHEN dbo.SemanticVersion('1.0.0-alpha') < dbo.SemanticVersion('1.0.0-alpha.1') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.0.0-alpha.1') < dbo.SemanticVersion('1.0.0-alpha.beta') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.0.0-alpha.beta') < dbo.SemanticVersion('1.0.0-beta') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.0.0-beta') < dbo.SemanticVersion('1.0.0-beta.2') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.0.0-beta.2') < dbo.SemanticVersion('1.0.0-beta.11') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.0.0-beta.11') < dbo.SemanticVersion('1.0.0-rc.1') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.0.0-rc.1') < dbo.SemanticVersion('1.0.0') THEN 'Success' ELSE 'Failure' END


SELECT CASE WHEN dbo.SemanticVersion('1.0.0-*') <= dbo.SemanticVersion('1.0.0') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.0.*') <= dbo.SemanticVersion('1.0.0') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.*') <= dbo.SemanticVersion('1.0.0') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('*') <= dbo.SemanticVersion('1.0.0') THEN 'Success' ELSE 'Failure' END

SELECT CASE WHEN dbo.SemanticVersion('1.0.0-*') <= dbo.SemanticVersion('1.0.0') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.0.1-*') > dbo.SemanticVersion('1.0.0') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.0.1-*') <= dbo.SemanticVersion('1.0.1') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.1.*') > dbo.SemanticVersion('1.0.9') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.1.*') <= dbo.SemanticVersion('1.2.0') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.*') <= dbo.SemanticVersion('2.0.0') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('1.*') > dbo.SemanticVersion('0.9.9-beta-219') THEN 'Success' ELSE 'Failure' END
SELECT CASE WHEN dbo.SemanticVersion('*') <= dbo.SemanticVersion('0.0.1-alpha-1') THEN 'Success' ELSE 'Failure' END
Eric Patrick
  • 2,097
  • 2
  • 20
  • 31
4

Two steps, first compare the left of the decimal point and after that compare the right.


Possible solution:

declare @v1 varchar(100) = '5.12'
declare @v2 varchar(100) = '5.8'

select case 
    when CONVERT(int, LEFT(@v1, CHARINDEX('.', @v1)-1)) < CONVERT(int, LEFT(@v2, CHARINDEX('.', @v2)-1)) then 'v2 is newer'
    when CONVERT(int, LEFT(@v1, CHARINDEX('.', @v1)-1)) > CONVERT(int, LEFT(@v2, CHARINDEX('.', @v2)-1)) then 'v1 is newer'
    when CONVERT(int, RIGHT(@v1, LEN(@v1) - CHARINDEX('.', @v1))) < CONVERT(int, RIGHT(@v2, LEN(@v2) - CHARINDEX('.', @v2))) then 'v2 is newer'
    when CONVERT(int, RIGHT(@v1, LEN(@v1) - CHARINDEX('.', @v1))) > CONVERT(int, RIGHT(@v2, LEN(@v2) - CHARINDEX('.', @v2))) then 'v1 is newer'
    else 'same!' end as 'Version Test'
aF.
  • 64,980
  • 43
  • 135
  • 198
2

Do not store in a string what is not a string. Alternative is creating your own data type (in C# - allowed for some time) that stored the versions as a sequence of bytes and implements proper comparison logic.

TomTom
  • 61,059
  • 10
  • 88
  • 148
  • Can you elaborate what you mean by "Do not store in a string what is not a string?" – Kermit Jul 06 '12 at 14:59
  • Simple 5.12 is NOT A STRING - it is a sequence of 2 numbers (5, 12). Stroring them in a varchar is forcing string semantics on that - which do not work. Storing them in a custom type means you can put proper semantics in. http://msdn.microsoft.com/en-us/library/ms131120.aspx has details about that. – TomTom Jul 06 '12 at 15:09
  • Perhaps I should have added that I am storing these as decimals. – Kermit Jul 06 '12 at 15:11
  • As bad - they are NOT DECIMALS either, so you (a) waste space and (b) impose a wrong semantics over them. – TomTom Jul 06 '12 at 15:19
2

As suggested by AF you can compare the int part and then the decimal part .Apart from all the answers given there is one more way to do it using parsename .You could try something like this

 case when cast(@var as int)>cast(@var2 as int) then 'Y' 
 when cast(PARSENAME(@var,1) as int) > cast(PARSENAME(@var2,1) as int) THEN 'Y'


 Declare @var float
 Declare @var2 float
 set @var=5.14
 set @var2=5.8
 Select case when cast(@var as int)>cast(@var2 as int) then 'Y' 
 when cast(PARSENAME(@var,1) as int)> cast(PARSENAME(@var2,1) as int) THEN 'Y'
 else 'N' END
praveen
  • 12,083
  • 1
  • 41
  • 49
  • I'm not sure if I'm not testing this correctly, but it seems to fail when `@var` = 5.14 and `@var2` = 5.8. – Kermit Jul 06 '12 at 15:44
  • I haven't executed the query but you can convert parsename to int example cast(PARSENAME(@var,1) as int .I have updated my answer !! – praveen Jul 06 '12 at 15:48
1

You don't say so in the question, but your comment under Tomtom's answer suggests you are storing the version numbers as [decimals][d]. I guess that you have a table like this:

CREATE TABLE ReleaseHistory (
  VersionNumber DECIMAL(6,3) NOT NULL
);
GO

INSERT INTO ReleaseHistory (
  VersionNumber
)
VALUES
  (5.12),
  (5.8),
  (12.34),
  (3.14),
  (0.78),
  (1.0);
GO

The following query is an attempt to rank versions by the order in which they would be released:

SELECT
  VersionNumber,
  RANK() OVER (ORDER BY VersionNumber) AS ReleaseOrder
FROM ReleaseHistory;

It produces the following result set:

VersionNumber                           ReleaseOrder
--------------------------------------- --------------------
0.780                                   1
1.000                                   2
3.140                                   3
5.120                                   4
5.800                                   5
12.340                                  6

This is not what we expect. Version 5.8 was released before version 5.12!

Split the version number into its major and minor components to rank the version numbers properly. One way to do this is to convert the decimal value to a string and split on the period. The T-SQL syntax for this is ugly (the language is not designed for string processing):

WITH VersionStrings AS (
  SELECT CAST(VersionNumber AS VARCHAR(6)) AS VersionString
  FROM ReleaseHistory
),
VersionNumberComponents AS (
  SELECT
    CAST(SUBSTRING(VersionString, 1, CHARINDEX('.', VersionString) - 1) AS INT) AS MajorVersionNumber,
    CAST(SUBSTRING(VersionString, CHARINDEX('.', VersionString) + 1, LEN(VersionString) - CHARINDEX('.', VersionString)) AS INT) AS MinorVersionNumber
  FROM VersionStrings
)
SELECT
  CAST(MajorVersionNumber AS VARCHAR(3)) + '.' + CAST(MinorVersionNumber AS VARCHAR(3)) AS VersionString,
  RANK() OVER (ORDER BY MajorVersionNumber, MinorVersionNumber) AS ReleaseOrder
FROM VersionNumberComponents;

But it provides the expected result:

VersionString ReleaseOrder
------------- --------------------
0.780         1
1.0           2
3.140         3
5.120         4
5.800         5
12.340        6

As Tomtom replied, decimal is a not a good type to store a version number. It would be better to store the version number in two positive integer columns, one containing the major version number and the other containing the minor version number.

Community
  • 1
  • 1
Iain Samuel McLean Elder
  • 19,791
  • 12
  • 64
  • 80
  • The current version number was compared to the version in a row, which was stored as a decimal. – Kermit Jul 07 '12 at 02:42
1

This is based on SeanW's answer but this solution allows for the following format [major].[minor].[build]. It maybe used for SQL 2K and when cursor is not an option.

declare @v1 varchar(100) = '1.4.020'
declare @v2 varchar(100) = '1.4.003'

declare @v1_dot1_pos smallint   /*position - 1st version - 1st dot */
declare @v1_dot2_pos smallint   /*position - 1st version - 2nd dot */
declare @v2_dot1_pos smallint   /*position - 2nd version - 1st dot */
declare @v2_dot2_pos smallint   /*position - 2nd version - 2nd dot */

-------------------------------------------------
-- get the pos of the first and second dots
-------------------------------------------------
SELECT 
@v1_dot1_pos=CHARINDEX('.', @v1),
@v2_dot1_pos=CHARINDEX('.', @v2),
@v1_dot2_pos=charindex( '.', @v1, charindex( '.', @v1 ) + 1 ),
@v2_dot2_pos=charindex( '.', @v2, charindex( '.', @v2 ) + 1 )


-------------------------------------------------
-- break down the parts
-------------------------------------------------
DECLARE @v1_major int, @v2_major int
DECLARE @v1_minor int, @v2_minor int
DECLARE @v1_build int, @v2_build int 

SELECT 
    @v1_major = CONVERT(int,LEFT(@v1,@v1_dot1_pos-1)),
    @v1_minor = CONVERT(int,SUBSTRING(@v1,@v1_dot1_pos+1,(@v1_dot2_pos-@v1_dot1_pos)-1)),
    @v1_build = CONVERT(int,RIGHT(@v1,(LEN(@v1)-@v1_dot2_pos))),
    @v2_major = CONVERT(int,LEFT(@v2,@v2_dot1_pos-1)),
    @v2_minor = CONVERT(int,SUBSTRING(@v2,@v2_dot1_pos+1,(@v2_dot2_pos-@v2_dot1_pos)-1)),
    @v2_build = CONVERT(int,RIGHT(@v2,(LEN(@v2)-@v2_dot2_pos)))


-------------------------------------------------
-- return the difference
-------------------------------------------------
SELECT
    Case    
        WHEN @v1_major < @v2_major then 'v2 is newer'
        WHEN @v1_major > @v2_major then 'v1 is newer'
        WHEN @v1_minor < @v2_minor then 'v2 is newer'
        WHEN @v1_minor > @v2_minor then 'v1 is newer'
        WHEN @v1_build < @v2_build then 'v2 is newer'
        WHEN @v1_build > @v2_build then 'v1 is newer'
        ELSE '!Same'
    END
tmez
  • 156
  • 1
  • 7
1

The solution that was implemented:

CREATE FUNCTION [dbo].[version_compare]
(
    @v1 VARCHAR(5), @v2 VARCHAR(5)
)
RETURNS tinyint
AS
BEGIN
    DECLARE @v1_int tinyint, @v1_frc tinyint, 
            @v2_int tinyint, @v2_frc tinyint, 
            @ResultVar tinyint

    SET @ResultVar = 0

    SET @v1_int = CONVERT(tinyint, LEFT(@v1, CHARINDEX('.', @v1) - 1))
    SET @v1_frc = CONVERT(tinyint, RIGHT(@v1, LEN(@v1) - CHARINDEX('.', @v1)))
    SET @v2_int = CONVERT(tinyint, LEFT(@v2, CHARINDEX('.', @v2) - 1))
    SET @v2_frc = CONVERT(tinyint, RIGHT(@v2, LEN(@v2) - CHARINDEX('.', @v2)))

    SELECT @ResultVar = CASE
        WHEN @v2_int > @v1_int THEN 2
        WHEN @v1_int > @v2_int THEN 1
        WHEN @v2_frc > @v1_frc THEN 2
        WHEN @v1_frc > @v2_frc THEN 1
    ELSE 0 END

    -- Return the result of the function
    RETURN @ResultVar
END
GO
Kermit
  • 33,827
  • 13
  • 85
  • 121
1

This recursive query would convert any '.'-separated version numbers into comparable strings left-padding each element to 10 characters thus allowing to compare versions with or without build number and accommodating for non-numeric characters:

WITH cte (VersionNumber) AS (
  SELECT '1.23.456' UNION ALL
  SELECT '2.3'      UNION ALL
  SELECT '0.alpha-3'
  ),
  parsed (VersionNumber, Padded) AS (
  SELECT
    CAST(SUBSTRING(VersionNumber, CHARINDEX('.', VersionNumber) + 1, LEN(VersionNumber)) + '.' AS NVARCHAR(MAX)),
    CAST(RIGHT(REPLICATE('0', 10) + LEFT(VersionNumber, CHARINDEX('.', VersionNumber) - 1), 10) AS NVARCHAR(MAX))
  FROM cte
  UNION ALL
  SELECT
    SUBSTRING(VersionNumber, CHARINDEX('.', VersionNumber) + 1, LEN(VersionNumber)),
    Padded + RIGHT(REPLICATE('0', 10) + LEFT(VersionNumber, CHARINDEX('.', VersionNumber) - 1), 10)
  FROM parsed WHERE CHARINDEX('.', VersionNumber) > 0
  )
SELECT Padded
FROM parsed
WHERE VersionNumber = ''
ORDER BY Padded;

Padded
------------------------------
0000000000000alpha-3
000000000100000000230000000456
00000000020000000003
Y.B.
  • 3,526
  • 14
  • 24
1

I have created (with inspiration from Eva Lacy (above)), this function:

CREATE or alter function dbo.IsVersionNewerThan
(
    @Source nvarchar(max),
    @Target nvarchar(max)
)
RETURNS table
as 
/*
-1 : target has higher version number (later version)
0 : same
1 : source has higher version number (later version)

test harness:
; WITH tmp
AS
(
    SELECT '1.0.0.5' AS Version
    UNION ALL SELECT '0.0.0.0'
    UNION ALL SELECT '1.5.0.6'
    UNION ALL SELECT '2.0.0'
    UNION ALL SELECT '2.0.0.0'
    UNION ALL SELECT '2.0.1.1'
    UNION ALL SELECT '15.15.1323.22'
    UNION ALL SELECT '15.15.622.55'
)
SELECT tmp.version, isGreather from tmp
outer apply (select * from dbo.IsVersionNewerThan(tmp.Version, '2.0.0.0')) as IsG

*/ 
    return (
        select CASE 
            when cast('/' + @Source + '/' as hierarchyid) > cast('/' + @Target + '/' as hierarchyid) THEN 1 
            when @Source = @Target then 0
            else -1 
        end as IsGreather
    )
go

The test script is included as a comment. It works, as long as you do not have versions like '1.5.06.2' (note the zero).
SQL Server thinks this function has is_inlineable = 1, which bodes well for the performance.

Then my SQL code can look like this:

declare @version varchar(10) = '2.30.1.12'
set @version = '2.30.1.1'
if exists(select * from dbo.IsVersionNewerThan(@version,'2.30.1.12') where IsGreather >= 0)
BEGIN
    print 'yes'
end
else print 'no'
Henrik Staun Poulsen
  • 13,154
  • 4
  • 23
  • 26
0

Here is what I did by modifying some code I found on StackOverflow and writing some myself. This is version 1 of the code so please let me know what you think. Usage examples and test cases are in the code comments.

First create this function if not using SQL 2016 or greater and you do not have access to STRING_SPLIT:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      <Author,,Name>
-- Create date: <Create Date,,>
-- Description: modified from https://stackoverflow.com/questions/10914576/t-sql-split-string/42000063#42000063
-- =============================================
CREATE FUNCTION [dbo].[SplitStringToRows]
(   
    @List VARCHAR(4000) 
    , @Delimiter VARCHAR(50)
)
RETURNS TABLE 
AS
RETURN 
(
    --For testing
    -- SELECT * FROM SplitStringToRows ('1.0.123','.')
    -- DECLARE @List VARCHAR(MAX) = '1.0.123', @Delimiter VARCHAR(50) = '.';

    WITH Casted AS
    (
        SELECT CAST(N'<x>' + REPLACE((SELECT REPLACE(@List,@Delimiter,N'§§Split$me$here§§') AS [*] FOR XML PATH('')),N'§§Split$me$here§§',N'</x><x>') + N'</x>' AS XML) AS SplitMe
    )
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS [Index]
    , x.value(N'.',N'nvarchar(max)') AS Part 
    FROM Casted
    CROSS APPLY SplitMe.nodes(N'/x') AS A(x)
)

Then create this function:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      Soenhay
-- Create date: 7/1/2017
-- Description: Returns -1 if VersionStringA is less than VersionStringB.
--              Returns 0 if VersionStringA equals VersionStringB.
--              Returns 1 if VersionSTringA is greater than VersionStringB.
-- =============================================
CREATE FUNCTION dbo.CompareVersionStrings
(   
    @VersionStringA VARCHAR(50)
    ,@VersionStringB VARCHAR(50)
)
RETURNS TABLE 
AS
RETURN 
(
    --CurrentVersion should be of the form:
    --major.minor[.build[.revision]] 
    --This is the same as the versioning system used in c#.
    --For applications the build and revision numbers will by dynamically set based on the current date and time of the build. 
    --Example: [assembly: AssemblyFileVersion("1.123.*")]//http://stackoverflow.com/questions/15505841/the-version-specified-for-the-file-version-is-not-in-the-normal-major-minor-b
    --Each component should be between 0 and 65534 ( UInt16.MaxValue - 1 )
    --Max version number would be 65534.65534.65534.65534

    --For Testing 
    -- SELECT * FROM dbo.CompareVersionStrings('', '')
    -- SELECT * FROM dbo.CompareVersionStrings('asdf.asdf', 'asdf.asdf') --returns 0
    -- SELECT * FROM dbo.CompareVersionStrings('asdf', 'fdas') --returns -1 
    -- SELECT * FROM dbo.CompareVersionStrings('zasdf', 'fdas') --returns 1 
    -- SELECT * FROM dbo.CompareVersionStrings('1.0.123.123', '1.1.123.123')  --Should return -1
    -- SELECT * FROM dbo.CompareVersionStrings('1.0.123.123', '1.0.123.123')  --Should return 0
    -- SELECT * FROM dbo.CompareVersionStrings('1.1.123.123', '1.0.123.123')  --Should return 1
    -- SELECT * FROM dbo.CompareVersionStrings('1.0.123.123', '1.0.124.123')  --Should return -1
    -- SELECT * FROM dbo.CompareVersionStrings('1.0.124.123', '1.0.123.123')  --Should return 1
    -- SELECT * FROM dbo.CompareVersionStrings('1.0.123.123', '1.0.123.124')  --Should return -1
    -- SELECT * FROM dbo.CompareVersionStrings('1.0.123.124', '1.0.123.123')  --Should return 1
    -- SELECT * FROM dbo.CompareVersionStrings('1.0', '1.1')  --Should return -1
    -- SELECT * FROM dbo.CompareVersionStrings('1.0', '1.0')  --Should return 0
    -- SELECT * FROM dbo.CompareVersionStrings('1.1', '1.0')  --Should return 1
    -- Declare @VersionStringA VARCHAR(50) = '' ,@VersionStringB VARCHAR(50) = '' ;
    -- Declare @VersionStringA VARCHAR(50) = '1.0.123.123' ,@VersionStringB VARCHAR(50) = '1.1.123.123' ;
    -- Declare @VersionStringA VARCHAR(50) = '1.1.123.123' ,@VersionStringB VARCHAR(50) = '1.1.123.123' ;
    -- Declare @VersionStringA VARCHAR(50) = '1.2.123.123' ,@VersionStringB VARCHAR(50) = '1.1.123.123' ;
    -- Declare @VersionStringA VARCHAR(50) = '1.1.123' ,@VersionStringB VARCHAR(50) = '1.1.123.123' ;
    -- Declare @VersionStringA VARCHAR(50) = '1.1.123.123' ,@VersionStringB VARCHAR(50) = '1.1.123' ;
    -- Declare @VersionStringA VARCHAR(50) = '1.1' ,@VersionStringB VARCHAR(50) = '1.1' ;
    -- Declare @VersionStringA VARCHAR(50) = '1.2' ,@VersionStringB VARCHAR(50) = '1.1' ;
    -- Declare @VersionStringA VARCHAR(50) = '1.1' ,@VersionStringB VARCHAR(50) = '1.2' ;

    WITH 
    Indexes AS
    (
        SELECT 1 AS [Index]
            , 'major' AS Name
        UNION
        SELECT 2
            , 'minor'
        UNION
        SELECT 3
            , 'build'
        UNION
        SELECT 4
            , 'revision'
    )
    , SplitA AS
    (
        SELECT * FROM dbo.SplitStringToRows(@VersionStringA, '.')
    )
    , SplitB AS
    (
        SELECT * FROM dbo.SplitStringToRows(@VersionStringB, '.')
    )
    SELECT
        CASE WHEN major = 0 THEN
                CASE WHEN minor = 0 THEN
                                    CASE WHEN build = 0 THEN
                                                        CASE WHEN revision = 0 THEN 0
                                                        ELSE revision END
                                        ELSE build END
                    ELSE minor END
            ELSE major END AS Compare
    FROM
    (
        SELECT 
             MAX(CASE WHEN [Index] = 1 THEN Compare ELSE NULL END) AS major
            ,MAX(CASE WHEN [Index] = 2 THEN Compare ELSE NULL END) AS minor
            ,MAX(CASE WHEN [Index] = 3 THEN Compare ELSE NULL END) AS build
            ,MAX(CASE WHEN [Index] = 4 THEN Compare ELSE NULL END) AS revision
        FROM(
            SELECT [Index], Name, 
                CASE WHEN A = B THEN 0
                    WHEN A < B THEN -1
                    WHEN A > B THEN 1
                    END AS Compare
            FROM
            (
                SELECT 
                     i.[Index]
                    ,i.Name
                    ,ISNULL(a.Part, 0) AS A
                    ,ISNULL(b.Part, 0) AS B
                FROM Indexes i
                    LEFT JOIN SplitA a
                ON  a.[Index] = i.[Index]
                    LEFT JOIN SplitB b
                ON  b.[Index] = i.[Index]
            ) q1
        ) q2
    ) q3

)
GO
Soenhay
  • 3,958
  • 5
  • 34
  • 60
0

I'll give you the most shortest answer of this.

with cte as (
    select  7.11 as ver
    union all
    select 7.6
)

select top 1 ver from cte
      order by parsename(ver, 2), parsename(cast(ver as float), 1)
Ed Bangga
  • 12,879
  • 4
  • 16
  • 30
0

Maybe converting build number to a value can help to understand the hierarchy between build versions.

DECLARE @version VARCHAR(25), @dot1 AS TINYINT, @dot2 AS TINYINT, @dot3 AS TINYINT, @MaxPower AS TINYINT, @Value AS BIGINT
SELECT @version = CAST(SERVERPROPERTY('ProductVersion') AS VARCHAR) --'14.0.1000.169' --'10.50.1600'
SELECT @dot1 = CHARINDEX('.', @version, 1)
SELECT @dot2 = CHARINDEX('.', @version, @dot1 + 1)
SELECT @dot3 = CHARINDEX('.', @version, @dot2 + 1)
SELECT @dot3 = CASE
    WHEN @dot3 = 0 THEN LEN(@version) + 1
    ELSE @dot3
END

SELECT @MaxPower = MAX(DotColumn) FROM (VALUES (@dot1-1), (@dot2-@dot1-1), (@dot3-@dot2-1)) AS DotTable(DotColumn)
SELECT @Value = POWER(10, @MaxPower)
--SELECT @version, @dot1, @dot2, @dot3, @MaxPower, @Value

SELECT 
--  @version AS [Build], 
    CAST(LEFT(@version, @dot1-1) AS INT) * POWER(@Value, 3) +
    CAST(SUBSTRING(@version, @dot1+1, @dot2-@dot1-1) AS INT) * POWER(@Value, 2) +
    CAST(SUBSTRING(@version, @dot2+1, @dot3-@dot2-1) AS INT) * @Value +
    CASE
        WHEN @dot3 = LEN(@version)+1 THEN CAST(0 AS INT)
        ELSE CAST(SUBSTRING(@version, @dot3+1, LEN(@version)-@dot3) AS INT)
    END AS [Value]
Emrah Saglam
  • 125
  • 1
  • 3
0

Ispired from @Sean answer, since I needed it for 4 parts, I wrote this (and it is easily modulable for more, comment on function in end of code):

CREATE OR REPLACE FUNCTION compareversions(v1 text,v2 text)
    RETURNS smallint
    LANGUAGE 'plpgsql'
    VOLATILE
    PARALLEL UNSAFE
    COST 100
AS $$
declare res int;
-- Set parts into variables (for now part 1 to 4 are used)
-- IMPORTANT: if you want to add part(s) think to add:
--   - Setting of part(s) to 0 in "Convert all empty or null parts to 0" below
--   - Proper tests in select/case below
-- IMPORTANT: do not use CAST here since it will lead to syntax error if a version or part is empty
-- v1
declare v1_1 text := split_part(v1, '.', 1);
declare v1_2 text := split_part(v1, '.', 2);
declare v1_3 text := split_part(v1, '.', 3);
declare v1_4 text := split_part(v1, '.', 4);
-- v2
declare v2_1 text := split_part(v2, '.', 1);
declare v2_2 text := split_part(v2, '.', 2);
declare v2_3 text := split_part(v2, '.', 3);
declare v2_4 text := split_part(v2, '.', 4);

begin
    -- Convert all empty or null parts to 0
    -- v1
    if v1_1 = '' or v1_1 is null then v1_1 = '0'; end if;
    if v1_2 = '' or v1_2 is null then v1_2 = '0'; end if;
    if v1_3 = '' or v1_3 is null then v1_3 = '0'; end if;
    if v1_4 = '' or v1_4 is null then v1_4 = '0'; end if;
    -- v2
    if v2_1 = '' or v2_1 is null then v2_1 = '0'; end if;
    if v2_2 = '' or v2_2 is null then v2_2 = '0'; end if;
    if v2_3 = '' or v2_3 is null then v2_3 = '0'; end if;
    if v2_4 = '' or v2_4 is null then v2_4 = '0'; end if;

    select
        case
        -------------
        -- Compare first part:
        --  - If v1_1 is inferior to v2_1 return -1 (v1 < v2),
        --  - If v1_1 is superior to v2_1 return 1 (v1 > v2).
        when CAST(v1_1 as int) < cast(v2_1 as int) then -1
        when CAST(v1_1 as int) > cast(v2_1 as int) then 1
        -------------

        -------------
        -- v1_1 is equal to v2_1, compare second part:
        --  - If v1_2 is inferior to v2_2 return -1 (v1 < v2),
        --  - If v1_2 is superior to v2_2 return 1 (v1 > v2).
        when CAST(v1_2 as int) < cast(v2_2 as int) then -1
        when CAST(v1_2 as int) > cast(v2_2 as int) then 1
        -------------

        -------------
        -- v1_1 is equal to v2_1 and v1_2 is equal to v2_2, compare third part:
        --  - If v1_3 is inferior to v2_3 return -1 (v1 < v2),
        --  - If v1_3 is superior to v2_3 return 1 (v1 > v2).
        when CAST(v1_3 as int) < cast(v2_3 as int) then -1
        when CAST(v1_3 as int) > cast(v2_3 as int) then 1
        -------------

        -------------
        -- Etc..., continuing with fourth part:
        when CAST(v1_4 as int) < cast(v2_4 as int) then -1
        when CAST(v1_4 as int) > cast(v2_4 as int) then 1
        -------------

        -- All parts are equals, meaning v1 == v2, return 0
        else 0
        end
        into res;

        return res;
end;
$$;
;

COMMENT ON FUNCTION compareversions(v1 text,v2 text)
    IS 'Function to compare 2 versions as strings, versions can have from 1 to 4 parts (e.g. "1", "2.3", "3.4.5", "5.6.78.9") but it is easy to add a part.
A version having less than 4 parts is considered having its last part(s) set to 0, i.e. "2.3" is considered as "2.3.0.0" so that comparing "1.2.3" to "1.2.3.0" returns "equal"). Indeed we consider first part is always major, second minor, etc ... whatever the number of part for any version.
Function returns:
    - -1 when v1 < v2
    - 1 when v1 > v2
    - 0 when v1 = v2
And, according to return value:
    - To compare if v1 < v2 check compareversions(v1, v2) == -1
    - To compare if v1 > v2 check compareversions(v1, v2) == 1
    - To compare if v1 == v2 check compareversions(v1, v2) == 0
    - To compare if v1 <= v2 check compareversions(v1, v2) <= 0
    - To compare if v1 >= v2 check compareversions(v1, v2) >= 0'
;

With this you can also for example compare a version "1.2" with "1.2.1" (will return -1, v1 < v2) as "1.2" will be considered as "1.2.0", it is not an usual check but in case during time a digit is added to version a "1.2" will actually be considered equal to "1.2.0".

And it's also easily modulable for another version format, for X.Y-Z for example, v1_1, etc... will be (not tested but you got the idea):

-- v1_1 = X
declare v1_1 text := split_part(v1, '.', 1);
-- tmp = Y-Z
declare tmp text := split_part(v1, '.', 2);
-- v1_2 = Y
declare v1_2 text := split_part(tmp, '-', 1);
-- v1_3 = Z
declare v1_3 text := split_part(tmp, '-', 2);
-- do the same for v2
gluttony
  • 402
  • 6
  • 14
0

@MartinSmith answer works best for up-to 5 decimals but if more than that (which might be rare). Here is what I could have done:

DECLARE @AppVersion1 VARCHAR(20) = '2.7.2.2.3.1'
DECLARE @AppVersion2 VARCHAR(20) = '2.7.2.2.4'

DECLARE @V1 AS INT = CASE WHEN LEN(@AppVersion1) < LEN(@AppVersion2) THEN CAST(REPLACE(@AppVersion2,'.','') AS INT) ELSE CAST(REPLACE(@AppVersion1,'.','') AS INT) END;
DECLARE @V2 AS INT = CASE WHEN LEN(@AppVersion1) < LEN(@AppVersion2) THEN CAST(REPLACE(@AppVersion1,'.','') AS INT) ELSE CAST(REPLACE(@AppVersion2,'.','') AS INT) END;

IF(LEN(@V2)< LEN(@V1))
    BEGIN
        SET @V2 = CAST( LTRIM(CAST(@V2 AS VARCHAR)) + ISNULL(REPLICATE('0',LEN(@V1)-LEN(@V2)),'') AS INT); 
    END;

SELECT CASE WHEN @V1 > @V2 THEN 'Y' ELSE 'N' END
prashantchalise
  • 496
  • 1
  • 5
  • 12