특정 열의 값에 대해 순위를(랭킹을) 매기기 위해서 다음과 같은 함수를 사용해볼 수 있습니다.

  • RANK
  • DENSE_RANK
  • ROW_NUMBER
  • NTILE

기본적인 함수 형식은 아래과 같습니다.

RANK() OVER ( [PARTITION BY colName1] ORDER BY colName2 [DESC] )
DENSE_RANK() OVER ( [PARTITION BY colName1] ORDER BY colName2 [DESC] )
ROW_NUMBER() OVER ( [PARTITION BY colName1] ORDER BY colName2 [DESC] )
NTILE() OVER ( [PARTITION BY colName1] ORDER BY colName2 [DESC] )

# [대괄호] 안의 값은 선택사항입니다

 

이렇게 보면 외계어같지만 실제로는 사용이 쉬운 함수들 입니다. (진짜루요)

제가 갖고 있는 데이터베이스에서 Employee의 사번, 이름, 젠더, 샐러리(연봉) 4개를 저장한 임시 테이블 TEMP를 가지고 차례차례 함수를 적용시켜 보겠습니다.

 

1. RANK()

RANK()함수는 내가 저장한 행(column)에 순위를 매겨서 정렬한 결과값을 보여주는 함수입니다.

SELECT emp_no, 
	CONCAT(first_name, ' ', last_name) AS full_name,
	salary,
	RANK() OVER(ORDER BY SALARY DESC) AS `rank`
FROM TEMP;

 

위의 쿼리에는 SALARY 행 내림차순을 기준으로 랭킹함수를 적용해서 rank라는 새로운 이름의 행(column)을 반환하도록 했습니다.

 

 

결과를 살펴봅시다. 잘 보시면 rank 행에 128등이 두 명입니다. 이유는 두 명의 salary 값이 중복되기 때문인데요. 이렇게 RANK() 함수에서는 tie : 중복값이 있는 데이터끼리는 같은 등수를 쉐어하게 됩니다. 그리고 그 등수를 쉐어하는 만큼 그 다음 등수는 밀려서 사라지게 되는데요. 128등이 두 명 오고 난 다음에 129등 없이 130등으로 시작하는 것을 보면 알 수 있습니다. 만약 128등이 3명이었다면 그 다음 등수는 131등으로 시작하겠죠.

 

2. DENSE_RANK()

DENSE_RANK() 함수는 RANK()와 거의 비슷하지만 약간 다릅니다.

SELECT emp_no, 
	CONCAT(first_name, ' ', last_name) AS full_name,
	salary,
	DENSE_RANK() OVER(ORDER BY SALARY DESC) AS `rank`
FROM TEMP;

 

 

DENSE_RANK()에서는 같은 값, 중복값의 존재나 갯수와 관계없이 무조건 1씩 차례로 증가합니다. 랭킹이 밀리지 않아요. 127등이 두 명 있다고 그 다음 등수가 129로 시작하지 않고, 128으로 시작하는 것을 보면 알 수 있어요.

 

예를 들어 올림픽에서 금메달을 2명이 공동 수상한다고 가정을 했을 때, 금메달이 2명이라고 은메달은 아무도 주지 않고 동메달을 주면 안 되잖아요? 금메달을 받은 사람의 수와 관계 없이 은메달도 반드시 준다, 라고 보면 됩니다. (금메달 공동 수상이 실제로 가능한지는.... 저도 모르지만요...)

 

그런데 보니까 아까 RANK()에서는 128등이 두 명이었는데 이번엔 127등이 두 명이네요, 왜 그럴까요? 데이터 이미지 중간에 생략된 부분에서 같은 값을 가지는 tie가 두 명 있었습니다. 그래서 RANK()는 숫자가 하나 밀려서 128등 두 명이 되었고, DENSE_RANK()는 숫자가 밀리지 않아서 127등 두 명이 되었어요.

 

3. ROW_NUMBER()

ROW_NUMBER() 함수는 기준에 따라 랭킹 정렬을 하되, 중복값과 관계 없이 무조건 1부터 차례대로 행 번호를 매겨 반환합니다.

SELECT emp_no, 
	CONCAT(first_name, ' ', last_name) AS full_name,
	salary,
	ROW_NUMBER() OVER(ORDER BY SALARY DESC) AS `rank`
FROM TEMP;

 

보시는 것처럼 같은 값이 있던 말던 무조건 1부터 시작해서 하나씩 줄번호를 매겨서 반환합니다.

 

직관적인 예시로 출석 번호나 키번호를 생각해볼 수 있을 것 같아요. 학급에 동명이인이 있다고 둘이 같은 출석번호를 쉐어하지는 않죠. 키가 같다고 키번호를 똑같이 쓰지도 않을 거구요. 이런 상황에서 ROW_NUMBER() 함수를 사용하면 되겠습니다.

 

4. NTILE()

NTILE 함수는 랭킹을 매기되, 내가 지정한 블럭 갯수만큼 구간을 나누어 등급 랭킹을 부여합니다.

SELECT emp_no, 
	CONCAT(first_name, ' ', last_name) AS full_name,
	salary,
	NTILE(5) OVER(ORDER BY SALARY DESC) AS `rank`
FROM TEMP;

 

위의 코드에서 NTILE(5)와 같이 NTILE 함수 안에 정수를 넣어 줬는데요. 이 말은 1, 2, 3, 4, 5등 구간으로 나누어 5개의 등급을 매겨 랭킹을 반환하라는 뜻이에요.

 

데이터 갯수가 많아 이렇게 초반부에는 1등급만 보이지만

샐러리를 내림차순으로 정렬한 다음 5개의 등급으로 나누어 랭킹이 1부터 5까지 차례대로 부여가 되었습니다.

 

5. PARTITION BY 추가 응용

그럼 PARTITION BY는 어떻게 활용할 수 있을까요?

SELECT emp_no, 
	CONCAT(first_name, ' ', last_name) AS full_name,
	salary,
	RANK() OVER(PARTITION BY emp_no ORDER BY SALARY DESC) AS `rank`
FROM TEMP;

 

 

 

제가 가지고 있던 TEMP 테이블에는 같은 사람의 샐러리가 매년 업데이트 되며 누적되어 있어서, 이렇게 이름에 따라서 샐러리 값을 여러 열(row)이 저장 되어 있었어요. OVER() 내부의 시작 부분에 PARTITION BY emp_no 를 추가해 주면서 직원 번호에 따라 파티션을 나누고 그 파티션 내부에서 랭킹을 매긴 값을 반환받았습니다. 이를 통해 직원별로 샐러리가(연봉이) 얼마나 상승했는지를 한 눈에 알아볼 수 있게 되었습니다.

 

https://school.programmers.co.kr/learn/courses/30/lessons/159994

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

프로그래머스 코딩테스트 1단계에 수록되어 있는 '카드 뭉치' 문제를 풀었습니다. 다른 사람의 풀이와 비교해보니 제가 조금 독특하게 풀었더라구요. 제 코드가 좀 지저분하긴 하지만 남들과 다른 아이디어로 푼 게 마음에 들어 sorted 문법 정리할 겸 풀이과정을 작성해보고자 합니다.

 

 

우선 제출한 답안은 다음과 같습니다.

def solution(cards1, cards2, goal):
    cards1_sorted = sorted(enumerate(cards1), 
    	key = lambda x: (goal.index(x[1]) if x[1] in goal else len(goal) + 1))
    cards2_sorted = sorted(enumerate(cards2), 
    	key = lambda x: (goal.index(x[1]) if x[1] in goal else len(goal) + 1))
                           
    index_1 = [index for index, value in cards1_sorted]
    index_2 = [index for index, value in cards2_sorted]
    
    if index_1 != sorted(index_1) or index_2 != sorted(index_2):
        return "No"
    else:
        return "Yes"
정확성: 100.0, 합계: 100.0 / 100.0

 

하나씩 쪼개어 정리해 보겠습니다.

테스트케이스의 2번째 예시로 설명해 보겠습니다.

 

 

 

1. 먼저, cards1과 cards2에 enumerate() 함수를 이용해서 (인덱스, 카드)값이 되도록 만들어 줍니다. list()함수를 씌워 결과값을 눈으로 확인하면 다음과 같습니다.

cards1 = ["i", "water", "drink"]
cards2 = ["want", "to"]	
goal = ["i", "want", "to", "drink", "water"]	

list(enumerate(cards1))
# [(0, 'i'), (1, 'water'), (2, 'drink')]

list(enumerate(cards2))
# [(0, 'want'), (1, 'to')]

 

 

 

 

2. 저는 이렇게 추출한 이뉴머레이트 결과값을, sorted()를 이용해서 정렬을 해줄 거에요. 이 때 정렬을 해 주는 기준으로 goal을 넣어 주어서, goal에 있는 순서대로 정렬되도록 하겠습니다.

cards1_sorted = sorted(enumerate(cards1), key = lambda x: goal.index(x[1])
cards2_sorted = sorted(enumerate(cards2), key = lambda x: goal.index(x[1])

cards1_sorted
# [(0, 'i'), (2, 'drink'), (1, 'water')]

cards2_sorted
# [(0, 'want'), (1, 'to')]

 

key = lambda x: goal.index(x[1]) 와 같이 정렬 조건을 걸어줌으로써, cards1의 이뉴머레이트 결과값의 인덱스 1번 벨류가 goal에 어떤 순서대로 있느냐?를 기준으로 정렬한 값을 반환하도록 하였습니다. 결과값을 확인해보니 cards2는 그대로이지만 cards1은 drink가 앞으로 온 것을 확인할 수 있습니다. 그런데 이 문법에는 문제가 있습니다. 만약 cards1에 있는 단어가 goal에 없다면 어떻게 될까요?

 

2 - (1)

cards1 = ["i", "water", "drink", "apple"]
cards1_sorted = sorted(list(enumerate(cards1)), key = lambda x: goal.index(x[1]))

# ValueError: 'apple' is not in list

 

cards1에 'apple'을 추가해 보니, 벨류 에러가 발생합니다. 따라서 이렇게 없는 값이 있을 경우에는 cards1_sorted의 맨 뒤에다가 값을 추가해주도록 조건을 걸어주겠습니다.

cards1_sorted = sorted(list(enumerate(cards1)), 
             	   key = lambda x: (goal.index(x[1]) if x[1] in goal else len(goal) + 1))
cards2_sorted = sorted(list(enumerate(cards2)), 
            	   key = lambda x: (goal.index(x[1]) if x[1] in goal else len(goal) + 1))

 

만약 x[1]이 goal에 있다면 기존대로 정렬을 하고, 없다면 뒤에 추가하도록 if else 구문을 사용하여 조건을 걸어줬습니다. 이 부분은 챗지피티의 도움으로 완성할 수 있었습니다^_ㅠ 엄청 직관적인 문법이 아닌것 같긴 합니다만 그래도 자주 사용해서 자유자재로 구사할 수 있도록 하려고 합니다.

 

다시 apple이 없었던 원래의 예시문제로 돌아가서 계속해서 설명해보겠습니다.

 

 

 

 

3.

index_1 = [index for index, value in cards1_sorted]
index_2 = [index for index, value in cards2_sorted]


index_1
# [0, 2, 1]
index_2
# [0, 1]

 

 

이제 cards1_sorted와 cards_2sorted에서 인덱스만 뽑아서 깔끔한 리스트 index_1, index_2로 뽑아냅니다. 저는 이 index 리스트의 결과값이 0-2-1처럼 순서에 맞지 않을 경우 카드를 앞에서부터 차례로 사용하는 조건에 어긋난다는 점을 발견했습니다. 

 

따라서 index_1, index_2가 오름차순으로 정렬되어 있다면 "Yes"를, 그렇지 않다면(순서가 뒤죽박죽이라면) "No"를 리턴하도록 함수를 작성했습니다.

def solution(cards1, cards2, goal):
    cards1_sorted = sorted(enumerate(cards1), key = lambda x: (goal.index(x[1]) if x[1] in goal else len(goal) + 1))
    cards2_sorted = sorted(enumerate(cards2), key = lambda x: (goal.index(x[1]) if x[1] in goal else len(goal) + 1))

    index_1 = [index for index, value in cards1_sorted]
    index_2 = [index for index, value in cards2_sorted]
    
    if index_1 != sorted(index_1) or index_2 != sorted(index_2):
        return "No"
    else:
        return "Yes"

 

 

 

최근에 sorted() 함수에 대해서 리뷰를 했으며 특히 key 뒤에 lambda 함수를 어떻게 활용할 수 있는 지 여러번 살펴보았기 때문에 위와 같은 아이디어로 문제를 풀 수 있었습니다. 다른 사람의 풀이를 확인해보니 이런 방식으로 풀이를 하신 분은 찾아보기 어려웠고 더 짧고 깔끔하고 반듯하게 푸신 분이 많아서 존경스러웠습니다! 아주 깔끔한 코드는 아니지만 그래도 재밌는 아이디어로 풀어낸 점을 스스로 높게 평가하고자 합니다 ^_^!

 

 

 

감사합니다.

 

 

 

1. 선택정렬

# 0번째 아이템이 가장 작다고 가정하고 시작한다. 
# 만약 0번째 아이템보다 더 작은 아이템이 있다면 그중 가장 작은 놈과 0번째 아이템을 swap 한다.

arr = [20, 12, 10, 15, 2]

for i_step in range(len(arr)):
    # 현재 이터레이션에서 가장 작은 값의 인덱스가 무엇인지 min_step에 기록한다.
    # 앞으로 계속 갱신해 나간다.
    min_step = i_step

    for i in range(i_step+1, len(arr)):
        if arr[i] < arr[min_step]:
            min_step = i
        else:
            pass

    arr[min_step], arr[i_step] = arr[i_step], arr[min_step]
arr
>>> [2, 10, 12, 15, 20]
 

내장함수 sorted()처럼 비파괴적 함수로 만들어보기

def my_sorted(arr) :
    arr_dup = [i for i in arr]
    for i_step in range(len(arr_dup)):
        # 현재 이터레이션에서 가장 작은 값의 인덱스가 무엇인지 min_step에 기록한다.
        # 앞으로 계속 갱신해 나간다.
        min_step = i_step

        for i in range(i_step+1, len(arr_dup)):
            if arr_dup[i] < arr_dup[min_step]:
                min_step = i
            else:
                pass

        arr_dup[min_step], arr_dup[i_step] = arr_dup[i_step], arr_dup[min_step]
    return arr_dup
 
my_sorted(arr)
>>> [2, 10, 12, 15, 20]

arr
>>> [20, 12, 10, 15, 2]
 

 

위 코드는 arr의 길이(len)가 길어질수록 효율성이 낮아질 위험이 있다.

따라서 반복문 속에 중첩을 최대한 제거해 주는 것이 좋다.

 

 

2. 삽입정렬

 

선택 정렬이 아닌 삽입정렬로 리스트를 정렬해보자.

arr = [9, 5, 1, 4, 3]

for step in range(1, len(arr)):
    for i in range(step, 0, -1):
        if arr[i-1] > arr[i]:
            arr[i-1], arr[i] = arr[i], arr[i-1]

arr
>>> [1, 3, 4, 5, 9]
 

삽입정렬은 누가 더 작은지 기록할 필요가 없기때문에

선택정렬과 비교해 보면 훨씬 더 간단하다.

 

위 알고리즘을 적용하여 비파괴적 함수로 만들어 보자.

arr = [9, 5, 1, 4, 3]

def my_sorted(arr):
    arr_dup = [i for i in arr]

    for step in range(1, len(arr_dup)):
        for i in range(step, 0, -1):
            if arr_dup[i-1] > arr_dup[i]:
                arr_dup[i-1], arr_dup[i] = arr_dup[i], arr_dup[i-1]
    return arr_dup
 
my_sorted(arr)
>>> [1, 3, 4, 5, 9]

arr
>>> [9, 5, 1, 4, 3]
 

 

삽입정렬 역시 선택정렬과 마찬가지로 arr의 길이가 커질수록 효율성이 낮아지고 시간의 복잡도가 늘어나는 단점이 있으므로 유의한다.

 

 

 

3. 버블정렬

앞뒤 원소를 비교해서 더 큰 값이 앞에 있으면 뒤로 보내준다.

arr의 길이만큼 이터레이션을 돌린다.

arr = [-2, 45, 0, 11, -9]

for i in range(len(arr)):
    for k in range(len(arr)-i-1):
        print(i, k)
 

0 0

0 1

0 2

0 3

 

1 0

1 1

1 2

 

2 0

2 1

 

3 0

arr = [-2, 45, 0, 11, -9]

for i in range(len(arr)):
    for k in range(len(arr)-i-1):
        if arr[k] > arr[k+1]:
            arr[k], arr[k+1] = arr[k+1], arr[k]

arr
>>> [-9, -2, 0, 11, 45]
 

삽입정렬과 나름 비슷한 형태로, 역시 arr의 길이가 길어질수록 효율성이 낮아진다.

 

시각화해보기

for i in range(len(arr)):
    for k in range(len(arr)-i-1):
        if arr[k] > arr[k+1]:
            print(i, k, 'swap!')
            arr[k], arr[k+1] = arr[k+1], arr[k]
            print(arr)
        else:
            print(i, k, 'pass!')
            print(arr)
 

모든 단계별로 i, k, 스왑여부, arr을 출력하여 과정을 시각화 해보았다.

 

0 0 pass!

[-2, 45, 0, 11, -9]

0 1 swap!

[-2, 0, 45, 11, -9]

0 2 swap!

[-2, 0, 11, 45, -9]

0 3 swap!

[-2, 0, 11, -9, 45] --> 45 확정

1 0 pass!

[-2, 0, 11, -9, 45]

1 1 pass!

[-2, 0, 11, -9, 45]

1 2 swap!

[-2, 0, -9, 11, 45] --> 11, 45 확정

2 0 pass!

[-2, 0, -9, 11, 45]

2 1 swap!

[-2, -9, 0, 11, 45] --> 0, 11, 45 확정

3 0 swap!

[-9, -2, 0, 11, 45]

 

+ Recent posts