使用 SQL 服务器,如何根据分隔字符串作为条件查询 table?

With SQL Server, How can I query a table based on a delimited string as the criteria?

我有以下表格:

tbl_File:

FileID | Filename
-----------------
1      | test.jpg

tbl_Tag:

TagID | TagName
---------------
1     | Red

tbl_Tag文件:

ID | TagID | FileID
-------------------
1  | 1     | 1

我需要针对这些表传递非包含查询。例如,想象一下 select 一个或多个标签的复选框列表,然后是一个搜索按钮。我需要将 TagID 作为 PIPE 分隔字符串传递给查询,例如“1|2|5|”

搜索结果需要不包含,比如必须满足所有条件。如果 select 编辑了 3 个标签,则结果将是所有 3 个标签都与之关联的文件。

我想我把它弄得太复杂了,但尝试使用 charindex 和其他东西遍历标签来处理字符串,但似乎必须有更简单的方法。

我想把它作为一个函数...比如

SELECT FileID, Filename 
FROM tbl_Files 
WHERE dbo.udf_FileExistswithTags(@Tags, FileID) = 1

有什么有效的方法吗?

从您的示例场景来看,实际的 "need" 并不是要传递竖线分隔的字符串。我强烈建议放弃该想法并在存储过程中使用 Table 值参数。这有很多优点,因为您不会达到数据类型限制或 "number of parameters" 限制,而这些限制可能会出现在非常大的条件集上。此外,它不再需要 运行 一个(可能非常慢的)UDF。

在应用程序端将字符串拆分为标记,然后将每个标记作为一行插入到 TVP 中。示例如下:

在您的数据库中创建 TVP 类型:

CREATE TYPE [dbo].[FileNameType] AS TABLE
(
    fileName varchar(1000)
)

在应用程序方面,将文件名标记列表构建到记录集中:

private static List<SqlDataRecord> BuildFileNameTokenRecords(IEnumerable<string> tokens)
        {
            var records = new List<SqlDataRecord>();
            foreach (string token in tokens){
var record = new SqlDataRecord(
                    new SqlMetaData[]
                    {
                        new SqlMetaData("fileName", SqlDbType.Varchar),
                    }
                );
                records.Add(record);
            }
            return records;
        }

无论你 运行 你的 proc 来自哪里(这里是粗略的代码):

var records = BuildFileNameTokenRecords(listofstrings);
var sqlCmd = sqlDb.GetStoredProcCommand("FileExists");
sqlDb.AddInParameter(sqlCmd, "tvpFilenameTokens", SqlDbType.Structured, records);
ExecuteNonQuery(sqlCmd);

过滤您的 select 语句然后简单地变成加入 table 参数中的标记的问题。像这样:

CREATE PROCEDURE dbo.FileExists
    (
     -- Put additional parameters here
     @tvpFilenameTokens dbo.FileNameType READONLY,
    )
    AS
BEGIN
    SELECT FileID, Filename 
    FROM tbl_Files INNER JOIN @tvpFilenameTokens
    ON tbl_Files.FileID = @tvpFilenameTokens.fileName 
END

这是 Jeff Moden 称为 DelimitedSplit8K 的函数。这用于拆分长度最大为 8000 的字符串。有关更多信息,请阅读:http://www.sqlservercentral.com/articles/Tally+Table/72993/

CREATE FUNCTION [dbo].[DelimitedSplit8K](
    @pString    VARCHAR(8000), --WARNING!!! DO NOT USE MAX DATA-TYPES HERE!  IT WILL KILL PERFORMANCE!
    @pDelimiter CHAR(1)
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN

WITH E1(N) AS (--10E+1 or 10 rows
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (
    SELECT TOP (ISNULL(DATALENGTH(@pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
    SELECT 1 UNION ALL
    SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(@pString, t.N, 1) = @pDelimiter
),
cteLen(N1, L1) AS(--==== Return start and length (for use in substring)
    SELECT 
        s.N1,
        ISNULL(NULLIF(CHARINDEX(@pDelimiter, @pString, s.N1), 0) - s.N1, 8000)
    FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT 
    ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
    Item       = SUBSTRING(@pString, l.N1, l.L1)
FROM cteLen l

您的查询现在是:

DECLARE @pString VARCHAR(8000) = '1|3|5'

SELECT
    f.*
FROM tbl_File f
INNER JOIN tbl_TagFile tf ON tf.FileID = f.FileID
WHERE
    tf.TagID IN(SELECT CAST(item AS INT) FROM dbo.DelimitedSplit8K(@pString, '|'))
GROUP BY f.FileID, f.FileName
HAVING COUNT(tf.ID) = (LEN(@pString) - LEN(REPLACE(@pString,'|','')) + 1)

下面的语句通过统计分隔符| + 1.

的出现次数来统计参数中TagID的个数
(LEN(@pString) - LEN(REPLACE(@pString,'|','')) + 1)

这是一个不需要 UDF 的选项。

可以说这也很复杂

DECLARE @TagList VARCHAR(50)

-- pass in this
SET @TagList = '1|3|6'

SELECT
FinalSet.FileID, 
FinalSet.Tag,
FinalSet.TotalMatches
FROM 
(
    SELECT 
    tbl_TagFile.FileID, 
    tbl_TagFile.Tag,
    COUNT(*) OVER(PARTITION BY tbl_TagFile.FileID) TotalMatches
    FROM 
    (
        SELECT 1 FileID, '1' Tag  UNION ALL
        SELECT 1 , '2'  UNION ALL
        SELECT 1 , '3'  UNION ALL
        SELECT 1 , '6'  UNION ALL
        SELECT 2 , '1'  UNION ALL
        SELECT 2 , '3'
    ) tbl_TagFile
    INNER JOIN
    (
        SELECT tbl_Tag.Tag
        FROM 
        (
        SELECT '1' Tag  UNION ALL
        SELECT '2' UNION ALL
        SELECT '3' UNION ALL
        SELECT '4' UNION ALL
        SELECT '5' UNION ALL
        SELECT '6' 
        ) tbl_Tag
        WHERE '|' + @TagList + '|' LIKE '%|' + Tag + '|%'
    ) LimitedTagTable
    ON LimitedTagTable.Tag = tbl_TagFile.Tag
) FinalSet
WHERE   
FinalSet.TotalMatches = (LEN(@TagList) - LEN(REPLACE(@TagList,'|','')) + 1)

在数据类型和索引等方面存在一些复杂性,但您可以看到这个概念 - 您只会获得与您传入的字符串匹配的记录。

子表 LimitedTagTable 是您的标签列表,由您的输入竖线分隔字符串过滤

子表FinalSet将您的有限标签列表加入您的文件列表

TotalMatches 计算出您的文件有多少标签匹配

最后,这一行将输出限制为那些具有足够匹配项的文件:

FinalSet.TotalMatches = (LEN(@TagList) - LEN(REPLACE(@TagList,'|','')) + 1)

请尝试不同的输入和数据集,看看它是否适合我做出的一些假设。

我正在回答我自己的问题,希望有人能让我知道 if/how 它有缺陷。到目前为止它似乎在工作,但只是早期测试。

函数:

ALTER FUNCTION [dbo].[udf_FileExistsByTags]
(
     @FileID int
    ,@Tags nvarchar(max)
)
RETURNS bit
AS
BEGIN
    DECLARE @Exists bit = 0
    DECLARE @Count int = 0
    DECLARE @TagTable TABLE ( FileID int, TagID int )
    DECLARE @Tag int

    WHILE len(@Tags) > 0
    BEGIN
        SET @Tag = CAST(LEFT(@Tags, charindex('|', @Tags + '|') -1) as int)
        SET @Count = @Count + 1
        IF EXISTS (SELECT * FROM tbl_FileTag WHERE FileID = @FileID AND TagID = @Tag )
            BEGIN
                INSERT INTO @TagTable ( FileID, TagID ) VALUES ( @FileID, @Tag )
            END
        SET @Tags = STUFF(@Tags, 1, charindex('|', @Tags + '|'), '')
    END

    SET @Exists = CASE WHEN @Count = (SELECT COUNT(*) FROM @TagTable) THEN 1 ELSE 0 END
    RETURN @Exists

END

然后在查询中:

SELECT * 从 tbl_File a WHERE dbo.udf_FileExistsByTags(a.FileID, @Tags) = 1

所以现在我正在寻找错误。

你怎么看?可能不是每一个都有效,但是这种搜索只会定期使用。

这是一个应该缩放的选项。 SQL Server 2005 可以使用所有功能。它使用 CTE 来分隔查询部分,该部分仅查找 FileID 具有 all 的部分传入的 TagID 的列表,然后将 FileID 的列表加入 [File] table 以获取详细信息。它还使用 INNER JOIN 而不是 IN 列表来匹配 TagID。

请注意,下面的示例使用了 SQLCLR 拆分器,它在 SQL# 库中免费提供(我编写的,但此功能在免费版本中)。使用的具体分离器不是重要部分;它应该只是 SQLCLR,一个内联计数-table(就像@wewesthemenace 的回答中使用的那个),或者是 XML 方法。只是不要使用基于 WHILE 循环或递归 CTE 的拆分器。

---- TEST SETUP
DECLARE @File TABLE
(
  FileID INT NOT NULL PRIMARY KEY,
  [Filename] NVARCHAR(200) NOT NULL
);

DECLARE @TagFile TABLE
(
  TagID INT NOT NULL,
  FileID INT NOT NULL,
  PRIMARY KEY (TagID, FileID)
);

INSERT INTO @File VALUES (1, 'File1.txt');
INSERT INTO @File VALUES (2, 'File2.txt');
INSERT INTO @File VALUES (3, 'File3.txt');

INSERT INTO @TagFile VALUES (1, 1);
INSERT INTO @TagFile VALUES (2, 1);
INSERT INTO @TagFile VALUES (5, 1);
INSERT INTO @TagFile VALUES (1, 2);
INSERT INTO @TagFile VALUES (2, 2);
INSERT INTO @TagFile VALUES (4, 2);
INSERT INTO @TagFile VALUES (1, 3);
INSERT INTO @TagFile VALUES (2, 3);
INSERT INTO @TagFile VALUES (5, 3);
INSERT INTO @TagFile VALUES (6, 3);
---- DONE WITH TEST SETUP

DECLARE @TagsToGet VARCHAR(100); -- this would be the proc input parameter
SET @TagsToGet = '1|2|5';

CREATE TABLE #Tags (TagID INT NOT NULL PRIMARY KEY);
DECLARE @NumTags INT;

INSERT INTO #Tags (TagID)
  SELECT split.SplitVal
  FROM   SQL#.String_Split4k(@TagsToGet, '|', 1) split;

SET @NumTags = @@ROWCOUNT;

;WITH files AS
(
  SELECT  tf.FileID
  FROM    @TagFile tf
  INNER JOIN #Tags tg
          ON tg.TagID = tf.TagID
  GROUP BY tf.FileID
  HAVING   COUNT(*) = @NumTags
)
SELECT fl.*
FROM   @File fl
INNER JOIN files
        ON files.FileID = fl.FileID
ORDER BY fl.[Filename] ASC;

DROP TABLE #Tags; -- don't need this if code above is placed in a proc

结果:

FileID   Filename
1        File1.txt
3        File3.txt

备注

  • 尽管我很喜欢 TVP(而且我确实喜欢,当它们正确完成并适当使用时),我想说它们对于这种小规模、一维数组来说有点过分了设想。与使用 SQLCLR 流式 TVF 字符串拆分器相比,实际上不会有任何性能提升,但它需要更多应用程序代码和额外的用户定义 Table 类型,必须先进行更新删除所有引用它的过程。这种情况不会一直发生,但需要考虑长期维护成本。

  • TagFile 和从拆分操作填充的临时 table 之间的 JOIN 应该比使用带有子查询的 IN 列表更有效拆分操作。 IN 列表是其中所有值作为它们自己的 OR 条件的简写形式。因此 JOIN 是一种完全基于集合的方法,它让查询优化器完成它的工作。

  • 我用来测试的结构@TagFile table里面只有两个相关的ID:TagIDFileID。它没有我认为是此 table 上的 IDENTITY 字段的 ID 字段。除非有非常具体的原因需要该 IDENTITY 字段,否则我建议将其删除。它增加了固有的好处,因为 TagIDFileID 的组合是 一个自然键(即它既不是 NULL 又是唯一的)。如果 table 的 Clustered PK 只是这两个字段,那么即使 TagFile 中有数百万行,连接到这些拆分 TagID 的临时 table 也会非常快.

  • 这种方法比尝试通过每个 FileID 的函数处理这个方法效果更好的一个原因是(除了明显的基于集合比基于游标的原因之外)是TagIDs 的列表对于所有要检查的文件都是相同的。所以把它分开不止一次是浪费精力。

  • 通过不在查询中内联拆分 TagID 列表,我能够捕获该列表中的元素数量而无需额外的努力。因此,这样就无需进行二次计算。