Search code examples
sqlt-sqlsql-server-ce

T-SQL LIKE condition on comma-separated list


Is it possible to write a LIKE condition in T-SQL to match a comma-separated list which includes wildcards to a string. Let me explain further with an example:

Say you have the following command separated list of urls in a field:

'/, /news/%, /about/'

Now here's some examples of strings I'd like to match with the string above:

  1. '/'
  2. '/news/'
  3. '/news/2/'
  4. '/about/'

And here's some strings which would not match:

  1. '/contact/'
  2. '/about/me/'

I've achieved this in the past by writing a split function and then doing a like on each one. However I'm trying to get my query to work in SQL Server CE which doesn't support functions.

In case you are wondering here's how I achieved it using the split function:

SELECT Widgets.Id 
FROM Widgets 
WHERE (SELECT COUNT(*) FROM [dbo].[Split](Urls, ',') WHERE @Input LIKE Data) > 0

And here's the split function:

CREATE FUNCTION [dbo].[Split]
(    
    @RowData NVARCHAR(MAX),
    @Separator NVARCHAR(MAX)
)
RETURNS @RtnValue TABLE 
(
    [Id] INT IDENTITY(1,1),
    [Data] NVARCHAR(MAX)
) 
AS
BEGIN 
    DECLARE @Iterator INT
    SET @Iterator = 1

    DECLARE @FoundIndex INT
    SET @FoundIndex = CHARINDEX(@Separator, @RowData)

    WHILE (@FoundIndex > 0)
    BEGIN
        INSERT INTO @RtnValue ([Data])
        SELECT Data = LTRIM(RTRIM(SUBSTRING(@RowData, 1, @FoundIndex - 1)))

        SET @RowData = SUBSTRING(@RowData, @FoundIndex + DATALENGTH(@Separator) / 2, LEN(@RowData))
        SET @Iterator = @Iterator + 1
        SET @FoundIndex = CHARINDEX(@Separator, @RowData)
    END

    INSERT INTO @RtnValue ([Data])
    SELECT Data = LTRIM(RTRIM(@RowData))

    RETURN
END

I'd appreciate it if someone could help. Thanks


Solution

  • I can think of several options:

    1. Use a session-keyed table: delete rows matching current spid, insert desired rows with current spid, read from table in SP, delete from table (again).

    2. Make your client submit a query with many OR ... LIKE ... clauses.

    3. Write an SP that does the same thing as your function and returns a recordset. INSERT YourTable EXEC SP @Strings and you are done!

    4. Use the numbers-table-charindex-into-string inside of a derived table method of splitting the string.

    Example

    Let me flesh this out a little for you with an example combining ideas #3 and #4. Of course, your code for your function could be adapted, too.

    Build a separate Numbers table. Here is example creation script:

    --Numbers Table with 8192 elements (keeping it small for CE)
    CREATE TABLE Numbers (
       N smallint NOT NULL CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED
    );
    INSERT Numbers VALUES (1);
    WHILE @@RowCount < 4096
       INSERT Numbers SELECT N + (SELECT Max(N) FROM Numbers) FROM Numbers;
    

    The SP:

    CREATE PROCEDURE dbo.StringSplitRowset
       @String varchar(8000)
    AS
    SELECT Substring(@String, l.StartPos, l.Chars) Item
    FROM (
       SELECT
          S.StartPos,
          IsNull(NullIf(CharIndex(',', @String, S.StartPos), 0) - S.StartPos, 8000)
       FROM (
          SELECT 1 UNION ALL
          SELECT N.N + 1 FROM Numbers N WHERE Substring(@String, N.N, 1) = ','
       ) S (StartPos)
    ) L (StartPos, Chars);
    

    And usage, easy as pie:

    DECLARE @String varchar(8000);
    SET @String = 'abc,def,ghi,jkl';
    CREATE TABLE #Split (S varchar(8000));
    INSERT #Split EXEC dbo.StringSplitRowset @String;
    SELECT * FROM #Split;
    

    Result:

    abc
    def
    ghi
    jkl
    

    And finally, if you don't want to build a numbers table, you can use this SP. I think you will find that one of these two SPs performs well enough for you. There are other implementations of string splitting that could work as well.

    ALTER PROCEDURE dbo.StringSplitRowset
      @String varchar(8000)
    AS
    SELECT Substring(@String, l.StartPos, l.Chars) Item
    FROM (
       SELECT
          S.StartPos,
          IsNull(NullIf(CharIndex(',', @String, S.StartPos), 0) - S.StartPos, 8000)
       FROM (
          SELECT 1 UNION ALL
          SELECT N.N + 1
          FROM (
             SELECT A.A * 4096 + B.B * 1024 + C.C * 256 + D.D * 64 + E.E * 16 + F.F * 4 + G.G N
             FROM
                (SELECT 0 UNION ALL SELECT 1) A (A),
                (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) G (G),
                (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) F (F),
                (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) E (E),
                (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) D (D),
                (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) C (C),
                (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) B (B)
          ) N (N)
          WHERE Substring(@String, N.N, 1) = ','
       ) S (StartPos)
    ) L (StartPos, Chars)
    

    Any SQL writer serious about understanding some of the performance implications of splitting strings different ways ought to see Aaron Bertrand's blog post on splitting strings.

    Also, any serious SQL Server database student ought to see Erland Sommarskog's How to Share Data between Stored Procedures.