iT邦幫忙

0

求SQL:分配數量 及 部份 SELECT

sql
  • 分享至 

  • xImage

最近最熱門的資料庫,莫過於疫苗預約系統了
看新聞報導寫得天花亂墜,我想自己實作看看
1.建了一個 TABLE
2.灌了一些測試資料

在「分配疫苗」的時侯
我只想得到寫程式去分配
想請教各位高手
如何用 SQL 指令去分配數量
能分配數量之後
如何用 SQL 指令 SELECT 出那些資料

為使題目單純一點,考慮:
1.以最單純的縣市(LEFT(AREA,3))來分配即可(假設全部的人都尚未接種)
2.以群組來看,「分配數小於需求數」及「分配數大於需求數」兩種情形
3.求得分配數之後,SELECT 出清單,做為通知之用

換個問法可能比較直覺:
今天送來了5000劑疫苗,要分給各縣市各多少數量?名單?

以下是資料結構及測試資料
有需補充之處請告知

CREATE TABLE `MAINTBL` (
  `PID` varchar(10) NOT NULL COMMENT '身分證字號',
  `NAME` varchar(20) NOT NULL COMMENT '姓名',
  `MOBILE` varchar(10) NOT NULL COMMENT '手機號碼',
  `PRIORITY` int(11) NOT NULL COMMENT '分類',
  `BIRTHYEAR` int(11) NOT NULL COMMENT '出生年',
  `AREA` varchar(20) NOT NULL COMMENT '注射行政區',
  `VACCINE` varchar(26) NOT NULL COMMENT '疫苗種類',
  `SHOT1` varchar(1) NOT NULL COMMENT '第一劑疫苗種類',
  `SHOT1DATE` date DEFAULT NULL COMMENT '第一劑注射日期',
  `SHOT2` varchar(1) NOT NULL COMMENT '第二劑疫苗種類',
  `SHOT2DATE` date DEFAULT NULL COMMENT '第二劑注射日期'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CSV(11,720筆)

補充母數如下
https://ithelp.ithome.com.tw/upload/images/20210714/20001787XDJsZIBMZ6.png

看更多先前的討論...收起先前的討論...
缺範例解釋@@|||
例如..
1.單純的縣市分配→有母數嗎??(還是母數=測試資料所有數?)
台北 30萬
新北 40萬
台中 20萬
補充於原發問中
可以用 5000 做為分配數
這算很有意思的題目。晚點我再寫看看
母數? 母數的定義是?
其實我也不知道什麼是「母數」-_-|||
就把 group count 貼上來...
母數就是如這張表的總量@@...
https://images.scribblelive.com/2021/7/13/de003bc2-b180-49e9-848f-2e083412e802_1000.png
因為不知道你的母數是多少@@...只能單純計算需求筆數..
不好意思我的描述不清楚,造成你的誤會
我這題目是
「給一個數量(例:5000),用 SQL 分配到各群組中」
而你上圖中的母數(總量)
已經是我的「答案」而不是「題目」

換個問法可能比較直覺:
今天送來了5000劑疫苗,要分給各縣市各多少數量?名單?
了解~~
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
1
純真的人
iT邦大師 1 級 ‧ 2021-07-14 17:21:12
最佳解答

我是用MSSQL去計算的~
我先回第一題@@..

1.以最單純的縣市(LEFT(AREA,3))來分配即可(假設全部的人都尚未接種)

匯入的資料
https://ithelp.ithome.com.tw/upload/images/20210714/20061369I949pKgYw0.png

分配的SQL

declare @疫苗數量 int = 5000

select *
,sum(疫苗分配)over() 總疫苗分配
from (
	select *
	,Convert(int,Convert(decimal(18, 4),Convert(decimal(18, 3),登記數) / Convert(decimal(18, 3),總登記)) * @疫苗數量) 疫苗分配
	from (
		select *
		,sum(登記數)over() 總登記
		from (
			select Left(注射行政區,3) 行政區
			,Count(0) 登記數
			from reservedCSV
			group by Left(注射行政區,3)
		) k
	) k
) k
order by 登記數 desc


https://ithelp.ithome.com.tw/upload/images/20210714/200613696SK5X3LlKv.png

今天工作好忙~一直被打擾XD...

--

2.以群組來看,「分配數小於需求數」及「分配數大於需求數」兩種情形

如下~以第一次有施打的情況區分

declare @疫苗數量 int = 5000

select *
,sum(疫苗分配)over() 總疫苗分配
from (
	select *
	,Convert(int,Convert(decimal(18, 4),Convert(decimal(18, 3),登記數) / Convert(decimal(18, 3),總登記)) * @疫苗數量) 疫苗分配
	from (
		select *
		,sum(登記數)over() 總登記
		from (
			select Left(注射行政區,3) 行政區
			,sum(case when 第一劑疫苗種類 is not null then 1 else 0 end) 已施打
			,sum(case when 第一劑疫苗種類 is null then 1 else 0 end) 從未施打
			,Count(0) 登記數
			from reservedCSV
			group by Left(注射行政區,3)
		) k
	) k
) k
order by 登記數 desc

https://ithelp.ithome.com.tw/upload/images/20210714/20061369OXn0bkI0op.png

3.求得分配數之後,SELECT 出清單,做為通知之用
最後如下匯出清單~(依照長幼有序排序@@~~)

declare @疫苗被分配 int = 838
select *
from (
	select Row_Number()Over(order by 出生年) Sort
	,*
	from reservedCSV
	where Left(注射行政區,3) = '新北市'
	and 第一劑疫苗種類 is null
) k
where Sort <= @疫苗被分配

https://ithelp.ithome.com.tw/upload/images/20210714/20061369QsDfrMr8lL.png


SQL何在一起總列出的話@@...如下共4969筆通知@@...

declare @疫苗數量 int = 5000
declare @Tmp table(
	縣市 nvarchar(50)
	,疫苗分配 int
)
insert into @Tmp
select 行政區
,疫苗分配
from (
	select *
	,Convert(int,Convert(decimal(18, 4),Convert(decimal(18, 3),登記數) / Convert(decimal(18, 3),總登記)) * @疫苗數量) 疫苗分配
	from (
		select *
		,sum(登記數)over() 總登記
		from (
			select Left(注射行政區,3) 行政區
			,Count(0) 登記數
			from reservedCSV
			group by Left(注射行政區,3)
		) k
	) k
) k

select *
from (
	select Row_Number()Over(PARTITION BY Left(注射行政區,3) order by 出生年) Sort
	,a.*
	,疫苗分配
	from reservedCSV a
	left join @Tmp on 縣市 = Left(注射行政區,3)
	where 第一劑疫苗種類 is null
) k
where Sort <= 疫苗分配
order by Left(注射行政區,3)

https://ithelp.ithome.com.tw/upload/images/20210715/20061369nBj6jkNgmn.png

看更多先前的回應...收起先前的回應...

謝謝回答
/images/emoticon/emoticon41.gif
吹毛求疵一點
「疫苗分配」的總數是 5005

說時遲那時快
已經有 4995 的版本了
/images/emoticon/emoticon12.gif

我有更新計算唷XD

接下來就是「名單」的部份
在各群組中挑出「疫苗分配」筆數的資料
例:新北市 從 1966 筆中挑出 838 筆資料

ok~就大概如上更新了~

有個不情之請
可不可以合成一個 SQL,不要分兩段
/images/emoticon/emoticon25.gif

我合在一起了唷~共4969筆要通知@@a

4995 vs 4969 的差別?

還是你比較強。我只做完前面比例分配。
但就是一直想不到後面取出對應數量。
看完你的寫法,才發現我方向錯了。

因為我一直再想怎麼去UPDATE的方式。腦袋就打結了。

4995是所有縣市分配
4969是所有縣市分配第一次施打的~
所以會殘留26筆@@..

1
IT 癡
iT邦高手 1 級 ‧ 2021-07-14 11:21:45

先建議你 schema 設定先做些更動:

  1. 疫苗種類:有些打二劑、有些一劑,所以如果種類為僅一劑,第二劑的種類跟日期理應轉為不可變動,並為灰階化,或是欄位自動隱藏

2a. 如果為二劑,如 AZ,第二劑在第一劑起打後的 8 週,所以第二劑的種類可以自動設定第一劑的種類,但因現在可能可以混打,所以第二劑種類可以維持開放修改狀態
2b. 如果打 az,不管第二劑何種類,都要到第 8 週後才可施打,所以第二劑的日期應要鎖定第一季的 8 週後,但因被施打者可能拖過 8 週後時間,所以第二劑的時間也要維持開放修改狀態,唯要設定不能低於第一劑的 8 週後第一天 (第 57 天)

他不是說他的程式已經寫好了??
希望用SQL表示看看@@?

謝謝兩位的回覆

他不是說他的程式已經寫好了??

其實我還沒寫程式
/images/emoticon/emoticon25.gif

1
喵凹咿唉思嗯
iT邦研究生 5 級 ‧ 2021-07-16 15:27:27

這題目好有趣, 分享我的版本, 但資料庫是Oracle, 其他的資料庫有些語法可能要調整或切段

利用累加跟取最小或最大的方式可以更好的解決四捨五入數字亂噴的問題

直接切換With clause最後的SELECT語法的TABLE可以取得分類跟名單

分配上考慮了可能施打不同疫苗跟時間超過的情況, 但這些規則在某些情況下可能會出BUG, 如果疫苗種類多也不太好用就是..

WITH
    -- 決定不同種類的總分配量
    VACCINE_QUANTITY AS (SELECT 5000 TOTAL_A, 0 TOTAL_M FROM DUAL), 
    -- 決定篩選條件(誰能打, 以及劑量數)
    -- 這邊要決定能打的種類的總數跟合計數量, 用來推估重疊的部份的可調整數量
    -- 以及決定在同區下的優先等級
    -- 混打的條件為A->M only, 故如果第一針已打的情況下, M 的數量將受限
    VACCINE_FILTER AS 
    (
    SELECT M.*, 
        CASE WHEN INSTR(VACCINE,'AM')> 0 AND SHOT1 IS NULL AND SHOT2 IS NULL THEN 'ALL' -- 第一針完全沒打過, 且無偏好
             WHEN INSTR(VACCINE,'AM')> 0 AND (SHOT1 ='A' AND SHOT1DATE <= SYSDATE) AND SHOT2 IS NULL THEN 'ALL' -- 第一針打A, 第二針無偏好, 打A或混打M, 不考慮混打這行要註記掉
             WHEN INSTR(VACCINE,'A')> 0 AND NVL(SHOT1,'A') = 'A' THEN 'A' -- 指定打A的, 或是只能打A的
             WHEN INSTR(VACCINE,'M')> 0 THEN 'M' -- 指定打M的, 或是只能打M的, 假定M第二針能混打, 所以不需考慮第一針是啥, 不考慮混打如果沒有第三種疫苗不考慮第一針也不會影響結果
             ELSE '' END VACC_TYPE, -- 都可以, 無受限
        SUBSTR(AREA,0,3) REGION
    FROM MAINTBL M
    WHERE 1=1 
      AND ( SYSDATE < SHOT1DATE OR -- 已排定第一劑但尚未施打
            SYSDATE < SHOT2DATE OR -- 已排定第二劑但尚未施打
            SHOT1DATE IS NULL  OR  -- 未排定第一劑
            SHOT2DATE IS NULL      -- 未排定第二劑(第一劑未排第二劑一定未排, 兩筆會是同一筆, 不需額外下第一劑已排條件) 
      )
    ORDER BY AREA),
    VACCINE_FILTER_TOTAL AS
    (
    SELECT 
        V.*, 
        COUNT(1) OVER(PARTITION BY SUBSTR(AREA,0,3))/COUNT(1) OVER() RATIO, -- 該區登記數推算佔總量比例
        COUNT(1) OVER() TOTAL, -- 總需求
        COUNT(1) OVER(PARTITION BY REGION,VACC_TYPE ) TOTAL_REGION_TYPE, -- 區域內總需求
        COUNT(1) OVER(PARTITION BY REGION) TOTAL_REGION, -- 區域內總需求
        RANK() OVER( PARTITION BY REGION,VACC_TYPE ORDER BY PRIORITY, NVL(SHOT2DATE,CASE WHEN SHOT1DATE< SYSDATE THEN NULL ELSE SHOT1DATE END) NULLS LAST, BIRTHYEAR, PID ) RNK -- 該區分配順序(優先分配等級), 已指定日期的優先分配(避免打不到), 這邊可以調整同區域施打順序
    FROM VACCINE_FILTER V
    ),
    TOTAL_APPLY_BY_RATIO AS -- 額定分配量, 在不考慮超出的情況下 每個區域依比例平均分配下可以支配的數量, 一定程度上這邊可以調整分配方式
    (
    SELECT 
        REGION, 
        MIN(TOTAL_REGION_TYPE) TOTAL_REGION_TYPE,
        MIN(TOTAL_REGION) TOTAL_REGION,
        ROUND(MIN(RATIO)*TOTAL_A)+1 A_FORECAST, -- A預估分配量, +1是為了避免在需求平均分散且極低分配量時無法分配
        ROUND(MIN(RATIO)*TOTAL_M)+1 M_FORECAST, -- M預估分配量, +1是為了避免在需求平均分散且極低分配量時無法分配
        VACC_TYPE
    FROM VACCINE_FILTER_TOTAL, VACCINE_QUANTITY
    GROUP BY REGION, VACC_TYPE,TOTAL_A,TOTAL_M
    ORDER BY REGION
    ),
    TOTAL_APPLY_BY_RATIO_PIVOT AS -- 將需求總數轉置成欄位, 方便推算分配量
    (SELECT * FROM TOTAL_APPLY_BY_RATIO R
    PIVOT (MIN(TOTAL_REGION_TYPE) FOR VACC_TYPE IN ('A' as "A_REQ",'M' as "M_REQ",'ALL' as "ALL_REQ")) T),
    REVISE_TOTAL_A_QTY AS -- 對疫苗進行累加跟數量修正(超出的部份就不再分配, 因為分配的時候由基數較小的區域分配, 所以最後一個區域即使移除了四捨五入造成多餘的部份也不會對分配比例影響太大)
    (SELECT 
        T.*,
        SUM(A_FORECAST) OVER(ORDER BY A_FORECAST,REGION) ACC_A_TOTAL, -- 累記預供量
        SUM(M_FORECAST) OVER(ORDER BY M_FORECAST,REGION) ACC_M_TOTAL, -- 累記預供量
        GREATEST(A_FORECAST - GREATEST(SUM(A_FORECAST) OVER(ORDER BY A_FORECAST,REGION) - TOTAL_A,0),0) A_FORECAST_R, -- 實際供量
        GREATEST(M_FORECAST - GREATEST(SUM(M_FORECAST) OVER(ORDER BY M_FORECAST,REGION) - TOTAL_M,0),0) M_FORECAST_R, -- 實際供量
        V.TOTAL_A,
        V.TOTAL_M
    FROM TOTAL_APPLY_BY_RATIO_PIVOT T,VACCINE_QUANTITY V ORDER BY M_FORECAST),
    TOTAL_APPLY_BY_ACUTUAL AS -- 推算實際分配量
    (SELECT 
        T.*,
        GREATEST(A_FORECAST_R - A_REQ,0) A_AVALIBLE_TO_ALL, -- 可分配至ALL 殘量A
        GREATEST(M_FORECAST_R - M_REQ,0) M_AVALIBLE_TO_ALL,  -- 可分配至ALL 殘量M
        LEAST(ALL_REQ, GREATEST(A_FORECAST_R - A_REQ,0)) A_TO_ALL , -- 分配給ALL組 A疫苗的量(先分)
        LEAST(LEAST(ALL_REQ - LEAST(ALL_REQ, GREATEST(A_FORECAST_R - A_REQ,0)),GREATEST(M_FORECAST_R - M_REQ,0)),TOTAL_M) M_TO_ALL -- 分配給ALL組 M疫苗的量
    FROM REVISE_TOTAL_A_QTY T
    ORDER BY TOTAL_REGION ASC),
    RESULT_TABLE AS -- 實際分配結果
    (SELECT 
        REGION,
        LEAST(A_FORECAST_R , A_REQ) + A_TO_ALL TOTAL_A_RESULT, -- A總配量
        LEAST(M_FORECAST_R , M_REQ) + M_TO_ALL TOTAL_M_RESULT, -- M總配量
        SUM(LEAST(M_FORECAST_R , M_REQ) + M_TO_ALL) OVER(order by LEAST(M_FORECAST_R , M_REQ) + M_TO_ALL) TOTAL_CHECK,
        LEAST(A_FORECAST_R , A_REQ) A_TO_A, -- 只能打A的對象配量
        LEAST(M_FORECAST_R , M_REQ) M_TO_M, -- 只能打M的對象配量
        A_TO_ALL, -- 兩種都能打, 分配給打A
        M_TO_ALL, -- 兩種都能打, 分配能打M
        --A_REQ, -- 只能打 A 需求數
        --M_REQ, -- 只能打 M 需求數
        --ALL_REQ, -- 兩種都能打需求數
        TOTAL_REGION -- 總申請數
    FROM TOTAL_APPLY_BY_ACUTUAL ORDER BY TOTAL_M_RESULT),
    RESULT_LIST AS -- 派發清單
    (SELECT VFT.* FROM VACCINE_FILTER_TOTAL VFT, RESULT_TABLE RT
    WHERE VFT.REGION = RT.REGION
    AND CASE VACC_TYPE WHEN 'A' THEN  A_TO_A WHEN 'M' THEN M_TO_M ELSE A_TO_ALL + M_TO_ALL END >= RNK
    )
    SELECT * FROM RESULT_LIST;
看更多先前的回應...收起先前的回應...

已經選出最佳解答了還來回答
真是非常感謝
/images/emoticon/emoticon41.gif/images/emoticon/emoticon41.gif/images/emoticon/emoticon41.gif

您考慮得很多
我試著補充說明如下
或許可以減少一些 SQL 指令的複雜度

理論上和實務上
一次只會分配一種疫苗('X')
那麼
1.疫苗意願
只要過濾 INSTR(VACCINE, 'X') 即可
2.混打的判斷
不行-只要判斷 SHOT1='X'
可以-不用檢查 SHOT1 / 檢查可混打規則(eg.X-Y,X-Z,Y-Z..)
3.SHOT1DATE, SHOT2DATE
這兩個我原本的想法是「已施打日期」(沒打就是 NULL)
如果要判斷是否施打第1,2劑
只要判斷 SHOT1/SHOT2 = "" 即可

末了要再說一次
我寫不出這個 SQL
只有佩服二字

單純因為這問題跟以前寫過的主替配料問題很像 所以手癢寫完想分享QAQ .. 混打原則上不會在第二針混AZ, 至少以現階段來看是這樣 所以才多了那段判斷, 如果疫苗使用上無差別, 那確實可以簡化非常多!

其實這問題我覺得最難的問題還是分配耶, 老實說我自己也還是覺得這樣分很有問題, 但我也想不出更好的解法...... 為了在只能打A或M的限制下能最大的利用(分配)疫苗, 優先選擇分給只能打A或M的對象, 結果就變成 明明我就想早點打到疫苗 所以AM都選可以的人, 反而沒辦法優先排到的蠢問題...囧

只能打A或M的限制下能最大的利用(分配)疫苗, 優先選擇分給只能打A或M的對象, 結果就變成 明明我就想早點打到疫苗 所以AM都選可以的人, 反而沒辦法優先排到

這點我有不同的想法
我只把登記意願拿來當成資料的「篩選條件」
而非「分配條件」或「排序依據」

篩選條件可多可少(例:年齡範圍、縣市、第10類...)
會影響一開始的「需求數」
不會影響「每次分配的邏輯」

這麼一說好像確實把同時能接受兩種的另外拉出來有點多餘, 畢境這問題目前沒要求成對配發疫苗 只要滿足條件的直接塞, 並多考慮已經被其他種類分配到的部份就好XDD 有閒的話我再來想想:)

/images/emoticon/emoticon12.gif

我要發表回答

立即登入回答