最近最熱門的資料庫,莫過於疫苗預約系統了
看新聞報導寫得天花亂墜,我想自己實作看看
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;
補充母數如下
我是用MSSQL去計算的~
我先回第一題@@..
1.以最單純的縣市(LEFT(AREA,3))來分配即可(假設全部的人都尚未接種)
匯入的資料
分配的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
今天工作好忙~一直被打擾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
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 <= @疫苗被分配
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)
謝謝回答
吹毛求疵一點
「疫苗分配」的總數是 5005
說時遲那時快
已經有 4995 的版本了
我有更新計算唷XD
接下來就是「名單」的部份
在各群組中挑出「疫苗分配」筆數的資料
例:新北市 從 1966 筆中挑出 838 筆資料
ok~就大概如上更新了~
有個不情之請
可不可以合成一個 SQL,不要分兩段
我合在一起了唷~共4969筆要通知@@a
先建議你 schema 設定先做些更動:
2a. 如果為二劑,如 AZ,第二劑在第一劑起打後的 8 週,所以第二劑的種類可以自動設定第一劑的種類,但因現在可能可以混打,所以第二劑種類可以維持開放修改狀態
2b. 如果打 az,不管第二劑何種類,都要到第 8 週後才可施打,所以第二劑的日期應要鎖定第一季的 8 週後,但因被施打者可能拖過 8 週後時間,所以第二劑的時間也要維持開放修改狀態,唯要設定不能低於第一劑的 8 週後第一天 (第 57 天)
這題目好有趣, 分享我的版本, 但資料庫是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;
已經選出最佳解答了還來回答
真是非常感謝
您考慮得很多
我試著補充說明如下
或許可以減少一些 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都選可以的人, 反而沒辦法優先排到的蠢問題...囧