سؤال

لدينا قاعدة بيانات كبيرة حيث لدينا ترقيم الصفحات الجانبي لقاعدة البيانات.يعد ذلك سريعًا، حيث يتم إرجاع صفحة مكونة من 50 صفًا من ملايين السجلات في جزء صغير من الثانية.

يمكن للمستخدمين تحديد الفرز الخاص بهم، واختيار العمود الذي سيتم الفرز حسبه.الأعمدة ديناميكية - بعضها يحتوي على قيم رقمية وبعضها تواريخ وبعضها نص.

في حين أن معظم أنواع النص كما هو متوقع يتم فرزه بطريقة غبية.حسنًا، أقول غبيًا، إنه منطقي لأجهزة الكمبيوتر، لكنه يحبط المستخدمين.

على سبيل المثال، الفرز حسب معرف سجل السلسلة يعطي شيئًا مثل:

rec1
rec10
rec14
rec2
rec20
rec3
rec4

...وما إلى ذلك وهلم جرا.

أريد أن يأخذ هذا في الاعتبار الرقم، لذلك:

rec1
rec2
rec3
rec4
rec10
rec14
rec20

لا يمكنني التحكم في الإدخال (وإلا فسأقوم فقط بالتنسيق بالآلاف البادئة) ولا يمكنني الاعتماد على تنسيق واحد - بعضها أشياء مثل "{alpha code}-{dept code}-{rec id}".

أعرف عدة طرق للقيام بذلك في لغة C#، ولكن لا يمكنني سحب جميع السجلات لفرزها، لأن ذلك سيكون بطيئًا.

هل يعرف أحد طريقة لتطبيق الفرز الطبيعي بسرعة في خادم Sql؟


كانوا يستخدمون:

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

وبعد ذلك نحن الترحيل من خلال ذلك.

يمكننا إضافة مشغلات، على الرغم من أننا لن نفعل ذلك.جميع مدخلاتهم محددة المعالم وما شابه ذلك، لكن لا يمكنني تغيير التنسيق - إذا أدخلوا "rec2" و"rec10" فإنهم يتوقعون إعادتهم بهذه الطريقة وبترتيب طبيعي.


لدينا مدخلات مستخدم صالحة تتبع تنسيقات مختلفة لعملاء مختلفين.

يمكن للمرء أن يذهب REC1، REC2، REC3، ...التوصية 100، التوصية 101

بينما قد يذهب آخر:grp1rec1، grp1rec2، ...grp20rec300، grp20rec301

عندما أقول أننا لا نستطيع التحكم في الإدخال، أعني أننا لا نستطيع إجبار المستخدمين على تغيير هذه المعايير - لديهم قيمة مثل grp1rec1 ولا يمكنني إعادة تنسيقها كـ grp01rec001، لأن ذلك سيؤدي إلى تغيير شيء يستخدم لعمليات البحث و الارتباط بالأنظمة الخارجية.

تختلف هذه التنسيقات كثيرًا، ولكنها غالبًا ما تكون عبارة عن خليط من الحروف والأرقام.

يعد فرز هذه العناصر في C# أمرًا سهلاً - فقط قم بتقسيمها إلى { "grp", 20, "rec", 301 } ثم قم بمقارنة قيم التسلسل بدورها.

ومع ذلك، قد يكون هناك ملايين السجلات ويتم تقسيم البيانات إلى صفحات، فأنا بحاجة إلى إجراء الفرز على خادم SQL.

يقوم خادم SQL بالفرز حسب القيمة، وليس المقارنة - في C# يمكنني تقسيم القيم للمقارنة، ولكن في SQL أحتاج إلى بعض المنطق الذي (بسرعة كبيرة) يحصل على قيمة واحدة يتم فرزها باستمرار.

@moebius - قد تنجح إجابتك، لكنها تبدو وكأنها حل وسط قبيح لإضافة مفتاح فرز لكل هذه القيم النصية.

هل كانت مفيدة؟

المحلول

معظم الحلول المستندة إلى SQL التي رأيتها تنقطع عندما تصبح البيانات معقدة بدرجة كافية (على سبيل المثال.فيه أكثر من رقم أو رقمين).في البداية حاولت تنفيذ وظيفة NaturalSort في T-SQL التي تلبي متطلباتي (من بين أمور أخرى، تتعامل مع عدد عشوائي من الأرقام داخل السلسلة)، ولكن الأداء كان سيئًا. طريق بطئ جدا.

في النهاية، كتبت دالة CLR عددية في لغة C# للسماح بالفرز الطبيعي، وحتى مع التعليمات البرمجية غير المحسنة، فإن الأداء الذي يتم استدعاؤه من SQL Server يكون سريعًا بشكل مذهل.لديها الخصائص التالية:

  • سيتم فرز أول 1000 حرف أو نحو ذلك بشكل صحيح (يمكن تعديلها بسهولة في التعليمات البرمجية أو تحويلها إلى معلمة)
  • يقوم بفرز الكسور العشرية بشكل صحيح، بحيث يأتي 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');
    }
}

لتسجيل هذا بحيث يمكنك الاتصال به من SQL Server، قم بتشغيل الأوامر التالية في محلل الاستعلام:

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 على غرار تم تعطيل تنفيذ تعليمات برمجية للمستخدم في .NET Framework.تمكين خيار التكوين "تمكين clr"., ، اتبع التعليمات هنا لتمكينه.تأكد من مراعاة الآثار الأمنية قبل القيام بذلك.إذا لم تكن مسؤول قاعدة البيانات، فتأكد من مناقشة هذا الأمر مع المسؤول قبل إجراء أي تغييرات على تكوين الخادم.

ملاحظة 2:هذا الرمز لا يدعم التدويل بشكل صحيح (على سبيل المثال، يفترض أن العلامة العشرية هي "."، ولم يتم تحسينها للسرعة، وما إلى ذلك.اقتراحات لتحسينه هي موضع ترحيب!

يحرر: تمت إعادة تسمية الوظيفة إلى تجنيس بدلاً من فرز طبيعي, ، لأنه لا يقوم بأي فرز فعلي.

نصائح أخرى

order by LEN(value), value

ليست مثالية، ولكنها تعمل بشكل جيد في كثير من الحالات.

أعلم أن هذا سؤال قديم ولكنني صادفته للتو وبما أنه لا يوجد لديه إجابة مقبولة.

لقد استخدمت دائمًا طرقًا مشابهة لهذا:

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 [Column]، ولكنه يمنحك النتيجة المطلوبة في OP.

يحرر:فقط لمزيد من التوضيح، هذا ما ورد أعلاه لن يعمل إذا كان لديك قيم عشرية مثل وجود 1, 1.15 و 1.5, ، (سوف يتم فرزهم كـ {1, 1.5, 1.15}) لأن هذا ليس ما هو مطلوب في OP، ولكن يمكن القيام بذلك بسهولة عن طريق:

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 لأنك الآن تدخل في مجموعات أرقام محددة جدًا بدلاً من النص البسيط + الرقم.

إجابة RedFilter يعد أمرًا رائعًا لمجموعات البيانات ذات الحجم المعقول حيث لا تكون الفهرسة أمرًا بالغ الأهمية، ولكن إذا كنت تريد فهرسًا، فستلزم إجراء العديد من التعديلات.

أولاً، ضع علامة على الوظيفة بأنها لا تقوم بأي وصول إلى البيانات وأنها حتمية ودقيقة:

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

بعد ذلك، لدى MSSQL حد يبلغ 900 بايت لحجم مفتاح الفهرس، لذا إذا كانت القيمة الطبيعية هي القيمة الوحيدة في الفهرس، فيجب أن يبلغ طولها 450 حرفًا على الأكثر.إذا كان الفهرس يتضمن أعمدة متعددة، فيجب أن تكون قيمة الإرجاع أصغر.تغييران:

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

وفي كود C#:

const int maxLength = 450;

أخيرًا، ستحتاج إلى إضافة عمود محسوب إلى جدولك، ويجب أن يستمر (لأن MSSQL لا يمكنه إثبات ذلك) Naturalize حتمية ودقيقة)، مما يعني أن القيمة الطبيعية مخزنة فعليًا في الجدول ولكن لا يزال يتم الاحتفاظ بها تلقائيًا:

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');
    }
}

أعلم أن هذا الأمر قديم بعض الشيء في هذه المرحلة، ولكن أثناء بحثي عن حل أفضل، صادفت هذا السؤال.أنا أستخدم حاليًا وظيفة للطلب من خلالها.إنه يعمل بشكل جيد لغرض فرز السجلات التي تمت تسميتها بأحرف أبجدية رقمية مختلطة ("العنصر 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)

فهو يضاعف وقت المعالجة ثلاث مرات بسهولة لقراءة بسيطة للبيانات، لذلك قد لا يكون الحل الأمثل.

إليك الحل المكتوب لـ SQL 2000.ربما يمكن تحسينه لإصدارات 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

إليك حل آخر يعجبني:http://www.dreamchain.com/sql-and-alpha-numeric-sort-order/

إنه ليس Microsoft SQL، ولكن بما أنني انتهى بي الأمر هنا عندما كنت أبحث عن حل لـ Postgres، اعتقدت أن إضافة هذا هنا سيساعد الآخرين.

للآتي 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)

إذا كنت تواجه مشكلة في تحميل البيانات من قاعدة البيانات لفرزها في لغة C#، فأنا متأكد من أنك ستصاب بخيبة أمل من أي طريقة للقيام بذلك برمجيًا في قاعدة البيانات.عندما يقوم الخادم بالفرز، يجب عليه حساب الترتيب "المدرك" تمامًا كما تفعل - في كل مرة.

أقترح عليك إضافة عمود إضافي لتخزين السلسلة القابلة للفرز التي تمت معالجتها مسبقًا، باستخدام طريقة C#، عند إدراج البيانات لأول مرة.قد تحاول تحويل الأرقام إلى نطاقات ذات عرض ثابت، على سبيل المثال، بحيث يتحول "xyz1" إلى "xyz00000001".ثم يمكنك استخدام الفرز العادي لـ SQL Server.

على الرغم من مخاطري، كتبت مقالة CodeProject توضح المشكلة كما تم طرحها في مقالة CodingHorror.لا تتردد في سرقة من الكود الخاص بي.

لقد قرأت للتو مقالا في مكان ما حول هذا الموضوع.النقطة الأساسية هي:ما عليك سوى قيمة العدد الصحيح لفرز البيانات، بينما تنتمي السلسلة "rec" إلى واجهة المستخدم.يمكنك تقسيم المعلومات إلى حقلين، على سبيل المثال alpha وnum، والفرز حسب alpha وnum (بشكل منفصل) ثم إظهار سلسلة مكونة من alpha + num.يمكنك استخدام عمود محسوب لإنشاء السلسلة أو العرض.نأمل أن يساعد

يمكنك استخدام الكود التالي لحل المشكلة:

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

التحيات ، Rabihkahaleh@hotmail.com

ببساطة تقوم بالفرز حسب

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

 ##

ما زلت لا أفهم (ربما بسبب ضعف لغتي الإنجليزية).

بامكانك ان تحاول:

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

لكنها لن تعمل مع ملايين السجلات.

لهذا السبب اقترحت استخدام الزناد الذي يملأ متفرق العمود مع القيمة الإنسانية.

علاوة على ذلك:

  • وظائف T-SQL مدمجة بطيئة حقًا وتقترح Microsoft استخدام وظائف .NET بدلاً من ذلك.
  • القيمة الإنسانية ثابت ، لذلك لا يوجد أي فائدة في حسابه في كل مرة عند تشغيل الاستعلام.
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top