SQL Server:选择列中多次出现正则表达式匹配的行

Posted

技术标签:

【中文标题】SQL Server:选择列中多次出现正则表达式匹配的行【英文标题】:SQL Server: Select rows with multiple occurrences of regex match in a column 【发布时间】:2018-04-11 18:27:58 【问题描述】:

我相当习惯使用 mysql,但对 SQL Server 不是特别熟悉。运气不好,我在这里处理的数据库是 SQL Server 2014。

我有一个表,其中有一列的值都是带有前导、分隔和尾随分号的整数,就像这三个虚构的行:

;905;1493;384;13387;29;933;467;28732;
;905;138;3084;1387;290;9353;4767;2732;
;9085;14493;3864;130387;289;933;4767;28732;

我现在要做的是选择从数字列表中提取的多个数字出现在此列中的所有行。例如,给定上面的三行,如果我有组 905,467,4767,我试图弄清楚如何构造的语句应该返回前两行:第一行包含 905 和 467;第二行包含 905 和 4767。第三行仅包含 4767,因此不应返回该行。

As far as I can tell,SQL Server 实际上并不直接支持正则表达式(我什至不知道托管代码是什么),这没有帮助。即使使用正则表达式,我也不知道从哪里开始。甲骨文似乎有a function that would be very useful,但那是甲骨文。

此处的大多数类似问题都涉及查找同一字符(通常是单数)的多个实例并通过replacing the string to match with nothing and counting the difference in length 解决问题。我想这在技术上也可以在这里工作,但是给定一个由 15 个数字组成的“过滤器”组,SELECT 语句将变得非常长、令人费解并且完全不可读。此外,我只想匹配 整个 数字(因此,如果要匹配的数字之一是 29,则值 29 将在第一行匹配,但值 290 在第二行不匹配) ,这意味着我必须在 REPLACE 子句中包含分号,然后在计算长度时将它们打折。一团糟。

我最想做的事情是这样的:

SELECT * FROM table WHERE REGEXP_COUNT(column, ';(905|467|4767);') > 1

- 但这显然行不通,出于各种原因(最明显的原因是在 Oracle 之外不存在 REGEXP_COUNT)。

是否有一些理智、可管理的方法来做到这一点?

【问题讨论】:

我想道德是使用联结表而不是在 strings 中存储 numbers 的列表。 @GordonLinoff 我完全同意!在某些时候,我确实希望将其充实到一个单独的数据透视表中,但遗憾的是,这已经是十多年来的方式了,我必须搜索无数的地方才能找到所有被提及的实例到散落在服务器周围的 ASP 文件中。 :-/ 【参考方案1】:

你能把你的参数放到一个表中(也许使用一个接受字符串(逗号分隔的整数)作为参数的表值函数)并使用类似的东西吗?

DECLARE @T table (String varchar(255))
INSERT INTO @T
VALUES
(';905;1493;384;13387;29;933;467;28732;')
, (';905;138;3084;1387;290;9353;4767;2732;')
, (';9085;14493;3864;130387;289;933;4767;28732;')

DECLARE @Arguments table (Arg int)
INSERT INTO @Arguments
VALUES
(905)
, (467)
, (4767)

SELECT String
FROM
    @T
    CROSS JOIN @Arguments
GROUP BY String
HAVING SUM(CASE WHEN PATINDEX('%;' + CAST(Arg AS varchar) + ';%', String) > 0 THEN 1 ELSE 0 END) > 1

以及将 this 与函数一起使用以生成参数的示例:

CREATE FUNCTION GenerateArguments (@Integers varchar(255))
RETURNS @Arguments table (Arg int)
AS

BEGIN

    WITH cte
    AS
    (
        SELECT
            PATINDEX('%,%', @Integers) p
            , LEFT(@Integers, PATINDEX('%,%', @Integers) - 1) n
        UNION ALL
        SELECT
            CASE WHEN PATINDEX('%,%', SUBSTRING(@Integers, p + 1, LEN(@Integers))) + p = p THEN 0 ELSE PATINDEX('%,%', SUBSTRING(@Integers, p + 1, LEN(@Integers))) + p END
            , CASE WHEN PATINDEX('%,%', SUBSTRING(@Integers, p + 1, LEN(@Integers))) = 0 THEN RIGHT(@Integers, PATINDEX('%,%', REVERSE(@Integers)) - 1) ELSE LEFT(SUBSTRING(@Integers, p + 1, LEN(@Integers)), PATINDEX('%,%', SUBSTRING(@Integers, p + 1, LEN(@Integers))) - 1) END
        FROM cte
        WHERE p <> 0
    )

    INSERT INTO @Arguments (Arg)

    SELECT n
    FROM cte

    RETURN

END
GO

DECLARE @T table (String varchar(255))
INSERT INTO @T
VALUES
(';905;1493;384;13387;29;933;467;28732;')
, (';905;138;3084;1387;290;9353;4767;2732;')
, (';9085;14493;3864;130387;289;933;4767;28732;')
;

SELECT String
FROM
    @T
    CROSS JOIN GenerateArguments('905,467,4767')
GROUP BY String
HAVING SUM(CASE WHEN PATINDEX('%;' + CAST(Arg AS varchar) + ';%', String) > 0 THEN 1 ELSE 0 END) > 1

【讨论】:

这看起来很巧妙!我将不得不试一试,看看我是否能让它做我想做的事(一旦我仔细考虑了它到底做了什么,以及如何做)。【参考方案2】:

你可以的

SELECT *
FROM   Mess
       CROSS APPLY (SELECT COUNT(*)
                    FROM   (VALUES (905),
                                   (467),
                                   (4767)) V(Num)
                    WHERE  Col LIKE CONCAT('%;', Num, ';%')) ca(count)
WHERE  count > 1 

SQL Fiddle

或者

WITH Nums
     AS (SELECT Num
         FROM   (VALUES (905),
                        (467),
                        (4767)) V(Num))
SELECT Mess.*
FROM   Mess
       CROSS APPLY (VALUES(CAST(CONCAT('<x>', REPLACE(Col, ';', '</x><x>'), '</x>') AS XML))) x(x)
       CROSS APPLY (SELECT COUNT(*)
                    FROM   (SELECT n.value('.', 'int')
                            FROM   x.x.nodes('/x') n(n)
                            WHERE  n.value('.', 'varchar') <> ''
                            INTERSECT
                            SELECT Num
                            FROM   Nums) T(count)
                    HAVING COUNT(*) > 1) ca2(count) 

【讨论】:

这里有几个我以前从未见过的函数,所以我不太明白到底发生了什么——但这似乎工作得很好,而且无需声明变量和临时表,也。太棒了! @JanusBahsJacquet VALUES 子句是一个 table value constructor 充当派生表,其中包含三个(在本例中)数字。对于每一行,CROSS APPLY 计算字符串中有多少。然后,WHERE 仅保留具有所需计数的那些【参考方案3】:

你可以像这样构建一个 where 子句:

WHERE
case when column like '%;905;%' then 1 else 0 end +
case when column like '%;467;%' then 1 else 0 end +
case when column like '%;4767;%' then 1 else 0 end >= 2

优点是您不需要辅助表。我不知道您是如何构建查询的,但以下方法也有效,如果数字位于 tsql 变量中,则很有用。

 case when column like ('%;' + @n + ';%')  then 1 else 0 end

【讨论】:

【参考方案4】:

您可以使用 regex 和 row_number 的 like 函数来确定匹配的数量。

这里我们声明用于测试的列值:

DECLARE @tbl TABLE (
string NVARCHAR(MAX)
)

INSERT @tbl VALUES
(';905;1493;384;13387;29;933;467;28732;'),
(';905;138;3084;1387;290;9353;4767;2732;'),
(';9085;14493;3864;130387;289;933;4767;28732;')

然后我们将您的搜索参数传递到要加入的表变量中:

DECLARE @search_tbl TABLE (
search_value INT
)

INSERT @search_tbl VALUES
(905),
(467),
(4767)

最后,我们将表与要搜索的列连接到搜索表中。我们应用 row_number 函数来确定它匹配的次数。我们从这个子查询中选择 row_number = 2 意味着它至少加入了两次。

SELECT
    string
FROM (
    SELECT
        tbl.string,
        ROW_NUMBER() OVER (PARTITION BY tbl.string ORDER BY tbl.string) AS rn
    FROM @tbl tbl
    JOIN @search_tbl search_tbl ON
        tbl.string LIKE '%;' + CAST(search_tbl.search_value AS NVARCHAR(MAX)) + ';%'
    ) tbl
WHERE rn = 2

【讨论】:

以上是关于SQL Server:选择列中多次出现正则表达式匹配的行的主要内容,如果未能解决你的问题,请参考以下文章

Oracle 正则表达式计算由逗号包围的字符串的多次出现

2021-05-08

Oracle 正则表达式替换用逗号包围的字符串的多次出现

5.2.1 正则表达式语法与子模式扩展语法

python 正则表达式

正则表达式