星期一, 4月 20, 2026

[SQL] 稀疏欄位 (Sparse Column)

稀疏欄位 (Sparse Column) 是 MS SQL 2008 功能,官方文件說明
Sparse columns are ordinary columns that have an optimized storage for null values. Sparse columns reduce the space requirements for null values at the cost of more overhead to retrieve non-NULL values. Consider using sparse columns when the space saved is at least 20 percent to 40 percent.
Sparse 關鍵字
USE AdventureWorks2025;  
GO  
  
CREATE TABLE DocumentStore  
    (DocID int PRIMARY KEY,  
     Title varchar(200) NOT NULL,  
     ProductionSpecification varchar(20) SPARSE NULL,  -- SPARSE Column 
     ProductionLocation smallint SPARSE NULL,          -- SPARSE Column
     MarketingSurveyGroup varchar(20) SPARSE NULL ) ;  -- SPARSE Column
GO  
Sparse Column 查詢
use AdventureWorks2025
go 

SELECT 
	[name] AS ColumnName ,
	is_nullable ,
	is_sparse ,
	is_column_set
FROM sys.columns
WHERE object_name(object_id) =  'DocumentStore'
限制
  • Sparse columns are a property of the storage layer, rather than the logical table. Therefore a SELECT ... INTO statement does not copy over the sparse column property into a new table.
  • A sparse column must be nullable and cannot have the ROWGUIDCOL or IDENTITY properties. A sparse column cannot be of the following data types: text, ntext, image, timestamp, user-defined data type, geometry, or geography; or have the FILESTREAM attribute.
  • A sparse column cannot have a default value.
  • Sparse columns are incompatible with data compression. Therefore sparse columns cannot be added to compressed tables, nor can any tables containing sparse columns be compressed.
  • Changing a column from sparse to non-sparse, or non-sparse to sparse, requires changing the storage format of the column. The SQL Server Database Engine uses the following procedure to accomplish this change: 
    •  Adds a new column to the table in the new storage size and format. 
    •  For each row in the table, updates and copies the value stored in the old column to the new column. 
    •  Removes the old column from the table schema. 
    •  Rebuilds the table (if there is no clustered index) or rebuilds the clustered index to reclaim space used by the old column.
以上只列出較感興趣限制,完整限制說明可以查看 Restrictions for using sparse columns

重構某單位 AP 時發現 Table 整併後,配合商業邏輯而新增欄位內有 90% 資料會是 null,原本是想要多拆一個 Table 來存放節省空間,但該情境似乎蠻符合 Sparse Column 就來使用看看囉

星期四, 4月 16, 2026

[GAS] 網頁應用程式 - Google 帳號

以往把 GAS 發佈為網頁應用程式時,設定值都是
  • 執行身份:我 (gmail 信箱)
  • 誰可以存取:所有人
該筆記使用
  • 執行身份:存取網頁應用程式的使用者
  • 誰可以存取:所有已登入 Google 帳戶的使用者
來記錄,並設定存取名單,不在存取名單內即使有登錄 Google 帳號也無法存取網頁
使用者 Email

透過 getActiveUser() 來存取 Google 帳號 Email
Gets information about the current user. If security policies do not allow access to the user's identity, User.getEmail() returns a blank string. The circumstances in which the email address is available vary: for example, the user's email address is not available in any context that allows a script to run without that user's authorization, like a simple onOpen(e) or onEdit(e) trigger, a custom function in Google Sheets, or a web app deployed to "execute as me" (that is, authorized by the developer instead of the user). However, these restrictions generally do not apply if the developer runs the script themselves or belongs to the same Google Workspace domain as the user.

網頁存取名單

在 GAS PropertyService 內設定存取存取名單,存取名單已逗號 (,) 分隔,例如:123@gmail.com,456@gmail.com,當然也可以放在 Google Sheet 或第三方資料庫來管理存取名單
gs Code
/**
 * 系統配置參數
 * @constant {Object}
 */
const CONFIG = {
  PROPERTY_KEY: 'WHITELIST' // 儲存在指令碼屬性中的鍵名
};

/**
 * 從指令碼屬性 (PropertiesService) 中獲取白名單。
 * @returns {string[]} 允許存取的 Google 帳號信箱陣列。
 */
const getAllowedEmails = () => {
  const whitelistStr = PropertiesService.getScriptProperties().getProperty(CONFIG.PROPERTY_KEY);
  
  if (!whitelistStr) {
    console.warn('目前沒有設定任何白名單。');
    return [];
  }

  // 將字串轉為陣列,過濾空白並轉為小寫
  return whitelistStr.split(',')
    .map(email => email.trim().toLowerCase())
    .filter(email => email !== '');
};

/**
 * 處理 Web App 的 HTTP GET 請求。
 * @param {Object} e - 事件物件。
 * @returns {GoogleAppsScript.HTML.HtmlOutput} 根據使用者權限回傳對應的 HTML 頁面。
 */
const doGet = (e) => {
  const currentUserEmail = Session.getActiveUser().getEmail().toLowerCase();
  const allowedEmails = getAllowedEmails();

  if (allowedEmails.includes(currentUserEmail)) {
    // 驗證通過
    const template = HtmlService.createTemplateFromFile('Index');
    template.userEmail = currentUserEmail; 
    
    return template.evaluate().setTitle('系統主控台');
  } else {
    // 驗證失敗
    const template = HtmlService.createTemplateFromFile('Error');
    template.userEmail = currentUserEmail;
    
    return template.evaluate().setTitle('存取被拒絕');
  }
};
Index.html 和 Error.html 就不特別列出來囉

實際執行

進入網頁時假如沒有登錄 Google 帳號,會出現 Google 登錄畫面

登錄 Google 帳號後則是檢查該帳號是否在存取白名單內



限制

實作發現,原來 getActiveUser() 在 [執行身份:我 (gmail 信箱)] 時,使用者執行網頁時會回傳空值,該情況只有執行身份信箱使用者可以透過 getActiveUser() 取得 gmail 資訊

[執行身份:存取網頁應用程式的使用者] 時,使用者執行網頁時雖然 getActiveUser() 可以取得 gmail 資訊進一步根據存取名單判斷是否有權限,但該身分初次存取網頁需授予權限,授予權限範圍則是依網頁使用相關服務不一,至少會要求存取使用者帳戶資訊,其實部屬畫面上就有簡易文字說明
原以為 [執行身份:我 (gmail 信箱)] 會類似一個服務帳號,該帳號擁有權限存取 Google 相關服務,而 getActiveUser() 則是可以抓到使用者 gmail 資訊,兩者搭配來實作存取名單功能,但在 GAS 內部架構下看起來沒有這麼理想

星期五, 4月 10, 2026

[GAS] Google Doc 轉為 PDF

GAS 內可以透過 getAs() 把 Google Doc 轉換為 PDF

限制
gs code
/**
 * 將指定的 Google Doc 文件轉換為 PDF,並儲存到指定的 Google Drive 資料夾中
 *
 * @param {string} documentId - 欲轉換的 Google Doc 檔案 ID
 * @param {string} folderId - 儲存 PDF 的目標資料夾 ID
 * @returns {string} 新建立的 PDF 檔案 URL
 * @throws {Error} 當缺少必要參數、找不到檔案、權限不足或轉換失敗時拋出錯誤
 */
function convertDocToPdf(documentId, folderId) {

  if (!documentId || !folderId) {
    throw new Error("執行失敗:必須同時提供 documentId (文件 ID) 與 folderId (目標資料夾 ID)。");
  }

  try {
    // 1. 透過 ID 取得指定的 Google 文件
    const docFile = DriveApp.getFileById(documentId);

    // 2. 將檔案轉換為 PDF 格式的 Blob (二進位大型物件)
    const pdfBlob = docFile.getAs(MimeType.PDF);

    // 3. 設定產出的 PDF 檔名 (預設為原檔名加上 .pdf)
    const newFileName = `${docFile.getName()}.pdf`;
    pdfBlob.setName(newFileName);

    // 4. 取得目標資料夾實體,並在該資料夾內直接建立 PDF 檔案
    const folder = DriveApp.getFolderById(folderId);
    const newPdfFile = folder.createFile(pdfBlob);
    return newPdfFile.getUrl();

  } catch (error) {
    throw new Error(`PDF 轉換程序發生錯誤: ${error.message}`);
  }
}
執行
function testConvertDocToPdf() {
  const targetDocumentId = "1x17o_MEpEU-S2jKzFhdVqfQ3bswGVPKf2ottem8TSRE"; 
  const targetFolderId = "1Odq3pbNO63RxXmrxWNM9vSGWuGWyBmbZ";

  try {
    const pdfUrl = convertDocToPdf(targetDocumentId, targetFolderId);
    console.log(`轉換完成!PDF 檔案已儲存至指定資料夾,連結請見: ${pdfUrl}`);
  } catch (error) {
    console.error(error);
  }
}

星期二, 4月 07, 2026

[Forms] 設定截止日或回覆限制

看 AI 教學時,無意中看到老師操作畫面上 Google Forms 有表單截止設定,沒想到自動關閉表單已經是內建功能囉

把表單發佈出去後就可以看見 [設定截止日或回覆限制],有兩種設定方式,分別為
  • 指定日期
  • 直到指定回覆數之後
指定日期:設定完成後,日期時間有包含時區
直到指定回覆數之後: 設定 3 則回覆,並自訂 [若表單不再接受回覆,作答者就會看到這則訊息] 為 [已達報名人數上限,無法繼續報名]
輸入 3 則回覆後表單就會自動關閉,再進入表單就會看見下圖
之前介紹過 Google Forms 外掛 [Forms] 外掛-formLimiter 就只剩下 spreadSheet cell value 特定條件結束表單功能囉

星期一, 4月 06, 2026

[Line] 使用預設瀏覽器開啟連結

在 iphone 上使用診所 Line 官方帳號要進行預約時發現,網頁沒有用手機上預設瀏覽器開啟,Line 有內建瀏覽器,之前完全沒有發現這點

在手機版本 Line 上,設定 => Line Labs => 使用預設瀏覽器開啟連結,該選項預設是開啟的