Regex-Muster in der SQL-Ersetzungsfunktion?

78
SELECT REPLACE('<strong>100</strong><b>.00 GB', '%^(^-?\d*\.{0,1}\d+$)%', '');

Ich möchte ein Markup zwischen zwei Teilen der Zahl durch den obigen regulären Ausdruck ersetzen, aber es scheint nicht zu funktionieren. Ich bin mir nicht sicher, ob es sich um eine falsche Regex-Syntax handelt, da ich eine einfachere versucht habe, z. B. '%[^0-9]%'nur zum Testen, aber es hat auch nicht funktioniert. Weiß jemand, wie ich das erreichen kann?

JanT
quelle
3
Vielleicht möchten Sie die Antwort noch einmal überprüfen.
Mukus
1
Was soll das Endergebnis sein? Erwarten Sie 100.00oder 100.00 GB? Und gibt es andere Beispiele für formatierte Zahlen, die nicht zum Muster des Markups passen und sich nur um den Teil links von der Dezimalstelle befinden? Kann Markup um die gesamte Zahl wie sein 100<i>.00</i> GB? Gibt es rechts immer einen 2-stelligen Währungscode?
Solomon Rutzky
@srutzky Ich möchte eine Zahl mit Dezimalstellen, wenn es welche gibt, nicht alle Werte haben sie, auch gibt es praktisch kein Muster für diese, da es generiert wird, sondern ein HTML-Generator eines Drittanbieters. Manchmal steht die Währung vor der Zahl, manchmal nach der Zahl, manchmal das Symbol - $, manchmal der Code - USD, ohne Leerzeichen .. etc etc. einfach sehr Müll Daten
JanT

Antworten:

61

Mit PATINDEX können Sie den ersten Index des Auftretens von Mustern (Zeichenfolgen) ermitteln. Verwenden Sie dann STUFF, um eine weitere Zeichenfolge in das übereinstimmende Muster (Zeichenfolge) einzufügen .

Schleife durch jede Reihe. Ersetzen Sie alle unzulässigen Zeichen durch das, was Sie wollen. In Ihrem Fall ersetzen Sie nicht numerische durch Leerzeichen. Die innere Schleife ist, wenn Sie mehr als ein unzulässiges Zeichen in einer aktuellen Zelle haben, das der Schleife.

DECLARE @counter int

SET @counter = 0

WHILE(@counter < (SELECT MAX(ID_COLUMN) FROM Table))
BEGIN  

    WHILE 1 = 1
    BEGIN
        DECLARE @RetVal varchar(50)

        SET @RetVal =  (SELECT Column = STUFF(Column, PATINDEX('%[^0-9.]%', Column),1, '')
        FROM Table
        WHERE ID_COLUMN = @counter)

        IF(@RetVal IS NOT NULL)       
          UPDATE Table SET
          Column = @RetVal
          WHERE ID_COLUMN = @counter
        ELSE
            break
    END

    SET @counter = @counter + 1
END

Achtung: Dies ist jedoch langsam! Eine Varchar-Säule kann sich auswirken. Die Verwendung von LTRIM RTRIM kann also etwas hilfreich sein. Egal, es ist langsam.

Kredit geht an dieser Stackoverflow Antwort.

EDIT Credit geht auch an @srutzky

Bearbeiten (von @Tmdean) Anstatt jeweils eine Zeile zu erstellen, kann diese Antwort an eine satzbasiertere Lösung angepasst werden. Es wird immer noch das Maximum der Anzahl nicht numerischer Zeichen in einer einzelnen Zeile wiederholt, daher ist es nicht ideal, aber ich denke, es sollte in den meisten Situationen akzeptabel sein.

WHILE 1 = 1 BEGIN
    WITH q AS
        (SELECT ID_Column, PATINDEX('%[^0-9.]%', Column) AS n
        FROM Table)
    UPDATE Table
    SET Column = STUFF(Column, q.n, 1, '')
    FROM q
    WHERE Table.ID_Column = q.ID_Column AND q.n != 0;

    IF @@ROWCOUNT = 0 BREAK;
END;

Sie können die Effizienz auch erheblich verbessern, wenn Sie eine Bitspalte in der Tabelle beibehalten, die angibt, ob das Feld noch bereinigt wurde. (NULL steht in meinem Beispiel für "Unbekannt" und sollte der Spaltenstandard sein.)

DECLARE @done bit = 0;
WHILE @done = 0 BEGIN
    WITH q AS
        (SELECT ID_Column, PATINDEX('%[^0-9.]%', Column) AS n
        FROM Table
        WHERE COALESCE(Scrubbed_Column, 0) = 0)
    UPDATE Table
    SET Column = STUFF(Column, q.n, 1, ''),
        Scrubbed_Column = 0
    FROM q
    WHERE Table.ID_Column = q.ID_Column AND q.n != 0;

    IF @@ROWCOUNT = 0 SET @done = 1;

    -- if Scrubbed_Column is still NULL, then the PATINDEX
    -- must have given 0
    UPDATE table
    SET Scrubbed_Column = CASE
        WHEN Scrubbed_Column IS NULL THEN 1
        ELSE NULLIF(Scrubbed_Column, 0)
    END;
END;

Wenn Sie Ihr Schema nicht ändern möchten, können Sie es einfach anpassen, um Zwischenergebnisse in einer Variablen mit Tabellenwert zu speichern, die am Ende auf die tatsächliche Tabelle angewendet wird.

Mukus
quelle
2
Damit diese Lösung funktioniert, müssen Sie dem PATINDEX-Muster mindestens einen Punkt hinzufügen. es sollte sein : [^0-9.]. Wenn nicht , dann Streifen Sie das Dezimalsystem und drehen , was sein sollte 100.00in 10000.
Solomon Rutzky
@srutzky ok fügte hinzu '.'Ich arbeitete tatsächlich an Nicht-Alphabet und dachte, ^ 0-9 würde funktionieren.
Mukus
+1 für Aufwand, aber (wie Sie auch betont haben) dies würde dazu führen, dass Berichte viel zu lange laufen, sie sind langsam wie sie sind ... aber für kleinere Daten ist dies eine ausgezeichnete Lösung!
9.
1
Ich habe gerade an etwas Ähnlichem gearbeitet, daher werde ich die Antwort mit einer schnelleren Lösung aktualisieren. Es ist immer noch nicht ideal, aber die Leistung sollte in den meisten Situationen akzeptabel sein.
Tmdean
@Tmdean: Danke, dass du dazu beigetragen hast. Ich probiere es aus, wenn ich das nächste Mal auf ein ähnliches Problem stoße.
Januar
23

Im Allgemeinen unterstützt SQL Server keine regulären Ausdrücke und Sie können sie nicht im nativen T-SQL-Code verwenden.

Sie können dazu eine CLR-Funktion schreiben. Siehe hier zum Beispiel.

Szymon
quelle
1
OK, das scheint nur der
richtige
21

Anstatt den gefundenen Charakter durch seine einzige Position zu entfernen, Replace(Column, BadFoundCharacter, '')könnte die Verwendung wesentlich schneller sein. Anstatt nur das eine schlechte Zeichen zu ersetzen, das als nächstes in jeder Spalte gefunden wird, werden alle gefundenen Zeichen ersetzt.

WHILE 1 = 1 BEGIN
    UPDATE dbo.YourTable
    SET Column = Replace(Column, Substring(Column, PatIndex('%[^0-9.-]%', Column), 1), '')
    WHERE Column LIKE '%[^0-9.-]%'
    If @@RowCount = 0 BREAK;
END;

Ich bin überzeugt, dass dies besser funktionieren wird als die akzeptierte Antwort, schon allein deshalb, weil es weniger Operationen ausführt. Es gibt andere Möglichkeiten, die möglicherweise auch schneller sind, aber ich habe momentan keine Zeit, diese zu erkunden.

ErikE
quelle
Sieht interessant aus, ich habe momentan keine Zeit, es zu versuchen, werde es aber tun, wenn ich es habe. Prost
14.
4
Dies half mir bei einem etwas unabhängigen Problem. Ich habe Ihr Replace(Column, Substring(Column, PatIndex('%[^0-9.-]%', Column), 1), '')Bit für eine ausgewählte Abfrage verwendet. So danke!
Jyoseph
1
@jyoseph Großartig! Beachten Sie jedoch, dass dies nur alle Instanzen eines bestimmten fehlerhaften Zeichens entfernt und wiederholt ausgeführt werden muss, wenn die
Anzahl
@ErikE Danke für das Heads Up! Ich habe es verwendet, um eine Spalte mit Telefonnummern abzufragen (das Muster wurde leicht auf% [^ 0-9]% geändert), um alles zu entfernen, was nicht numerisch ist. Ein Benutzer könnte also 333-1234 abfragen und es würde mit den als 3331234 eingegebenen Telefonnummern übereinstimmen. Wenn ich das richtig verstehe, sagen Sie, dass in dem Fall, in dem die Telefonnummer (333) -333-1234 lautet, nur die erste entfernt wird "("? Ich muss das ein bisschen mehr testen.
Jyoseph
Richtig. Sie könnten ein CLR-Modul installieren. Oder im Idealfall einfach im Programmcode.
ErikE
4

Ich bin über diesen Beitrag gestolpert und habe nach etwas anderem gesucht, dachte aber, ich würde eine Lösung erwähnen, die weitaus effizienter ist - und die eigentlich die Standardimplementierung jeder Funktion sein sollte, wenn sie mit einer satzbasierten Abfrage verwendet wird -, nämlich die Verwendung eines angewendeten Kreuzes Tabellenfunktion. Das Thema scheint noch aktiv zu sein, also ist dies hoffentlich für jemanden nützlich.

Die Beispiellaufzeit für einige der bisherigen Antworten, die auf der Ausführung rekursiver satzbasierter Abfragen oder der Skalarfunktion basieren, basierend auf einem 1-m-Zeilentestsatz, bei dem die Zeichen aus einer zufälligen neuen ID entfernt werden, reicht von 34 s bis 2 m 05 s für die WHILE-Schleifenbeispiele und von 1 m 3 s bis { für immer} für die Funktionsbeispiele.

Die Verwendung einer Tabellenfunktion mit Kreuzanwendung erreicht das gleiche Ziel in 10 Sekunden . Möglicherweise müssen Sie es an Ihre Bedürfnisse anpassen, z. B. an die maximale Länge, die es handhabt.

Funktion:

CREATE FUNCTION [dbo].[RemoveChars](@InputUnit VARCHAR(40))
RETURNS TABLE
AS
RETURN
    (
        WITH Numbers_prep(Number) AS
            (
                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
            )
        ,Numbers(Number) AS
            (
                SELECT TOP (ISNULL(LEN(@InputUnit),0))
                    row_number() OVER (ORDER BY (SELECT NULL))
                FROM Numbers_prep a
                    CROSS JOIN Numbers_prep b
            )
        SELECT
            OutputUnit
        FROM
            (
                SELECT
                    substring(@InputUnit,Number,1)
                FROM  Numbers
                WHERE substring(@InputUnit,Number,1) like '%[0-9]%'
                ORDER BY Number
                FOR XML PATH('')
            ) Sub(OutputUnit)
    )

Verwendung:

UPDATE t
SET column = o.OutputUnit
FROM ##t t
CROSS APPLY [dbo].[RemoveChars](t.column) o
SQLGobbleDeGook
quelle
4

Hier ist eine Funktion, die ich geschrieben habe, um dies basierend auf den vorherigen Antworten zu erreichen.

CREATE FUNCTION dbo.RepetitiveReplace
(
    @P_String VARCHAR(MAX),
    @P_Pattern VARCHAR(MAX),
    @P_ReplaceString VARCHAR(MAX),
    @P_ReplaceLength INT = 1
)
RETURNS VARCHAR(MAX)
BEGIN
    DECLARE @Index INT;

    -- Get starting point of pattern
    SET @Index = PATINDEX(@P_Pattern, @P_String);

    while @Index > 0
    begin
        --replace matching charactger at index
        SET @P_String = STUFF(@P_String, PATINDEX(@P_Pattern, @P_String), @P_ReplaceLength, @P_ReplaceString);
        SET @Index = PATINDEX(@P_Pattern, @P_String);
    end

    RETURN @P_String;
END;

Kern

Bearbeiten:

Ursprünglich hatte ich hier eine rekursive Funktion, die mit SQL Server nicht gut funktioniert, da sie ein Limit von 32 Verschachtelungsstufen hat, was zu einem Fehler wie dem folgenden führen würde, wenn Sie versuchen, mehr als 32 Ersetzungen mit der Funktion vorzunehmen. Anstatt zu versuchen, eine Änderung auf Serverebene vorzunehmen, um mehr Verschachtelung zu ermöglichen (was gefährlich sein kann, wie das Zulassen von nie endenden Schleifen), ist das Wechseln zu einer while-Schleife viel sinnvoller.

Die maximale Verschachtelungsstufe für gespeicherte Prozeduren, Funktionen, Trigger oder Ansichten wurde überschritten (Limit 32).

jkdba
quelle
2

Das Umschließen der Lösung in eine SQL-Funktion kann hilfreich sein, wenn Sie sie wiederverwenden möchten. Ich mache es sogar auf Zellebene, deshalb setze ich dies als eine andere Antwort:

CREATE FUNCTION [dbo].[fnReplaceInvalidChars] (@string VARCHAR(300))
RETURNS VARCHAR(300)
BEGIN
    DECLARE @str VARCHAR(300) = @string;
    DECLARE @Pattern VARCHAR (20) = '%[^a-zA-Z0-9]%';
    DECLARE @Len INT;
    SELECT @Len = LEN(@String); 
    WHILE @Len > 0 
    BEGIN
        SET @Len = @Len - 1;
        IF (PATINDEX(@Pattern,@str) > 0)
            BEGIN
                SELECT @str = STUFF(@str, PATINDEX(@Pattern,@str),1,'');    
            END
        ELSE
        BEGIN
            BREAK;
        END
    END     
    RETURN @str
END
Ivan Rascon
quelle
2

Ich habe diese Funktion erstellt, um eine Zeichenfolge zu bereinigen, die nicht numerische Zeichen in einem Zeitfeld enthält. Die Zeit enthielt Fragezeichen, als sie die Minuten nicht hinzufügten, so etwas wie diese 20: ??. Die Funktion durchläuft jedes Zeichen und ersetzt das? mit einer 0:

 CREATE FUNCTION [dbo].[CleanTime]
(
    -- Add the parameters for the function here
    @intime nvarchar(10) 
)
RETURNS nvarchar(5)
AS
BEGIN
    -- Declare the return variable here
    DECLARE @ResultVar nvarchar(5)
    DECLARE @char char(1)
    -- Add the T-SQL statements to compute the return value here
    DECLARE @i int = 1
    WHILE @i <= LEN(@intime)
    BEGIN
    SELECT @char =  CASE WHEN substring(@intime,@i,1) like '%[0-9:]%' THEN substring(@intime,@i,1) ELSE '0' END
    SELECT @ResultVar = concat(@ResultVar,@char)   
    set @i  = @i + 1       
    END;
    -- Return the result of the function
    RETURN @ResultVar

END
Nordin
quelle
1

Wenn Sie dies nur für einen Parameter tun, der in eine gespeicherte Prozedur eingeht, können Sie Folgendes verwenden:

declare @badIndex int
set @badIndex = PatIndex('%[^0-9]%', @Param)
while @badIndex > 0
    set @Param = Replace(@Param, Substring(@Param, @badIndex, 1), '')
    set @badIndex = PatIndex('%[^0-9]%', @Param)
goddess_elli
quelle
0

Ich denke, ein einfacherer und schnellerer Ansatz wird von jedem Zeichen des Alphabets wiederholt:

DECLARE @i int
SET @i = 0

WHILE(@i < 256)
BEGIN  

    IF char(@i) NOT IN ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.')      

      UPDATE Table SET Column = replace(Column, char(@i), '')

    SET @i = @i + 1

END
Gregorio
quelle
1
Bitte verwenden Sie so etwas nicht in der Produktion. Sie führen 245 Updates ohne where-Klausel durch. Es funktioniert, aber es ist alles andere als ein effizienter Ansatz. Eine bessere Idee könnte darin bestehen, Zeichen zu durchlaufen, die entfernt werden sollen, stattdessen alle im Alphabet verfügbaren Zeichen. Aber auch das könnte zu etwas Besserem verbessert werden.
Anderson Silva