2026/07/01

오늘의 이야기

SQLite 튜토리얼: Oracle 개발자를 위한 핵심 차이점 정리


sqlite



 


SQLite는 가볍고 단순한 구조 덕분에 다양한 환경에서 널리 쓰입니다. 하지만 Oracle SQL에 익숙한 개발자라면 몇 가지 문법 차이 때문에 혼란스러울 수 있습니다. 이번 튜토리얼에서는 WITH 절, DECODE 함수, ROWNUM, 버전 확인, OUTER JOIN을 중심으로 살펴봅니다.


1. WITH 절 (CTE)


SQLite에서도 공통 테이블 표현식(CTE)를 지원합니다.




sql




WITH recent_orders AS (
SELECT *
FROM orders
WHERE order_date > '2026-01-01'
)
SELECT customer_id, COUNT(*)
FROM recent_orders
GROUP BY customer_id;




👉 Oracle과 거의 동일하게 사용할 수 있습니다.


2. DECODE 함수 대체


Oracle의 DECODE는 SQLite에 없습니다. 대신 CASE 문을 사용하세요.




sql




SELECT 
CASE status
WHEN 'A' THEN 'Active'
WHEN 'I' THEN 'Inactive'
ELSE 'Unknown'
END AS status_text
FROM users;




👉 CASE 문이 더 범용적이고 가독성도 좋습니다.


3. ROWNUM 대신 쓰는 방법


SQLite에는 ROWNUM이 없습니다. 대신 두 가지 방법을 씁니다.


(1) LIMIT / OFFSET




sql




SELECT *
FROM users
LIMIT 10 OFFSET 20; -- 21번째 행부터 10개




(2) ROW_NUMBER() 윈도우 함수




sql




SELECT ROW_NUMBER() OVER (ORDER BY id) AS rn, name
FROM users;




👉 SQLite 3.25.0 이상에서 지원됩니다.


4. SQLite 버전 확인


버전 확인은 간단합니다.



  • SQL 쿼리:



  • sql




    SELECT sqlite_version();




  • CLI:



  • bash




    sqlite3 --version




  • Python:



  • python




    import sqlite3
    print(sqlite3.sqlite_version)





5. OUTER JOIN


SQLite는 LEFT OUTER JOIN만 지원합니다.




sql




SELECT u.id, u.name, o.order_date
FROM users u
LEFT OUTER JOIN orders o
ON u.id = o.user_id;





  • RIGHT OUTER JOIN → 테이블 순서를 바꿔서 LEFT JOIN으로 대체

  • FULL OUTER JOIN → LEFT JOIN과 RIGHT JOIN을 UNION으로 합쳐서 구현


마무리


Oracle에서 SQLite로 넘어올 때 가장 많이 부딪히는 차이점들을 정리했습니다.



  • WITH 절은 그대로 사용 가능

  • DECODE는 CASE로 대체

  • ROWNUM은 LIMIT/OFFSET 또는 ROW_NUMBER()

  • 버전 확인은 sqlite_version()

  • OUTER JOIN은 LEFT만 지원, FULL은 UNION으로 구현


👉 이 튜토리얼을 따라가면 Oracle 개발자도 SQLite 환경에서 빠르게 적응할 수 있습니다.





오늘의 이야기


#스하리1000명프로젝트,
한국에서 길을 잃었나요? 한국어를 못하더라도 이 앱을 사용하면 쉽게 돌아다닐 수 있습니다.
귀하의 언어로 말하면 귀하의 언어로 번역, 검색 및 결과가 표시됩니다.
여행자에게 좋습니다! 영어, 일본어, 중국어, 베트남어 등 10개 이상의 언어를 지원합니다.
지금 사용해 보세요!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/06/30

오늘의 이야기



#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase

🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 “혹시나~” 하면서 봤거든 ㅋㅋ

근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨

그것도 구글 Gemini로다가!

그래서 앱 하나 만들었지
👉 “로또 예상번호 by Gemini” 🎱

AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨

재미로 해도 좋고…
혹시 모르는 거잖아? 😏


https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127




오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle, um aplicativo obrigatório para clubes de badminton!
👉 Match Play – Grave pontuações e encontre oponentes 🎉
Perfeito para qualquer lugar, sozinho, com amigos ou em um clube! 🤝
Se você gosta de badminton, definitivamente experimente

Acesse o aplicativo 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기


#스하리1000명프로젝트,
Às vezes é difícil conversar com trabalhadores estrangeiros, certo?
Fiz um aplicativo simples que ajuda! Você escreve na sua língua e os outros veem na deles.
Ele é traduzido automaticamente com base nas configurações.
Muito útil para bate-papos fáceis. Dê uma olhada quando tiver uma chance!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기


#스하리1000명프로젝트

오늘 내가 만든앱 하나 알려주고 싶어, 이 앱은 알림수집기 라고 이름을 붙였는 데,
내 폰에 표시 되는 알림을 읽어서 내가 지정한 단어가 들어 있고, 지출기록을 남겨야 하는 알림이
있으면 수집하고, 카카오톡으로 친구에게 전달해 주는 기능을 구현해 줄꺼야. 📲

이번 패치에서는 하루 한번 지정한 시간에 나에게 알림(노티) 하도록 기능을 추가 했어. 🙏
한번 써보고 불편한 거 있으면 말해줘.

앱 바로가기
👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao





오늘의 이야기


#스하리1000명프로젝트,
In Korea verloren? Auch wenn Sie kein Koreanisch sprechen, hilft Ihnen diese App dabei, sich problemlos fortzubewegen.
Sprechen Sie einfach Ihre Sprache – es übersetzt, sucht und zeigt Ergebnisse in Ihrer Sprache an.
Ideal für Reisende! Unterstützt mehr als 10 Sprachen, darunter Englisch, Japanisch, Chinesisch, Vietnamesisch und mehr.
Probieren Sie es jetzt aus!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/06/29

오늘의 이야기



#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase

🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 “혹시나~” 하면서 봤거든 ㅋㅋ

근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨

그것도 구글 Gemini로다가!

그래서 앱 하나 만들었지
👉 “로또 예상번호 by Gemini” 🎱

AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨

재미로 해도 좋고…
혹시 모르는 거잖아? 😏


https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127




오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle, een onmisbare app voor badmintonclubs!
👉 Matchplay - Registreer scores en vind tegenstanders 🎉
Perfect voor overal, alleen, met vrienden of in een club! 🤝
Als je van badminton houdt, probeer het dan zeker

Ga naar appen 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기


#스하리1000명프로젝트

오늘 내가 만든앱 하나 알려주고 싶어, 이 앱은 알림수집기 라고 이름을 붙였는 데,
내 폰에 표시 되는 알림을 읽어서 내가 지정한 단어가 들어 있고, 지출기록을 남겨야 하는 알림이
있으면 수집하고, 카카오톡으로 친구에게 전달해 주는 기능을 구현해 줄꺼야. 📲

이번 패치에서는 하루 한번 지정한 시간에 나에게 알림(노티) 하도록 기능을 추가 했어. 🙏
한번 써보고 불편한 거 있으면 말해줘.

앱 바로가기
👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao





오늘의 이야기

오늘은


우편번호 검색 페이지 무한정 호출해 보기 예제 수정편 입니다. 


 


주소검색 페이지 예제



 


일상적인 업무에서 주소 검색은 언제든 진행 되어야 합니다.  다만, API 을 사용하게 되면 역시나, 비용 발생이 되는 관계로


다가 무한정 호출 가능한 우편번호/주소 검색 기능을 구현하기 위해... 카카오가 제공하는 주소 검색 스크립트을 이용하는 방법을 


정리해 보려고 합니다. 


 


예전 버전을 적용해 사용했던 기억이 있지만, 현재는 해당 기능도 업데이트 되어 수정이 필요 했습니다.   


https://postcode.map.kakao.com/guide



 


Kakao 우편번호 서비스


우편번호 검색과 도로명 주소 입력 기능을 너무 간단하게 적용할 수 있는 방법. Kakao 우편번호 서비스를 이용해보세요. 어느 사이트에서나 무료로 제약없이 사용 가능하답니다.


postcode.map.kakao.com




위 페이지에 기술된 내용을 참고 했습니다. 


 


 


아래 스크립트 부분은 blogger.com 의 페이지에서 사용할 수 있게 추가한 부분 입니다. html 코드을 지원하는 블로그 페이지는 어디든 지원이 될 것으로 판단 됩니다.


<!--카카오 우편번호 서비스 불러오기-->
<script src="//t1.kakaocdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>

<!--주소검색 버튼과 결과 표시 영역-->
<button onclick="execDaumPostcode()">주소 검색</button>
<div id="result"></div>

<script>
function execDaumPostcode() {
new kakao.Postcode({
oncomplete: function(data) {
// 선택된 주소 정보 표시
const roadAddr = data.roadAddress; // 도로명 주소
const jibunAddr = data.jibunAddress; // 지번 주소
const zonecode = data.zonecode; // 우편번호
const address = data.roadAddress || data.address ;

document.getElementById("result").innerHTML =
"<p><b>도로명 주소:</b> " + roadAddr + "</p>" +
"<p><b>지번 주소:</b> " + jibunAddr + "</p>" +
"<p><b>우편번호:</b> " + zonecode + "</p>";
// 이 부분은 안드로이드 에서 참조 하도록 하기 위함이라 web 에서는 에러 발생됨
window.Android.processDATA(address)
}
}).open();
}
</script>

 


 


아래 코드는 kotlin 코드로 작성된 주소 검색 페이지 activity 입니다.  이 페이지의 기능은 주소검색 script 을 androidview 로 호출하고, 결과을 받아 원래 호출 했던 위치로 return 해 주는 기능만 존재 합니다.


import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.os.Message
import android.net.Uri
import android.webkit.JavascriptInterface
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.viewinterop.AndroidView
import com.billcoreatech.multichat416.R
import com.billcoreatech.multichat416.ui.theme.Multichat416Theme
import com.billcoreatech.multichat416.ui.theme.appColorScheme
import com.billcoreatech.multichat416.ui.theme.appTypography

class AddressFindActivity : ComponentActivity() {

inner class MyJavaScriptInterface {

@JavascriptInterface
fun processDATA(data: String?) {
returnAddress(data)
}

@JavascriptInterface
fun postMessage(data: String?) {
returnAddress(data)
}
}

fun returnAddress(data: String?) {
Log.e("TAG", "returnAddress ${data}")

val intent = Intent()
intent.putExtra("data", data)
setResult(AppCompatActivity.RESULT_OK, intent)
finish()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Multichat416Theme(dynamicColor = true) {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
) {
Scaffold(
modifier = Modifier
.fillMaxSize()
.padding(top = 20.dp), topBar = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(5.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = {
Log.e("", "backArrow")
finish()
}) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close"
)
}
Spacer(modifier = Modifier.padding(5.dp))
Text(
text = stringResource(R.string.foundAddress),
color = appColorScheme.primary,
lineHeight = 1.33.em,
style = appTypography.titleLarge,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(align = Alignment.CenterVertically)
)
}
}, bottomBar = {

}
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) {

WebViewForAddress(this@AddressFindActivity,
doFinish = {
val intent = Intent()
setResult(AppCompatActivity.RESULT_CANCELED, intent)
finish()
},
)
}
}
}
}
}
}
}

@SuppressLint("JavascriptInterface", "SetJavaScriptEnabled")
@Composable
fun WebViewForAddress(addressFindActivity: AddressFindActivity, doFinish:() -> Unit) {

val blogspot = "https://billcoreatech.blogspot.com/2026/06/blog-post_480.html"; // "https://billcoreatech.blogspot.com/2022/06/blog-post.html"

var webViewContainer by remember { mutableStateOf<FrameLayout?>(null) }
var mainWebView by remember { mutableStateOf<WebView?>(null) }
var webView by remember { mutableStateOf<WebView?>(null) }
var canGoBack by remember { mutableStateOf(false) }
var isPostcodeRequested by remember { mutableStateOf(false) }

AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
val container = FrameLayout(context)
webViewContainer = container

fun WebView.configureForAddressSearch() {
settings.run {
javaScriptEnabled = true
domStorageEnabled = true
javaScriptCanOpenWindowsAutomatically = true
setSupportMultipleWindows(true)
}
val javascriptInterface = addressFindActivity.MyJavaScriptInterface()
addJavascriptInterface(javascriptInterface, "Android")
addJavascriptInterface(javascriptInterface, "android")
addJavascriptInterface(javascriptInterface, "ReactNativeWebView")
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return handleAddressCallbackUrl(
url = request.url,
addressFindActivity = addressFindActivity
)
}

override fun onPageFinished(view: WebView, url: String?) {
canGoBack = view.canGoBack()
if (!isPostcodeRequested) {
isPostcodeRequested = true
view.evaluateJavascript("execDaumPostcode();", null)
}
}
}
webChromeClient = object : WebChromeClient() {
override fun onCreateWindow(
view: WebView,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message
): Boolean {
val popupWebView = WebView(view.context).apply {
configureForAddressSearch()
webChromeClient = object : WebChromeClient() {
override fun onCloseWindow(window: WebView) {
container.removeView(window)
window.destroy()
webView = view
canGoBack = view.canGoBack()
}
}
}

container.addView(
popupWebView,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
)

webView = popupWebView
val transport = resultMsg.obj as WebView.WebViewTransport
transport.webView = popupWebView
resultMsg.sendToTarget()
return true
}
}
}

WebView(context).apply {
configureForAddressSearch()
loadUrl(blogspot, emptyMap())
mainWebView = this
webView = this
container.addView(
this,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
)
}
container
},
update = {
canGoBack = webView?.canGoBack() == true
},
)

BackHandler(enabled = true) {
val view = webView
if (view != null && canGoBack) {
view.goBack()
} else if (view != null && view !== mainWebView) {
webViewContainer?.removeView(view)
view.destroy()
webView = mainWebView
canGoBack = mainWebView?.canGoBack() == true
} else {
doFinish()
}
}

DisposableEffect(Unit) {
onDispose {
webViewContainer?.let { container ->
for (index in container.childCount - 1 downTo 0) {
(container.getChildAt(index) as? WebView)?.destroy()
}
container.removeAllViews()
}
mainWebView = null
webView = null
webViewContainer = null
}
}
}

private fun handleAddressCallbackUrl(
url: Uri,
addressFindActivity: AddressFindActivity
): Boolean {
if (url.scheme != "multichat416" || url.host != "address") {
return false
}

val data = url.getQueryParameter("data").orEmpty()
addressFindActivity.returnAddress(data)
return true
}​

 


코드 사용 예제는 훗날 추가해 보겠습니다.  개발을 마무리 하고 나서.





오늘의 이야기

#스하리1000명프로젝트, 外国人労働者と話すのが難しいこともありますよね? 簡単に役立つアプリを作りました!あなたは自分の言語で書き、他の人は自分の言語でそれを見ます。 設定に基づいて自動翻訳します。 簡単なチャットに非常に便利です。機会があったら見てみてください!...