SQL-запрос для разделения значения одного столбца на несколько строк - PullRequest
1 голос
/ 26 апреля 2011

Я хочу создать SQL-запрос для разделения значения одного столбца на несколько строк, например:

SELECT ID, PRODUCT_COUNT FROM MERCHANT WHERE ID = 3050

ID          PRODUCT_COUNT
----------- -------------
3050        591

Исходя из этого результата, я хочу получить 6 строк следующим образом:

ID      RANGE    
3050    0-100
3050    101-200
3050    201-300
3050    301-400
3050    401-500
3050    501-591

Как я могу получить это в запросе?

Ответы [ 5 ]

2 голосов
/ 27 апреля 2011
WITH cte AS (
  SELECT
    m.ID,
    PRODUCT_COUNT,
    LoBound = (v.number - 1) * 100 + 1,
    HiBound = v.number * 100
  FROM MERCHANT m
    INNER JOIN master..spt_values v
      ON v.type = 'P' AND v.number BETWEEN 1 AND (m.PRODUCT_COUNT - 1) / 100 + 1
  WHERE m.ID = 3050
)
SELECT
  ID,
  RANGE = CAST(CASE LoBound
                 WHEN 1 THEN 0
                 ELSE LoBound
               END AS varchar)
        + '-'
        + CAST(CASE
                 WHEN HiBound < PRODUCT_COUNT THEN HiBound
                 ELSE PRODUCT_COUNT
               END AS varchar)
FROM cte

Первый CASE гарантирует, что первый диапазон начинается с 0, а не с 1, так же, как и в вашем примере вывода.

1 голос
/ 08 мая 2011

Извините ... код удален. Я сделал ошибку, когда если Product_Count был равномерно делится на 100, это дало бы неверную последнюю строку.

UPDATE: Код Андрея все еще правильный. Мне не хватало "-1" в моем. Я исправил это и поместил как тестовую установку, так и мое альтернативное решение.

И код Андрея, и мой код выводят вывод в правильном порядке для этого эксперимента, но я добавил ORDER BY, чтобы гарантировать его.

Вот код для настройки теста ...

    --===== Conditionally drop and create a test table for
         -- everyone to work against.
         IF OBJECT_ID('tempdb..#Merchant','U') IS NOT NULL
            DROP TABLE #Merchant
    ;
     SELECT TOP 10000
            ID            = IDENTITY(INT,1,1),
            Product_Count = ABS(CHECKSUM(NEWID()))%100000
       INTO #Merchant
       FROM sys.all_columns ac1
      CROSS JOIN sys.all_columns ac2
    ;
      ALTER TABLE #Merchant
        ADD PRIMARY KEY CLUSTERED (ID)
    ;
    --===== Make several entries where there's a known test setup.
     UPDATE #Merchant
        SET Product_Count = CASE
                                WHEN ID = 1 THEN 0
                                WHEN ID = 2 THEN 1
                                WHEN ID = 3 THEN 99
                                WHEN ID = 4 THEN 100
                                WHEN ID = 5 THEN 101
                                WHEN ID = 6 THEN 99999
                                WHEN ID = 7 THEN 100000
                                WHEN ID = 8 THEN 100001
                            END
  WHERE ID < = 8
 ;

Вот альтернатива, которую я выложил ранее с коррекцией -1.

WITH
cteCreateRanges AS
(--==== This determines what the ranges are
 SELECT m.ID,
        RangeStart = t.Number*100+SIGN(t.Number),
        RangeEnd   =(t.Number+1)*100,
        Product_Count
   FROM master.dbo.spt_Values t
  CROSS JOIN #Merchant        m
  WHERE t.Number BETWEEN 0 AND (m.Product_Count-1)/100
    AND t.Type = 'P'
    AND m.ID BETWEEN 1 AND 8 -- = @FindID -<<<---<<< Or use a single variable to find.
)--==== This makes the output "pretty" and sorts in correct order
 SELECT ID,
        [Range] = CAST(RangeStart AS VARCHAR(10)) + '-'
                + CASE
                  WHEN RangeEnd <= Product_Count
                  THEN CAST(RangeEnd AS VARCHAR(10))
                  ELSE CAST(Product_Count AS VARCHAR(10))
                  END
   FROM cteCreateRanges
  ORDER BY ID, RangeStart
;

Извините за ранее допущенную ошибку. Спасибо, Андрей, что поймали его.

0 голосов
/ 26 апреля 2011

Похоже, что эти диапазоны являются частью данных, поэтому они действительно должны быть в таблице (даже если вы не ожидаете, что они изменятся, потому что они будут). У этого есть хорошая побочная выгода сделать эту задачу тривиальной:

CREATE TABLE My_Ranges (    -- Use a more descriptive name
    range_start    SMALLINT    NOT NULL,
    range_end      SMALLINT    NOT NULL,
    CONSTRAINT PK_My_Ranges PRIMARY KEY CLUSTERED (range_start)
)
GO

SELECT
    P.id,
    R.range_start,
    CASE
        WHEN R.range_end < P.product_count THEN R.range_end
        ELSE P.product_count
    END AS range_end
FROM
    Products P
INNER JOIN My_Ranges R ON
    R.range_start <= P.product_count

Если ваши диапазоны всегда будут смежными, вы можете опустить столбец range_end. Ваш запрос станет немного сложнее, но вам не придется беспокоиться о перекрытии диапазонов или пробелах в ваших диапазонах.

0 голосов
/ 26 апреля 2011

Вы можете попробовать рекурсивный CTE.

WITH CTE AS
(
    SELECT Id, 0 MinB, 100 MaxB, [Range]
    FROM YourTable
    UNION ALL
    SELECT Id, CASE WHEN MinB = 0 THEN MinB+101 ELSE MinB+100 END, MaxB + 100, [Range]
    FROM CTE
    WHERE MinB < [Range]
)

SELECT  Id, 
        CAST(MinB AS VARCHAR) + ' - ' + CAST(CASE WHEN MaxB>[Range] THEN [Range] ELSE MaxB END AS VARCHAR) [Range]
FROM CTE
WHERE MinB < [Range]
ORDER BY Id, [Range]
OPTION(MAXRECURSION 5000)

Я наложил ограничение на уровень рекурсии на 5000, но вы можете изменить его (или оставить его равным нулю, что означает, в основном, продолжать делать рекурсию, пока это не может)

0 голосов
/ 26 апреля 2011

Вы можете создать таблицу, подобную этой (я изменяю первый диапазон, чтобы включить 100 элементов, подобных другим, чтобы упростить его, и основал его на одном, чтобы индексы соответствовали общему количеству):

CountRangeBoundary

MinIndexInRange
---------------
1
101
201
301
401
501
601
...

Затем выполните θ-соединение следующим образом:

SELECT m.ID,
       crb.MinIndexInRange AS RANGE_MIN,
       MIN( crb.MinIndexInRange + 100, m.PRODUCT_COUNT) AS RANGE_MAX
FROM   MERCHANT m
       JOIN CountRangeBoundry crb ON crb.MinIndexInRange <= m.PRODUCT_COUNT
WHERE  m.ID = 3050
...