45

DB側のページネーションを持つ大規模なデータベースがあります。これは迅速で、数百万のレコードからわずか 1 秒で 50 行のページを返します。

ユーザーは、基本的にソートする列を選択して、独自のソートを定義できます。列は動的です - 数値、日付、テキストを持つものがあります。

ほとんどの場合、テキストは期待どおりにソートされますが、愚かな方法でソートされます。それはコンピューターにとっては理にかなっていますが、ユーザーを苛立たせます。

たとえば、文字列レコード ID で並べ替えると、次のようになります。

rec1
rec10
rec14
rec2
rec20
rec3
rec4

...等々。

これに番号を考慮してもらいたいので、次のようにします。

rec1
rec2
rec3
rec4
rec10
rec14
rec20

入力を制御できず (そうでなければ先頭の 000 でフォーマットするだけです)、単一のフォーマットに頼ることもできません - いくつかは "{alpha code}-{dept code}-{rec id}" のようなものです。

C# でこれを行ういくつかの方法を知っていますが、すべてのレコードをプルダウンしてソートすることはできません。

Sqlサーバーで自然な並べ替えをすばやく適用する方法を知っている人はいますか?


私たちは使用しています:

ROW_NUMBER() over (order by {field name} asc)

そして、それによってページングしています。

トリガーを追加することはできますが、追加することはできません。彼らの入力はすべてパラメータ化されていますが、形式を変更することはできません.「rec2」と「rec10」を入力すると、そのように自然な順序で返されることを期待しています.


クライアントごとに異なる形式に従う有効なユーザー入力があります。

rec1、rec2、rec3、... rec100、rec101

別の可能性があります: grp1rec1、grp1rec2、... grp20rec300、grp20rec301

入力を制御できないというのは、ユーザーにこれらの標準を変更するよう強制できないという意味です。これらの標準には grp1rec1 のような値があり、それを grp01rec001 として再フォーマットすることはできません。外部システムへのリンク。

これらの形式はさまざまですが、多くの場合、文字と数字が混在しています。

これらを C# で並べ替えるのは簡単です。それを分割して{ "grp", 20, "rec", 301 }、シーケンス値を順番に比較するだけです。

ただし、何百万ものレコードがあり、データがページングされている可能性があるため、SQL サーバーで並べ替えを行う必要があります。

SQLサーバーは比較ではなく値でソートします-C#では値を分割して比較できますが、SQLでは一貫してソートする単一の値を(非常に迅速に)取得するロジックが必要です。

@moebius - あなたの答えはうまくいくかもしれませんが、これらすべてのテキスト値にソートキーを追加するのは醜い妥協のように感じます.

4

14 に答える 14

45
order by LEN(value), value

完全ではありませんが、多くの場合にうまく機能します。

于 2009-02-23T19:31:53.130 に答える
29

私が見たSQLベースのソリューションのほとんどは、データが十分に複雑になると壊れます(たとえば、1つまたは2つ以上の数値)。最初は、要件を満たす(特に、文字列内の任意の数の数値を処理する)NaturalSort関数をT-SQLに実装しようとしましたが、パフォーマンスが遅すぎました

最終的に、自然ソートを可能にするためにC#でスカラーCLR関数を作成しました。最適化されていないコードでも、SQLServerから呼び出すパフォーマンスは非常に高速です。次の特徴があります。

  • 最初の1,000文字程度を正しく並べ替えます(コードで簡単に変更したり、パラメーターにしたりできます)
  • 小数を適切にソートするため、123.333は123.45より前になります
  • 上記の理由により、IPアドレスなどを正しくソートできない可能性があります。別の動作が必要な場合は、コードを変更してください
  • 任意の数の数値を含む文字列の並べ替えをサポート
  • 最大25桁の数値を正しくソートします(コードで簡単に変更したり、パラメーターにしたりできます)

コードはここにあります:

using System;
using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;

public class UDF
{
    [SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic=true)]
    public static SqlString Naturalize(string val)
    {
        if (String.IsNullOrEmpty(val))
            return val;

        while(val.Contains("  "))
            val = val.Replace("  ", " ");

        const int maxLength = 1000;
        const int padLength = 25;

        bool inNumber = false;
        bool isDecimal = false;
        int numStart = 0;
        int numLength = 0;
        int length = val.Length < maxLength ? val.Length : maxLength;

        //TODO: optimize this so that we exit for loop once sb.ToString() >= maxLength
        var sb = new StringBuilder();
        for (var i = 0; i < length; i++)
        {
            int charCode = (int)val[i];
            if (charCode >= 48 && charCode <= 57)
            {
                if (!inNumber)
                {
                    numStart = i;
                    numLength = 1;
                    inNumber = true;
                    continue;
                }
                numLength++;
                continue;
            }
            if (inNumber)
            {
                sb.Append(PadNumber(val.Substring(numStart, numLength), isDecimal, padLength));
                inNumber = false;
            }
            isDecimal = (charCode == 46);
            sb.Append(val[i]);
        }
        if (inNumber)
            sb.Append(PadNumber(val.Substring(numStart, numLength), isDecimal, padLength));

        var ret = sb.ToString();
        if (ret.Length > maxLength)
            return ret.Substring(0, maxLength);

        return ret;
    }

    static string PadNumber(string num, bool isDecimal, int padLength)
    {
        return isDecimal ? num.PadRight(padLength, '0') : num.PadLeft(padLength, '0');
    }
}

これを登録してSQLServerから呼び出せるようにするには、クエリアナライザで次のコマンドを実行します。

CREATE ASSEMBLY SqlServerClr FROM 'SqlServerClr.dll' --put the full path to DLL here
go
CREATE FUNCTION Naturalize(@val as nvarchar(max)) RETURNS nvarchar(1000) 
EXTERNAL NAME SqlServerClr.UDF.Naturalize
go

次に、次のように使用できます。

select *
from MyTable
order by dbo.Naturalize(MyTextField)

:SQL Serverでエラーが発生した場合、.NETFrameworkでのユーザーコードの実行は無効になっています。「clrenabled」構成オプションを有効にします。、こちらの手順に従って有効にしてください。そうする前に、セキュリティへの影響を考慮してください。db管理者でない場合は、サーバー構成に変更を加える前に、必ず管理者とこれについて話し合ってください。

注2:このコードは国際化を適切にサポートしていません(たとえば、小数点マーカーが「。」であると想定している、速度が最適化されていないなど)。改善に関する提案を歓迎します。

編集:実際の並べ替えを行わないため、関数の名前をNaturalSortではなくNaturalizeに変更しました。

于 2010-01-13T22:59:22.020 に答える
14

私はこれが古い質問であることを知っていますが、私はちょうどそれに出くわしました、そしてそれは受け入れられた答えを得ていないので。

私はいつもこれに似た方法を使用してきました:

SELECT [Column] FROM [Table]
ORDER BY RIGHT(REPLICATE('0', 1000) + LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX)))), 1000)

これで問題が発生する一般的なのは、列がVARCHAR(MAX)にキャストされない場合、またはLEN([Column])> 1000(ただし、必要に応じてその1000を別のものに変更できる場合)ですが、この大まかなアイデアを必要なものに使用できます。

また、これは通常のORDER BY [列]よりもパフォーマンスが大幅に低下しますが、OPで要求された結果が得られます。

編集:さらに明確にするために、これは、、、、(として並べ替えられます)などの10進値がある場合は機能し1ません。これは、OPで要求されているものではないためですが、次の方法で簡単に実行できます。1.151.5{1, 1.5, 1.15}

SELECT [Column] FROM [Table]
ORDER BY REPLACE(RIGHT(REPLICATE('0', 1000) + LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX)))) + REPLICATE('0', 100 - CHARINDEX('.', REVERSE(LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX))))), 1)), 1000), '.', '0')

結果:{1, 1.15, 1.5}

そして、それでもすべて完全にSQL内にあります。単純なテキスト+数字ではなく、非常に特定の数字の組み合わせになっているため、これはIPアドレスをソートしません。

于 2010-10-08T05:25:21.000 に答える
7

これがSQL2000用に作成されたソリューションです。新しいSQLバージョンではおそらく改善される可能性があります。

/**
 * Returns a string formatted for natural sorting. This function is very useful when having to sort alpha-numeric strings.
 *
 * @author Alexandre Potvin Latreille (plalx)
 * @param {nvarchar(4000)} string The formatted string.
 * @param {int} numberLength The length each number should have (including padding). This should be the length of the longest number. Defaults to 10.
 * @param {char(50)} sameOrderChars A list of characters that should have the same order. Ex: '.-/'. Defaults to empty string.
 *
 * @return {nvarchar(4000)} A string for natural sorting.
 * Example of use: 
 * 
 *      SELECT Name FROM TableA ORDER BY Name
 *  TableA (unordered)              TableA (ordered)
 *  ------------                    ------------
 *  ID  Name                        ID  Name
 *  1.  A1.                         1.  A1-1.       
 *  2.  A1-1.                       2.  A1.
 *  3.  R1             -->          3.  R1
 *  4.  R11                         4.  R11
 *  5.  R2                          5.  R2
 *
 *  
 *  As we can see, humans would expect A1., A1-1., R1, R2, R11 but that's not how SQL is sorting it.
 *  We can use this function to fix this.
 *
 *      SELECT Name FROM TableA ORDER BY dbo.udf_NaturalSortFormat(Name, default, '.-')
 *  TableA (unordered)              TableA (ordered)
 *  ------------                    ------------
 *  ID  Name                        ID  Name
 *  1.  A1.                         1.  A1.     
 *  2.  A1-1.                       2.  A1-1.
 *  3.  R1              -->         3.  R1
 *  4.  R11                         4.  R2
 *  5.  R2                          5.  R11
 */
ALTER FUNCTION [dbo].[udf_NaturalSortFormat](
    @string nvarchar(4000),
    @numberLength int = 10,
    @sameOrderChars char(50) = ''
)
RETURNS varchar(4000)
AS
BEGIN
    DECLARE @sortString varchar(4000),
        @numStartIndex int,
        @numEndIndex int,
        @padLength int,
        @totalPadLength int,
        @i int,
        @sameOrderCharsLen int;

    SELECT 
        @totalPadLength = 0,
        @string = RTRIM(LTRIM(@string)),
        @sortString = @string,
        @numStartIndex = PATINDEX('%[0-9]%', @string),
        @numEndIndex = 0,
        @i = 1,
        @sameOrderCharsLen = LEN(@sameOrderChars);

    -- Replace all char that have the same order by a space.
    WHILE (@i <= @sameOrderCharsLen)
    BEGIN
        SET @sortString = REPLACE(@sortString, SUBSTRING(@sameOrderChars, @i, 1), ' ');
        SET @i = @i + 1;
    END

    -- Pad numbers with zeros.
    WHILE (@numStartIndex <> 0)
    BEGIN
        SET @numStartIndex = @numStartIndex + @numEndIndex;
        SET @numEndIndex = @numStartIndex;

        WHILE(PATINDEX('[0-9]', SUBSTRING(@string, @numEndIndex, 1)) = 1)
        BEGIN
            SET @numEndIndex = @numEndIndex + 1;
        END

        SET @numEndIndex = @numEndIndex - 1;

        SET @padLength = @numberLength - (@numEndIndex + 1 - @numStartIndex);

        IF @padLength < 0
        BEGIN
            SET @padLength = 0;
        END

        SET @sortString = STUFF(
            @sortString,
            @numStartIndex + @totalPadLength,
            0,
            REPLICATE('0', @padLength)
        );

        SET @totalPadLength = @totalPadLength + @padLength;
        SET @numStartIndex = PATINDEX('%[0-9]%', RIGHT(@string, LEN(@string) - @numEndIndex));
    END

    RETURN @sortString;
END
于 2011-04-07T20:37:24.410 に答える
7

RedFilter の回答は、インデックス作成が重要ではない適度なサイズのデータ​​セットには最適ですが、インデックスが必要な場合は、いくつかの調整が必要です。

まず、関数がデータ アクセスを行わず、決定論的で正確であることをマークします。

[SqlFunction(DataAccess = DataAccessKind.None,
                          SystemDataAccess = SystemDataAccessKind.None,
                          IsDeterministic = true, IsPrecise = true)]

次に、MSSQL にはインデックス キーのサイズに 900 バイトの制限があるため、自然化された値がインデックス内の唯一の値である場合、その長さは最大 450 文字にする必要があります。インデックスに複数の列が含まれる場合、戻り値はさらに小さくする必要があります。2 つの変更:

CREATE FUNCTION Naturalize(@str AS nvarchar(max)) RETURNS nvarchar(450)
    EXTERNAL NAME ClrExtensions.Util.Naturalize

C# コードでは次のようになります。

const int maxLength = 450;

最後に、計算列をテーブルに追加する必要があります。これは永続化する必要があります (MSSQLNaturalizeは決定論的かつ正確であることを証明できないため)。つまり、自然化された値は実際にはテーブルに格納されますが、自動的に維持されます。

ALTER TABLE YourTable ADD nameNaturalized AS dbo.Naturalize(name) PERSISTED

これでインデックスを作成できます。

CREATE INDEX idx_YourTable_n ON YourTable (nameNaturalized)

また、RedFilter のコードにいくつかの変更を加えました。わかりやすくするために文字を使用し、重複スペースの削除をメイン ループに組み込み、結果が制限を超えたら終了し、部分文字列なしで最大長を設定します。結果は次のとおりです。

using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;

public static class Util
{
    [SqlFunction(DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None, IsDeterministic = true, IsPrecise = true)]
    public static SqlString Naturalize(string str)
    {
        if (string.IsNullOrEmpty(str))
            return str;

        const int maxLength = 450;
        const int padLength = 15;

        bool isDecimal = false;
        bool wasSpace = false;
        int numStart = 0;
        int numLength = 0;

        var sb = new StringBuilder();
        for (var i = 0; i < str.Length; i++)
        {
            char c = str[i];
            if (c >= '0' && c <= '9')
            {
                if (numLength == 0)
                    numStart = i;
                numLength++;
            }
            else
            {
                if (numLength > 0)
                {
                    sb.Append(pad(str.Substring(numStart, numLength), isDecimal, padLength));
                    numLength = 0;
                }
                if (c != ' ' || !wasSpace)
                    sb.Append(c);
                isDecimal = c == '.';
                if (sb.Length > maxLength)
                    break;
            }
            wasSpace = c == ' ';
        }
        if (numLength > 0)
            sb.Append(pad(str.Substring(numStart, numLength), isDecimal, padLength));

        if (sb.Length > maxLength)
            sb.Length = maxLength;
        return sb.ToString();
    }

    private static string pad(string num, bool isDecimal, int padLength)
    {
        return isDecimal ? num.PadRight(padLength, '0') : num.PadLeft(padLength, '0');
    }
}
于 2013-10-19T22:24:26.983 に答える
6

現時点ではこれが少し古いことはわかっていますが、より良い解決策を探しているときに、この質問に出くわしました。現在、注文する機能を使用しています。英数字が混在する名前のレコード(「アイテム1」、「アイテム10」、「アイテム2」など)をソートするという私の目的には問題なく機能します

CREATE FUNCTION [dbo].[fnMixSort]
(
    @ColValue NVARCHAR(255)
)
RETURNS NVARCHAR(1000)
AS

BEGIN
    DECLARE @p1 NVARCHAR(255),
        @p2 NVARCHAR(255),
        @p3 NVARCHAR(255),
        @p4 NVARCHAR(255),
        @Index TINYINT

    IF @ColValue LIKE '[a-z]%'
        SELECT  @Index = PATINDEX('%[0-9]%', @ColValue),
            @p1 = LEFT(CASE WHEN @Index = 0 THEN @ColValue ELSE LEFT(@ColValue, @Index - 1) END + REPLICATE(' ', 255), 255),
            @ColValue = CASE WHEN @Index = 0 THEN '' ELSE SUBSTRING(@ColValue, @Index, 255) END
    ELSE
        SELECT  @p1 = REPLICATE(' ', 255)

    SELECT  @Index = PATINDEX('%[^0-9]%', @ColValue)

    IF @Index = 0
        SELECT  @p2 = RIGHT(REPLICATE(' ', 255) + @ColValue, 255),
            @ColValue = ''
    ELSE
        SELECT  @p2 = RIGHT(REPLICATE(' ', 255) + LEFT(@ColValue, @Index - 1), 255),
            @ColValue = SUBSTRING(@ColValue, @Index, 255)

    SELECT  @Index = PATINDEX('%[0-9,a-z]%', @ColValue)

    IF @Index = 0
        SELECT  @p3 = REPLICATE(' ', 255)
    ELSE
        SELECT  @p3 = LEFT(REPLICATE(' ', 255) + LEFT(@ColValue, @Index - 1), 255),
            @ColValue = SUBSTRING(@ColValue, @Index, 255)

    IF PATINDEX('%[^0-9]%', @ColValue) = 0
        SELECT  @p4 = RIGHT(REPLICATE(' ', 255) + @ColValue, 255)
    ELSE
        SELECT  @p4 = LEFT(@ColValue + REPLICATE(' ', 255), 255)

    RETURN  @p1 + @p2 + @p3 + @p4

END

それから電話する

select item_name from my_table order by fnMixSort(item_name)

単純なデータ読み取りの処理時間が簡単に 3 倍になるため、完全なソリューションではない可能性があります。

于 2009-02-07T02:45:32.287 に答える
4

これが私が好きな他の解決策です: http ://www.dreamchain.com/sql-and-alpha-numeric-sort-order/

これはMicrosoftSQLではありませんが、Postgresのソリューションを探していたときにここにたどり着いたので、ここにこれを追加すると他の人に役立つと思いました。

編集:リンクがなくなった場合のコードは次のとおりです。

CREATE or REPLACE FUNCTION pad_numbers(text) RETURNS text AS $$
  SELECT regexp_replace(regexp_replace(regexp_replace(regexp_replace(($1 collate "C"),
    E'(^|\\D)(\\d{1,3}($|\\D))', E'\\1000\\2', 'g'),
      E'(^|\\D)(\\d{4,6}($|\\D))', E'\\1000\\2', 'g'),
        E'(^|\\D)(\\d{7}($|\\D))', E'\\100\\2', 'g'),
          E'(^|\\D)(\\d{8}($|\\D))', E'\\10\\2', 'g');
$$ LANGUAGE SQL;

「C」はpostgresqlのデフォルトの照合です。必要な照合を指定するか、テーブルの列に非決定的な照合が割り当てられないことが確実な場合は、照合ステートメントを削除できます。

利用方法:

SELECT * FROM wtf w 
  WHERE TRUE
  ORDER BY pad_numbers(w.my_alphanumeric_field)
于 2012-11-14T08:33:40.400 に答える
3

次のvarcharデータの場合:

BR1
BR2
External Location
IR1
IR2
IR3
IR4
IR5
IR6
IR7
IR8
IR9
IR10
IR11
IR12
IR13
IR14
IR16
IR17
IR15
VCR

これは私にとって最もうまくいきました:

ORDER BY substring(fieldName, 1, 1), LEN(fieldName)
于 2011-09-14T00:59:21.200 に答える
1

並べ替えるだけで

ORDER BY 
cast (substring(name,(PATINDEX('%[0-9]%',name)),len(name))as int)

 ##
于 2011-04-15T21:04:44.597 に答える
1

DB からデータをロードして C# で並べ替えるのに問題がある場合は、DB でプログラムによってそれを実行するアプローチに失望することでしょう。サーバーがソートするときは、「認識された」順序を毎回計算する必要があります。

データが最初に挿入されるときに、C# メソッドを使用して、前処理された並べ替え可能な文字列を格納する列を追加することをお勧めします。たとえば、数値を固定幅の範囲に変換しようとすると、"xyz1" は "xyz00000001" になります。次に、通常の SQL Server の並べ替えを使用できます。

私は自分自身の角をかぶる危険を冒して、CodingHorror 記事で提起された問題を実装する CodeProject 記事を書きました。私のコードから自由に盗んでください。

于 2008-09-16T21:34:51.937 に答える
0

You can use the following code to resolve the problem:

Select *, 
    substring(Cote,1,len(Cote) - Len(RIGHT(Cote, LEN(Cote) - PATINDEX('%[0-9]%', Cote)+1)))alpha,
    CAST(RIGHT(Cote, LEN(Cote) - PATINDEX('%[0-9]%', Cote)+1) AS INT)intv 
FROM Documents 
   left outer join Sites ON Sites.IDSite = Documents.IDSite 
Order BY alpha, intv

regards, rabihkahaleh@hotmail.com

于 2009-04-15T09:15:48.710 に答える
0

このようなトピックに関する記事をどこかで読みました。重要な点は、「rec」文字列は UI に属しているのに対し、データを並べ替えるには整数値のみが必要であるということです。情報を alpha と num の 2 つのフィールドに分割し、alpha と num で (別々に) 並べ替えてから、alpha + num で構成される文字列を表示できます。計算列を使用して、文字列またはビューを構成できます。それが役に立てば幸い

于 2009-04-15T10:14:50.893 に答える
-1

私はまだ理解できません(おそらく私の英語力が低いためです)。

あなたは試すことができます:

ROW_NUMBER() OVER (ORDER BY dbo.human_sort(field_name) ASC)

しかし、何百万ものレコードでは機能しません。

そのため、別の列を人間の価値で埋める トリガーを使用することを提案しました。

さらに:

  • 組み込みの T-SQL 関数は非常に遅く、Microsoft は代わりに .NET 関数を使用することを推奨しています。
  • 人間の価値は一定であるため、クエリが実行されるたびに計算する意味はありません。
于 2008-08-29T21:54:43.477 に答える