【问题标题】:Replace Multiple Special Chars in Multiple Fields SQL替换多个字段中的多个特殊字符 SQL
【发布时间】:2015-09-28 20:52:27
【问题描述】:

我有一个 C# 应用程序,它生成一个 SQL 查询,该查询应该用于从 SQL Server 中用户选择的列中删除特殊字符。我现在的查询是:

UPDATE [TableA] 
SET [EpiNum] = REPLACE([EpiNum], SUBSTRING([EpiNum], PATINDEX('%[^a-zA-Z0-9 ]%', [EpiNum]), 1), ''), 
    [Name] = REPLACE([Name], SUBSTRING([Name], PATINDEX('%[^a-zA-Z0-9 ]%', [Name]), 1), ''), 
    [Acct] = REPLACE([Acct], SUBSTRING([Acct], PATINDEX('%[^a-zA-Z0-9 ]%', [Acct]), 1), '') 
WHERE PATINDEX('%[^a-zA-Z0-9 ]%', [EpiNum]) <> 0 OR 
      PATINDEX('%[^a-zA-Z0-9 ]%', [Name]) <> 0 OR 
      PATINDEX('%[^a-zA-Z0-9 ]%', [Acct]) <> 0;
GO

这可以删除第一个特殊字符,但如果字符串有多个特殊字符,它只会删除第一个

  1. “Salaries & Wages”变成“Salaries Wages”好!

但是

  1. “工资和工资 - 其他”变成“工资工资 - 其他”不好!

我的问题是:

如何修改上述查询以删除 多个 特殊字符,同时 仍然 能够通过 C# 执行该查询?

感谢您的宝贵时间。


编辑。显然我可以做类似的事情

declare @input varchar(500), @Action char(1)
set @Input = '80-82/5 O$%*#@)(J^#oh!@!n & '' Bacon St'
set @Action = 'A'

    DECLARE @i int
    DECLARE @result varchar(500)
    SET @result = @input

    if @Action = 'A'
    BEGIN
        SET @i = patindex('%[^a-zA-Z0-9 ]%', @result)
        WHILE @i > 0
        BEGIN
            SET @result = STUFF(@result, @i, 1, '')
            SET @i = patindex('%[^a-zA-Z0-9 ]%', @result)
        END
    END

print @Input
print @Result

但我看不到如何调整这样的查询以处理多个字段和来自 C#。如有任何帮助,我们将不胜感激。

【问题讨论】:

    标签: c# sql sql-server


    【解决方案1】:

    您可以使用Recursive CTE 来递归应用REPLACE 函数:

    ;WITH StripSpecialChars AS (
       SELECT id, 0 AS lvl,
              [EpiNum] = REPLACE([EpiNum], SUBSTRING([EpiNum], x.i, 1), ''), 
              [Name] = REPLACE([Name], SUBSTRING([Name], y.i, 1), ''), 
              [Acct] = REPLACE([Acct], SUBSTRING([Acct], z.i, 1), '')   
       FROM TableA
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [EpiNum])) AS x(i)
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [Name])) AS y(i)
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [Acct])) AS z(i)
       WHERE x.i <> 0 OR y.i <> 0 OR z.i <> 0
    
       UNION ALL
    
       SELECT id, lvl = lvl + 1,                       
              [EpiNum] = REPLACE([EpiNum], SUBSTRING([EpiNum], x.i, 1), ''), 
              [Name] = REPLACE([Name], SUBSTRING([Name], y.i, 1), ''), 
              [Acct] = REPLACE([Acct], SUBSTRING([Acct], z.i, 1), '') 
       FROM StripSpecialChars 
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [EpiNum])) AS x(i)
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [Name])) AS y(i)
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [Acct])) AS z(i)
       WHERE x.i <> 0 OR y.i <> 0 OR z.i <> 0
    )
    

    只要没有更多的特殊字符要替换,CTE 就会终止。

    每个id 具有最大lvl 值的行是包含[EpiNum]、[Name]、[Acct] 字段的精简值的行。因此,您可以使用以下代码在单个 SQL 语句中执行 UPDATE:

    ;WITH StripSpecialChars AS (
     ... above query here ...
    )
    UPDATE t1
    SET t1.[EpiNum] = t2.[EpiNum],
        t1.[Name] = t2.[Name],
        t1.[Acct] = t2.[Acct]   
    FROM TableA AS t1
    INNER JOIN (SELECT id, [EpiNum], [Name], [Acct],
                       ROW_NUMBER() OVER (PARTITION BY id 
                                          ORDER BY lvl DESC) AS rn 
                From StripSpecialChars) AS t2
    ON t1.id = t2.id AND t2.rn = 1
    

    Demo here

    编辑:

    如果TableA中没有PK列,那么你可以将你的表包裹在CTE中,使用ROW_NUMBER模拟PK,最后在CTE上执行更新:

    ;WITH TableA_PK AS (
      SELECT [EpiNum], [Name], [Acct],
             ROW_NUMBER() OVER (ORDER BY [EpiNum]) AS id
      FROM TableA
    ), StripSpecialChars AS (
       SELECT id, 0 AS lvl,
              [EpiNum] = REPLACE([EpiNum], SUBSTRING([EpiNum], x.i, 1), ''), 
              [Name] = REPLACE([Name], SUBSTRING([Name], y.i, 1), ''), 
              [Acct] = REPLACE([Acct], SUBSTRING([Acct], z.i, 1), '')          
       FROM TableA_PK
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [EpiNum])) AS x(i)
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [Name])) AS y(i)
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [Acct])) AS z(i)
       WHERE x.i <> 0 OR y.i <> 0 OR z.i <> 0
    
       UNION ALL
    
       SELECT id, lvl = lvl + 1,                       
              [EpiNum] = REPLACE([EpiNum], SUBSTRING([EpiNum], x.i, 1), ''), 
              [Name] = REPLACE([Name], SUBSTRING([Name], y.i, 1), ''), 
              [Acct] = REPLACE([Acct], SUBSTRING([Acct], z.i, 1), '')             
       FROM StripSpecialChars 
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [EpiNum])) AS x(i)
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [Name])) AS y(i)
       CROSS APPLY (SELECT PATINDEX('%[^a-zA-Z0-9 ]%', [Acct])) AS z(i)
       WHERE x.i <> 0 OR y.i <> 0 OR z.i <> 0
    )
    UPDATE t1
    SET t1.[EpiNum] = t2.[EpiNum],
        t1.[Name] = t2.[Name],
        t1.[Acct] = t2.[Acct]   
    FROM TableA_PK AS t1
    INNER JOIN (SELECT id, [EpiNum], [Name], [Acct],
                       ROW_NUMBER() OVER (PARTITION BY id 
                                          ORDER BY lvl DESC) AS rn
                FROM StripSpecialChars) AS t2
    ON t1.id = t2.id AND t2.rn = 1
    

    Demo here

    【讨论】:

    • 非常感谢您的回复/时间。我认为这是我的赢家,因为它不需要函数调用并且看起来会很快。不过有一个问题,我不想通过添加id 列来修改我的初始表结构。我不清楚这是否可以使用上述方法完成?任何想法都非常感谢......
    【解决方案2】:

    这可能看起来有点复杂,但我用以下方法解决了类似的挑战:

    只需将其粘贴到一个空的查询窗口中并适应您的需要...

    --This function comes back with a running set of numbers - very handsome
    CREATE FUNCTION [dbo].[RunningNumbers](@counter INT=1000000, @StartAt INT=0)
    RETURNS TABLE
    AS 
    RETURN
        WITH E1(N) AS(SELECT 1 FROM(VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))t(N)), --10 ^ 1
        E2(N) AS(SELECT 1 FROM E1 a CROSS JOIN E1 b), -- 10 ^ 2 = 100 rows
        E4(N) AS(SELECT 1 FROM E2 a CROSS JOIN E2 b), -- 10 ^ 4 = 10,000 rows
        E8(N) AS(SELECT 1 FROM E4 a CROSS JOIN E4 b), -- 10 ^ 8 = 10,000,000 rows
        CteTally AS
        (
            SELECT TOP(ISNULL(@counter,1000000)) ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) -1 + ISNULL(@StartAt,0) As Nmbr
            FROM E8
        )
        SELECT * FROM CteTally;
    GO
    
    --This function breaks down a string into a one-char-table with one char in each row.
    --You can decide for any ascii code what you want to do with this character.
    --At the end the whole thing is concatenated again.
    CREATE FUNCTION [dbo].[GetPrintableChars]
    (
         @Txt VARCHAR(MAX)
    )
    RETURNS VARCHAR(MAX)
    AS
    BEGIN
        SET @Txt=LTRIM(RTRIM(ISNULL(@Txt,'')));
    
        DECLARE @rslt VARCHAR(MAX);
        SET @rslt =
            (
                SELECT Repl.ASCII_Code
                FROM dbo.RunningNumbers(LEN(@Txt),1) AS pos
                --ASCII-Codes of all characters in your text
                OUTER APPLY(SELECT ASCII(SUBSTRING(@Txt,pos.Nmbr,1)) AS ASCII_Code) AS OneChar  
                --re-code 
                CROSS APPLY
                (
                    SELECT CASE 
                        WHEN OneChar.ASCII_Code IN(9,10,13) THEN CHAR(OneChar.ASCII_Code) --line and page break
                        WHEN OneChar.ASCII_Code BETWEEN 32 AND 126 THEN CHAR(OneChar.ASCII_Code) --normal printable
                        WHEN OneChar.ASCII_Code IN(132,142,148,153,174,175) THEN CHAR(OneChar.ASCII_Code) --extended to keep
                        WHEN OneChar.ASCII_Code BETWEEN 128 AND 154 THEN CHAR(176) --extended to get rid of
                        ELSE ''
                    END AS ASCII_Code
                ) AS Repl    
                FOR XML PATH(''),TYPE
            ).value('.','varchar(max)');
        RETURN @rslt;
    END
    GO
    
    --One example to get rid of some characters.
    SELECT dbo.GetPrintableChars('This is a Test for special characters: ÐðÑñ')
    GO
    
    --And clean up for testing
    DROP FUNCTION dbo.GetPrintableChars;
    GO
    DROP FUNCTION dbo.RunningNumbers;
    

    【讨论】:

      【解决方案3】:

      尽管 Gordon Linoff 提出了一个很好的限制条件。 如果您想为多个字段重用循环代码,您可以将它放在一个函数中:

      CREATE FUNCTION dbo.RemoveSpecialCharacters (
          @String NVARCHAR(max)
      )
      RETURNS NVARCHAR(max)
      BEGIN
          DECLARE @i int
      
          SET @i = patindex('%[^a-zA-Z0-9 ]%', @String)
          WHILE @i > 0
          BEGIN
              SET @String = STUFF(@String, @i, 1, '')
              SET @i = patindex('%[^a-zA-Z0-9 ]%', @String)
          END
          RETURN @String
      END
      

      然后重用函数:

      UPDATE [TableA] 
      SET [EpiNum] = dbo.RemoveSpecialCharacters([EpiNum]), 
          [Name] = dbo.RemoveSpecialCharacters([Name]), 
          [Acct] = dbo.RemoveSpecialCharacters([Acct])
      WHERE PATINDEX('%[^a-zA-Z0-9 ]%', [EpiNum]) <> 0 OR 
            PATINDEX('%[^a-zA-Z0-9 ]%', [Name]) <> 0 OR 
            PATINDEX('%[^a-zA-Z0-9 ]%', [Acct]) <> 0;
      

      测试性能!如果您想在 c# 中检查结果,只需在选择中使用该函数并更新它是否正确。

      【讨论】:

      • 我非常喜欢这个。但是,理想情况下,我不想在完成后创建一个函数并删除它。是否有一种明确的方法可以调整查询以避免函数调用 - 我看不到有,至少对我来说并不明显?
      • @Killercam 查看 Giorgos Betsos 的回答。它有点复杂,但不需要函数(并且性能会更好)。不过,我认为保留该功能没有问题,因为您可能有一天需要重用它。
      • 感谢您的宝贵时间。
      【解决方案4】:

      创建这个函数:

      CREATE function f_removebadcharacters
      (
        @string varchar(2000)
      )
      RETURNS varchar(2000)
      as
      BEGIN
        DECLARE @badcharacters varchar(100) = '%[^A-Z0-9 ]%'
      
        WHILE @string like @badcharacters
          SET @string = STUFF(@string, patindex(@badcharacters, @string), 1, '')
      
        RETURN @string
      END
      

      这样调用函数:

      SELECT dbo.f_removebadcharacters('Salaries & Wages - Other')
      

      在您的更新中,使用以下语法:

      UPDATE [TableA] 
      SET [EpiNum] = dbo.f_removebadcharacters([EpiNum])
      WHERE [EpiNum] LIKE '%[^A-Z0-9 ]%'
      

      这是一个工作示例:

      DECLARE @TableA table([EpiNum] varchar(2000))
      INSERT @TableA 
        values('Salaries & Wages - Other'),
              ('80-82/5 O$%*#@)(J^#oh!@!n & '''' Bacon St')
      
      
      UPDATE @TableA
      SET [EpiNum] = dbo.f_removebadcharacters([EpiNum])
      WHERE [EpiNum] LIKE '%[^A-Z0-9 ]%'
      
      SELECT * FROM @TableA
      

      结果:

      EpiNum
      Salaries  Wages  Other
      80825 OJohn   Bacon St
      

      【讨论】:

        【解决方案5】:

        如果这是一次性工作,我建议多次运行update,直到所有字符都消失。这可能是实现这一目标的最快方法。

        完成此操作后,将表修复为具有仅接受所需值的约束:

        alter table table1
            add constraint chk_EpiNum_Valie check (EpiNum NOT LIKE '%[^a-zA-Z0-9 ]%');
        

        (并对每个这样的列重复。)

        那么数据库会保证insert和update上的列的有效性。

        【讨论】:

        • 嗨,戈登,感谢您的回复。但是我怎样才能进行多次更新并检查 C# 的结果 - 这太混乱了。一定有办法让我的 SQL 查询循环。该系统的工作方式是从大量不同的来源和馈线系统导入原始数据,因此添加约束不是一种选择。当用户试图为我们的成本计算系统准备他们的数据时,就需要这样做。总之,
        • @Killercam 。 . .当然你可以做一个循环(正如其他答案所证明的那样)。我的观点是:修复数据库中的数据。添加约束。然后你就完成了。完全没有 C# 代码,约束将在未来保持列干净。
        • 好的,谢谢。但我不能使用约束。非常感谢您的帮助。
        【解决方案6】:

        应用更新乘法时间和控制结果的方法

        declare @l int;
        select @l= COUNT(*) from sys.views  --just to set @@ROWCOUNT to 1
        
        while @@ROWCOUNT >0
        begin
          UPDATE [TableA] 
          SET [EpiNum] = REPLACE([EpiNum], SUBSTRING([EpiNum], PATINDEX('%[^a-zA-Z0-9 ]%', [EpiNum]), 1), ''), 
            [Name] = REPLACE([Name], SUBSTRING([Name], PATINDEX('%[^a-zA-Z0-9 ]%', [Name]), 1), ''), 
            [Acct] = REPLACE([Acct], SUBSTRING([Acct], PATINDEX('%[^a-zA-Z0-9 ]%', [Acct]), 1), '') 
          WHERE PATINDEX('%[^a-zA-Z0-9 ]%', [EpiNum]) <> 0 OR 
              PATINDEX('%[^a-zA-Z0-9 ]%', [Name]) <> 0 OR 
              PATINDEX('%[^a-zA-Z0-9 ]%', [Acct]) <> 0;
        end
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-09-21
          • 2022-07-01
          • 1970-01-01
          • 1970-01-01
          • 2011-05-12
          • 1970-01-01
          相关资源
          最近更新 更多