sql server 排序之让其中数字排序重写教程

这篇教程..

数据库中字符串与数值比较的常见问题及解决方案

1. 问题背景

在实际开发中,我们经常遇到编号字段存储为字符串类型,但需要按数值逻辑进行排序和比较的场景。例如:产品编号、订单号、设备编号等。

2. 典型问题示例

假设我们有一个产品表,产品编号格式为 P + 数字

产品编号 产品名称
P1 产品A
P2 产品B
P10 产品C
P20 产品D
P100 产品E

问题场景:查找下一个编号

当我们想查找编号P2的下一个产品时:

-- 错误的查询方式
SELECT TOP 1 * FROM Products 
WHERE ProductNo > 'P2' 
ORDER BY ProductNo

-- 结果:P20 (错误!应该是P10)

为什么会出现这个问题?

字符串比较规则:数据库按字符逐个比较ASCII码值

  • "P10" vs "P2"
  • 比较过程:P=P, 1<2
  • 结果:"P10" < "P2"

这导致排序结果为:P1, P10, P100, P2, P20,而不是我们期望的数值顺序。

3. 解决方案

方案一:数值提取比较

-- 正确的查询方式
SELECT TOP 1 * FROM Products 
WHERE CAST(SUBSTRING(ProductNo, 2, LEN(ProductNo)-1) AS INT) > 
      CAST(SUBSTRING('P2', 2, LEN('P2')-1) AS INT)
ORDER BY CAST(SUBSTRING(ProductNo, 2, LEN(ProductNo)-1) AS INT)

-- 结果:P10 (正确!)

方案二:补零对齐

为显示和排序创建虚拟字段:也就是 长度不足则补0

SELECT ProductNo, ProductName,
    -- 创建补零后的排序字段
    'P' + RIGHT('00000' + SUBSTRING(ProductNo, 2, LEN(ProductNo)-1), 5) AS SortKey
FROM Products
ORDER BY SortKey

结果:

产品编号 产品名称 排序键
P1 产品A P00001
P2 产品B P00002
P10 产品C P00010
P20 产品D P00020
P100 产品E P00100

4. 实际应用案例

场景:查找设备编号的下一个设备

-- 数据示例
CREATE TABLE Equipment (
    EquipmentNo VARCHAR(20),
    EquipmentName VARCHAR(50)
);

INSERT INTO Equipment VALUES 
('EQ1', '设备001'),
('EQ5', '设备005'),
('EQ12', '设备012'),
('EQ25', '设备025'),
('EQ100', '设备100');

错误的查询方式

-- 查找EQ5的下一个设备
SELECT TOP 1 * FROM Equipment 
WHERE EquipmentNo > 'EQ5' 
ORDER BY EquipmentNo;

-- 错误结果:EQ100(因为字符串比较 "EQ100" < "EQ12" < "EQ25")

正确的查询方式

-- 方法1:数值比较
SELECT TOP 1 * FROM Equipment 
WHERE CAST(SUBSTRING(EquipmentNo, 3, LEN(EquipmentNo)-2) AS INT) > 
      CAST(SUBSTRING('EQ5', 3, LEN('EQ5')-2) AS INT)
ORDER BY CAST(SUBSTRING(EquipmentNo, 3, LEN(EquipmentNo)-2) AS INT);

-- 正确结果:EQ12

-- 方法2:使用函数封装
CREATE FUNCTION GetNumericPart(@input VARCHAR(20))
RETURNS INT
AS
BEGIN
    RETURN CAST(SUBSTRING(@input, 3, LEN(@input)-2) AS INT)
END

-- 使用函数简化查询
SELECT TOP 1 * FROM Equipment 
WHERE dbo.GetNumericPart(EquipmentNo) > dbo.GetNumericPart('EQ5')
ORDER BY dbo.GetNumericPart(EquipmentNo);

5. 最佳实践建议

1. 数据库设计阶段

  • 考虑将编号拆分为前缀和数字两个字段
  • 或者统一编号长度,使用固定位数(如:P001, P010, P100)

2. 查询优化

  • 创建计算列存储数值部分
  • 使用索引优化排序性能
-- 添加计算列
ALTER TABLE Products 
ADD NumericPart AS CAST(SUBSTRING(ProductNo, 2, LEN(ProductNo)-1) AS INT);

-- 在计算列上创建索引
CREATE INDEX IX_Products_NumericPart ON Products(NumericPart);

3. 应用层处理

  • 在应用程序中进行自然排序
  • 使用专门的排序函数库

函数封装

CREATE FUNCTION GetNextCode(@currentCode VARCHAR(20), @prefix VARCHAR(10))
RETURNS VARCHAR(20)
AS
BEGIN
    DECLARE @result VARCHAR(20) = NULL
    DECLARE @currentNum INT = 0
    DECLARE @prefixLen INT = LEN(@prefix)
    
    -- 提取当前编号的数值部分
    IF @currentCode != '' AND LEFT(@currentCode, @prefixLen) = @prefix
        SET @currentNum = CAST(SUBSTRING(@currentCode, @prefixLen + 1, LEN(@currentCode) - @prefixLen) AS INT)
    
    -- 模拟查找下一个编号(实际应用中替换为实际表查询)
    SET @result = @prefix + CAST(@currentNum + 1 AS VARCHAR(10))
    
    RETURN @result
END

-- 使用示例
SELECT dbo.GetNextCode('P29', 'P') AS NextProductCode  -- 返回:P30
SELECT dbo.GetNextCode('EQ5', 'EQ') AS NextEquipmentCode  -- 返回:EQ6

编号比较

CREATE FUNCTION CompareCodeNumeric(@code1 VARCHAR(20), @code2 VARCHAR(20), @prefix VARCHAR(10))
RETURNS INT  -- 返回:-1(code1<code2), 0(相等), 1(code1>code2)
AS
BEGIN
    DECLARE @num1 INT, @num2 INT
    DECLARE @prefixLen INT = LEN(@prefix)
    
    -- 提取数值部分
    SET @num1 = CAST(SUBSTRING(@code1, @prefixLen + 1, LEN(@code1) - @prefixLen) AS INT)
    SET @num2 = CAST(SUBSTRING(@code2, @prefixLen + 1, LEN(@code2) - @prefixLen) AS INT)
    
    RETURN CASE 
        WHEN @num1 < @num2 THEN -1
        WHEN @num1 > @num2 THEN 1
        ELSE 0
    END
END

-- 使用示例
CREATE FUNCTION CompareCodeNumeric(@code1 VARCHAR(20), @code2 VARCHAR(20), @prefix VARCHAR(10))
RETURNS INT  -- 返回:-1(code1<code2), 0(相等), 1(code1>code2)
AS
BEGIN
    DECLARE @num1 INT, @num2 INT
    DECLARE @prefixLen INT = LEN(@prefix)
    
    -- 提取数值部分
    SET @num1 = CAST(SUBSTRING(@code1, @prefixLen + 1, LEN(@code1) - @prefixLen) AS INT)
    SET @num2 = CAST(SUBSTRING(@code2, @prefixLen + 1, LEN(@code2) - @prefixLen) AS INT)
    
    RETURN CASE 
        WHEN @num1 < @num2 THEN -1
        WHEN @num1 > @num2 THEN 1
        ELSE 0
    END
END

-- 使用示例
SELECT dbo.CompareCodeNumeric('P29', 'P119', 'P')  -- 返回:-1(P29 < P119)
SELECT dbo.CompareCodeNumeric('P29', 'P119', 'P')  -- 返回:-1(P29 < P119)
CREATE FUNCTION GetNextCodeFromTable(
    @currentCode VARCHAR(20), 
    @tableName VARCHAR(50),
    @codeColumn VARCHAR(50),
    @prefix VARCHAR(10)
)
RETURNS VARCHAR(20)
AS
BEGIN
    DECLARE @result VARCHAR(20) = NULL
    DECLARE @currentNum INT = 0
    DECLARE @sql NVARCHAR(MAX)
    DECLARE @prefixLen INT = LEN(@prefix)
    
    -- 提取当前编号的数值部分
    IF @currentCode != '' AND LEFT(@currentCode, @prefixLen) = @prefix
        SET @currentNum = CAST(SUBSTRING(@currentCode, @prefixLen + 1, LEN(@currentCode) - @prefixLen) AS INT)
    
    -- 动态SQL查找下一个编号
    SET @sql = N'
        SELECT TOP 1 @result = ' + @codeColumn + N'
        FROM ' + @tableName + N'
        WHERE CAST(SUBSTRING(' + @codeColumn + N', ' + CAST(@prefixLen + 1 AS VARCHAR) + N', 
                  LEN(' + @codeColumn + N') - ' + CAST(@prefixLen AS VARCHAR) + N') AS INT) > ' + CAST(@currentNum AS VARCHAR) + N'
        ORDER BY CAST(SUBSTRING(' + @codeColumn + N', ' + CAST(@prefixLen + 1 AS VARCHAR) + N', 
                      LEN(' + @codeColumn + N') - ' + CAST(@prefixLen AS VARCHAR) + N') AS INT)'
    
    EXEC sp_executesql @sql, N'@result VARCHAR(20) OUTPUT', @result OUTPUT
    
    RETURN @result
END
CREATE FUNCTION GetNextProduct(@currentProduct VARCHAR(20), @category VARCHAR(10))
RETURNS @result TABLE (ProductNo VARCHAR(20))
AS
BEGIN
    IF @currentProduct = ''
    BEGIN
        -- 获取第一个产品
        INSERT INTO @result
        SELECT TOP 1 ProductNo FROM Products 
        WHERE CategoryCode = @category
        ORDER BY CAST(SUBSTRING(ProductNo, 2, LEN(ProductNo)-1) AS INT)
    END
    ELSE
    BEGIN
        -- 获取下一个产品
        INSERT INTO @result
        SELECT TOP 1 ProductNo FROM Products 
        WHERE CategoryCode = @category
        AND CAST(SUBSTRING(ProductNo, 2, LEN(ProductNo)-1) AS INT) > 
            CAST(SUBSTRING(@currentProduct, 2, LEN(@currentProduct)-1) AS INT)
        ORDER BY CAST(SUBSTRING(ProductNo, 2, LEN(ProductNo)-1) AS INT)
    END
    RETURN
END

-- 使用示例
SELECT * FROM GetNextProduct('P29', 'A')  -- 返回:P30 或 P119
SELECT * FROM GetNextProduct('', 'A')     -- 返回:第一个产品

函数封装多区域推荐的思路
上面的不太靠谱,下面是我实际上用上的逻辑,自己想的一种思路 解决多区域思路.
比如LZ13333 由于他们没有分割1_后面的,把第一位当做区域1 后面的是第二个区域所以就有了下面的解决代码

思路就是 内层查询把 排序区域组装好 成一个,然后 只需要判断区域_sort> cast区域(传入的参数)就行,

也就是传入的参数 也需要转换为区域,

ALTER FUNCTION [dbo].[Fun_calcNextArea] (@area VARCHAR(20),@sequenceid VARCHAR(20) )
RETURNS @lztable TABLE (AERA_NO VARCHAR(30) ) AS
BEGIN
if @area = ''
begin
    INSERT INTO @lztable
    select AERA_NO from(
        SELECT TOP 1 AERA_NO, CASE 
        WHEN CHARINDEX('LZ', AERA_NO) > 0 THEN
            SUBSTRING(AERA_NO, 3,1)
        ELSE
            '0'
    END as 机台,
        CASE 
            WHEN CHARINDEX('LZ', AERA_NO) > 0 THEN
                CAST(SUBSTRING(AERA_NO, CHARINDEX('LZ', AERA_NO) + 3, LEN(AERA_NO)) as INT)
            ELSE
                CAST(AERA_NO as INT)
        END as sort_no 
        FROM G_AREA_INFO 
        where sequence_no = @sequenceid 
        order by  机台,sort_no
    ) x
end
else
begin
    INSERT INTO @lztable
    select top 1 AERA_NO from(
        SELECT top 5000 AERA_NO,
        
       (CASE 
        WHEN CHARINDEX('LZ', AERA_NO) > 0 THEN
            SUBSTRING(AERA_NO, 3,1)
        ELSE
            '0'
    END)+'_'+ RIGHT('000000'+(CASE 
            WHEN CHARINDEX('LZ', AERA_NO) > 0 THEN
            SUBSTRING(AERA_NO, CHARINDEX('LZ', AERA_NO) + 3, LEN(AERA_NO)) 
            ELSE
           AERA_NO 
        END),6) as sort_merge 
        FROM G_AREA_INFO 
        where sequence_no = @sequenceid 
        
 
        order by sort_merge
    ) x where sort_merge>
        
       (CASE 
        WHEN CHARINDEX('LZ', @area) > 0 THEN
            SUBSTRING(@area, 3,1)
        ELSE
            '0'
    END)+'_'+ RIGHT('000000'+(CASE 
            WHEN CHARINDEX('LZ', @area) > 0 THEN
            SUBSTRING(@area, CHARINDEX('LZ', @area) + 3, LEN(@area)) 
            ELSE
           '0' 
        END),6)
end
return
END
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容